@quilibrium/quorum-shared 2.1.0-1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +1 -49
- package/dist/index.d.ts +1 -49
- package/dist/index.js +30 -125
- package/dist/index.mjs +30 -125
- package/package.json +3 -7
- package/src/sync/service.ts +37 -312
- package/src/types/conversation.ts +0 -9
- package/src/types/index.ts +1 -1
- package/src/sync/service.test.ts +0 -822
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quilibrium/quorum-shared",
|
|
3
|
-
"version": "2.1.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Shared types, hooks, and utilities for Quorum mobile and desktop apps",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -16,9 +16,7 @@
|
|
|
16
16
|
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
17
17
|
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
18
18
|
"lint": "eslint src --ext .ts,.tsx",
|
|
19
|
-
"typecheck": "tsc --noEmit"
|
|
20
|
-
"test": "vitest",
|
|
21
|
-
"test:run": "vitest run"
|
|
19
|
+
"typecheck": "tsc --noEmit"
|
|
22
20
|
},
|
|
23
21
|
"dependencies": {
|
|
24
22
|
"@noble/hashes": "^1.3.0"
|
|
@@ -30,11 +28,9 @@
|
|
|
30
28
|
"devDependencies": {
|
|
31
29
|
"@tanstack/react-query": "^5.62.0",
|
|
32
30
|
"@types/react": "^18.2.0",
|
|
33
|
-
"@vitest/coverage-v8": "^4.0.16",
|
|
34
31
|
"react": "^18.2.0",
|
|
35
32
|
"tsup": "^8.0.0",
|
|
36
|
-
"typescript": "^5.3.0"
|
|
37
|
-
"vitest": "^4.0.16"
|
|
33
|
+
"typescript": "^5.3.0"
|
|
38
34
|
},
|
|
39
35
|
"files": [
|
|
40
36
|
"dist",
|
package/src/sync/service.ts
CHANGED
|
@@ -32,13 +32,11 @@ import type {
|
|
|
32
32
|
SyncSummary,
|
|
33
33
|
DeletedMessageTombstone,
|
|
34
34
|
} from './types';
|
|
35
|
-
import { sha256 } from '@noble/hashes/sha2';
|
|
36
|
-
import { bytesToHex } from '../utils/encoding';
|
|
37
35
|
import {
|
|
38
36
|
createManifest,
|
|
39
|
-
createMessageDigest,
|
|
40
37
|
createMemberDigest,
|
|
41
|
-
|
|
38
|
+
createSyncSummary,
|
|
39
|
+
computeManifestHash,
|
|
42
40
|
computeMessageDiff,
|
|
43
41
|
computeMemberDiff,
|
|
44
42
|
computePeerDiff,
|
|
@@ -60,29 +58,6 @@ export interface SyncServiceConfig {
|
|
|
60
58
|
requestExpiry?: number;
|
|
61
59
|
/** Callback when sync should be initiated */
|
|
62
60
|
onInitiateSync?: (spaceId: string, target: string) => void;
|
|
63
|
-
/** Cache TTL in ms (default: 5000) */
|
|
64
|
-
cacheTtl?: number;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ============ Cache Types ============
|
|
68
|
-
|
|
69
|
-
interface SyncPayloadCache {
|
|
70
|
-
/** Space and channel IDs */
|
|
71
|
-
spaceId: string;
|
|
72
|
-
channelId: string;
|
|
73
|
-
/** Message map for delta building - O(1) lookup */
|
|
74
|
-
messageMap: Map<string, Message>;
|
|
75
|
-
/** Member map for delta building - O(1) lookup */
|
|
76
|
-
memberMap: Map<string, SpaceMember>;
|
|
77
|
-
/** Message digest map - O(1) lookup/update */
|
|
78
|
-
digestMap: Map<string, MessageDigest>;
|
|
79
|
-
/** Member digest map - O(1) lookup/update */
|
|
80
|
-
memberDigestMap: Map<string, MemberDigest>;
|
|
81
|
-
/** Tracked timestamps for O(1) summary updates */
|
|
82
|
-
oldestTimestamp: number;
|
|
83
|
-
newestTimestamp: number;
|
|
84
|
-
/** XOR-based manifest hash bytes for O(1) incremental updates */
|
|
85
|
-
manifestHashBytes: Uint8Array;
|
|
86
61
|
}
|
|
87
62
|
|
|
88
63
|
// ============ SyncService Class ============
|
|
@@ -99,9 +74,6 @@ export class SyncService {
|
|
|
99
74
|
/** Deleted message tombstones (caller must persist these) */
|
|
100
75
|
private tombstones: DeletedMessageTombstone[] = [];
|
|
101
76
|
|
|
102
|
-
/** Pre-computed sync payload cache per space:channel - always ready to use */
|
|
103
|
-
private payloadCache: Map<string, SyncPayloadCache> = new Map();
|
|
104
|
-
|
|
105
77
|
constructor(config: SyncServiceConfig) {
|
|
106
78
|
this.storage = config.storage;
|
|
107
79
|
this.maxMessages = config.maxMessages ?? 1000;
|
|
@@ -109,268 +81,6 @@ export class SyncService {
|
|
|
109
81
|
this.onInitiateSync = config.onInitiateSync;
|
|
110
82
|
}
|
|
111
83
|
|
|
112
|
-
// ============ Payload Cache Management ============
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Get cache key for space/channel
|
|
116
|
-
*/
|
|
117
|
-
private getCacheKey(spaceId: string, channelId: string): string {
|
|
118
|
-
return `${spaceId}:${channelId}`;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Get or initialize the payload cache for a space/channel.
|
|
123
|
-
* If not cached, loads from storage and builds the payload once.
|
|
124
|
-
*/
|
|
125
|
-
private async getPayloadCache(spaceId: string, channelId: string): Promise<SyncPayloadCache> {
|
|
126
|
-
const key = this.getCacheKey(spaceId, channelId);
|
|
127
|
-
const cached = this.payloadCache.get(key);
|
|
128
|
-
|
|
129
|
-
if (cached) {
|
|
130
|
-
logger.log(`[SyncService] Using cached payload for ${spaceId.substring(0, 12)}:${channelId.substring(0, 12)}`);
|
|
131
|
-
return cached;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Initial load from storage - this only happens once per space/channel
|
|
135
|
-
logger.log(`[SyncService] Building initial payload cache for ${spaceId.substring(0, 12)}:${channelId.substring(0, 12)}`);
|
|
136
|
-
const messages = await this.getChannelMessages(spaceId, channelId);
|
|
137
|
-
const members = await this.storage.getSpaceMembers(spaceId);
|
|
138
|
-
|
|
139
|
-
const payload = this.buildPayloadCache(spaceId, channelId, messages, members);
|
|
140
|
-
this.payloadCache.set(key, payload);
|
|
141
|
-
|
|
142
|
-
return payload;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Hash a message ID to bytes for XOR-based manifest hash
|
|
147
|
-
*/
|
|
148
|
-
private hashMessageId(messageId: string): Uint8Array {
|
|
149
|
-
return sha256(new TextEncoder().encode(messageId));
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* XOR hash bytes into the accumulator - O(1)
|
|
154
|
-
*/
|
|
155
|
-
private xorIntoHash(accumulator: Uint8Array, hash: Uint8Array): void {
|
|
156
|
-
for (let i = 0; i < 32; i++) {
|
|
157
|
-
accumulator[i] ^= hash[i];
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Build the payload cache from messages and members - O(n) initial build
|
|
163
|
-
*/
|
|
164
|
-
private buildPayloadCache(
|
|
165
|
-
spaceId: string,
|
|
166
|
-
channelId: string,
|
|
167
|
-
messages: Message[],
|
|
168
|
-
members: SpaceMember[]
|
|
169
|
-
): SyncPayloadCache {
|
|
170
|
-
const messageMap = new Map(messages.map(m => [m.messageId, m]));
|
|
171
|
-
const memberMap = new Map(members.map(m => [m.address, m]));
|
|
172
|
-
const digestMap = new Map(messages.map(m => [m.messageId, createMessageDigest(m)]));
|
|
173
|
-
const memberDigestMap = new Map(members.map(m => [m.address, createMemberDigest(m)]));
|
|
174
|
-
|
|
175
|
-
// Compute timestamps
|
|
176
|
-
let oldestTimestamp = Infinity;
|
|
177
|
-
let newestTimestamp = 0;
|
|
178
|
-
for (const msg of messages) {
|
|
179
|
-
if (msg.createdDate < oldestTimestamp) oldestTimestamp = msg.createdDate;
|
|
180
|
-
if (msg.createdDate > newestTimestamp) newestTimestamp = msg.createdDate;
|
|
181
|
-
}
|
|
182
|
-
if (messages.length === 0) {
|
|
183
|
-
oldestTimestamp = 0;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Compute XOR-based manifest hash - O(n) initial build
|
|
187
|
-
const manifestHashBytes = new Uint8Array(32);
|
|
188
|
-
for (const msg of messages) {
|
|
189
|
-
this.xorIntoHash(manifestHashBytes, this.hashMessageId(msg.messageId));
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return {
|
|
193
|
-
spaceId,
|
|
194
|
-
channelId,
|
|
195
|
-
messageMap,
|
|
196
|
-
memberMap,
|
|
197
|
-
digestMap,
|
|
198
|
-
memberDigestMap,
|
|
199
|
-
oldestTimestamp,
|
|
200
|
-
newestTimestamp,
|
|
201
|
-
manifestHashBytes,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Get manifest hash as hex string - O(1)
|
|
207
|
-
*/
|
|
208
|
-
private getManifestHash(cache: SyncPayloadCache): string {
|
|
209
|
-
return bytesToHex(cache.manifestHashBytes);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Get the manifest from cache - builds it on demand
|
|
214
|
-
*/
|
|
215
|
-
private getManifest(cache: SyncPayloadCache): SyncManifest {
|
|
216
|
-
const digests = [...cache.digestMap.values()].sort((a, b) => a.createdDate - b.createdDate);
|
|
217
|
-
|
|
218
|
-
// Collect reaction digests
|
|
219
|
-
const reactionDigests: ReturnType<typeof createReactionDigest> = [];
|
|
220
|
-
for (const msg of cache.messageMap.values()) {
|
|
221
|
-
if (msg.reactions && msg.reactions.length > 0) {
|
|
222
|
-
reactionDigests.push(...createReactionDigest(msg.messageId, msg.reactions));
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return {
|
|
227
|
-
spaceId: cache.spaceId,
|
|
228
|
-
channelId: cache.channelId,
|
|
229
|
-
messageCount: cache.digestMap.size,
|
|
230
|
-
oldestTimestamp: cache.oldestTimestamp,
|
|
231
|
-
newestTimestamp: cache.newestTimestamp,
|
|
232
|
-
digests,
|
|
233
|
-
reactionDigests,
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Get the summary from cache - O(1)
|
|
239
|
-
*/
|
|
240
|
-
private getSummary(cache: SyncPayloadCache): SyncSummary {
|
|
241
|
-
return {
|
|
242
|
-
memberCount: cache.memberDigestMap.size,
|
|
243
|
-
messageCount: cache.digestMap.size,
|
|
244
|
-
newestMessageTimestamp: cache.newestTimestamp,
|
|
245
|
-
oldestMessageTimestamp: cache.oldestTimestamp,
|
|
246
|
-
manifestHash: this.getManifestHash(cache),
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Get member digests from cache - O(m)
|
|
252
|
-
*/
|
|
253
|
-
private getMemberDigests(cache: SyncPayloadCache): MemberDigest[] {
|
|
254
|
-
return [...cache.memberDigestMap.values()];
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Invalidate cache for a space/channel (forces reload from storage on next access)
|
|
259
|
-
*/
|
|
260
|
-
invalidateCache(spaceId: string, channelId?: string): void {
|
|
261
|
-
if (channelId) {
|
|
262
|
-
const key = this.getCacheKey(spaceId, channelId);
|
|
263
|
-
this.payloadCache.delete(key);
|
|
264
|
-
logger.log(`[SyncService] Invalidated cache for ${spaceId.substring(0, 12)}:${channelId.substring(0, 12)}`);
|
|
265
|
-
} else {
|
|
266
|
-
// Invalidate all channels for this space
|
|
267
|
-
for (const key of this.payloadCache.keys()) {
|
|
268
|
-
if (key.startsWith(`${spaceId}:`)) {
|
|
269
|
-
this.payloadCache.delete(key);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
logger.log(`[SyncService] Invalidated all caches for space ${spaceId.substring(0, 12)}`);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Update cache with a new/updated message - O(1) incremental update
|
|
278
|
-
*/
|
|
279
|
-
updateCacheWithMessage(spaceId: string, channelId: string, message: Message): void {
|
|
280
|
-
const key = this.getCacheKey(spaceId, channelId);
|
|
281
|
-
const cached = this.payloadCache.get(key);
|
|
282
|
-
|
|
283
|
-
if (cached) {
|
|
284
|
-
const isNew = !cached.messageMap.has(message.messageId);
|
|
285
|
-
|
|
286
|
-
// O(1) - update maps
|
|
287
|
-
cached.messageMap.set(message.messageId, message);
|
|
288
|
-
cached.digestMap.set(message.messageId, createMessageDigest(message));
|
|
289
|
-
|
|
290
|
-
// O(1) - update timestamps if needed
|
|
291
|
-
if (message.createdDate < cached.oldestTimestamp) {
|
|
292
|
-
cached.oldestTimestamp = message.createdDate;
|
|
293
|
-
}
|
|
294
|
-
if (message.createdDate > cached.newestTimestamp) {
|
|
295
|
-
cached.newestTimestamp = message.createdDate;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// O(1) - XOR in the new message ID hash (only for new messages)
|
|
299
|
-
if (isNew) {
|
|
300
|
-
this.xorIntoHash(cached.manifestHashBytes, this.hashMessageId(message.messageId));
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
logger.log(`[SyncService] Updated cache with message ${message.messageId.substring(0, 12)} (O(1))`);
|
|
304
|
-
}
|
|
305
|
-
// If no cache exists, next getPayloadCache call will load from storage
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Remove a message from cache - O(1) for removal, but may need O(n) for timestamp recalc
|
|
310
|
-
*/
|
|
311
|
-
removeCacheMessage(spaceId: string, channelId: string, messageId: string): void {
|
|
312
|
-
const key = this.getCacheKey(spaceId, channelId);
|
|
313
|
-
const cached = this.payloadCache.get(key);
|
|
314
|
-
|
|
315
|
-
if (cached) {
|
|
316
|
-
const message = cached.messageMap.get(messageId);
|
|
317
|
-
if (!message) return; // Message not in cache
|
|
318
|
-
|
|
319
|
-
cached.messageMap.delete(messageId);
|
|
320
|
-
cached.digestMap.delete(messageId);
|
|
321
|
-
|
|
322
|
-
// O(1) - XOR out the removed message ID hash (XOR is its own inverse)
|
|
323
|
-
this.xorIntoHash(cached.manifestHashBytes, this.hashMessageId(messageId));
|
|
324
|
-
|
|
325
|
-
// Check if we need to recalculate timestamps (only if removed message was at boundary)
|
|
326
|
-
if (message.createdDate === cached.oldestTimestamp || message.createdDate === cached.newestTimestamp) {
|
|
327
|
-
// O(n) recalculation needed - but this is rare (only when deleting oldest/newest)
|
|
328
|
-
this.recalculateTimestamps(cached);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
logger.log(`[SyncService] Removed message ${messageId.substring(0, 12)} from cache`);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Recalculate timestamps from scratch - O(n), only called when necessary
|
|
337
|
-
*/
|
|
338
|
-
private recalculateTimestamps(cache: SyncPayloadCache): void {
|
|
339
|
-
let oldestTimestamp = Infinity;
|
|
340
|
-
let newestTimestamp = 0;
|
|
341
|
-
|
|
342
|
-
for (const msg of cache.messageMap.values()) {
|
|
343
|
-
if (msg.createdDate < oldestTimestamp) oldestTimestamp = msg.createdDate;
|
|
344
|
-
if (msg.createdDate > newestTimestamp) newestTimestamp = msg.createdDate;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
cache.oldestTimestamp = cache.messageMap.size === 0 ? 0 : oldestTimestamp;
|
|
348
|
-
cache.newestTimestamp = newestTimestamp;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Update cache with a new/updated member - O(1) incremental update
|
|
353
|
-
*/
|
|
354
|
-
updateCacheWithMember(spaceId: string, channelId: string, member: SpaceMember): void {
|
|
355
|
-
const key = this.getCacheKey(spaceId, channelId);
|
|
356
|
-
const cached = this.payloadCache.get(key);
|
|
357
|
-
|
|
358
|
-
if (cached) {
|
|
359
|
-
// O(1) - update maps
|
|
360
|
-
cached.memberMap.set(member.address, member);
|
|
361
|
-
cached.memberDigestMap.set(member.address, createMemberDigest(member));
|
|
362
|
-
|
|
363
|
-
logger.log(`[SyncService] Updated cache with member ${member.address.substring(0, 12)} (O(1))`);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Check if cache exists for a space/channel
|
|
369
|
-
*/
|
|
370
|
-
hasCachedPayload(spaceId: string, channelId: string): boolean {
|
|
371
|
-
return this.payloadCache.has(this.getCacheKey(spaceId, channelId));
|
|
372
|
-
}
|
|
373
|
-
|
|
374
84
|
// ============ Session Management ============
|
|
375
85
|
|
|
376
86
|
/**
|
|
@@ -432,7 +142,10 @@ export class SyncService {
|
|
|
432
142
|
channelId: string,
|
|
433
143
|
inboxAddress: string
|
|
434
144
|
): Promise<SyncRequestPayload> {
|
|
435
|
-
const
|
|
145
|
+
const messages = await this.getChannelMessages(spaceId, channelId);
|
|
146
|
+
const members = await this.storage.getSpaceMembers(spaceId);
|
|
147
|
+
|
|
148
|
+
const summary = createSyncSummary(messages, members.length);
|
|
436
149
|
const expiry = Date.now() + this.requestExpiry;
|
|
437
150
|
|
|
438
151
|
// Create session to track candidates
|
|
@@ -448,7 +161,7 @@ export class SyncService {
|
|
|
448
161
|
type: 'sync-request',
|
|
449
162
|
inboxAddress,
|
|
450
163
|
expiry,
|
|
451
|
-
summary
|
|
164
|
+
summary,
|
|
452
165
|
};
|
|
453
166
|
}
|
|
454
167
|
|
|
@@ -486,18 +199,19 @@ export class SyncService {
|
|
|
486
199
|
theirSummary: SyncSummary
|
|
487
200
|
): Promise<SyncInfoPayload | null> {
|
|
488
201
|
logger.log(`[SyncService] buildSyncInfo called for space=${spaceId.substring(0, 12)}, channel=${channelId.substring(0, 12)}`);
|
|
489
|
-
const
|
|
490
|
-
const
|
|
202
|
+
const messages = await this.getChannelMessages(spaceId, channelId);
|
|
203
|
+
const members = await this.storage.getSpaceMembers(spaceId);
|
|
491
204
|
|
|
492
|
-
logger.log(`[SyncService] buildSyncInfo: our data - ${
|
|
205
|
+
logger.log(`[SyncService] buildSyncInfo: our data - ${messages.length} messages, ${members.length} members`);
|
|
493
206
|
logger.log(`[SyncService] buildSyncInfo: their summary:`, theirSummary);
|
|
494
207
|
|
|
495
208
|
// Nothing to offer
|
|
496
|
-
if (
|
|
209
|
+
if (messages.length === 0 && members.length === 0) {
|
|
497
210
|
logger.log(`[SyncService] buildSyncInfo: returning null - we have no data`);
|
|
498
211
|
return null;
|
|
499
212
|
}
|
|
500
213
|
|
|
214
|
+
const ourSummary = createSyncSummary(messages, members.length);
|
|
501
215
|
logger.log(`[SyncService] buildSyncInfo: our summary:`, ourSummary);
|
|
502
216
|
|
|
503
217
|
// Quick check: if manifest hashes match and member counts match, likely in sync
|
|
@@ -604,7 +318,11 @@ export class SyncService {
|
|
|
604
318
|
return null;
|
|
605
319
|
}
|
|
606
320
|
|
|
607
|
-
const
|
|
321
|
+
const messages = await this.getChannelMessages(spaceId, channelId);
|
|
322
|
+
const members = await this.storage.getSpaceMembers(spaceId);
|
|
323
|
+
|
|
324
|
+
const manifest = createManifest(spaceId, channelId, messages);
|
|
325
|
+
const memberDigests = members.map(createMemberDigest);
|
|
608
326
|
|
|
609
327
|
// Mark sync in progress and store target
|
|
610
328
|
this.setSyncInProgress(spaceId, true);
|
|
@@ -615,8 +333,8 @@ export class SyncService {
|
|
|
615
333
|
payload: {
|
|
616
334
|
type: 'sync-initiate',
|
|
617
335
|
inboxAddress,
|
|
618
|
-
manifest
|
|
619
|
-
memberDigests
|
|
336
|
+
manifest,
|
|
337
|
+
memberDigests,
|
|
620
338
|
peerIds,
|
|
621
339
|
},
|
|
622
340
|
};
|
|
@@ -633,13 +351,14 @@ export class SyncService {
|
|
|
633
351
|
peerIds: number[],
|
|
634
352
|
inboxAddress: string
|
|
635
353
|
): Promise<SyncManifestPayload> {
|
|
636
|
-
const
|
|
354
|
+
const messages = await this.getChannelMessages(spaceId, channelId);
|
|
355
|
+
const members = await this.storage.getSpaceMembers(spaceId);
|
|
637
356
|
|
|
638
357
|
return {
|
|
639
358
|
type: 'sync-manifest',
|
|
640
359
|
inboxAddress,
|
|
641
|
-
manifest:
|
|
642
|
-
memberDigests:
|
|
360
|
+
manifest: createManifest(spaceId, channelId, messages),
|
|
361
|
+
memberDigests: members.map(createMemberDigest),
|
|
643
362
|
peerIds,
|
|
644
363
|
};
|
|
645
364
|
}
|
|
@@ -658,30 +377,36 @@ export class SyncService {
|
|
|
658
377
|
theirPeerIds: number[],
|
|
659
378
|
ourPeerEntries: Map<number, PeerEntry>
|
|
660
379
|
): Promise<SyncDeltaPayload[]> {
|
|
661
|
-
const
|
|
380
|
+
const messages = await this.getChannelMessages(spaceId, channelId);
|
|
381
|
+
const members = await this.storage.getSpaceMembers(spaceId);
|
|
382
|
+
|
|
383
|
+
const ourManifest = createManifest(spaceId, channelId, messages);
|
|
384
|
+
const ourMemberDigests = members.map(createMemberDigest);
|
|
662
385
|
const ourPeerIds = [...ourPeerEntries.keys()];
|
|
663
386
|
|
|
664
|
-
// Compute diffs
|
|
665
|
-
const ourManifest = this.getManifest(cache);
|
|
666
|
-
const ourMemberDigests = this.getMemberDigests(cache);
|
|
387
|
+
// Compute diffs - note: computeMessageDiff(ourManifest, theirManifest) returns extraIds = messages we have that they don't
|
|
667
388
|
const messageDiff = computeMessageDiff(ourManifest, theirManifest);
|
|
668
389
|
const memberDiff = computeMemberDiff(theirMemberDigests, ourMemberDigests);
|
|
669
390
|
const peerDiff = computePeerDiff(theirPeerIds, ourPeerIds);
|
|
670
391
|
|
|
671
|
-
// Build
|
|
392
|
+
// Build maps for lookups
|
|
393
|
+
const messageMap = new Map(messages.map((m) => [m.messageId, m]));
|
|
394
|
+
const memberMap = new Map(members.map((m) => [m.address, m]));
|
|
395
|
+
|
|
396
|
+
// Build deltas
|
|
672
397
|
const messageDelta = buildMessageDelta(
|
|
673
398
|
spaceId,
|
|
674
399
|
channelId,
|
|
675
400
|
messageDiff,
|
|
676
|
-
|
|
401
|
+
messageMap,
|
|
677
402
|
this.tombstones
|
|
678
403
|
);
|
|
679
404
|
|
|
680
405
|
// Build reaction delta for messages they're missing or have outdated
|
|
681
406
|
const reactionMessageIds = [...messageDiff.extraIds, ...messageDiff.outdatedIds];
|
|
682
|
-
const reactionDelta = buildReactionDelta(spaceId, channelId,
|
|
407
|
+
const reactionDelta = buildReactionDelta(spaceId, channelId, messageMap, reactionMessageIds);
|
|
683
408
|
|
|
684
|
-
const memberDelta = buildMemberDelta(spaceId, memberDiff,
|
|
409
|
+
const memberDelta = buildMemberDelta(spaceId, memberDiff, memberMap);
|
|
685
410
|
|
|
686
411
|
// Build peer map delta
|
|
687
412
|
const peerMapDelta: PeerMapDelta = {
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
* Conversation (DM) types for Quorum
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export type ConversationSource = 'quorum' | 'farcaster';
|
|
6
|
-
|
|
7
5
|
export type Conversation = {
|
|
8
6
|
conversationId: string;
|
|
9
7
|
type: 'direct' | 'group';
|
|
@@ -15,11 +13,4 @@ export type Conversation = {
|
|
|
15
13
|
isRepudiable?: boolean;
|
|
16
14
|
saveEditHistory?: boolean;
|
|
17
15
|
lastMessageId?: string;
|
|
18
|
-
// Farcaster-specific fields
|
|
19
|
-
source?: ConversationSource; // 'quorum' (E2EE) or 'farcaster' (direct cast)
|
|
20
|
-
farcasterConversationId?: string; // Farcaster conversation ID (e.g., "123-456")
|
|
21
|
-
farcasterFid?: number; // Counterparty's Farcaster FID (for 1:1 DMs)
|
|
22
|
-
farcasterUsername?: string; // Counterparty's Farcaster username
|
|
23
|
-
farcasterParticipantFids?: number[]; // All participant FIDs except current user (for group chats)
|
|
24
|
-
unreadCount?: number; // Farcaster unread count
|
|
25
16
|
};
|
package/src/types/index.ts
CHANGED