@topgunbuild/core 0.1.0 → 0.2.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.mjs CHANGED
@@ -374,6 +374,225 @@ var LWWMap = class {
374
374
  }
375
375
  };
376
376
 
377
+ // src/ORMapMerkle.ts
378
+ function timestampToString(ts) {
379
+ return `${ts.millis}:${ts.counter}:${ts.nodeId}`;
380
+ }
381
+ function stringifyValue(value) {
382
+ if (value === null || value === void 0) {
383
+ return String(value);
384
+ }
385
+ if (typeof value === "object") {
386
+ return JSON.stringify(value, Object.keys(value).sort());
387
+ }
388
+ return String(value);
389
+ }
390
+ function hashORMapEntry(key, records) {
391
+ const sortedTags = Array.from(records.keys()).sort();
392
+ const parts = [`key:${key}`];
393
+ for (const tag of sortedTags) {
394
+ const record = records.get(tag);
395
+ const valuePart = stringifyValue(record.value);
396
+ let recordStr = `${tag}:${valuePart}:${timestampToString(record.timestamp)}`;
397
+ if (record.ttlMs !== void 0) {
398
+ recordStr += `:ttl=${record.ttlMs}`;
399
+ }
400
+ parts.push(recordStr);
401
+ }
402
+ return hashString(parts.join("|"));
403
+ }
404
+ function hashORMapRecord(record) {
405
+ const valuePart = stringifyValue(record.value);
406
+ let str = `${record.tag}:${valuePart}:${timestampToString(record.timestamp)}`;
407
+ if (record.ttlMs !== void 0) {
408
+ str += `:ttl=${record.ttlMs}`;
409
+ }
410
+ return hashString(str);
411
+ }
412
+ function compareTimestamps(a, b) {
413
+ if (a.millis !== b.millis) {
414
+ return a.millis - b.millis;
415
+ }
416
+ if (a.counter !== b.counter) {
417
+ return a.counter - b.counter;
418
+ }
419
+ return a.nodeId.localeCompare(b.nodeId);
420
+ }
421
+
422
+ // src/ORMapMerkleTree.ts
423
+ var ORMapMerkleTree = class {
424
+ constructor(depth = 3) {
425
+ this.depth = depth;
426
+ this.root = { hash: 0, children: {} };
427
+ }
428
+ /**
429
+ * Update tree from ORMap data.
430
+ * Rebuilds hashes for all entries in the map.
431
+ */
432
+ updateFromORMap(map) {
433
+ this.root = { hash: 0, children: {} };
434
+ const snapshot = map.getSnapshot();
435
+ for (const [key, records] of snapshot.items) {
436
+ if (records.size > 0) {
437
+ const keyStr = String(key);
438
+ const entryHash = hashORMapEntry(keyStr, records);
439
+ const pathHash = hashString(keyStr).toString(16).padStart(8, "0");
440
+ this.updateNode(this.root, keyStr, entryHash, pathHash, 0);
441
+ }
442
+ }
443
+ }
444
+ /**
445
+ * Incrementally update a single key's hash.
446
+ * Call this when records for a key change.
447
+ */
448
+ update(key, records) {
449
+ const pathHash = hashString(key).toString(16).padStart(8, "0");
450
+ if (records.size === 0) {
451
+ this.removeNode(this.root, key, pathHash, 0);
452
+ } else {
453
+ const entryHash = hashORMapEntry(key, records);
454
+ this.updateNode(this.root, key, entryHash, pathHash, 0);
455
+ }
456
+ }
457
+ /**
458
+ * Remove a key from the tree.
459
+ * Called when all records for a key are removed.
460
+ */
461
+ remove(key) {
462
+ const pathHash = hashString(key).toString(16).padStart(8, "0");
463
+ this.removeNode(this.root, key, pathHash, 0);
464
+ }
465
+ updateNode(node, key, entryHash, pathHash, level) {
466
+ if (level >= this.depth) {
467
+ if (!node.entries) node.entries = /* @__PURE__ */ new Map();
468
+ node.entries.set(key, entryHash);
469
+ let h2 = 0;
470
+ for (const val of node.entries.values()) {
471
+ h2 = h2 + val | 0;
472
+ }
473
+ node.hash = h2 >>> 0;
474
+ return node.hash;
475
+ }
476
+ const bucketChar = pathHash[level];
477
+ if (!node.children) node.children = {};
478
+ if (!node.children[bucketChar]) {
479
+ node.children[bucketChar] = { hash: 0 };
480
+ }
481
+ this.updateNode(node.children[bucketChar], key, entryHash, pathHash, level + 1);
482
+ let h = 0;
483
+ for (const child of Object.values(node.children)) {
484
+ h = h + child.hash | 0;
485
+ }
486
+ node.hash = h >>> 0;
487
+ return node.hash;
488
+ }
489
+ removeNode(node, key, pathHash, level) {
490
+ if (level >= this.depth) {
491
+ if (node.entries) {
492
+ node.entries.delete(key);
493
+ let h2 = 0;
494
+ for (const val of node.entries.values()) {
495
+ h2 = h2 + val | 0;
496
+ }
497
+ node.hash = h2 >>> 0;
498
+ }
499
+ return node.hash;
500
+ }
501
+ const bucketChar = pathHash[level];
502
+ if (node.children && node.children[bucketChar]) {
503
+ this.removeNode(node.children[bucketChar], key, pathHash, level + 1);
504
+ }
505
+ let h = 0;
506
+ if (node.children) {
507
+ for (const child of Object.values(node.children)) {
508
+ h = h + child.hash | 0;
509
+ }
510
+ }
511
+ node.hash = h >>> 0;
512
+ return node.hash;
513
+ }
514
+ /**
515
+ * Get the root hash for quick comparison.
516
+ */
517
+ getRootHash() {
518
+ return this.root.hash;
519
+ }
520
+ /**
521
+ * Get node at a specific path.
522
+ */
523
+ getNode(path) {
524
+ let current = this.root;
525
+ for (const char of path) {
526
+ if (!current.children || !current.children[char]) {
527
+ return void 0;
528
+ }
529
+ current = current.children[char];
530
+ }
531
+ return current;
532
+ }
533
+ /**
534
+ * Returns the hashes of the children at the given path.
535
+ * Used by the client/server to compare buckets.
536
+ */
537
+ getBuckets(path) {
538
+ const node = this.getNode(path);
539
+ if (!node || !node.children) return {};
540
+ const result = {};
541
+ for (const [key, child] of Object.entries(node.children)) {
542
+ result[key] = child.hash;
543
+ }
544
+ return result;
545
+ }
546
+ /**
547
+ * For a leaf node (bucket), returns the actual keys it contains.
548
+ * Used to request specific keys when a bucket differs.
549
+ */
550
+ getKeysInBucket(path) {
551
+ const node = this.getNode(path);
552
+ if (!node || !node.entries) return [];
553
+ return Array.from(node.entries.keys());
554
+ }
555
+ /**
556
+ * Find keys that differ between this tree and bucket info from remote.
557
+ * Returns keys that:
558
+ * - Exist locally but have different hash on remote
559
+ * - Exist on remote but not locally
560
+ * - Exist locally but not on remote
561
+ */
562
+ findDiffKeys(path, remoteEntries) {
563
+ const diffKeys = /* @__PURE__ */ new Set();
564
+ const node = this.getNode(path);
565
+ const localEntries = node?.entries || /* @__PURE__ */ new Map();
566
+ for (const [key, hash] of localEntries) {
567
+ const remoteHash = remoteEntries.get(key);
568
+ if (remoteHash === void 0 || remoteHash !== hash) {
569
+ diffKeys.add(key);
570
+ }
571
+ }
572
+ for (const key of remoteEntries.keys()) {
573
+ if (!localEntries.has(key)) {
574
+ diffKeys.add(key);
575
+ }
576
+ }
577
+ return diffKeys;
578
+ }
579
+ /**
580
+ * Get all entry hashes at a leaf path.
581
+ * Used when sending bucket details to remote.
582
+ */
583
+ getEntryHashes(path) {
584
+ const node = this.getNode(path);
585
+ return node?.entries || /* @__PURE__ */ new Map();
586
+ }
587
+ /**
588
+ * Check if a path leads to a leaf node.
589
+ */
590
+ isLeaf(path) {
591
+ const node = this.getNode(path);
592
+ return node !== void 0 && node.entries !== void 0 && node.entries.size > 0;
593
+ }
594
+ };
595
+
377
596
  // src/ORMap.ts
378
597
  var ORMap = class {
379
598
  constructor(hlc) {
@@ -381,6 +600,7 @@ var ORMap = class {
381
600
  this.hlc = hlc;
382
601
  this.items = /* @__PURE__ */ new Map();
383
602
  this.tombstones = /* @__PURE__ */ new Set();
603
+ this.merkleTree = new ORMapMerkleTree();
384
604
  }
385
605
  onChange(callback) {
386
606
  this.listeners.push(callback);
@@ -425,6 +645,7 @@ var ORMap = class {
425
645
  this.items.set(key, keyMap);
426
646
  }
427
647
  keyMap.set(tag, record);
648
+ this.updateMerkleTree(key);
428
649
  this.notify();
429
650
  return record;
430
651
  }
@@ -449,6 +670,7 @@ var ORMap = class {
449
670
  if (keyMap.size === 0) {
450
671
  this.items.delete(key);
451
672
  }
673
+ this.updateMerkleTree(key);
452
674
  this.notify();
453
675
  return tagsToRemove;
454
676
  }
@@ -458,6 +680,7 @@ var ORMap = class {
458
680
  clear() {
459
681
  this.items.clear();
460
682
  this.tombstones.clear();
683
+ this.merkleTree = new ORMapMerkleTree();
461
684
  this.notify();
462
685
  }
463
686
  /**
@@ -507,9 +730,10 @@ var ORMap = class {
507
730
  }
508
731
  /**
509
732
  * Applies a record from a remote source (Sync).
733
+ * Returns true if the record was applied (not tombstoned).
510
734
  */
511
735
  apply(key, record) {
512
- if (this.tombstones.has(record.tag)) return;
736
+ if (this.tombstones.has(record.tag)) return false;
513
737
  let keyMap = this.items.get(key);
514
738
  if (!keyMap) {
515
739
  keyMap = /* @__PURE__ */ new Map();
@@ -517,7 +741,9 @@ var ORMap = class {
517
741
  }
518
742
  keyMap.set(record.tag, record);
519
743
  this.hlc.update(record.timestamp);
744
+ this.updateMerkleTree(key);
520
745
  this.notify();
746
+ return true;
521
747
  }
522
748
  /**
523
749
  * Applies a tombstone (deletion) from a remote source.
@@ -528,6 +754,7 @@ var ORMap = class {
528
754
  if (keyMap.has(tag)) {
529
755
  keyMap.delete(tag);
530
756
  if (keyMap.size === 0) this.items.delete(key);
757
+ this.updateMerkleTree(key);
531
758
  break;
532
759
  }
533
760
  }
@@ -540,6 +767,7 @@ var ORMap = class {
540
767
  * - Updates HLC with observed timestamps.
541
768
  */
542
769
  merge(other) {
770
+ const changedKeys = /* @__PURE__ */ new Set();
543
771
  for (const tag of other.tombstones) {
544
772
  this.tombstones.add(tag);
545
773
  }
@@ -553,6 +781,7 @@ var ORMap = class {
553
781
  if (!this.tombstones.has(tag)) {
554
782
  if (!localKeyMap.has(tag)) {
555
783
  localKeyMap.set(tag, record);
784
+ changedKeys.add(key);
556
785
  }
557
786
  this.hlc.update(record.timestamp);
558
787
  }
@@ -562,12 +791,16 @@ var ORMap = class {
562
791
  for (const tag of localKeyMap.keys()) {
563
792
  if (this.tombstones.has(tag)) {
564
793
  localKeyMap.delete(tag);
794
+ changedKeys.add(key);
565
795
  }
566
796
  }
567
797
  if (localKeyMap.size === 0) {
568
798
  this.items.delete(key);
569
799
  }
570
800
  }
801
+ for (const key of changedKeys) {
802
+ this.updateMerkleTree(key);
803
+ }
571
804
  this.notify();
572
805
  }
573
806
  /**
@@ -587,6 +820,108 @@ var ORMap = class {
587
820
  }
588
821
  return removedTags;
589
822
  }
823
+ // ============ Merkle Sync Methods ============
824
+ /**
825
+ * Get the Merkle Tree for this ORMap.
826
+ * Used for efficient synchronization.
827
+ */
828
+ getMerkleTree() {
829
+ return this.merkleTree;
830
+ }
831
+ /**
832
+ * Get a snapshot of internal state for Merkle Tree synchronization.
833
+ * Returns references to internal structures - do not modify!
834
+ */
835
+ getSnapshot() {
836
+ return {
837
+ items: this.items,
838
+ tombstones: this.tombstones
839
+ };
840
+ }
841
+ /**
842
+ * Get all keys in this ORMap.
843
+ */
844
+ allKeys() {
845
+ return Array.from(this.items.keys());
846
+ }
847
+ /**
848
+ * Get the internal records map for a key.
849
+ * Returns Map<tag, record> or undefined if key doesn't exist.
850
+ * Used for Merkle sync.
851
+ */
852
+ getRecordsMap(key) {
853
+ return this.items.get(key);
854
+ }
855
+ /**
856
+ * Merge remote records for a specific key into local state.
857
+ * Implements Observed-Remove CRDT semantics.
858
+ * Used during Merkle Tree synchronization.
859
+ *
860
+ * @param key The key to merge
861
+ * @param remoteRecords Array of records from remote
862
+ * @param remoteTombstones Array of tombstone tags from remote
863
+ * @returns Result with count of added and updated records
864
+ */
865
+ mergeKey(key, remoteRecords, remoteTombstones = []) {
866
+ let added = 0;
867
+ let updated = 0;
868
+ for (const tag of remoteTombstones) {
869
+ if (!this.tombstones.has(tag)) {
870
+ this.tombstones.add(tag);
871
+ }
872
+ }
873
+ let localKeyMap = this.items.get(key);
874
+ if (!localKeyMap) {
875
+ localKeyMap = /* @__PURE__ */ new Map();
876
+ this.items.set(key, localKeyMap);
877
+ }
878
+ for (const tag of localKeyMap.keys()) {
879
+ if (this.tombstones.has(tag)) {
880
+ localKeyMap.delete(tag);
881
+ }
882
+ }
883
+ for (const remoteRecord of remoteRecords) {
884
+ if (this.tombstones.has(remoteRecord.tag)) {
885
+ continue;
886
+ }
887
+ const localRecord = localKeyMap.get(remoteRecord.tag);
888
+ if (!localRecord) {
889
+ localKeyMap.set(remoteRecord.tag, remoteRecord);
890
+ added++;
891
+ } else if (compareTimestamps(remoteRecord.timestamp, localRecord.timestamp) > 0) {
892
+ localKeyMap.set(remoteRecord.tag, remoteRecord);
893
+ updated++;
894
+ }
895
+ this.hlc.update(remoteRecord.timestamp);
896
+ }
897
+ if (localKeyMap.size === 0) {
898
+ this.items.delete(key);
899
+ }
900
+ this.updateMerkleTree(key);
901
+ if (added > 0 || updated > 0) {
902
+ this.notify();
903
+ }
904
+ return { added, updated };
905
+ }
906
+ /**
907
+ * Check if a tag is tombstoned.
908
+ */
909
+ isTombstoned(tag) {
910
+ return this.tombstones.has(tag);
911
+ }
912
+ /**
913
+ * Update the Merkle Tree for a specific key.
914
+ * Called internally after any modification.
915
+ */
916
+ updateMerkleTree(key) {
917
+ const keyStr = String(key);
918
+ const keyMap = this.items.get(key);
919
+ if (!keyMap || keyMap.size === 0) {
920
+ this.merkleTree.remove(keyStr);
921
+ } else {
922
+ this.merkleTree.update(keyStr, keyMap);
923
+ }
924
+ }
590
925
  };
591
926
 
592
927
  // src/serializer.ts
@@ -850,6 +1185,91 @@ var TopicMessageEventSchema = z.object({
850
1185
  timestamp: z.number()
851
1186
  })
852
1187
  });
1188
+ var PingMessageSchema = z.object({
1189
+ type: z.literal("PING"),
1190
+ timestamp: z.number()
1191
+ // Client's Date.now()
1192
+ });
1193
+ var PongMessageSchema = z.object({
1194
+ type: z.literal("PONG"),
1195
+ timestamp: z.number(),
1196
+ // Echo back client's timestamp
1197
+ serverTime: z.number()
1198
+ // Server's Date.now() (for clock skew detection)
1199
+ });
1200
+ var ORMapSyncInitSchema = z.object({
1201
+ type: z.literal("ORMAP_SYNC_INIT"),
1202
+ mapName: z.string(),
1203
+ rootHash: z.number(),
1204
+ bucketHashes: z.record(z.string(), z.number()),
1205
+ // path -> hash
1206
+ lastSyncTimestamp: z.number().optional()
1207
+ });
1208
+ var ORMapSyncRespRootSchema = z.object({
1209
+ type: z.literal("ORMAP_SYNC_RESP_ROOT"),
1210
+ payload: z.object({
1211
+ mapName: z.string(),
1212
+ rootHash: z.number(),
1213
+ timestamp: TimestampSchema
1214
+ })
1215
+ });
1216
+ var ORMapSyncRespBucketsSchema = z.object({
1217
+ type: z.literal("ORMAP_SYNC_RESP_BUCKETS"),
1218
+ payload: z.object({
1219
+ mapName: z.string(),
1220
+ path: z.string(),
1221
+ buckets: z.record(z.string(), z.number())
1222
+ })
1223
+ });
1224
+ var ORMapMerkleReqBucketSchema = z.object({
1225
+ type: z.literal("ORMAP_MERKLE_REQ_BUCKET"),
1226
+ payload: z.object({
1227
+ mapName: z.string(),
1228
+ path: z.string()
1229
+ })
1230
+ });
1231
+ var ORMapSyncRespLeafSchema = z.object({
1232
+ type: z.literal("ORMAP_SYNC_RESP_LEAF"),
1233
+ payload: z.object({
1234
+ mapName: z.string(),
1235
+ path: z.string(),
1236
+ entries: z.array(z.object({
1237
+ key: z.string(),
1238
+ records: z.array(ORMapRecordSchema),
1239
+ tombstones: z.array(z.string())
1240
+ // Tombstone tags for this key's records
1241
+ }))
1242
+ })
1243
+ });
1244
+ var ORMapDiffRequestSchema = z.object({
1245
+ type: z.literal("ORMAP_DIFF_REQUEST"),
1246
+ payload: z.object({
1247
+ mapName: z.string(),
1248
+ keys: z.array(z.string())
1249
+ })
1250
+ });
1251
+ var ORMapDiffResponseSchema = z.object({
1252
+ type: z.literal("ORMAP_DIFF_RESPONSE"),
1253
+ payload: z.object({
1254
+ mapName: z.string(),
1255
+ entries: z.array(z.object({
1256
+ key: z.string(),
1257
+ records: z.array(ORMapRecordSchema),
1258
+ tombstones: z.array(z.string())
1259
+ }))
1260
+ })
1261
+ });
1262
+ var ORMapPushDiffSchema = z.object({
1263
+ type: z.literal("ORMAP_PUSH_DIFF"),
1264
+ payload: z.object({
1265
+ mapName: z.string(),
1266
+ entries: z.array(z.object({
1267
+ key: z.string(),
1268
+ records: z.array(ORMapRecordSchema),
1269
+ tombstones: z.array(z.string())
1270
+ }))
1271
+ })
1272
+ });
853
1273
  var MessageSchema = z.discriminatedUnion("type", [
854
1274
  AuthMessageSchema,
855
1275
  QuerySubMessageSchema,
@@ -865,7 +1285,18 @@ var MessageSchema = z.discriminatedUnion("type", [
865
1285
  LockReleaseSchema,
866
1286
  TopicSubSchema,
867
1287
  TopicUnsubSchema,
868
- TopicPubSchema
1288
+ TopicPubSchema,
1289
+ PingMessageSchema,
1290
+ PongMessageSchema,
1291
+ // ORMap Sync Messages
1292
+ ORMapSyncInitSchema,
1293
+ ORMapSyncRespRootSchema,
1294
+ ORMapSyncRespBucketsSchema,
1295
+ ORMapMerkleReqBucketSchema,
1296
+ ORMapSyncRespLeafSchema,
1297
+ ORMapDiffRequestSchema,
1298
+ ORMapDiffResponseSchema,
1299
+ ORMapPushDiffSchema
869
1300
  ]);
870
1301
  export {
871
1302
  AuthMessageSchema,
@@ -880,8 +1311,19 @@ export {
880
1311
  MerkleTree,
881
1312
  MessageSchema,
882
1313
  ORMap,
1314
+ ORMapDiffRequestSchema,
1315
+ ORMapDiffResponseSchema,
1316
+ ORMapMerkleReqBucketSchema,
1317
+ ORMapMerkleTree,
1318
+ ORMapPushDiffSchema,
883
1319
  ORMapRecordSchema,
1320
+ ORMapSyncInitSchema,
1321
+ ORMapSyncRespBucketsSchema,
1322
+ ORMapSyncRespLeafSchema,
1323
+ ORMapSyncRespRootSchema,
884
1324
  OpBatchMessageSchema,
1325
+ PingMessageSchema,
1326
+ PongMessageSchema,
885
1327
  PredicateNodeSchema,
886
1328
  PredicateOpSchema,
887
1329
  Predicates,
@@ -898,9 +1340,13 @@ export {
898
1340
  TopicSubSchema,
899
1341
  TopicUnsubSchema,
900
1342
  combineHashes,
1343
+ compareTimestamps,
901
1344
  deserialize,
902
1345
  evaluatePredicate,
1346
+ hashORMapEntry,
1347
+ hashORMapRecord,
903
1348
  hashString,
904
- serialize
1349
+ serialize,
1350
+ timestampToString
905
1351
  };
906
1352
  //# sourceMappingURL=index.mjs.map