[Feature Enhancement]: UserSessionRegistry as a Protocol
Summary
As a developer building an ios app, it would be nice to have UserSessionRegistry as a protocol, so that users can pass in their own implmentation to persist the sessions
Pain points
I've been building out an app using this package and found that the UserSessionRegistry seems to be only in memory. I propose changing this to be a protocol so each system can provide a way to save these, for me that was with SwiftData. and also saving the accessToken in the keychain, like the refreshToken is
Considered Alternatives
There is a chance that I may have misunderstood how to use UserSessionRegistry, but I could not find a solution that would allow that to persist besides reloading it manually every reboot of the app
Is this a breaking change?
No
Additional Context
I acutally already have a working branch found here fatfingers23/ATProtoKit/tree/feature/UserSessionRegistryProtocol. So far it seems to be working, but I would like to clean it up, add proper formating, add documentation, and test more in a real life scenario before making a PR. But feel free to use it or propose a different approach, and I don't mind putting in the time to make it a proper PR to give back to the project
I have a SwiftData implementation of the UserSessionRegistry protocol here that is working in my app
import ATProtoKit
import Foundation
import SwiftData
@MainActor
public struct PersistentUserSessionRegistry: UserSessionRegistry {
private var modelContext: ModelContext
init(modelContext: ModelContext) {
self.modelContext = modelContext
}
public func register(_ id: UUID, session: UserSession) async {
let model = UserSessionToModel(id, session: session)
self.modelContext.insert(model)
do {
try self.modelContext.save()
} catch {
print("Error saving user session: \(error)")
}
}
public func getSession(for id: UUID) async -> UserSession? {
let descriptor = FetchDescriptor<UserSessionModel>(
predicate: #Predicate { $0.id == id }
)
do {
let models = try self.modelContext.fetch(descriptor)
guard let model = models.first else { return nil }
return ModelToUserSession(model)
} catch {
return nil
}
}
public func containsSession(for id: UUID) async -> Bool {
let descriptor = FetchDescriptor<UserSessionModel>(
predicate: #Predicate { $0.id == id }
)
do {
let count = try self.modelContext.fetchCount(descriptor)
return count > 0
} catch {
return false
}
}
public func removeSession(for id: UUID) async {
let descriptor = FetchDescriptor<UserSessionModel>(
predicate: #Predicate { $0.id == id }
)
do {
let models = try self.modelContext.fetch(descriptor)
for model in models {
self.modelContext.delete(model)
}
try self.modelContext.save()
} catch {
// Handle error silently or log as needed
}
}
public func removeAllSessions() async {
let descriptor = FetchDescriptor<UserSessionModel>()
do {
let models = try self.modelContext.fetch(descriptor)
for model in models {
self.modelContext.delete(model)
}
try self.modelContext.save()
} catch {
// Handle error silently or log as needed
}
}
public func getAllSessions() async -> [UUID: UserSession] {
let descriptor = FetchDescriptor<UserSessionModel>()
do {
let models = try self.modelContext.fetch(descriptor)
var sessions: [UUID: UserSession] = [:]
for model in models {
let session = ModelToUserSession(model)
sessions[model.id] = session
}
return sessions
} catch {
return [:]
}
}
}
private func ModelToUserSession(_ model: UserSessionModel) -> UserSession {
return UserSession(
handle: model.handle,
sessionDID: model.sessionDID,
email: model.email,
isEmailConfirmed: model.isEmailConfirmed,
isEmailAuthenticationFactorEnabled: model.isEmailAuthenticationFactorEnabled,
didDocument: model.didDocument,
isActive: model.isActive,
status: model.status,
serviceEndpoint: model.serviceEndpoint,
pdsURL: model.pdsURL
)
}
private func UserSessionToModel(_ id: UUID, session: UserSession) -> UserSessionModel {
return UserSessionModel(
sessionId: id,
handle: session.handle,
sessionDID: session.sessionDID,
email: session.email,
isEmailConfirmed: session.isEmailConfirmed,
isEmailAuthenticationFactorEnabled: session.isEmailAuthenticationFactorEnabled,
didDocument: session.didDocument,
isActive: session.isActive,
status: session.status,
serviceEndpoint: session.serviceEndpoint,
pdsURL: session.pdsURL)
}
I'm okay with this, so long as it's concurrency-safe and the current implementation still works. Please don't use @unchecked Sendable, as it means the compiler will not check to guarantee that it's safe.
However, the amount of bandwidth I have with making this is wearing thin as I have other projects to attend to. If you don't mind, please create a PR and I'll review it, make the necessary requests, and approve of it once it's ready. Please be sure to add documentation to it as well. I know you've pasted some code in the issue, but I don't feel comfortable with copying and pasting the code myself; I'd rather you create the PR and take the credit for that code.
Thank you for the tip about @unchecked Sendable, I had missed that warning.
And no worries! It is some changes I've made that work for my app, and I would love to give back to the project, so I don't mind taking it on. I will work on cleaning it up some more and making the PR.
As for the SwiftData implementation I pasted in, I was not sure if it really fits into the library, but I wanted to show a proof of concept of using the protocol for it.
SwiftData would not be recommended to be added within ATProtoKit, for two reasons:
- SwiftData isn't the only way to store data. I would like ATProtoKit to be as generic as possible. I'm open to helper declarations, but I would like to come up with a solution to that which would work well. Perhaps a separate package within ATProtoKit could work, but give me some time to think about it.
- SwiftData is only available on Apple platforms. This package is supposed to work on Apple platforms, Linux, Windows, WebAssembly, and Android. If I were to put SwiftData as-is, it would make the Linux, Windows, and Android builds fail, as well as create additional problems for Wasm. Hopefully that makes sense.
Hey getting ready to go through and clean up the protocol and changes to make a PR. Do you have a swift-format file to make sure I've listed the code correctly? I use one in xcode and it has gotten it off a bit from what was in the project, and I wanted to make sure I matched the repo's formatting?
Unfortunately, I don't have a SwiftFormat file, though I plan on making one in the future. It's mainly because I never got around to it. I'll look into doing that this week.
For now, just stick to the one from Xcode. I'll go ahead and make the necessary tweaks if needed.