Skip to content

Commit 806a75c

Browse files
authored
chore: kickoff release
2 parents 79d062d + cb80b91 commit 806a75c

File tree

36 files changed

+780
-186
lines changed

36 files changed

+780
-186
lines changed

Amplify/Core/Configuration/AmplifyOutputsData.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,8 @@ public struct AmplifyOutputsData: Codable {
251251
public struct AmplifyOutputs {
252252

253253
/// A closure that resolves the `AmplifyOutputsData` configuration
254-
let resolveConfiguration: () throws -> AmplifyOutputsData
254+
@_spi(InternalAmplifyConfiguration)
255+
public let resolveConfiguration: () throws -> AmplifyOutputsData
255256

256257
/// Resolves configuration with `amplify_outputs.json` in the main bundle.
257258
public static let amplifyOutputs: AmplifyOutputs = {

AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncRealTimeClient.swift

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -165,14 +165,15 @@ actor AppSyncRealTimeClient: AppSyncRealTimeClientProtocol {
165165
// Placing the actual subscription work in a deferred task and
166166
// promptly returning the filtered publisher for downstream consumption of all error messages.
167167
defer {
168-
Task { [weak self] in
168+
let task = Task { [weak self] in
169169
guard let self = self else { return }
170170
if !(await self.isConnected) {
171171
try await connect()
172172
try await waitForState(.connected)
173173
}
174-
await self.bindCancellableToConnection(try await self.startSubscription(id))
175-
}.toAnyCancellable.store(in: &cancellablesBindToConnection)
174+
await self.storeInConnectionCancellables(try await self.startSubscription(id))
175+
}
176+
self.storeInConnectionCancellables(task.toAnyCancellable)
176177
}
177178

178179
return filterAppSyncSubscriptionEvent(with: id)
@@ -236,24 +237,29 @@ actor AppSyncRealTimeClient: AppSyncRealTimeClientProtocol {
236237
}
237238

238239
private func subscribeToWebSocketEvent() async {
239-
await self.webSocketClient.publisher.sink { [weak self] _ in
240+
let cancellable = await self.webSocketClient.publisher.sink { [weak self] _ in
240241
self?.log.debug("[AppSyncRealTimeClient] WebSocketClient terminated")
241242
} receiveValue: { webSocketEvent in
242243
Task { [weak self] in
243-
await self?.onWebSocketEvent(webSocketEvent)
244-
}.toAnyCancellable.store(in: &self.cancellables)
244+
let task = Task { [weak self] in
245+
await self?.onWebSocketEvent(webSocketEvent)
246+
}
247+
await self?.storeInCancellables(task.toAnyCancellable)
248+
}
245249
}
246-
.store(in: &cancellables)
250+
self.storeInCancellables(cancellable)
247251
}
248252

249253
private func resumeExistingSubscriptions() {
250254
log.debug("[AppSyncRealTimeClient] Resuming existing subscriptions")
251255
for (id, _) in self.subscriptions {
252-
Task {
256+
Task { [weak self] in
253257
do {
254-
try await self.startSubscription(id).store(in: &cancellablesBindToConnection)
258+
if let cancellable = try await self?.startSubscription(id) {
259+
await self?.storeInConnectionCancellables(cancellable)
260+
}
255261
} catch {
256-
log.debug("[AppSyncRealTimeClient] Failed to resume existing subscription with id: (\(id))")
262+
Self.log.debug("[AppSyncRealTimeClient] Failed to resume existing subscription with id: (\(id))")
257263
}
258264
}
259265
}
@@ -286,7 +292,7 @@ actor AppSyncRealTimeClient: AppSyncRealTimeClientProtocol {
286292
subject.filter {
287293
switch $0 {
288294
case .success(let response): return response.id == id || response.type == .connectionError
289-
case .failure(let error): return true
295+
case .failure: return true
290296
}
291297
}
292298
.map { result -> AppSyncSubscriptionEvent? in
@@ -350,10 +356,6 @@ actor AppSyncRealTimeClient: AppSyncRealTimeClientProtocol {
350356
return errors.compactMap(AppSyncRealTimeRequest.parseResponseError(error:))
351357
}
352358

353-
private func bindCancellableToConnection(_ cancellable: AnyCancellable) {
354-
cancellable.store(in: &cancellablesBindToConnection)
355-
}
356-
357359
}
358360

359361
// MARK: - On WebSocket Events
@@ -366,8 +368,11 @@ extension AppSyncRealTimeClient {
366368
if self.state.value == .connectionDropped {
367369
log.debug("[AppSyncRealTimeClient] reconnecting appSyncClient after connection drop")
368370
Task { [weak self] in
369-
try? await self?.connect()
370-
}.toAnyCancellable.store(in: &cancellablesBindToConnection)
371+
let task = Task { [weak self] in
372+
try? await self?.connect()
373+
}
374+
await self?.storeInConnectionCancellables(task.toAnyCancellable)
375+
}
371376
}
372377

373378
case let .disconnected(closeCode, reason): //
@@ -425,24 +430,37 @@ extension AppSyncRealTimeClient {
425430
}
426431
}
427432

428-
private func monitorHeartBeats(_ connectionAck: JSONValue?) {
433+
func monitorHeartBeats(_ connectionAck: JSONValue?) {
429434
let timeoutMs = connectionAck?.connectionTimeoutMs?.intValue ?? 0
430435
log.debug("[AppSyncRealTimeClient] Starting heart beat monitor with interval \(timeoutMs) ms")
431-
heartBeats.eraseToAnyPublisher()
436+
let cancellable = heartBeats.eraseToAnyPublisher()
432437
.debounce(for: .milliseconds(timeoutMs), scheduler: DispatchQueue.global())
433438
.first()
434-
.sink(receiveValue: {
435-
self.log.debug("[AppSyncRealTimeClient] KeepAlive timed out, disconnecting")
439+
.sink(receiveValue: { [weak self] in
440+
Self.log.debug("[AppSyncRealTimeClient] KeepAlive timed out, disconnecting")
436441
Task { [weak self] in
437-
await self?.reconnect()
438-
}.toAnyCancellable.store(in: &self.cancellables)
442+
let task = Task { [weak self] in
443+
await self?.reconnect()
444+
}
445+
await self?.storeInCancellables(task.toAnyCancellable)
446+
}
439447
})
440-
.store(in: &cancellablesBindToConnection)
448+
self.storeInConnectionCancellables(cancellable)
441449
// start counting down
442450
heartBeats.send(())
443451
}
444452
}
445453

454+
extension AppSyncRealTimeClient {
455+
private func storeInCancellables(_ cancellable: AnyCancellable) {
456+
self.cancellables.insert(cancellable)
457+
}
458+
459+
private func storeInConnectionCancellables(_ cancellable: AnyCancellable) {
460+
self.cancellablesBindToConnection.insert(cancellable)
461+
}
462+
}
463+
446464
extension Publisher where Output == AppSyncRealTimeSubscription.State, Failure == Never {
447465
func toAppSyncSubscriptionEventStream() -> AnyPublisher<AppSyncSubscriptionEvent, Never> {
448466
self.compactMap { subscriptionState -> AppSyncSubscriptionEvent? in

AmplifyPlugins/API/Tests/AWSAPIPluginTests/AppSyncRealTimeClient/AppSyncRealTimeClientTests.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,4 +551,32 @@ class AppSyncRealTimeClientTests: XCTestCase {
551551
await fulfillment(of: [startTriggered, errorReceived], timeout: 2)
552552

553553
}
554+
555+
func testReconnect_whenHeartBeatSignalIsNotReceived() async throws {
556+
var cancellables = Set<AnyCancellable>()
557+
let timeout = 1.0
558+
let mockWebSocketClient = MockWebSocketClient()
559+
let mockAppSyncRequestInterceptor = MockAppSyncRequestInterceptor()
560+
let appSyncClient = AppSyncRealTimeClient(
561+
endpoint: URL(string: "https://example.com")!,
562+
requestInterceptor: mockAppSyncRequestInterceptor,
563+
webSocketClient: mockWebSocketClient
564+
)
565+
566+
// start monitoring
567+
await appSyncClient.monitorHeartBeats(.object([
568+
"connectionTimeoutMs": 100
569+
]))
570+
571+
let reconnect = expectation(description: "webSocket triggers event to connection")
572+
await mockWebSocketClient.actionSubject.sink { action in
573+
switch action {
574+
case .connect:
575+
reconnect.fulfill()
576+
default: break
577+
}
578+
}.store(in: &cancellables)
579+
await fulfillment(of: [reconnect], timeout: 2)
580+
}
581+
554582
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
import Amplify // Amplify.Auth
10+
import AWSPluginsCore // AuthAWSCredentialsProvider
11+
import AWSClientRuntime // AWSClientRuntime.CredentialsProviding
12+
import ClientRuntime // SdkHttpRequestBuilder
13+
import AwsCommonRuntimeKit // CommonRuntimeKit.initialize()
14+
15+
extension AWSCognitoAuthPlugin {
16+
17+
18+
/// Creates a AWS IAM SigV4 signer capable of signing AWS AppSync requests.
19+
///
20+
/// **Note**. Although this method is static, **Amplify.Auth** is required to be configured with **AWSCognitoAuthPlugin** as
21+
/// it depends on the credentials provider from Cognito through `Amplify.Auth.fetchAuthSession()`. The static type allows
22+
/// developers to simplify their callsite without having to access the method on the plugin instance.
23+
///
24+
/// - Parameter region: The region of the AWS AppSync API
25+
/// - Returns: A closure that takes in a requestand returns a signed request.
26+
public static func createAppSyncSigner(region: String) -> ((URLRequest) async throws -> URLRequest) {
27+
return { request in
28+
try await signAppSyncRequest(request,
29+
region: region)
30+
}
31+
}
32+
33+
static func signAppSyncRequest(_ urlRequest: URLRequest,
34+
region: Swift.String,
35+
signingName: Swift.String = "appsync",
36+
date: ClientRuntime.Date = Date()) async throws -> URLRequest {
37+
CommonRuntimeKit.initialize()
38+
39+
// Convert URLRequest to SDK's HTTPRequest
40+
guard let requestBuilder = try createAppSyncSdkHttpRequestBuilder(
41+
urlRequest: urlRequest) else {
42+
return urlRequest
43+
}
44+
45+
// Retrieve the credentials from credentials provider
46+
let credentials: AWSClientRuntime.AWSCredentials
47+
let authSession = try await Amplify.Auth.fetchAuthSession()
48+
if let awsCredentialsProvider = authSession as? AuthAWSCredentialsProvider {
49+
let awsCredentials = try awsCredentialsProvider.getAWSCredentials().get()
50+
credentials = awsCredentials.toAWSSDKCredentials()
51+
} else {
52+
let error = AuthError.unknown("Auth session does not include AWS credentials information")
53+
throw error
54+
}
55+
56+
// Prepare signing
57+
let flags = SigningFlags(useDoubleURIEncode: true,
58+
shouldNormalizeURIPath: true,
59+
omitSessionToken: false)
60+
let signedBodyHeader: AWSSignedBodyHeader = .none
61+
let signedBodyValue: AWSSignedBodyValue = .empty
62+
let signingConfig = AWSSigningConfig(credentials: credentials,
63+
signedBodyHeader: signedBodyHeader,
64+
signedBodyValue: signedBodyValue,
65+
flags: flags,
66+
date: date,
67+
service: signingName,
68+
region: region,
69+
signatureType: .requestHeaders,
70+
signingAlgorithm: .sigv4)
71+
72+
// Sign request
73+
guard let httpRequest = await AWSSigV4Signer.sigV4SignedRequest(
74+
requestBuilder: requestBuilder,
75+
76+
signingConfig: signingConfig
77+
) else {
78+
return urlRequest
79+
}
80+
81+
// Update original request with new headers
82+
return setHeaders(from: httpRequest, to: urlRequest)
83+
}
84+
85+
static func setHeaders(from sdkRequest: SdkHttpRequest, to urlRequest: URLRequest) -> URLRequest {
86+
var urlRequest = urlRequest
87+
for header in sdkRequest.headers.headers {
88+
urlRequest.setValue(header.value.joined(separator: ","), forHTTPHeaderField: header.name)
89+
}
90+
return urlRequest
91+
}
92+
93+
static func createAppSyncSdkHttpRequestBuilder(urlRequest: URLRequest) throws -> SdkHttpRequestBuilder? {
94+
95+
guard let url = urlRequest.url,
96+
let host = url.host else {
97+
return nil
98+
}
99+
100+
var headers = urlRequest.allHTTPHeaderFields ?? [:]
101+
headers.updateValue(host, forKey: "host")
102+
103+
let httpMethod = (urlRequest.httpMethod?.uppercased())
104+
.flatMap(HttpMethodType.init(rawValue:)) ?? .get
105+
106+
let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems?
107+
.map { ClientRuntime.SDKURLQueryItem(name: $0.name, value: $0.value)} ?? []
108+
109+
let requestBuilder = SdkHttpRequestBuilder()
110+
.withHost(host)
111+
.withPath(url.path)
112+
.withQueryItems(queryItems)
113+
.withMethod(httpMethod)
114+
.withPort(443)
115+
.withProtocol(.https)
116+
.withHeaders(.init(headers))
117+
.withBody(.data(urlRequest.httpBody))
118+
119+
return requestBuilder
120+
}
121+
}
122+
123+
extension AWSPluginsCore.AWSCredentials {
124+
125+
func toAWSSDKCredentials() -> AWSClientRuntime.AWSCredentials {
126+
if let tempCredentials = self as? AWSTemporaryCredentials {
127+
return AWSClientRuntime.AWSCredentials(
128+
accessKey: tempCredentials.accessKeyId,
129+
secret: tempCredentials.secretAccessKey,
130+
expirationTimeout: tempCredentials.expiration,
131+
sessionToken: tempCredentials.sessionToken)
132+
} else {
133+
return AWSClientRuntime.AWSCredentials(
134+
accessKey: accessKeyId,
135+
secret: secretAccessKey,
136+
expirationTimeout: Date())
137+
}
138+
139+
}
140+
}

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/CredentialStorage/AWSCognitoAuthCredentialStore.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,12 @@ struct AWSCognitoAuthCredentialStore {
6565
newIdentityConfigData != nil &&
6666
oldIdentityPoolConfiguration == newIdentityConfigData
6767
{
68-
6968
// retrieve data from the old namespace and save with the new namespace
7069
if let oldCognitoCredentialsData = try? keychain._getData(oldNameSpace) {
7170
try? keychain._set(oldCognitoCredentialsData, key: newNameSpace)
7271
}
73-
} else if oldAuthConfigData != currentAuthConfig {
72+
} else if oldAuthConfigData != currentAuthConfig &&
73+
oldNameSpace != newNameSpace {
7474
// Clear the old credentials
7575
try? keychain._remove(oldNameSpace)
7676
}

0 commit comments

Comments
 (0)