@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quilibrium/quorum-shared",
3
- "version": "2.1.0-1",
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",
@@ -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
- createReactionDigest,
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 cache = await this.getPayloadCache(spaceId, channelId);
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: this.getSummary(cache),
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 cache = await this.getPayloadCache(spaceId, channelId);
490
- const ourSummary = this.getSummary(cache);
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 - ${cache.messageMap.size} messages, ${cache.memberMap.size} members`);
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 (cache.messageMap.size === 0 && cache.memberMap.size === 0) {
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 cache = await this.getPayloadCache(spaceId, channelId);
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: this.getManifest(cache),
619
- memberDigests: this.getMemberDigests(cache),
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 cache = await this.getPayloadCache(spaceId, channelId);
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: this.getManifest(cache),
642
- memberDigests: this.getMemberDigests(cache),
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 cache = await this.getPayloadCache(spaceId, channelId);
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 using cached manifest and digests
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 deltas using cached maps
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
- cache.messageMap,
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, cache.messageMap, reactionMessageIds);
407
+ const reactionDelta = buildReactionDelta(spaceId, channelId, messageMap, reactionMessageIds);
683
408
 
684
- const memberDelta = buildMemberDelta(spaceId, memberDiff, cache.memberMap);
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
  };
@@ -40,7 +40,7 @@ export type {
40
40
  } from './message';
41
41
 
42
42
  // Conversation types
43
- export type { Conversation, ConversationSource } from './conversation';
43
+ export type { Conversation } from './conversation';
44
44
 
45
45
  // User types
46
46
  export type {