Skip to content

Commit 3a6d5e5

Browse files
grdsdevclaude
andcommitted
refactor(auth): replace SessionManager facade with SessionStateMachine actor
Replaces the two-layer `SessionManager` struct + `LiveSessionManager` actor with a single `SessionStateMachine` actor that owns all session state transitions explicitly. - Introduces `SessionState` enum with `.uninitialized`, `.unauthenticated`, `.authenticated`, and `.refreshing` cases - State starts as `.uninitialized` and loads from storage on first access, avoiding a dependency on `Dependencies` being registered before init - Refresh coalescing is now an explicit property of the `.refreshing` state rather than an implicit nullable `Task?` field - `remove()` cancels any in-flight refresh task via the `.refreshing` state - `.tokenRefreshed` event emission is consolidated inside `refresh(token:)` - Removes the `SessionManager` struct facade; consumers hold a `SessionStateMachine` reference directly via `Dependencies` Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent fe75f12 commit 3a6d5e5

10 files changed

Lines changed: 233 additions & 194 deletions

Sources/Auth/AuthClient.swift

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ public actor AuthClient {
8181
}
8282

8383
nonisolated private var date: @Sendable () -> Date { Dependencies[clientID].date }
84-
nonisolated private var sessionManager: SessionManager { Dependencies[clientID].sessionManager }
84+
nonisolated private var sessionMachine: SessionStateMachine {
85+
Dependencies[clientID].sessionMachine
86+
}
8587
nonisolated private var eventEmitter: AuthStateChangeEventEmitter {
8688
Dependencies[clientID].eventEmitter
8789
}
@@ -96,7 +98,7 @@ public actor AuthClient {
9698
/// If no session can be found, a ``AuthError/sessionMissing`` error is thrown.
9799
public var session: Session {
98100
get async throws {
99-
try await sessionManager.session()
101+
try await sessionMachine.validSession()
100102
}
101103
}
102104

@@ -139,7 +141,7 @@ public actor AuthClient {
139141
api: APIClient(clientID: clientID),
140142
codeVerifierStorage: .live(clientID: clientID),
141143
sessionStorage: .live(clientID: clientID),
142-
sessionManager: .live(clientID: clientID),
144+
sessionMachine: SessionStateMachine(clientID: clientID),
143145
logger: configuration.logger.map {
144146
AuthClientLoggerDecorator(clientID: clientID, decoratee: $0)
145147
}
@@ -351,7 +353,7 @@ public actor AuthClient {
351353
)
352354

353355
if let session = response.session {
354-
await sessionManager.update(session)
356+
await sessionMachine.update(session)
355357
eventEmitter.emit(.signedIn, session: session)
356358
}
357359

@@ -457,7 +459,7 @@ public actor AuthClient {
457459
decoder: configuration.decoder
458460
)
459461

460-
await sessionManager.update(session)
462+
await sessionMachine.update(session)
461463
eventEmitter.emit(.signedIn, session: session)
462464

463465
return session
@@ -635,7 +637,7 @@ public actor AuthClient {
635637

636638
codeVerifierStorage.set(nil)
637639

638-
await sessionManager.update(session)
640+
await sessionMachine.update(session)
639641
eventEmitter.emit(.signedIn, session: session)
640642

641643
return session
@@ -895,7 +897,7 @@ public actor AuthClient {
895897
user: user
896898
)
897899

898-
await sessionManager.update(session)
900+
await sessionMachine.update(session)
899901
eventEmitter.emit(.signedIn, session: session)
900902

901903
if let type = params["type"], type == "recovery" {
@@ -960,7 +962,7 @@ public actor AuthClient {
960962
)
961963
}
962964

963-
await sessionManager.update(session)
965+
await sessionMachine.update(session)
964966
eventEmitter.emit(.signedIn, session: session)
965967
return session
966968
}
@@ -976,7 +978,7 @@ public actor AuthClient {
976978
}
977979

978980
if scope != .others {
979-
await sessionManager.remove()
981+
await sessionMachine.remove()
980982
eventEmitter.emit(.signedOut, session: nil)
981983
}
982984

@@ -1084,7 +1086,7 @@ public actor AuthClient {
10841086
)
10851087

10861088
if let session = response.session {
1087-
await sessionManager.update(session)
1089+
await sessionMachine.update(session)
10881090
eventEmitter.emit(.signedIn, session: session)
10891091
}
10901092

@@ -1189,7 +1191,7 @@ public actor AuthClient {
11891191
user.codeChallengeMethod = codeChallengeMethod
11901192
}
11911193

1192-
var session = try await sessionManager.session()
1194+
var session = try await sessionMachine.validSession()
11931195
let updatedUser = try await api.authorizedExecute(
11941196
.init(
11951197
url: configuration.url.appendingPathComponent("user"),
@@ -1206,7 +1208,7 @@ public actor AuthClient {
12061208
)
12071209
).decoded(as: User.self, decoder: configuration.decoder)
12081210
session.user = updatedUser
1209-
await sessionManager.update(session)
1211+
await sessionMachine.update(session)
12101212
eventEmitter.emit(.userUpdated, session: session)
12111213
return updatedUser
12121214
}
@@ -1234,7 +1236,7 @@ public actor AuthClient {
12341236
)
12351237
).decoded(as: Session.self, decoder: configuration.decoder)
12361238

1237-
await sessionManager.update(session)
1239+
await sessionMachine.update(session)
12381240
eventEmitter.emit(.userUpdated, session: session)
12391241

12401242
return session
@@ -1385,19 +1387,19 @@ public actor AuthClient {
13851387
throw AuthError.sessionMissing
13861388
}
13871389

1388-
return try await sessionManager.refreshSession(refreshToken)
1390+
return try await sessionMachine.refresh(token: refreshToken)
13891391
}
13901392

13911393
/// Starts an auto-refresh process in the background. The session is checked every few seconds. Close to the time of expiration a process is started to refresh the session. If refreshing fails it will be retried for as long as necessary.
13921394
///
13931395
/// If you set ``Configuration/autoRefreshToken`` you don't need to call this function, it will be called for you.
13941396
public func startAutoRefresh() {
1395-
Task { await sessionManager.startAutoRefresh() }
1397+
Task { await sessionMachine.startAutoRefresh() }
13961398
}
13971399

13981400
/// Stops an active auto refresh process running in the background (if any).
13991401
public func stopAutoRefresh() {
1400-
Task { await sessionManager.stopAutoRefresh() }
1402+
Task { await sessionMachine.stopAutoRefresh() }
14011403
}
14021404

14031405
private func emitInitialSession(forToken token: ObservationToken) async {
@@ -1411,8 +1413,8 @@ public actor AuthClient {
14111413

14121414
Task {
14131415
if currentSession.isExpired {
1414-
_ = try? await sessionManager.refreshSession(currentSession.refreshToken)
1415-
// No need to emit `tokenRefreshed` nor `signOut` event since the `refreshSession` does it already.
1416+
_ = try? await sessionMachine.refresh(token: currentSession.refreshToken)
1417+
// No need to emit `tokenRefreshed` nor `signOut` event since `refresh` does it already.
14161418
}
14171419
}
14181420
} else {

Sources/Auth/AuthMFA.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public struct AuthMFA: Sendable {
88
var api: APIClient { Dependencies[clientID].api }
99
var encoder: JSONEncoder { Dependencies[clientID].encoder }
1010
var decoder: JSONDecoder { Dependencies[clientID].decoder }
11-
var sessionManager: SessionManager { Dependencies[clientID].sessionManager }
11+
var sessionMachine: SessionStateMachine { Dependencies[clientID].sessionMachine }
1212
var eventEmitter: AuthStateChangeEventEmitter { Dependencies[clientID].eventEmitter }
1313

1414
/// Starts the enrollment process for a new Multi-Factor Authentication (MFA) factor. This method
@@ -63,7 +63,7 @@ public struct AuthMFA: Sendable {
6363
)
6464
).decoded(decoder: decoder)
6565

66-
await sessionManager.update(response)
66+
await sessionMachine.update(response)
6767

6868
eventEmitter.emit(.mfaChallengeVerified, session: response, token: nil)
6969

@@ -108,7 +108,7 @@ public struct AuthMFA: Sendable {
108108
///
109109
/// - Returns: An authentication response with the list of MFA factors.
110110
public func listFactors() async throws -> AuthMFAListFactorsResponse {
111-
let user = try await sessionManager.session().user
111+
let user = try await sessionMachine.validSession().user
112112
let factors = user.factors ?? []
113113
let totp = factors.filter {
114114
$0.factorType == "totp" && $0.status == .verified
@@ -122,9 +122,11 @@ public struct AuthMFA: Sendable {
122122
/// Returns the Authenticator Assurance Level (AAL) for the active session.
123123
///
124124
/// - Returns: An authentication response with the Authenticator Assurance Level.
125-
public func getAuthenticatorAssuranceLevel() async throws -> AuthMFAGetAuthenticatorAssuranceLevelResponse {
125+
public func getAuthenticatorAssuranceLevel() async throws
126+
-> AuthMFAGetAuthenticatorAssuranceLevelResponse
127+
{
126128
do {
127-
let session = try await sessionManager.session()
129+
let session = try await sessionMachine.validSession()
128130
let payload = JWT.decodePayload(session.accessToken)
129131

130132
var currentLevel: AuthenticatorAssuranceLevels?

Sources/Auth/Internal/APIClient.swift

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ struct APIClient: Sendable {
2727
Dependencies[clientID].configuration
2828
}
2929

30-
var sessionManager: SessionManager {
31-
Dependencies[clientID].sessionManager
30+
var sessionMachine: SessionStateMachine {
31+
Dependencies[clientID].sessionMachine
3232
}
3333

3434
var eventEmitter: AuthStateChangeEventEmitter {
@@ -66,11 +66,7 @@ struct APIClient: Sendable {
6666

6767
@discardableResult
6868
func authorizedExecute(_ request: Helpers.HTTPRequest) async throws -> Helpers.HTTPResponse {
69-
var sessionManager: SessionManager {
70-
Dependencies[clientID].sessionManager
71-
}
72-
73-
let session = try await sessionManager.session()
69+
let session = try await sessionMachine.validSession()
7470

7571
var request = request
7672
request.headers[.authorization] = "Bearer \(session.accessToken)"
@@ -118,7 +114,7 @@ struct APIClient: Sendable {
118114
// The `session_id` inside the JWT does not correspond to a row in the
119115
// `sessions` table. This usually means the user has signed out, has been
120116
// deleted, or their session has somehow been terminated.
121-
await sessionManager.remove()
117+
await sessionMachine.remove()
122118
eventEmitter.emit(.signedOut, session: nil)
123119
return .sessionMissing
124120
} else {

Sources/Auth/Internal/Dependencies.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ struct Dependencies: Sendable {
77
var api: APIClient
88
var codeVerifierStorage: CodeVerifierStorage
99
var sessionStorage: SessionStorage
10-
var sessionManager: SessionManager
10+
var sessionMachine: SessionStateMachine
1111

1212
var eventEmitter = AuthStateChangeEventEmitter()
1313
var date: @Sendable () -> Date = { Date() }

Sources/Auth/Internal/SessionManager.swift

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

0 commit comments

Comments
 (0)