Skip to content

Commit d03f5a6

Browse files
harsh625d
andauthored
fix(Auth): Refactoring state machine logic to fix memory leak (#3613)
* fix(Auth): Fixing memory leaks happening because of the state machine * Update AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/hierarchical-state-machine-swift/StateMachine.swift Co-authored-by: Di Wu <[email protected]> * worked on review comment --------- Co-authored-by: Di Wu <[email protected]>
1 parent af4a5f7 commit d03f5a6

File tree

3 files changed

+58
-84
lines changed

3 files changed

+58
-84
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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 Combine
9+
10+
class CancellableAsyncStream<Element>: AsyncSequence {
11+
12+
typealias AsyncIterator = AsyncStream<Element>.AsyncIterator
13+
private let asyncStream: AsyncStream<Element>
14+
private let cancellable: AnyCancellable?
15+
16+
deinit {
17+
cancel()
18+
}
19+
20+
init(asyncStream: AsyncStream<Element>, cancellable: AnyCancellable?) {
21+
self.asyncStream = asyncStream
22+
self.cancellable = cancellable
23+
}
24+
25+
convenience init(with publisher: AnyPublisher<Element, Never>) {
26+
var cancellable: AnyCancellable?
27+
self.init(asyncStream: AsyncStream { continuation in
28+
cancellable = publisher.sink { _ in
29+
continuation.finish()
30+
} receiveValue: {
31+
continuation.yield($0)
32+
}
33+
}, cancellable: cancellable)
34+
}
35+
36+
func makeAsyncIterator() -> AsyncStream<Element>.AsyncIterator {
37+
asyncStream.makeAsyncIterator()
38+
}
39+
40+
func cancel() {
41+
cancellable?.cancel()
42+
}
43+
}

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/hierarchical-state-machine-swift/StateAsyncSequence.swift

Lines changed: 0 additions & 36 deletions
This file was deleted.

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/hierarchical-state-machine-swift/StateMachine.swift

Lines changed: 15 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,7 @@
55
// SPDX-License-Identifier: Apache-2.0
66
//
77

8-
import Foundation
9-
10-
/// Captures a weak reference to the value
11-
class WeakWrapper<T> where T: AnyObject {
12-
private(set) weak var value: T?
13-
init(_ value: T) {
14-
self.value = value
15-
}
16-
}
8+
import Combine
179

1810
/// Models, evolves, and processes effects for a system.
1911
///
@@ -31,15 +23,18 @@ actor StateMachine<
3123
EnvironmentType: Environment
3224
> where StateType: State {
3325

34-
/// AsyncSequences are invoked a minimum of one time: Each sequence receives the current
35-
/// state as soon as `listen()` is invoked, and will receive each subsequent state change.
36-
typealias StateChangeSequence = StateAsyncSequence<StateType>
37-
3826
private let environment: EnvironmentType
3927
private let resolver: AnyResolver<StateType>
4028

41-
private(set) var currentState: StateType
42-
private var subscribers: [WeakWrapper<StateAsyncSequence<StateType>>]
29+
public var currentState: StateType {
30+
currentStateSubject.value
31+
}
32+
33+
private let currentStateSubject: CurrentValueSubject<StateType, Never>
34+
35+
deinit {
36+
currentStateSubject.send(completion: .finished)
37+
}
4338

4439
init<ResolverType>(
4540
resolver: ResolverType,
@@ -48,22 +43,16 @@ actor StateMachine<
4843
) where ResolverType: StateMachineResolver, ResolverType.StateType == StateType {
4944
self.resolver = resolver.eraseToAnyResolver()
5045
self.environment = environment
51-
self.currentState = initialState ?? resolver.defaultState
52-
53-
self.subscribers = []
46+
self.currentStateSubject = CurrentValueSubject(initialState ?? resolver.defaultState)
5447
}
5548

5649
/// Start listening to state change updates. The current state and all subsequent state changes will be sent to the sequence.
5750
///
5851
/// - Returns: An async sequence that get states asynchronously
59-
func listen() -> StateChangeSequence {
60-
let sequence = StateAsyncSequence<StateType>()
61-
let currentState = self.currentState
62-
let wrappedSequence = WeakWrapper(sequence)
63-
subscribers.append(wrappedSequence)
64-
sequence.send(currentState)
65-
return sequence
52+
func listen() -> CancellableAsyncStream<StateType> {
53+
CancellableAsyncStream(with: currentStateSubject.eraseToAnyPublisher())
6654
}
55+
6756
}
6857

6958
extension StateMachine: EventDispatcher {
@@ -88,33 +77,11 @@ extension StateMachine: EventDispatcher {
8877
)
8978

9079
if currentState != resolution.newState {
91-
currentState = resolution.newState
92-
subscribers.removeAll { item in
93-
!notify(subscriberElement: item, about: resolution.newState)
94-
}
80+
currentStateSubject.send(resolution.newState)
9581
}
9682
execute(resolution.actions)
9783
}
9884

99-
/// - Parameters:
100-
/// - subscriberElement: A weak wrapped async sequence
101-
/// - newState: The new state to be sent
102-
/// - Returns: true if the subscriber was notified, false if the wrapper reference was nil or a cancellation was pending
103-
private func notify(
104-
subscriberElement: WeakWrapper<StateChangeSequence>,
105-
about newState: StateType
106-
) -> Bool {
107-
108-
// If weak reference has become nil, do not process, and return false so caller can remove
109-
// the subscription from the subscribers list
110-
guard let sequence = subscriberElement.value else {
111-
return false
112-
}
113-
114-
sequence.send(newState)
115-
return true
116-
}
117-
11885
private func execute(_ actions: [Action]) {
11986
guard !actions.isEmpty else {
12087
return

0 commit comments

Comments
 (0)