Skip to content

[Feature Enhancement]: UserSessionRegistry as a Protocol #190

@fatfingers23

Description

@fatfingers23

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)
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementAn enhancement to an existing feature.

    Projects

    Status

    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions