Skip to content

Commit 765b401

Browse files
authored
fix(Auth): emit initial session events (#241)
* Improve EventEmitter implementation * Improve tests for auth client
1 parent f028d63 commit 765b401

File tree

6 files changed

+185
-124
lines changed

6 files changed

+185
-124
lines changed

Sources/Auth/AuthClient.swift

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,29 @@ import Foundation
55
import FoundationNetworking
66
#endif
77

8+
public final class AuthStateChangeListenerHandle {
9+
var onCancel: (@Sendable () -> Void)?
10+
11+
public func cancel() {
12+
onCancel?()
13+
onCancel = nil
14+
}
15+
16+
deinit {
17+
cancel()
18+
}
19+
}
20+
21+
public typealias AuthStateChangeListener = @Sendable (
22+
_ event: AuthChangeEvent,
23+
_ session: Session?
24+
) -> Void
25+
826
public actor AuthClient {
927
/// FetchHandler is a type alias for asynchronous network request handling.
10-
public typealias FetchHandler =
11-
@Sendable (_ request: URLRequest) async throws -> (Data, URLResponse)
28+
public typealias FetchHandler = @Sendable (
29+
_ request: URLRequest
30+
) async throws -> (Data, URLResponse)
1231

1332
/// Configuration struct represents the client configuration.
1433
public struct Configuration: Sendable {
@@ -150,7 +169,7 @@ public actor AuthClient {
150169
sessionManager: .live,
151170
codeVerifierStorage: .live,
152171
api: api,
153-
eventEmitter: .live,
172+
eventEmitter: EventEmitter(),
154173
sessionStorage: .live,
155174
logger: configuration.logger
156175
)
@@ -187,19 +206,38 @@ public actor AuthClient {
187206
)
188207
}
189208

209+
/// Listen for auth state changes.
210+
///
211+
/// An `.initialSession` is always emitted when this method is called.
212+
@discardableResult
213+
public func onAuthStateChange(
214+
_ listener: @escaping AuthStateChangeListener
215+
) -> AuthStateChangeListenerHandle {
216+
let handle = eventEmitter.attachListener(listener)
217+
Task {
218+
await emitInitialSession(forHandle: handle)
219+
}
220+
return handle
221+
}
222+
190223
/// Listen for auth state changes.
191224
///
192225
/// An `.initialSession` is always emitted when this method is called.
193226
public var authStateChanges: AsyncStream<(
194227
event: AuthChangeEvent,
195228
session: Session?
196229
)> {
197-
let (id, stream) = eventEmitter.attachListener()
198-
logger?.debug("auth state change listener with id '\(id.uuidString)' attached.")
230+
let (stream, continuation) = AsyncStream<(
231+
event: AuthChangeEvent,
232+
session: Session?
233+
)>.makeStream()
234+
235+
let handle = onAuthStateChange { event, session in
236+
continuation.yield((event, session))
237+
}
199238

200-
Task { [id] in
201-
await emitInitialSession(forStreamWithID: id)
202-
logger?.debug("initial session for listener with id '\(id.uuidString)' emitted.")
239+
continuation.onTermination = { _ in
240+
handle.cancel()
203241
}
204242

205243
return stream
@@ -884,9 +922,9 @@ public actor AuthClient {
884922
return session
885923
}
886924

887-
private func emitInitialSession(forStreamWithID id: UUID) async {
925+
private func emitInitialSession(forHandle handle: AuthStateChangeListenerHandle) async {
888926
let session = try? await session
889-
eventEmitter.emit(.initialSession, session, id)
927+
eventEmitter.emit(.initialSession, session: session, handle: handle)
890928
}
891929

892930
private func prepareForPKCE() -> (codeChallenge: String?, codeChallengeMethod: String?) {
Lines changed: 40 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,50 @@
11
import ConcurrencyExtras
22
import Foundation
33

4-
struct EventEmitter: Sendable {
5-
var attachListener: @Sendable () -> (
6-
id: UUID,
7-
stream: AsyncStream<(event: AuthChangeEvent, session: Session?)>
8-
)
9-
var emit: @Sendable (_ event: AuthChangeEvent, _ session: Session?, _ id: UUID?) -> Void
10-
}
4+
class EventEmitter: @unchecked Sendable {
5+
let listeners = LockIsolated<[ObjectIdentifier: AuthStateChangeListener]>([:])
6+
7+
func attachListener(_ listener: @escaping AuthStateChangeListener)
8+
-> AuthStateChangeListenerHandle
9+
{
10+
let handle = AuthStateChangeListenerHandle()
11+
let key = ObjectIdentifier(handle)
12+
13+
handle.onCancel = { [weak self] in
14+
self?.listeners.withValue {
15+
$0[key] = nil
16+
}
17+
}
1118

12-
extension EventEmitter {
13-
func emit(_ event: AuthChangeEvent, session: Session?) {
14-
emit(event, session, nil)
19+
listeners.withValue {
20+
$0[key] = listener
21+
}
22+
23+
return handle
1524
}
16-
}
1725

18-
extension EventEmitter {
19-
static var live: Self = {
20-
let continuations = LockIsolated(
21-
[UUID: AsyncStream<(event: AuthChangeEvent, session: Session?)>.Continuation]()
26+
func emit(
27+
_ event: AuthChangeEvent,
28+
session: Session?,
29+
handle: AuthStateChangeListenerHandle? = nil
30+
) {
31+
NotificationCenter.default.post(
32+
name: AuthClient.didChangeAuthStateNotification,
33+
object: nil,
34+
userInfo: [
35+
AuthClient.authChangeEventInfoKey: event,
36+
AuthClient.authChangeSessionInfoKey: session as Any,
37+
]
2238
)
2339

24-
return Self(
25-
attachListener: {
26-
let id = UUID()
27-
28-
let (stream, continuation) = AsyncStream<(event: AuthChangeEvent, session: Session?)>
29-
.makeStream()
30-
31-
continuation.onTermination = { [id] _ in
32-
continuations.withValue {
33-
$0[id] = nil
34-
}
35-
}
36-
37-
continuations.withValue {
38-
$0[id] = continuation
39-
}
40-
41-
return (id, stream)
42-
},
43-
emit: { event, session, id in
44-
NotificationCenter.default.post(
45-
name: AuthClient.didChangeAuthStateNotification,
46-
object: nil,
47-
userInfo: [
48-
AuthClient.authChangeEventInfoKey: event,
49-
AuthClient.authChangeSessionInfoKey: session as Any,
50-
]
51-
)
52-
if let id {
53-
continuations.value[id]?.yield((event, session))
54-
} else {
55-
for continuation in continuations.value.values {
56-
continuation.yield((event, session))
57-
}
58-
}
40+
let listeners = listeners.value
41+
42+
if let handle {
43+
listeners[ObjectIdentifier(handle)]?(event, session)
44+
} else {
45+
for listener in listeners.values {
46+
listener(event, session)
5947
}
60-
)
61-
}()
48+
}
49+
}
6250
}

0 commit comments

Comments
 (0)