mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 03:05:19 +00:00
Feature models observe per-conversation objects directly: PublicChatModel forwards the active Conversation's objectWillChange, PrivateInboxModel republishes only for the selected peer's conversation - background appends no longer invalidate foreground views. LegacyConversationStore, the coalescing bridge, and IdentityResolver are deleted (resolver canonicalization proved display-invisible: nothing enumerates direct conversations, lookups are by exact peer ID, and raw keying is strictly more robust - documented as a design deviation). Selection state moves into the store. ChatViewModel.messages/privateChats survive as derived read views for coordinators that genuinely need them; hot paths use a new store-direct privateMessages(for:) witness. Final numbers vs pre-migration baselines: pipeline.privateIngest 9.7k -> 24.0k msg/s (2.5x) pipeline.publicIngest 6.8k -> 13.7k msg/s (2.0x) delivery updates 38k -> 117-133k/s Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
103 lines
2.5 KiB
Swift
103 lines
2.5 KiB
Swift
import BitFoundation
|
|
import Combine
|
|
import Foundation
|
|
|
|
enum SharedContentKind: String, Sendable, Equatable {
|
|
case text
|
|
case url
|
|
}
|
|
|
|
enum RuntimeScenePhase: String, Sendable, Equatable {
|
|
case active
|
|
case inactive
|
|
case background
|
|
}
|
|
|
|
enum TorLifecycleEvent: String, Sendable, Equatable {
|
|
case willStart
|
|
case willRestart
|
|
case didBecomeReady
|
|
case preferenceChanged
|
|
}
|
|
|
|
enum AppEvent: Sendable, Equatable {
|
|
case launched
|
|
case startupCompleted
|
|
case scenePhaseChanged(RuntimeScenePhase)
|
|
case openedURL(String)
|
|
case sharedContentAccepted(SharedContentKind)
|
|
case notificationOpened(peerID: PeerID?)
|
|
case deepLinkOpened(String)
|
|
case torLifecycleChanged(TorLifecycleEvent)
|
|
case nostrRelayConnectionChanged(Bool)
|
|
case terminationRequested
|
|
}
|
|
|
|
actor AppEventStream {
|
|
private var continuations: [UUID: AsyncStream<AppEvent>.Continuation] = [:]
|
|
|
|
func stream() -> AsyncStream<AppEvent> {
|
|
let id = UUID()
|
|
return AsyncStream { continuation in
|
|
continuations[id] = continuation
|
|
continuation.onTermination = { [id] _ in
|
|
Task {
|
|
await self.removeContinuation(id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func emit(_ event: AppEvent) {
|
|
for continuation in continuations.values {
|
|
continuation.yield(event)
|
|
}
|
|
}
|
|
|
|
func finish() {
|
|
for continuation in continuations.values {
|
|
continuation.finish()
|
|
}
|
|
continuations.removeAll()
|
|
}
|
|
|
|
private func removeContinuation(_ id: UUID) {
|
|
continuations.removeValue(forKey: id)
|
|
}
|
|
}
|
|
|
|
/// Identity key for a direct conversation. Equality and hashing use the
|
|
/// canonical `id` only; `routingPeerID` carries the transport-level peer ID
|
|
/// the conversation is keyed under (see `ConversationID.directPeer`).
|
|
struct PeerHandle: Sendable, Identifiable {
|
|
let id: String
|
|
let routingPeerID: PeerID
|
|
}
|
|
|
|
extension PeerHandle: Equatable {
|
|
static func == (lhs: PeerHandle, rhs: PeerHandle) -> Bool {
|
|
lhs.id == rhs.id
|
|
}
|
|
}
|
|
|
|
extension PeerHandle: Hashable {
|
|
func hash(into hasher: inout Hasher) {
|
|
hasher.combine(id)
|
|
}
|
|
}
|
|
|
|
enum ConversationID: Hashable, Sendable {
|
|
case mesh
|
|
case geohash(String)
|
|
case direct(PeerHandle)
|
|
|
|
init(channelID: ChannelID) {
|
|
switch channelID {
|
|
case .mesh:
|
|
self = .mesh
|
|
case .location(let channel):
|
|
self = .geohash(channel.geohash.lowercased())
|
|
}
|
|
}
|
|
}
|