-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathGroupStore.swift
More file actions
151 lines (134 loc) · 6.4 KB
/
Copy pathGroupStore.swift
File metadata and controls
151 lines (134 loc) · 6.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import Foundation
import ObjectBox
/// Actor wrapper around the two NIP-29 ObjectBox boxes (`GroupMetaEntity`,
/// `GroupMessageEntity`). Mirrors the Android pattern of debounced batched
/// writes for messages so we don't pay a `box.put` per incoming chat message.
actor GroupStore {
static let shared = GroupStore()
private var metaBox: Box<GroupMetaEntity>?
private var messageBox: Box<GroupMessageEntity>?
private var pendingMessages: [GroupMessageEntity] = []
private var flushTask: Task<Void, Never>?
/// Debounce window. Matches the Android client.
private let flushDelay: Duration = .milliseconds(200)
/// Batch size threshold — flush eagerly past this many queued messages.
private let flushThreshold = 50
private func ensureBoxes() -> (Box<GroupMetaEntity>, Box<GroupMessageEntity>)? {
if metaBox == nil { metaBox = ObjectBoxSetup.store.box(for: GroupMetaEntity.self) }
if messageBox == nil { messageBox = ObjectBoxSetup.store.box(for: GroupMessageEntity.self) }
guard let metaBox, let messageBox else { return nil }
return (metaBox, messageBox)
}
// MARK: - Meta (rooms)
func upsertMeta(ownerPubkey: String, room: GroupRoom) {
guard let (metaBox, _) = ensureBoxes() else { return }
let key = groupRoomKey(ownerPubkey: ownerPubkey, relayUrl: room.relayUrl, groupId: room.groupId)
do {
let existing = try metaBox.query { GroupMetaEntity.roomKey == key }.build().findFirst()
let entity = existing ?? GroupMetaEntity()
entity.roomKey = key
entity.ownerPubkey = ownerPubkey
entity.relayUrl = room.relayUrl
entity.groupId = room.groupId
entity.name = room.metadata?.name ?? entity.name
entity.picture = room.metadata?.picture ?? entity.picture
entity.about = room.metadata?.about ?? entity.about
entity.isPrivate = room.metadata?.isPrivate ?? entity.isPrivate
entity.isClosed = room.metadata?.isClosed ?? entity.isClosed
entity.isRestricted = room.metadata?.isRestricted ?? entity.isRestricted
entity.isHidden = room.metadata?.isHidden ?? entity.isHidden
if !room.admins.isEmpty {
entity.adminsJson = (try? String(data: JSONSerialization.data(withJSONObject: room.admins), encoding: .utf8)) ?? entity.adminsJson
}
if !room.members.isEmpty {
entity.membersJson = (try? String(data: JSONSerialization.data(withJSONObject: room.members), encoding: .utf8)) ?? entity.membersJson
}
if room.lastMessageAt > entity.lastMessageAt {
entity.lastMessageAt = room.lastMessageAt
}
try metaBox.put(entity)
} catch { /* swallow — DB errors are non-fatal here */ }
}
func deleteMeta(ownerPubkey: String, relayUrl: String, groupId: String) {
guard let (metaBox, messageBox) = ensureBoxes() else { return }
let key = groupRoomKey(ownerPubkey: ownerPubkey, relayUrl: relayUrl, groupId: groupId)
do {
if let entity = try metaBox.query({ GroupMetaEntity.roomKey == key }).build().findFirst() {
try metaBox.remove(entity)
}
// Also wipe its messages.
let msgQuery = try messageBox.query({ GroupMessageEntity.roomKey == key }).build()
_ = try msgQuery.remove()
} catch {}
}
func loadAllMeta(ownerPubkey: String) -> [GroupRoom] {
guard let (metaBox, _) = ensureBoxes() else { return [] }
do {
let query = try metaBox.query { GroupMetaEntity.ownerPubkey == ownerPubkey }.build()
let entities = try query.find()
return entities.map { $0.toRoom() }
} catch { return [] }
}
// MARK: - Messages (debounced)
func enqueueMessage(ownerPubkey: String, relayUrl: String, groupId: String, message: GroupMessage) {
let entity = GroupMessageEntity(ownerPubkey: ownerPubkey, relayUrl: relayUrl,
groupId: groupId, message: message)
pendingMessages.append(entity)
if pendingMessages.count >= flushThreshold {
flushNow()
} else {
scheduleFlush()
}
}
private func scheduleFlush() {
if flushTask != nil { return }
flushTask = Task { [weak self] in
try? await Task.sleep(for: .milliseconds(200))
await self?.flushNow()
}
}
private func flushNow() {
flushTask?.cancel()
flushTask = nil
guard !pendingMessages.isEmpty,
let (_, messageBox) = ensureBoxes() else { return }
let batch = pendingMessages
pendingMessages.removeAll()
do { try messageBox.put(batch) } catch {}
}
func loadMessages(ownerPubkey: String, relayUrl: String, groupId: String,
limit: Int = 200) -> [GroupMessage] {
guard let (_, messageBox) = ensureBoxes() else { return [] }
let key = groupRoomKey(ownerPubkey: ownerPubkey, relayUrl: relayUrl, groupId: groupId)
do {
let query = try messageBox.query { GroupMessageEntity.roomKey == key }
.ordered(by: GroupMessageEntity.createdAt, flags: .descending)
.build()
let entities = try query.find(offset: 0, limit: limit)
return entities.map { $0.toMessage() }.reversed() // chronological for UI
} catch { return [] }
}
func wipe(ownerPubkey: String) {
guard let (metaBox, messageBox) = ensureBoxes() else { return }
do {
let metaQ = try metaBox.query { GroupMetaEntity.ownerPubkey == ownerPubkey }.build()
let metaEntities = try metaQ.find()
let keys = metaEntities.map { $0.roomKey }
_ = try metaQ.remove()
for key in keys {
let q = try messageBox.query { GroupMessageEntity.roomKey == key }.build()
_ = try q.remove()
}
} catch {}
}
/// Drop every cached group + message across all owners. Called from
/// `AppDataWipe` on logout. Pending in-memory writes are abandoned.
func removeAll() {
flushTask?.cancel()
flushTask = nil
pendingMessages.removeAll()
guard let (metaBox, messageBox) = ensureBoxes() else { return }
try? metaBox.removeAll()
try? messageBox.removeAll()
}
}