This adds a sync mechanism in Ndb.swift to coordinate certain usage of nostrdb.c calls and the need to close nostrdb due to app lifecycle requirements. Furthermore, it fixes the order of operations when re-opening NostrDB, to avoid race conditions where a query uses an older Ndb generation. This sync mechanism allows multiple queries to happen simultaneously (from the Swift-side), while preventing ndb from simultaneously closing during such usages. It also does that while keeping the Ndb interface sync and nonisolated, which keeps the API easy to use from Swift/SwiftUI and allows for parallel operations to occur. If Swift Actors were to be used (e.g. creating an NdbActor), the Ndb.swift interface would change in such a way that it would propagate the need for several changes throughout the codebase, including loading logic in some ViewModels. Furthermore, it would likely decrease performance by forcing Ndb.swift operations to run sequentially when they could run in parallel. Changelog-Fixed: Fixed crashes that happened when the app went into background mode Closes: https://github.com/damus-io/damus/issues/3245 Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
141 lines
3.7 KiB
Swift
141 lines
3.7 KiB
Swift
//
|
|
// Profiles.swift
|
|
// damus
|
|
//
|
|
// Created by William Casarin on 2022-04-17.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
class ValidationModel: ObservableObject {
|
|
@Published var validated: NIP05?
|
|
|
|
init() {
|
|
self.validated = nil
|
|
}
|
|
}
|
|
|
|
class ProfileData {
|
|
var status: UserStatusModel
|
|
var validation_model: ValidationModel
|
|
var zapper: Pubkey?
|
|
|
|
init() {
|
|
status = .init()
|
|
validation_model = .init()
|
|
zapper = nil
|
|
}
|
|
}
|
|
|
|
class Profiles {
|
|
private var ndb: Ndb
|
|
|
|
static let db_freshness_threshold: TimeInterval = 24 * 60 * 8
|
|
|
|
@MainActor
|
|
private var profiles: [Pubkey: ProfileData] = [:]
|
|
|
|
// Map of validated NIP-05 address to pubkey.
|
|
@MainActor
|
|
var nip05_pubkey: [String: Pubkey] = [:]
|
|
|
|
init(ndb: Ndb) {
|
|
self.ndb = ndb
|
|
}
|
|
|
|
@MainActor
|
|
func is_validated(_ pk: Pubkey) -> NIP05? {
|
|
self.profile_data(pk).validation_model.validated
|
|
}
|
|
|
|
@MainActor
|
|
func invalidate_nip05(_ pk: Pubkey) {
|
|
self.profile_data(pk).validation_model.validated = nil
|
|
}
|
|
|
|
@MainActor
|
|
func set_validated(_ pk: Pubkey, nip05: NIP05?) {
|
|
self.profile_data(pk).validation_model.validated = nip05
|
|
}
|
|
|
|
@MainActor
|
|
func profile_data(_ pubkey: Pubkey) -> ProfileData {
|
|
guard let data = profiles[pubkey] else {
|
|
let data = ProfileData()
|
|
profiles[pubkey] = data
|
|
return data
|
|
}
|
|
|
|
return data
|
|
}
|
|
|
|
@MainActor
|
|
func lookup_zapper(pubkey: Pubkey) -> Pubkey? {
|
|
profile_data(pubkey).zapper
|
|
}
|
|
|
|
func lookup_with_timestamp<T>(_ pubkey: Pubkey, borrow lendingFunction: (_: borrowing ProfileRecord?) throws -> T) throws -> T {
|
|
return try ndb.lookup_profile(pubkey, borrow: lendingFunction)
|
|
}
|
|
|
|
func lookup_lnurl(_ pubkey: Pubkey) throws -> String? {
|
|
return try lookup_with_timestamp(pubkey, borrow: { pr in
|
|
switch pr {
|
|
case .some(let pr): return pr.lnurl
|
|
case .none: return nil
|
|
}
|
|
})
|
|
}
|
|
|
|
func lookup_by_key<T>(key: ProfileKey, borrow lendingFunction: (_: borrowing ProfileRecord?) throws -> T) throws -> T {
|
|
return try ndb.lookup_profile_by_key(key: key, borrow: lendingFunction)
|
|
}
|
|
|
|
func search(_ query: String, limit: Int) throws -> [Pubkey] {
|
|
try ndb.search_profile(query, limit: limit)
|
|
}
|
|
|
|
func lookup(id: Pubkey) throws -> Profile? {
|
|
return try ndb.lookup_profile(id, borrow: { pr in
|
|
switch pr {
|
|
case .none:
|
|
return nil
|
|
case .some(let profileRecord):
|
|
// This will clone the value to make it owned and safe to return.
|
|
return profileRecord.profile
|
|
}
|
|
})
|
|
}
|
|
|
|
func lookup_key_by_pubkey(_ pubkey: Pubkey) throws -> ProfileKey? {
|
|
try ndb.lookup_profile_key(pubkey)
|
|
}
|
|
|
|
func has_fresh_profile(id: Pubkey) throws -> Bool {
|
|
guard let fetched_at = try ndb.read_profile_last_fetched(pubkey: id)
|
|
else {
|
|
return false
|
|
}
|
|
|
|
// In situations where a batch of profiles was fetched all at once,
|
|
// this will reduce the herding of the profile requests
|
|
let fuzz = Double.random(in: -60...60)
|
|
let threshold = Profiles.db_freshness_threshold + fuzz
|
|
let fetch_date = Date(timeIntervalSince1970: Double(fetched_at))
|
|
|
|
let since = Date.now.timeIntervalSince(fetch_date)
|
|
let fresh = since < threshold
|
|
|
|
//print("fresh = \(fresh): fetch_date \(since) < threshold \(threshold) \(id)")
|
|
|
|
return fresh
|
|
}
|
|
}
|
|
|
|
|
|
@MainActor
|
|
func invalidate_zapper_cache(pubkey: Pubkey, profiles: Profiles, lnurl: LNUrls) {
|
|
profiles.profile_data(pubkey).zapper = nil
|
|
lnurl.endpoints.removeValue(forKey: pubkey)
|
|
}
|