@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 +32 -7
- package/dist/index.d.ts +32 -7
- package/dist/index.js +141 -33
- package/dist/index.mjs +141 -33
- package/package.json +1 -1
- package/src/sync/service.ts +1 -0
- package/src/sync/utils.ts +17 -7
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
*
|
|
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
|
-
|
|
1922
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
2201
|
-
const
|
|
2202
|
-
|
|
2203
|
-
|
|
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
|
-
*
|
|
2254
|
+
* Get the manifest from cache - builds it on demand
|
|
2207
2255
|
*/
|
|
2208
|
-
|
|
2209
|
-
const
|
|
2210
|
-
const
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
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
|
|
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
|
-
|
|
2241
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
2455
|
-
memberDigests: cache
|
|
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
|
|
2470
|
-
memberDigests: cache
|
|
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
|
|
2483
|
-
const
|
|
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
|
-
|
|
1813
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
2092
|
-
const
|
|
2093
|
-
|
|
2094
|
-
|
|
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
|
-
*
|
|
2145
|
+
* Get the manifest from cache - builds it on demand
|
|
2098
2146
|
*/
|
|
2099
|
-
|
|
2100
|
-
const
|
|
2101
|
-
const
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
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
|
|
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
|
-
|
|
2132
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
2346
|
-
memberDigests: cache
|
|
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
|
|
2361
|
-
memberDigests: cache
|
|
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
|
|
2374
|
-
const
|
|
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
package/src/sync/service.ts
CHANGED
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
|
-
*
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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 ============
|