@quilibrium/quorum-shared 2.1.0-1 → 2.1.0-2

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 CHANGED
@@ -2133,7 +2133,8 @@ declare function computeMemberHash(member: SpaceMember): {
2133
2133
  };
2134
2134
  /**
2135
2135
  * Compute manifest hash for quick comparison.
2136
- * Hash of sorted message IDs.
2136
+ * Uses XOR of SHA-256 hashes of message IDs for O(1) incremental updates.
2137
+ * XOR is order-independent, so same message set = same hash regardless of order.
2137
2138
  */
2138
2139
  declare function computeManifestHash(digests: MessageDigest[]): string;
2139
2140
  /**
@@ -2283,27 +2284,51 @@ declare class SyncService {
2283
2284
  */
2284
2285
  private getPayloadCache;
2285
2286
  /**
2286
- * Build the payload cache from messages and members
2287
+ * Hash a message ID to bytes for XOR-based manifest hash
2288
+ */
2289
+ private hashMessageId;
2290
+ /**
2291
+ * XOR hash bytes into the accumulator - O(1)
2292
+ */
2293
+ private xorIntoHash;
2294
+ /**
2295
+ * Build the payload cache from messages and members - O(n) initial build
2287
2296
  */
2288
2297
  private buildPayloadCache;
2289
2298
  /**
2290
- * Rebuild derived fields (manifest, digests, summary) from the maps
2299
+ * Get manifest hash as hex string - O(1)
2300
+ */
2301
+ private getManifestHash;
2302
+ /**
2303
+ * Get the manifest from cache - builds it on demand
2291
2304
  */
2292
- private rebuildPayloadCache;
2305
+ private getManifest;
2306
+ /**
2307
+ * Get the summary from cache - O(1)
2308
+ */
2309
+ private getSummary;
2310
+ /**
2311
+ * Get member digests from cache - O(m)
2312
+ */
2313
+ private getMemberDigests;
2293
2314
  /**
2294
2315
  * Invalidate cache for a space/channel (forces reload from storage on next access)
2295
2316
  */
2296
2317
  invalidateCache(spaceId: string, channelId?: string): void;
2297
2318
  /**
2298
- * Update cache with a new/updated message (incremental update - no storage query)
2319
+ * Update cache with a new/updated message - O(1) incremental update
2299
2320
  */
2300
2321
  updateCacheWithMessage(spaceId: string, channelId: string, message: Message): void;
2301
2322
  /**
2302
- * Remove a message from cache (incremental update - no storage query)
2323
+ * Remove a message from cache - O(1) for removal, but may need O(n) for timestamp recalc
2303
2324
  */
2304
2325
  removeCacheMessage(spaceId: string, channelId: string, messageId: string): void;
2305
2326
  /**
2306
- * Update cache with a new/updated member (incremental update - no storage query)
2327
+ * Recalculate timestamps from scratch - O(n), only called when necessary
2328
+ */
2329
+ private recalculateTimestamps;
2330
+ /**
2331
+ * Update cache with a new/updated member - O(1) incremental update
2307
2332
  */
2308
2333
  updateCacheWithMember(spaceId: string, channelId: string, member: SpaceMember): void;
2309
2334
  /**
package/dist/index.d.ts CHANGED
@@ -2133,7 +2133,8 @@ declare function computeMemberHash(member: SpaceMember): {
2133
2133
  };
2134
2134
  /**
2135
2135
  * Compute manifest hash for quick comparison.
2136
- * Hash of sorted message IDs.
2136
+ * Uses XOR of SHA-256 hashes of message IDs for O(1) incremental updates.
2137
+ * XOR is order-independent, so same message set = same hash regardless of order.
2137
2138
  */
2138
2139
  declare function computeManifestHash(digests: MessageDigest[]): string;
2139
2140
  /**
@@ -2283,27 +2284,51 @@ declare class SyncService {
2283
2284
  */
2284
2285
  private getPayloadCache;
2285
2286
  /**
2286
- * Build the payload cache from messages and members
2287
+ * Hash a message ID to bytes for XOR-based manifest hash
2288
+ */
2289
+ private hashMessageId;
2290
+ /**
2291
+ * XOR hash bytes into the accumulator - O(1)
2292
+ */
2293
+ private xorIntoHash;
2294
+ /**
2295
+ * Build the payload cache from messages and members - O(n) initial build
2287
2296
  */
2288
2297
  private buildPayloadCache;
2289
2298
  /**
2290
- * Rebuild derived fields (manifest, digests, summary) from the maps
2299
+ * Get manifest hash as hex string - O(1)
2300
+ */
2301
+ private getManifestHash;
2302
+ /**
2303
+ * Get the manifest from cache - builds it on demand
2291
2304
  */
2292
- private rebuildPayloadCache;
2305
+ private getManifest;
2306
+ /**
2307
+ * Get the summary from cache - O(1)
2308
+ */
2309
+ private getSummary;
2310
+ /**
2311
+ * Get member digests from cache - O(m)
2312
+ */
2313
+ private getMemberDigests;
2293
2314
  /**
2294
2315
  * Invalidate cache for a space/channel (forces reload from storage on next access)
2295
2316
  */
2296
2317
  invalidateCache(spaceId: string, channelId?: string): void;
2297
2318
  /**
2298
- * Update cache with a new/updated message (incremental update - no storage query)
2319
+ * Update cache with a new/updated message - O(1) incremental update
2299
2320
  */
2300
2321
  updateCacheWithMessage(spaceId: string, channelId: string, message: Message): void;
2301
2322
  /**
2302
- * Remove a message from cache (incremental update - no storage query)
2323
+ * Remove a message from cache - O(1) for removal, but may need O(n) for timestamp recalc
2303
2324
  */
2304
2325
  removeCacheMessage(spaceId: string, channelId: string, messageId: string): void;
2305
2326
  /**
2306
- * Update cache with a new/updated member (incremental update - no storage query)
2327
+ * Recalculate timestamps from scratch - O(n), only called when necessary
2328
+ */
2329
+ private recalculateTimestamps;
2330
+ /**
2331
+ * Update cache with a new/updated member - O(1) incremental update
2307
2332
  */
2308
2333
  updateCacheWithMember(spaceId: string, channelId: string, member: SpaceMember): void;
2309
2334
  /**
package/dist/index.js CHANGED
@@ -1917,13 +1917,18 @@ function computeMemberHash(member) {
1917
1917
  const iconHash = computeHash(member.profile_image || "");
1918
1918
  return { displayNameHash, iconHash };
1919
1919
  }
1920
+ function xorBuffers(a, b) {
1921
+ for (let i = 0; i < 32; i++) {
1922
+ a[i] ^= b[i];
1923
+ }
1924
+ }
1920
1925
  function computeManifestHash(digests) {
1921
- if (digests.length === 0) {
1922
- return computeHash("");
1926
+ const result = new Uint8Array(32);
1927
+ for (const digest of digests) {
1928
+ const idHash = (0, import_sha2.sha256)(new TextEncoder().encode(digest.messageId));
1929
+ xorBuffers(result, idHash);
1923
1930
  }
1924
- const sorted = [...digests].sort((a, b) => a.messageId.localeCompare(b.messageId));
1925
- const ids = sorted.map((d) => d.messageId).join(":");
1926
- return computeHash(ids);
1931
+ return bytesToHex(result);
1927
1932
  }
1928
1933
  function createMessageDigest(message) {
1929
1934
  return {
@@ -2153,6 +2158,7 @@ function createSyncSummary(messages, memberCount) {
2153
2158
  }
2154
2159
 
2155
2160
  // src/sync/service.ts
2161
+ var import_sha22 = require("@noble/hashes/sha2");
2156
2162
  var SyncService = class {
2157
2163
  constructor(config2) {
2158
2164
  /** Active sync sessions by spaceId */
@@ -2192,25 +2198,96 @@ var SyncService = class {
2192
2198
  return payload;
2193
2199
  }
2194
2200
  /**
2195
- * Build the payload cache from messages and members
2201
+ * Hash a message ID to bytes for XOR-based manifest hash
2202
+ */
2203
+ hashMessageId(messageId) {
2204
+ return (0, import_sha22.sha256)(new TextEncoder().encode(messageId));
2205
+ }
2206
+ /**
2207
+ * XOR hash bytes into the accumulator - O(1)
2208
+ */
2209
+ xorIntoHash(accumulator, hash) {
2210
+ for (let i = 0; i < 32; i++) {
2211
+ accumulator[i] ^= hash[i];
2212
+ }
2213
+ }
2214
+ /**
2215
+ * Build the payload cache from messages and members - O(n) initial build
2196
2216
  */
2197
2217
  buildPayloadCache(spaceId, channelId, messages, members) {
2198
2218
  const messageMap = new Map(messages.map((m) => [m.messageId, m]));
2199
2219
  const memberMap = new Map(members.map((m) => [m.address, m]));
2200
- const manifest = createManifest(spaceId, channelId, messages);
2201
- const memberDigests = members.map(createMemberDigest);
2202
- const summary = createSyncSummary(messages, members.length);
2203
- return { manifest, memberDigests, summary, messageMap, memberMap };
2220
+ const digestMap = new Map(messages.map((m) => [m.messageId, createMessageDigest(m)]));
2221
+ const memberDigestMap = new Map(members.map((m) => [m.address, createMemberDigest(m)]));
2222
+ let oldestTimestamp = Infinity;
2223
+ let newestTimestamp = 0;
2224
+ for (const msg of messages) {
2225
+ if (msg.createdDate < oldestTimestamp) oldestTimestamp = msg.createdDate;
2226
+ if (msg.createdDate > newestTimestamp) newestTimestamp = msg.createdDate;
2227
+ }
2228
+ if (messages.length === 0) {
2229
+ oldestTimestamp = 0;
2230
+ }
2231
+ const manifestHashBytes = new Uint8Array(32);
2232
+ for (const msg of messages) {
2233
+ this.xorIntoHash(manifestHashBytes, this.hashMessageId(msg.messageId));
2234
+ }
2235
+ return {
2236
+ spaceId,
2237
+ channelId,
2238
+ messageMap,
2239
+ memberMap,
2240
+ digestMap,
2241
+ memberDigestMap,
2242
+ oldestTimestamp,
2243
+ newestTimestamp,
2244
+ manifestHashBytes
2245
+ };
2246
+ }
2247
+ /**
2248
+ * Get manifest hash as hex string - O(1)
2249
+ */
2250
+ getManifestHash(cache) {
2251
+ return bytesToHex(cache.manifestHashBytes);
2204
2252
  }
2205
2253
  /**
2206
- * Rebuild derived fields (manifest, digests, summary) from the maps
2254
+ * Get the manifest from cache - builds it on demand
2207
2255
  */
2208
- rebuildPayloadCache(spaceId, channelId, cache) {
2209
- const messages = [...cache.messageMap.values()];
2210
- const members = [...cache.memberMap.values()];
2211
- cache.manifest = createManifest(spaceId, channelId, messages);
2212
- cache.memberDigests = members.map(createMemberDigest);
2213
- cache.summary = createSyncSummary(messages, members.length);
2256
+ getManifest(cache) {
2257
+ const digests = [...cache.digestMap.values()].sort((a, b) => a.createdDate - b.createdDate);
2258
+ const reactionDigests = [];
2259
+ for (const msg of cache.messageMap.values()) {
2260
+ if (msg.reactions && msg.reactions.length > 0) {
2261
+ reactionDigests.push(...createReactionDigest(msg.messageId, msg.reactions));
2262
+ }
2263
+ }
2264
+ return {
2265
+ spaceId: cache.spaceId,
2266
+ channelId: cache.channelId,
2267
+ messageCount: cache.digestMap.size,
2268
+ oldestTimestamp: cache.oldestTimestamp,
2269
+ newestTimestamp: cache.newestTimestamp,
2270
+ digests,
2271
+ reactionDigests
2272
+ };
2273
+ }
2274
+ /**
2275
+ * Get the summary from cache - O(1)
2276
+ */
2277
+ getSummary(cache) {
2278
+ return {
2279
+ memberCount: cache.memberDigestMap.size,
2280
+ messageCount: cache.digestMap.size,
2281
+ newestMessageTimestamp: cache.newestTimestamp,
2282
+ oldestMessageTimestamp: cache.oldestTimestamp,
2283
+ manifestHash: this.getManifestHash(cache)
2284
+ };
2285
+ }
2286
+ /**
2287
+ * Get member digests from cache - O(m)
2288
+ */
2289
+ getMemberDigests(cache) {
2290
+ return [...cache.memberDigestMap.values()];
2214
2291
  }
2215
2292
  /**
2216
2293
  * Invalidate cache for a space/channel (forces reload from storage on next access)
@@ -2230,39 +2307,68 @@ var SyncService = class {
2230
2307
  }
2231
2308
  }
2232
2309
  /**
2233
- * Update cache with a new/updated message (incremental update - no storage query)
2310
+ * Update cache with a new/updated message - O(1) incremental update
2234
2311
  */
2235
2312
  updateCacheWithMessage(spaceId, channelId, message) {
2236
2313
  const key = this.getCacheKey(spaceId, channelId);
2237
2314
  const cached = this.payloadCache.get(key);
2238
2315
  if (cached) {
2316
+ const isNew = !cached.messageMap.has(message.messageId);
2239
2317
  cached.messageMap.set(message.messageId, message);
2240
- this.rebuildPayloadCache(spaceId, channelId, cached);
2241
- logger.log(`[SyncService] Updated cache with message ${message.messageId.substring(0, 12)}`);
2318
+ cached.digestMap.set(message.messageId, createMessageDigest(message));
2319
+ if (message.createdDate < cached.oldestTimestamp) {
2320
+ cached.oldestTimestamp = message.createdDate;
2321
+ }
2322
+ if (message.createdDate > cached.newestTimestamp) {
2323
+ cached.newestTimestamp = message.createdDate;
2324
+ }
2325
+ if (isNew) {
2326
+ this.xorIntoHash(cached.manifestHashBytes, this.hashMessageId(message.messageId));
2327
+ }
2328
+ logger.log(`[SyncService] Updated cache with message ${message.messageId.substring(0, 12)} (O(1))`);
2242
2329
  }
2243
2330
  }
2244
2331
  /**
2245
- * Remove a message from cache (incremental update - no storage query)
2332
+ * Remove a message from cache - O(1) for removal, but may need O(n) for timestamp recalc
2246
2333
  */
2247
2334
  removeCacheMessage(spaceId, channelId, messageId) {
2248
2335
  const key = this.getCacheKey(spaceId, channelId);
2249
2336
  const cached = this.payloadCache.get(key);
2250
2337
  if (cached) {
2338
+ const message = cached.messageMap.get(messageId);
2339
+ if (!message) return;
2251
2340
  cached.messageMap.delete(messageId);
2252
- this.rebuildPayloadCache(spaceId, channelId, cached);
2341
+ cached.digestMap.delete(messageId);
2342
+ this.xorIntoHash(cached.manifestHashBytes, this.hashMessageId(messageId));
2343
+ if (message.createdDate === cached.oldestTimestamp || message.createdDate === cached.newestTimestamp) {
2344
+ this.recalculateTimestamps(cached);
2345
+ }
2253
2346
  logger.log(`[SyncService] Removed message ${messageId.substring(0, 12)} from cache`);
2254
2347
  }
2255
2348
  }
2256
2349
  /**
2257
- * Update cache with a new/updated member (incremental update - no storage query)
2350
+ * Recalculate timestamps from scratch - O(n), only called when necessary
2351
+ */
2352
+ recalculateTimestamps(cache) {
2353
+ let oldestTimestamp = Infinity;
2354
+ let newestTimestamp = 0;
2355
+ for (const msg of cache.messageMap.values()) {
2356
+ if (msg.createdDate < oldestTimestamp) oldestTimestamp = msg.createdDate;
2357
+ if (msg.createdDate > newestTimestamp) newestTimestamp = msg.createdDate;
2358
+ }
2359
+ cache.oldestTimestamp = cache.messageMap.size === 0 ? 0 : oldestTimestamp;
2360
+ cache.newestTimestamp = newestTimestamp;
2361
+ }
2362
+ /**
2363
+ * Update cache with a new/updated member - O(1) incremental update
2258
2364
  */
2259
2365
  updateCacheWithMember(spaceId, channelId, member) {
2260
2366
  const key = this.getCacheKey(spaceId, channelId);
2261
2367
  const cached = this.payloadCache.get(key);
2262
2368
  if (cached) {
2263
2369
  cached.memberMap.set(member.address, member);
2264
- this.rebuildPayloadCache(spaceId, channelId, cached);
2265
- logger.log(`[SyncService] Updated cache with member ${member.address.substring(0, 12)}`);
2370
+ cached.memberDigestMap.set(member.address, createMemberDigest(member));
2371
+ logger.log(`[SyncService] Updated cache with member ${member.address.substring(0, 12)} (O(1))`);
2266
2372
  }
2267
2373
  }
2268
2374
  /**
@@ -2334,7 +2440,7 @@ var SyncService = class {
2334
2440
  type: "sync-request",
2335
2441
  inboxAddress,
2336
2442
  expiry,
2337
- summary: cache.summary
2443
+ summary: this.getSummary(cache)
2338
2444
  };
2339
2445
  }
2340
2446
  /**
@@ -2358,7 +2464,7 @@ var SyncService = class {
2358
2464
  async buildSyncInfo(spaceId, channelId, inboxAddress, theirSummary) {
2359
2465
  logger.log(`[SyncService] buildSyncInfo called for space=${spaceId.substring(0, 12)}, channel=${channelId.substring(0, 12)}`);
2360
2466
  const cache = await this.getPayloadCache(spaceId, channelId);
2361
- const ourSummary = cache.summary;
2467
+ const ourSummary = this.getSummary(cache);
2362
2468
  logger.log(`[SyncService] buildSyncInfo: our data - ${cache.messageMap.size} messages, ${cache.memberMap.size} members`);
2363
2469
  logger.log(`[SyncService] buildSyncInfo: their summary:`, theirSummary);
2364
2470
  if (cache.messageMap.size === 0 && cache.memberMap.size === 0) {
@@ -2451,8 +2557,8 @@ var SyncService = class {
2451
2557
  payload: {
2452
2558
  type: "sync-initiate",
2453
2559
  inboxAddress,
2454
- manifest: cache.manifest,
2455
- memberDigests: cache.memberDigests,
2560
+ manifest: this.getManifest(cache),
2561
+ memberDigests: this.getMemberDigests(cache),
2456
2562
  peerIds
2457
2563
  }
2458
2564
  };
@@ -2466,8 +2572,8 @@ var SyncService = class {
2466
2572
  return {
2467
2573
  type: "sync-manifest",
2468
2574
  inboxAddress,
2469
- manifest: cache.manifest,
2470
- memberDigests: cache.memberDigests,
2575
+ manifest: this.getManifest(cache),
2576
+ memberDigests: this.getMemberDigests(cache),
2471
2577
  peerIds
2472
2578
  };
2473
2579
  }
@@ -2479,8 +2585,10 @@ var SyncService = class {
2479
2585
  async buildSyncDelta(spaceId, channelId, theirManifest, theirMemberDigests, theirPeerIds, ourPeerEntries) {
2480
2586
  const cache = await this.getPayloadCache(spaceId, channelId);
2481
2587
  const ourPeerIds = [...ourPeerEntries.keys()];
2482
- const messageDiff = computeMessageDiff(cache.manifest, theirManifest);
2483
- const memberDiff = computeMemberDiff(theirMemberDigests, cache.memberDigests);
2588
+ const ourManifest = this.getManifest(cache);
2589
+ const ourMemberDigests = this.getMemberDigests(cache);
2590
+ const messageDiff = computeMessageDiff(ourManifest, theirManifest);
2591
+ const memberDiff = computeMemberDiff(theirMemberDigests, ourMemberDigests);
2484
2592
  const peerDiff = computePeerDiff(theirPeerIds, ourPeerIds);
2485
2593
  const messageDelta = buildMessageDelta(
2486
2594
  spaceId,
package/dist/index.mjs CHANGED
@@ -1808,13 +1808,18 @@ function computeMemberHash(member) {
1808
1808
  const iconHash = computeHash(member.profile_image || "");
1809
1809
  return { displayNameHash, iconHash };
1810
1810
  }
1811
+ function xorBuffers(a, b) {
1812
+ for (let i = 0; i < 32; i++) {
1813
+ a[i] ^= b[i];
1814
+ }
1815
+ }
1811
1816
  function computeManifestHash(digests) {
1812
- if (digests.length === 0) {
1813
- return computeHash("");
1817
+ const result = new Uint8Array(32);
1818
+ for (const digest of digests) {
1819
+ const idHash = sha256(new TextEncoder().encode(digest.messageId));
1820
+ xorBuffers(result, idHash);
1814
1821
  }
1815
- const sorted = [...digests].sort((a, b) => a.messageId.localeCompare(b.messageId));
1816
- const ids = sorted.map((d) => d.messageId).join(":");
1817
- return computeHash(ids);
1822
+ return bytesToHex(result);
1818
1823
  }
1819
1824
  function createMessageDigest(message) {
1820
1825
  return {
@@ -2044,6 +2049,7 @@ function createSyncSummary(messages, memberCount) {
2044
2049
  }
2045
2050
 
2046
2051
  // src/sync/service.ts
2052
+ import { sha256 as sha2562 } from "@noble/hashes/sha2";
2047
2053
  var SyncService = class {
2048
2054
  constructor(config2) {
2049
2055
  /** Active sync sessions by spaceId */
@@ -2083,25 +2089,96 @@ var SyncService = class {
2083
2089
  return payload;
2084
2090
  }
2085
2091
  /**
2086
- * Build the payload cache from messages and members
2092
+ * Hash a message ID to bytes for XOR-based manifest hash
2093
+ */
2094
+ hashMessageId(messageId) {
2095
+ return sha2562(new TextEncoder().encode(messageId));
2096
+ }
2097
+ /**
2098
+ * XOR hash bytes into the accumulator - O(1)
2099
+ */
2100
+ xorIntoHash(accumulator, hash) {
2101
+ for (let i = 0; i < 32; i++) {
2102
+ accumulator[i] ^= hash[i];
2103
+ }
2104
+ }
2105
+ /**
2106
+ * Build the payload cache from messages and members - O(n) initial build
2087
2107
  */
2088
2108
  buildPayloadCache(spaceId, channelId, messages, members) {
2089
2109
  const messageMap = new Map(messages.map((m) => [m.messageId, m]));
2090
2110
  const memberMap = new Map(members.map((m) => [m.address, m]));
2091
- const manifest = createManifest(spaceId, channelId, messages);
2092
- const memberDigests = members.map(createMemberDigest);
2093
- const summary = createSyncSummary(messages, members.length);
2094
- return { manifest, memberDigests, summary, messageMap, memberMap };
2111
+ const digestMap = new Map(messages.map((m) => [m.messageId, createMessageDigest(m)]));
2112
+ const memberDigestMap = new Map(members.map((m) => [m.address, createMemberDigest(m)]));
2113
+ let oldestTimestamp = Infinity;
2114
+ let newestTimestamp = 0;
2115
+ for (const msg of messages) {
2116
+ if (msg.createdDate < oldestTimestamp) oldestTimestamp = msg.createdDate;
2117
+ if (msg.createdDate > newestTimestamp) newestTimestamp = msg.createdDate;
2118
+ }
2119
+ if (messages.length === 0) {
2120
+ oldestTimestamp = 0;
2121
+ }
2122
+ const manifestHashBytes = new Uint8Array(32);
2123
+ for (const msg of messages) {
2124
+ this.xorIntoHash(manifestHashBytes, this.hashMessageId(msg.messageId));
2125
+ }
2126
+ return {
2127
+ spaceId,
2128
+ channelId,
2129
+ messageMap,
2130
+ memberMap,
2131
+ digestMap,
2132
+ memberDigestMap,
2133
+ oldestTimestamp,
2134
+ newestTimestamp,
2135
+ manifestHashBytes
2136
+ };
2137
+ }
2138
+ /**
2139
+ * Get manifest hash as hex string - O(1)
2140
+ */
2141
+ getManifestHash(cache) {
2142
+ return bytesToHex(cache.manifestHashBytes);
2095
2143
  }
2096
2144
  /**
2097
- * Rebuild derived fields (manifest, digests, summary) from the maps
2145
+ * Get the manifest from cache - builds it on demand
2098
2146
  */
2099
- rebuildPayloadCache(spaceId, channelId, cache) {
2100
- const messages = [...cache.messageMap.values()];
2101
- const members = [...cache.memberMap.values()];
2102
- cache.manifest = createManifest(spaceId, channelId, messages);
2103
- cache.memberDigests = members.map(createMemberDigest);
2104
- cache.summary = createSyncSummary(messages, members.length);
2147
+ getManifest(cache) {
2148
+ const digests = [...cache.digestMap.values()].sort((a, b) => a.createdDate - b.createdDate);
2149
+ const reactionDigests = [];
2150
+ for (const msg of cache.messageMap.values()) {
2151
+ if (msg.reactions && msg.reactions.length > 0) {
2152
+ reactionDigests.push(...createReactionDigest(msg.messageId, msg.reactions));
2153
+ }
2154
+ }
2155
+ return {
2156
+ spaceId: cache.spaceId,
2157
+ channelId: cache.channelId,
2158
+ messageCount: cache.digestMap.size,
2159
+ oldestTimestamp: cache.oldestTimestamp,
2160
+ newestTimestamp: cache.newestTimestamp,
2161
+ digests,
2162
+ reactionDigests
2163
+ };
2164
+ }
2165
+ /**
2166
+ * Get the summary from cache - O(1)
2167
+ */
2168
+ getSummary(cache) {
2169
+ return {
2170
+ memberCount: cache.memberDigestMap.size,
2171
+ messageCount: cache.digestMap.size,
2172
+ newestMessageTimestamp: cache.newestTimestamp,
2173
+ oldestMessageTimestamp: cache.oldestTimestamp,
2174
+ manifestHash: this.getManifestHash(cache)
2175
+ };
2176
+ }
2177
+ /**
2178
+ * Get member digests from cache - O(m)
2179
+ */
2180
+ getMemberDigests(cache) {
2181
+ return [...cache.memberDigestMap.values()];
2105
2182
  }
2106
2183
  /**
2107
2184
  * Invalidate cache for a space/channel (forces reload from storage on next access)
@@ -2121,39 +2198,68 @@ var SyncService = class {
2121
2198
  }
2122
2199
  }
2123
2200
  /**
2124
- * Update cache with a new/updated message (incremental update - no storage query)
2201
+ * Update cache with a new/updated message - O(1) incremental update
2125
2202
  */
2126
2203
  updateCacheWithMessage(spaceId, channelId, message) {
2127
2204
  const key = this.getCacheKey(spaceId, channelId);
2128
2205
  const cached = this.payloadCache.get(key);
2129
2206
  if (cached) {
2207
+ const isNew = !cached.messageMap.has(message.messageId);
2130
2208
  cached.messageMap.set(message.messageId, message);
2131
- this.rebuildPayloadCache(spaceId, channelId, cached);
2132
- logger.log(`[SyncService] Updated cache with message ${message.messageId.substring(0, 12)}`);
2209
+ cached.digestMap.set(message.messageId, createMessageDigest(message));
2210
+ if (message.createdDate < cached.oldestTimestamp) {
2211
+ cached.oldestTimestamp = message.createdDate;
2212
+ }
2213
+ if (message.createdDate > cached.newestTimestamp) {
2214
+ cached.newestTimestamp = message.createdDate;
2215
+ }
2216
+ if (isNew) {
2217
+ this.xorIntoHash(cached.manifestHashBytes, this.hashMessageId(message.messageId));
2218
+ }
2219
+ logger.log(`[SyncService] Updated cache with message ${message.messageId.substring(0, 12)} (O(1))`);
2133
2220
  }
2134
2221
  }
2135
2222
  /**
2136
- * Remove a message from cache (incremental update - no storage query)
2223
+ * Remove a message from cache - O(1) for removal, but may need O(n) for timestamp recalc
2137
2224
  */
2138
2225
  removeCacheMessage(spaceId, channelId, messageId) {
2139
2226
  const key = this.getCacheKey(spaceId, channelId);
2140
2227
  const cached = this.payloadCache.get(key);
2141
2228
  if (cached) {
2229
+ const message = cached.messageMap.get(messageId);
2230
+ if (!message) return;
2142
2231
  cached.messageMap.delete(messageId);
2143
- this.rebuildPayloadCache(spaceId, channelId, cached);
2232
+ cached.digestMap.delete(messageId);
2233
+ this.xorIntoHash(cached.manifestHashBytes, this.hashMessageId(messageId));
2234
+ if (message.createdDate === cached.oldestTimestamp || message.createdDate === cached.newestTimestamp) {
2235
+ this.recalculateTimestamps(cached);
2236
+ }
2144
2237
  logger.log(`[SyncService] Removed message ${messageId.substring(0, 12)} from cache`);
2145
2238
  }
2146
2239
  }
2147
2240
  /**
2148
- * Update cache with a new/updated member (incremental update - no storage query)
2241
+ * Recalculate timestamps from scratch - O(n), only called when necessary
2242
+ */
2243
+ recalculateTimestamps(cache) {
2244
+ let oldestTimestamp = Infinity;
2245
+ let newestTimestamp = 0;
2246
+ for (const msg of cache.messageMap.values()) {
2247
+ if (msg.createdDate < oldestTimestamp) oldestTimestamp = msg.createdDate;
2248
+ if (msg.createdDate > newestTimestamp) newestTimestamp = msg.createdDate;
2249
+ }
2250
+ cache.oldestTimestamp = cache.messageMap.size === 0 ? 0 : oldestTimestamp;
2251
+ cache.newestTimestamp = newestTimestamp;
2252
+ }
2253
+ /**
2254
+ * Update cache with a new/updated member - O(1) incremental update
2149
2255
  */
2150
2256
  updateCacheWithMember(spaceId, channelId, member) {
2151
2257
  const key = this.getCacheKey(spaceId, channelId);
2152
2258
  const cached = this.payloadCache.get(key);
2153
2259
  if (cached) {
2154
2260
  cached.memberMap.set(member.address, member);
2155
- this.rebuildPayloadCache(spaceId, channelId, cached);
2156
- logger.log(`[SyncService] Updated cache with member ${member.address.substring(0, 12)}`);
2261
+ cached.memberDigestMap.set(member.address, createMemberDigest(member));
2262
+ logger.log(`[SyncService] Updated cache with member ${member.address.substring(0, 12)} (O(1))`);
2157
2263
  }
2158
2264
  }
2159
2265
  /**
@@ -2225,7 +2331,7 @@ var SyncService = class {
2225
2331
  type: "sync-request",
2226
2332
  inboxAddress,
2227
2333
  expiry,
2228
- summary: cache.summary
2334
+ summary: this.getSummary(cache)
2229
2335
  };
2230
2336
  }
2231
2337
  /**
@@ -2249,7 +2355,7 @@ var SyncService = class {
2249
2355
  async buildSyncInfo(spaceId, channelId, inboxAddress, theirSummary) {
2250
2356
  logger.log(`[SyncService] buildSyncInfo called for space=${spaceId.substring(0, 12)}, channel=${channelId.substring(0, 12)}`);
2251
2357
  const cache = await this.getPayloadCache(spaceId, channelId);
2252
- const ourSummary = cache.summary;
2358
+ const ourSummary = this.getSummary(cache);
2253
2359
  logger.log(`[SyncService] buildSyncInfo: our data - ${cache.messageMap.size} messages, ${cache.memberMap.size} members`);
2254
2360
  logger.log(`[SyncService] buildSyncInfo: their summary:`, theirSummary);
2255
2361
  if (cache.messageMap.size === 0 && cache.memberMap.size === 0) {
@@ -2342,8 +2448,8 @@ var SyncService = class {
2342
2448
  payload: {
2343
2449
  type: "sync-initiate",
2344
2450
  inboxAddress,
2345
- manifest: cache.manifest,
2346
- memberDigests: cache.memberDigests,
2451
+ manifest: this.getManifest(cache),
2452
+ memberDigests: this.getMemberDigests(cache),
2347
2453
  peerIds
2348
2454
  }
2349
2455
  };
@@ -2357,8 +2463,8 @@ var SyncService = class {
2357
2463
  return {
2358
2464
  type: "sync-manifest",
2359
2465
  inboxAddress,
2360
- manifest: cache.manifest,
2361
- memberDigests: cache.memberDigests,
2466
+ manifest: this.getManifest(cache),
2467
+ memberDigests: this.getMemberDigests(cache),
2362
2468
  peerIds
2363
2469
  };
2364
2470
  }
@@ -2370,8 +2476,10 @@ var SyncService = class {
2370
2476
  async buildSyncDelta(spaceId, channelId, theirManifest, theirMemberDigests, theirPeerIds, ourPeerEntries) {
2371
2477
  const cache = await this.getPayloadCache(spaceId, channelId);
2372
2478
  const ourPeerIds = [...ourPeerEntries.keys()];
2373
- const messageDiff = computeMessageDiff(cache.manifest, theirManifest);
2374
- const memberDiff = computeMemberDiff(theirMemberDigests, cache.memberDigests);
2479
+ const ourManifest = this.getManifest(cache);
2480
+ const ourMemberDigests = this.getMemberDigests(cache);
2481
+ const messageDiff = computeMessageDiff(ourManifest, theirManifest);
2482
+ const memberDiff = computeMemberDiff(theirMemberDigests, ourMemberDigests);
2375
2483
  const peerDiff = computePeerDiff(theirPeerIds, ourPeerIds);
2376
2484
  const messageDelta = buildMessageDelta(
2377
2485
  spaceId,
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-2",
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,6 +16,7 @@ import type { Message, SpaceMember } from '../types';
16
16
  import { logger } from '../utils/logger';
17
17
  import type {
18
18
  SyncManifest,
19
+ MessageDigest,
19
20
  MessageDelta,
20
21
  ReactionDelta,
21
22
  MemberDigest,
package/src/sync/utils.ts CHANGED
@@ -146,19 +146,29 @@ export function computeMemberHash(member: SpaceMember): {
146
146
  return { displayNameHash, iconHash };
147
147
  }
148
148
 
149
+ /**
150
+ * XOR two Uint8Array buffers together (modifies first buffer in place)
151
+ */
152
+ function xorBuffers(a: Uint8Array, b: Uint8Array): void {
153
+ for (let i = 0; i < 32; i++) {
154
+ a[i] ^= b[i];
155
+ }
156
+ }
157
+
149
158
  /**
150
159
  * Compute manifest hash for quick comparison.
151
- * Hash of sorted message IDs.
160
+ * Uses XOR of SHA-256 hashes of message IDs for O(1) incremental updates.
161
+ * XOR is order-independent, so same message set = same hash regardless of order.
152
162
  */
153
163
  export function computeManifestHash(digests: MessageDigest[]): string {
154
- if (digests.length === 0) {
155
- return computeHash('');
164
+ const result = new Uint8Array(32);
165
+
166
+ for (const digest of digests) {
167
+ const idHash = sha256(new TextEncoder().encode(digest.messageId));
168
+ xorBuffers(result, idHash);
156
169
  }
157
170
 
158
- // Sort by messageId for deterministic hash
159
- const sorted = [...digests].sort((a, b) => a.messageId.localeCompare(b.messageId));
160
- const ids = sorted.map((d) => d.messageId).join(':');
161
- return computeHash(ids);
171
+ return bytesToHex(result);
162
172
  }
163
173
 
164
174
  // ============ Digest Creation ============