@topgunbuild/core 0.10.1 → 0.12.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
@@ -1,10 +1,3 @@
1
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
- }) : x)(function(x) {
4
- if (typeof require !== "undefined") return require.apply(this, arguments);
5
- throw Error('Dynamic require of "' + x + '" is not supported');
6
- });
7
-
8
1
  // src/utils/logger.ts
9
2
  import pino from "pino";
10
3
  var logLevel = typeof process !== "undefined" && process.env && process.env.LOG_LEVEL || "info";
@@ -18,9 +11,13 @@ var logger = pino({
18
11
  // src/HLC.ts
19
12
  var HLC = class {
20
13
  constructor(nodeId, options = {}) {
14
+ if (nodeId.includes(":")) {
15
+ throw new Error('Node ID must not contain ":" (used as delimiter in timestamp format)');
16
+ }
21
17
  this.nodeId = nodeId;
22
18
  this.strictMode = options.strictMode ?? false;
23
19
  this.maxDriftMs = options.maxDriftMs ?? 6e4;
20
+ this.clockSource = options.clockSource ?? { now: () => Date.now() };
24
21
  this.lastMillis = 0;
25
22
  this.lastCounter = 0;
26
23
  }
@@ -33,12 +30,19 @@ var HLC = class {
33
30
  get getMaxDriftMs() {
34
31
  return this.maxDriftMs;
35
32
  }
33
+ /**
34
+ * Returns the clock source used by this HLC instance.
35
+ * Useful for LWWMap/ORMap to access the same clock for TTL checks.
36
+ */
37
+ getClockSource() {
38
+ return this.clockSource;
39
+ }
36
40
  /**
37
41
  * Generates a new unique timestamp for a local event.
38
42
  * Ensures monotonicity: always greater than any previously generated or received timestamp.
39
43
  */
40
44
  now() {
41
- const systemTime = Date.now();
45
+ const systemTime = this.clockSource.now();
42
46
  if (systemTime > this.lastMillis) {
43
47
  this.lastMillis = systemTime;
44
48
  this.lastCounter = 0;
@@ -56,27 +60,33 @@ var HLC = class {
56
60
  * Must be called whenever a message/event is received from another node.
57
61
  */
58
62
  update(remote) {
59
- const systemTime = Date.now();
60
- const drift = remote.millis - systemTime;
63
+ const remoteMillis = Number(remote.millis);
64
+ const remoteCounter = Number(remote.counter);
65
+ if (!Number.isFinite(remoteMillis) || !Number.isFinite(remoteCounter)) {
66
+ logger.warn({ remoteMillis, remoteCounter, remote }, "HLC.update() received invalid timestamp, ignoring");
67
+ return;
68
+ }
69
+ const systemTime = this.clockSource.now();
70
+ const drift = remoteMillis - systemTime;
61
71
  if (drift > this.maxDriftMs) {
62
72
  if (this.strictMode) {
63
- throw new Error(`Clock drift detected: Remote time ${remote.millis} is ${drift}ms ahead of local ${systemTime} (threshold: ${this.maxDriftMs}ms)`);
73
+ throw new Error(`Clock drift detected: Remote time ${remoteMillis} is ${drift}ms ahead of local ${systemTime} (threshold: ${this.maxDriftMs}ms)`);
64
74
  } else {
65
75
  logger.warn({
66
76
  drift,
67
- remoteMillis: remote.millis,
77
+ remoteMillis,
68
78
  localMillis: systemTime,
69
79
  maxDriftMs: this.maxDriftMs
70
80
  }, "Clock drift detected");
71
81
  }
72
82
  }
73
- const maxMillis = Math.max(this.lastMillis, systemTime, remote.millis);
74
- if (maxMillis === this.lastMillis && maxMillis === remote.millis) {
75
- this.lastCounter = Math.max(this.lastCounter, remote.counter) + 1;
83
+ const maxMillis = Math.max(this.lastMillis, systemTime, remoteMillis);
84
+ if (maxMillis === this.lastMillis && maxMillis === remoteMillis) {
85
+ this.lastCounter = Math.max(this.lastCounter, remoteCounter) + 1;
76
86
  } else if (maxMillis === this.lastMillis) {
77
87
  this.lastCounter++;
78
- } else if (maxMillis === remote.millis) {
79
- this.lastCounter = remote.counter + 1;
88
+ } else if (maxMillis === remoteMillis) {
89
+ this.lastCounter = remoteCounter + 1;
80
90
  } else {
81
91
  this.lastCounter = 0;
82
92
  }
@@ -119,17 +129,7 @@ var HLC = class {
119
129
  };
120
130
 
121
131
  // src/utils/hash.ts
122
- var nativeHash = null;
123
- var nativeLoadAttempted = false;
124
- function tryLoadNative() {
125
- if (nativeLoadAttempted) return;
126
- nativeLoadAttempted = true;
127
- try {
128
- nativeHash = __require("@topgunbuild/native");
129
- } catch {
130
- }
131
- }
132
- function fnv1aHash(str) {
132
+ function hashString(str) {
133
133
  let hash = 2166136261;
134
134
  for (let i = 0; i < str.length; i++) {
135
135
  hash ^= str.charCodeAt(i);
@@ -137,13 +137,6 @@ function fnv1aHash(str) {
137
137
  }
138
138
  return hash >>> 0;
139
139
  }
140
- function hashString(str) {
141
- tryLoadNative();
142
- if (nativeHash && nativeHash.isNativeHashAvailable()) {
143
- return nativeHash.hashString(str);
144
- }
145
- return fnv1aHash(str);
146
- }
147
140
  function combineHashes(hashes) {
148
141
  let result = 0;
149
142
  for (const h of hashes) {
@@ -151,18 +144,6 @@ function combineHashes(hashes) {
151
144
  }
152
145
  return result >>> 0;
153
146
  }
154
- function isUsingNativeHash() {
155
- tryLoadNative();
156
- return nativeHash?.isNativeHashAvailable() === true;
157
- }
158
- function disableNativeHash() {
159
- nativeHash = null;
160
- nativeLoadAttempted = true;
161
- }
162
- function resetNativeHash() {
163
- nativeHash = null;
164
- nativeLoadAttempted = false;
165
- }
166
147
  function hashObject(obj) {
167
148
  const json = JSON.stringify(obj, (_, value) => {
168
149
  if (value && typeof value === "object" && !Array.isArray(value)) {
@@ -340,7 +321,7 @@ var LWWMap = class {
340
321
  return void 0;
341
322
  }
342
323
  if (record.ttlMs) {
343
- const now = Date.now();
324
+ const now = this.hlc.getClockSource().now();
344
325
  if (record.timestamp.millis + record.ttlMs < now) {
345
326
  return void 0;
346
327
  }
@@ -417,7 +398,7 @@ var LWWMap = class {
417
398
  */
418
399
  entries() {
419
400
  const iterator = this.data.entries();
420
- const now = Date.now();
401
+ const clockSource = this.hlc.getClockSource();
421
402
  return {
422
403
  [Symbol.iterator]() {
423
404
  return this;
@@ -427,7 +408,7 @@ var LWWMap = class {
427
408
  while (!result.done) {
428
409
  const [key, record] = result.value;
429
410
  if (record.value !== null) {
430
- if (record.ttlMs && record.timestamp.millis + record.ttlMs < now) {
411
+ if (record.ttlMs && record.timestamp.millis + record.ttlMs < clockSource.now()) {
431
412
  result = iterator.next();
432
413
  continue;
433
414
  }
@@ -764,7 +745,7 @@ var ORMap = class {
764
745
  const keyMap = this.items.get(key);
765
746
  if (!keyMap) return [];
766
747
  const values = [];
767
- const now = Date.now();
748
+ const now = this.hlc.getClockSource().now();
768
749
  for (const [tag, record] of keyMap.entries()) {
769
750
  if (!this.tombstones.has(tag)) {
770
751
  if (record.ttlMs && record.timestamp.millis + record.ttlMs < now) {
@@ -784,7 +765,7 @@ var ORMap = class {
784
765
  const keyMap = this.items.get(key);
785
766
  if (!keyMap) return [];
786
767
  const records = [];
787
- const now = Date.now();
768
+ const now = this.hlc.getClockSource().now();
788
769
  for (const [tag, record] of keyMap.entries()) {
789
770
  if (!this.tombstones.has(tag)) {
790
771
  if (record.ttlMs && record.timestamp.millis + record.ttlMs < now) {
@@ -1000,11 +981,49 @@ var ORMap = class {
1000
981
  // src/serializer.ts
1001
982
  import { pack, unpack } from "msgpackr";
1002
983
  function serialize(data) {
1003
- return pack(data);
984
+ return pack(stripUndefined(data));
1004
985
  }
1005
986
  function deserialize(data) {
1006
987
  const buffer = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
1007
- return unpack(buffer);
988
+ const result = unpack(buffer);
989
+ return coerceBigInts(result);
990
+ }
991
+ function stripUndefined(value) {
992
+ if (value === void 0) {
993
+ return null;
994
+ }
995
+ if (Array.isArray(value)) {
996
+ return value.map(stripUndefined);
997
+ }
998
+ if (value !== null && typeof value === "object") {
999
+ const result = {};
1000
+ for (const [k, v] of Object.entries(value)) {
1001
+ if (v !== void 0) {
1002
+ result[k] = stripUndefined(v);
1003
+ }
1004
+ }
1005
+ return result;
1006
+ }
1007
+ return value;
1008
+ }
1009
+ function coerceBigInts(value) {
1010
+ if (typeof value === "bigint") {
1011
+ return Number(value);
1012
+ }
1013
+ if (Array.isArray(value)) {
1014
+ for (let i = 0; i < value.length; i++) {
1015
+ value[i] = coerceBigInts(value[i]);
1016
+ }
1017
+ return value;
1018
+ }
1019
+ if (value !== null && typeof value === "object") {
1020
+ const obj = value;
1021
+ for (const key of Object.keys(obj)) {
1022
+ obj[key] = coerceBigInts(obj[key]);
1023
+ }
1024
+ return obj;
1025
+ }
1026
+ return value;
1008
1027
  }
1009
1028
 
1010
1029
  // src/PNCounter.ts
@@ -2179,6 +2198,7 @@ var ORMapRecordSchema = z3.object({
2179
2198
  tag: z3.string(),
2180
2199
  ttlMs: z3.number().optional()
2181
2200
  });
2201
+ var ChangeEventTypeSchema = z3.enum(["ENTER", "UPDATE", "LEAVE"]);
2182
2202
  var PredicateOpSchema = z3.enum([
2183
2203
  "eq",
2184
2204
  "neq",
@@ -2219,7 +2239,11 @@ var ClientOpSchema = z3.object({
2219
2239
  });
2220
2240
  var AuthMessageSchema = z3.object({
2221
2241
  type: z3.literal("AUTH"),
2222
- token: z3.string()
2242
+ token: z3.string(),
2243
+ protocolVersion: z3.number().optional()
2244
+ });
2245
+ var AuthRequiredMessageSchema = z3.object({
2246
+ type: z3.literal("AUTH_REQUIRED")
2223
2247
  });
2224
2248
 
2225
2249
  // src/schemas/sync-schemas.ts
@@ -2275,6 +2299,11 @@ var MerkleReqBucketMessageSchema = z4.object({
2275
2299
  path: z4.string()
2276
2300
  })
2277
2301
  });
2302
+ var ORMapEntrySchema = z4.object({
2303
+ key: z4.string(),
2304
+ records: z4.array(ORMapRecordSchema),
2305
+ tombstones: z4.array(z4.string())
2306
+ });
2278
2307
  var ORMapSyncInitSchema = z4.object({
2279
2308
  type: z4.literal("ORMAP_SYNC_INIT"),
2280
2309
  mapName: z4.string(),
@@ -2310,11 +2339,7 @@ var ORMapSyncRespLeafSchema = z4.object({
2310
2339
  payload: z4.object({
2311
2340
  mapName: z4.string(),
2312
2341
  path: z4.string(),
2313
- entries: z4.array(z4.object({
2314
- key: z4.string(),
2315
- records: z4.array(ORMapRecordSchema),
2316
- tombstones: z4.array(z4.string())
2317
- }))
2342
+ entries: z4.array(ORMapEntrySchema)
2318
2343
  })
2319
2344
  });
2320
2345
  var ORMapDiffRequestSchema = z4.object({
@@ -2328,22 +2353,14 @@ var ORMapDiffResponseSchema = z4.object({
2328
2353
  type: z4.literal("ORMAP_DIFF_RESPONSE"),
2329
2354
  payload: z4.object({
2330
2355
  mapName: z4.string(),
2331
- entries: z4.array(z4.object({
2332
- key: z4.string(),
2333
- records: z4.array(ORMapRecordSchema),
2334
- tombstones: z4.array(z4.string())
2335
- }))
2356
+ entries: z4.array(ORMapEntrySchema)
2336
2357
  })
2337
2358
  });
2338
2359
  var ORMapPushDiffSchema = z4.object({
2339
2360
  type: z4.literal("ORMAP_PUSH_DIFF"),
2340
2361
  payload: z4.object({
2341
2362
  mapName: z4.string(),
2342
- entries: z4.array(z4.object({
2343
- key: z4.string(),
2344
- records: z4.array(ORMapRecordSchema),
2345
- tombstones: z4.array(z4.string())
2346
- }))
2363
+ entries: z4.array(ORMapEntrySchema)
2347
2364
  })
2348
2365
  });
2349
2366
  var OpResultSchema = z4.object({
@@ -2438,7 +2455,7 @@ var SearchRespMessageSchema = z6.object({
2438
2455
  type: z6.literal("SEARCH_RESP"),
2439
2456
  payload: SearchRespPayloadSchema
2440
2457
  });
2441
- var SearchUpdateTypeSchema = z6.enum(["ENTER", "UPDATE", "LEAVE"]);
2458
+ var SearchUpdateTypeSchema = ChangeEventTypeSchema;
2442
2459
  var SearchSubPayloadSchema = z6.object({
2443
2460
  subscriptionId: z6.string(),
2444
2461
  mapName: z6.string(),
@@ -2455,7 +2472,7 @@ var SearchUpdatePayloadSchema = z6.object({
2455
2472
  value: z6.unknown(),
2456
2473
  score: z6.number(),
2457
2474
  matchedTerms: z6.array(z6.string()),
2458
- type: SearchUpdateTypeSchema
2475
+ changeType: SearchUpdateTypeSchema
2459
2476
  });
2460
2477
  var SearchUpdateMessageSchema = z6.object({
2461
2478
  type: z6.literal("SEARCH_UPDATE"),
@@ -2477,17 +2494,37 @@ var PartitionMapRequestSchema = z7.object({
2477
2494
  currentVersion: z7.number().optional()
2478
2495
  }).optional()
2479
2496
  });
2497
+ var NodeInfoSchema = z7.object({
2498
+ nodeId: z7.string(),
2499
+ endpoints: z7.object({
2500
+ websocket: z7.string(),
2501
+ http: z7.string().optional()
2502
+ }),
2503
+ status: z7.enum(["ACTIVE", "JOINING", "LEAVING", "SUSPECTED", "FAILED"])
2504
+ });
2505
+ var PartitionInfoSchema = z7.object({
2506
+ partitionId: z7.number(),
2507
+ ownerNodeId: z7.string(),
2508
+ backupNodeIds: z7.array(z7.string())
2509
+ });
2510
+ var PartitionMapPayloadSchema = z7.object({
2511
+ version: z7.number(),
2512
+ partitionCount: z7.number(),
2513
+ nodes: z7.array(NodeInfoSchema),
2514
+ partitions: z7.array(PartitionInfoSchema),
2515
+ generatedAt: z7.number()
2516
+ });
2517
+ var PartitionMapMessageSchema = z7.object({
2518
+ type: z7.literal("PARTITION_MAP"),
2519
+ payload: PartitionMapPayloadSchema
2520
+ });
2480
2521
  var ClusterSubRegisterPayloadSchema = z7.object({
2481
2522
  subscriptionId: z7.string(),
2482
2523
  coordinatorNodeId: z7.string(),
2483
2524
  mapName: z7.string(),
2484
2525
  type: z7.enum(["SEARCH", "QUERY"]),
2485
2526
  searchQuery: z7.string().optional(),
2486
- searchOptions: z7.object({
2487
- limit: z7.number().int().positive().optional(),
2488
- minScore: z7.number().optional(),
2489
- boost: z7.record(z7.string(), z7.number()).optional()
2490
- }).optional(),
2527
+ searchOptions: SearchOptionsSchema.optional(),
2491
2528
  queryPredicate: z7.any().optional(),
2492
2529
  querySort: z7.record(z7.string(), z7.enum(["asc", "desc"])).optional()
2493
2530
  });
@@ -2519,7 +2556,7 @@ var ClusterSubUpdatePayloadSchema = z7.object({
2519
2556
  value: z7.unknown(),
2520
2557
  score: z7.number().optional(),
2521
2558
  matchedTerms: z7.array(z7.string()).optional(),
2522
- changeType: z7.enum(["ENTER", "UPDATE", "LEAVE"]),
2559
+ changeType: ChangeEventTypeSchema,
2523
2560
  timestamp: z7.number()
2524
2561
  });
2525
2562
  var ClusterSubUpdateMessageSchema = z7.object({
@@ -2537,10 +2574,8 @@ var ClusterSearchReqPayloadSchema = z7.object({
2537
2574
  requestId: z7.string(),
2538
2575
  mapName: z7.string(),
2539
2576
  query: z7.string(),
2540
- options: z7.object({
2577
+ options: SearchOptionsSchema.extend({
2541
2578
  limit: z7.number().int().positive().max(1e3),
2542
- minScore: z7.number().optional(),
2543
- boost: z7.record(z7.string(), z7.number()).optional(),
2544
2579
  includeMatchedTerms: z7.boolean().optional(),
2545
2580
  afterScore: z7.number().optional(),
2546
2581
  afterKey: z7.string().optional()
@@ -2592,7 +2627,7 @@ var ClusterSearchUpdatePayloadSchema = z7.object({
2592
2627
  value: z7.unknown(),
2593
2628
  score: z7.number(),
2594
2629
  matchedTerms: z7.array(z7.string()).optional(),
2595
- type: SearchUpdateTypeSchema
2630
+ changeType: ChangeEventTypeSchema
2596
2631
  });
2597
2632
  var ClusterSearchUpdateMessageSchema = z7.object({
2598
2633
  type: z7.literal("CLUSTER_SEARCH_UPDATE"),
@@ -2835,7 +2870,7 @@ var QueryUpdatePayloadSchema = z9.object({
2835
2870
  queryId: z9.string(),
2836
2871
  key: z9.string(),
2837
2872
  value: z9.unknown(),
2838
- type: z9.enum(["ENTER", "UPDATE", "REMOVE"])
2873
+ changeType: ChangeEventTypeSchema
2839
2874
  });
2840
2875
  var QueryUpdateMessageSchema = z9.object({
2841
2876
  type: z9.literal("QUERY_UPDATE"),
@@ -2848,50 +2883,135 @@ var GcPruneMessageSchema = z9.object({
2848
2883
  type: z9.literal("GC_PRUNE"),
2849
2884
  payload: GcPrunePayloadSchema
2850
2885
  });
2886
+ var AuthAckMessageSchema = z9.object({
2887
+ type: z9.literal("AUTH_ACK"),
2888
+ protocolVersion: z9.number().optional(),
2889
+ userId: z9.string().optional()
2890
+ });
2851
2891
  var AuthFailMessageSchema = z9.object({
2852
2892
  type: z9.literal("AUTH_FAIL"),
2853
2893
  error: z9.string().optional(),
2854
2894
  code: z9.number().optional()
2855
2895
  });
2856
- var HybridQueryRespPayloadSchema = z9.object({
2857
- subscriptionId: z9.string(),
2858
- results: z9.array(z9.object({
2859
- key: z9.string(),
2860
- value: z9.unknown(),
2861
- score: z9.number(),
2862
- matchedTerms: z9.array(z9.string())
2863
- })),
2864
- nextCursor: z9.string().optional(),
2865
- hasMore: z9.boolean().optional(),
2866
- cursorStatus: CursorStatusSchema.optional()
2867
- });
2868
- var HybridQueryDeltaPayloadSchema = z9.object({
2869
- subscriptionId: z9.string(),
2870
- key: z9.string(),
2871
- value: z9.unknown().nullable(),
2872
- score: z9.number().optional(),
2873
- matchedTerms: z9.array(z9.string()).optional(),
2874
- type: z9.enum(["ENTER", "UPDATE", "LEAVE"])
2896
+ var ErrorMessageSchema = z9.object({
2897
+ type: z9.literal("ERROR"),
2898
+ payload: z9.object({
2899
+ code: z9.number(),
2900
+ message: z9.string(),
2901
+ details: z9.unknown().optional()
2902
+ })
2875
2903
  });
2876
2904
  var LockGrantedPayloadSchema = z9.object({
2877
2905
  requestId: z9.string(),
2906
+ name: z9.string(),
2878
2907
  fencingToken: z9.number()
2879
2908
  });
2880
2909
  var LockReleasedPayloadSchema = z9.object({
2881
2910
  requestId: z9.string(),
2911
+ name: z9.string(),
2882
2912
  success: z9.boolean()
2883
2913
  });
2914
+ var LockGrantedMessageSchema = z9.object({
2915
+ type: z9.literal("LOCK_GRANTED"),
2916
+ payload: LockGrantedPayloadSchema
2917
+ });
2918
+ var LockReleasedMessageSchema = z9.object({
2919
+ type: z9.literal("LOCK_RELEASED"),
2920
+ payload: LockReleasedPayloadSchema
2921
+ });
2884
2922
  var SyncResetRequiredPayloadSchema = z9.object({
2885
2923
  mapName: z9.string(),
2886
2924
  reason: z9.string()
2887
2925
  });
2926
+ var SyncResetRequiredMessageSchema = z9.object({
2927
+ type: z9.literal("SYNC_RESET_REQUIRED"),
2928
+ payload: SyncResetRequiredPayloadSchema
2929
+ });
2888
2930
 
2889
- // src/schemas/index.ts
2931
+ // src/schemas/http-sync-schemas.ts
2890
2932
  import { z as z10 } from "zod";
2891
- var MessageSchema = z10.discriminatedUnion("type", [
2933
+ var SyncMapEntrySchema = z10.object({
2934
+ mapName: z10.string(),
2935
+ lastSyncTimestamp: TimestampSchema
2936
+ });
2937
+ var HttpQueryRequestSchema = z10.object({
2938
+ queryId: z10.string(),
2939
+ mapName: z10.string(),
2940
+ filter: z10.any(),
2941
+ limit: z10.number().optional(),
2942
+ offset: z10.number().optional()
2943
+ });
2944
+ var HttpSearchRequestSchema = z10.object({
2945
+ searchId: z10.string(),
2946
+ mapName: z10.string(),
2947
+ query: z10.string(),
2948
+ options: z10.any().optional()
2949
+ });
2950
+ var HttpSyncRequestSchema = z10.object({
2951
+ // Client identification
2952
+ clientId: z10.string(),
2953
+ // Client's current HLC for causality tracking
2954
+ clientHlc: TimestampSchema,
2955
+ // Batch of operations to push (optional)
2956
+ operations: z10.array(ClientOpSchema).optional(),
2957
+ // Maps the client wants deltas for, with their last known sync HLC timestamp
2958
+ syncMaps: z10.array(SyncMapEntrySchema).optional(),
2959
+ // One-shot queries to execute (optional)
2960
+ queries: z10.array(HttpQueryRequestSchema).optional(),
2961
+ // One-shot search requests (optional)
2962
+ searches: z10.array(HttpSearchRequestSchema).optional()
2963
+ });
2964
+ var DeltaRecordSchema = z10.object({
2965
+ key: z10.string(),
2966
+ record: LWWRecordSchema,
2967
+ eventType: z10.enum(["PUT", "REMOVE"])
2968
+ });
2969
+ var MapDeltaSchema = z10.object({
2970
+ mapName: z10.string(),
2971
+ records: z10.array(DeltaRecordSchema),
2972
+ serverSyncTimestamp: TimestampSchema
2973
+ });
2974
+ var HttpQueryResultSchema = z10.object({
2975
+ queryId: z10.string(),
2976
+ results: z10.array(z10.any()),
2977
+ hasMore: z10.boolean().optional(),
2978
+ nextCursor: z10.string().optional()
2979
+ });
2980
+ var HttpSearchResultSchema = z10.object({
2981
+ searchId: z10.string(),
2982
+ results: z10.array(z10.any()),
2983
+ totalCount: z10.number().optional()
2984
+ });
2985
+ var HttpSyncErrorSchema = z10.object({
2986
+ code: z10.number(),
2987
+ message: z10.string(),
2988
+ context: z10.string().optional()
2989
+ });
2990
+ var HttpSyncResponseSchema = z10.object({
2991
+ // Server's current HLC
2992
+ serverHlc: TimestampSchema,
2993
+ // Acknowledgment of received operations
2994
+ ack: z10.object({
2995
+ lastId: z10.string(),
2996
+ results: z10.array(OpResultSchema).optional()
2997
+ }).optional(),
2998
+ // Delta records for requested maps (new/changed since lastSyncTimestamp)
2999
+ deltas: z10.array(MapDeltaSchema).optional(),
3000
+ // Query results
3001
+ queryResults: z10.array(HttpQueryResultSchema).optional(),
3002
+ // Search results
3003
+ searchResults: z10.array(HttpSearchResultSchema).optional(),
3004
+ // Errors for individual operations
3005
+ errors: z10.array(HttpSyncErrorSchema).optional()
3006
+ });
3007
+
3008
+ // src/schemas/index.ts
3009
+ import { z as z11 } from "zod";
3010
+ var MessageSchema = z11.discriminatedUnion("type", [
3011
+ // --- Base ---
2892
3012
  AuthMessageSchema,
2893
- QuerySubMessageSchema,
2894
- QueryUnsubMessageSchema,
3013
+ AuthRequiredMessageSchema,
3014
+ // --- Sync ---
2895
3015
  ClientOpMessageSchema,
2896
3016
  OpBatchMessageSchema,
2897
3017
  SyncInitMessageSchema,
@@ -2899,13 +3019,10 @@ var MessageSchema = z10.discriminatedUnion("type", [
2899
3019
  SyncRespBucketsMessageSchema,
2900
3020
  SyncRespLeafMessageSchema,
2901
3021
  MerkleReqBucketMessageSchema,
2902
- LockRequestSchema,
2903
- LockReleaseSchema,
2904
- TopicSubSchema,
2905
- TopicUnsubSchema,
2906
- TopicPubSchema,
2907
- PingMessageSchema,
2908
- PongMessageSchema,
3022
+ OpAckMessageSchema,
3023
+ OpRejectedMessageSchema,
3024
+ BatchMessageSchema,
3025
+ // --- ORMap Sync ---
2909
3026
  ORMapSyncInitSchema,
2910
3027
  ORMapSyncRespRootSchema,
2911
3028
  ORMapSyncRespBucketsSchema,
@@ -2914,18 +3031,54 @@ var MessageSchema = z10.discriminatedUnion("type", [
2914
3031
  ORMapDiffRequestSchema,
2915
3032
  ORMapDiffResponseSchema,
2916
3033
  ORMapPushDiffSchema,
3034
+ // --- Query ---
3035
+ QuerySubMessageSchema,
3036
+ QueryUnsubMessageSchema,
3037
+ QueryRespMessageSchema,
3038
+ QueryUpdateMessageSchema,
3039
+ // --- Search ---
3040
+ SearchMessageSchema,
3041
+ SearchRespMessageSchema,
3042
+ SearchSubMessageSchema,
3043
+ SearchUpdateMessageSchema,
3044
+ SearchUnsubMessageSchema,
3045
+ // --- Cluster ---
2917
3046
  PartitionMapRequestSchema,
3047
+ PartitionMapMessageSchema,
3048
+ ClusterSubRegisterMessageSchema,
3049
+ ClusterSubAckMessageSchema,
3050
+ ClusterSubUpdateMessageSchema,
3051
+ ClusterSubUnregisterMessageSchema,
3052
+ ClusterSearchReqMessageSchema,
3053
+ ClusterSearchRespMessageSchema,
3054
+ ClusterSearchSubscribeMessageSchema,
3055
+ ClusterSearchUnsubscribeMessageSchema,
3056
+ ClusterSearchUpdateMessageSchema,
3057
+ // --- Messaging ---
3058
+ TopicSubSchema,
3059
+ TopicUnsubSchema,
3060
+ TopicPubSchema,
3061
+ TopicMessageEventSchema,
3062
+ LockRequestSchema,
3063
+ LockReleaseSchema,
2918
3064
  CounterRequestSchema,
2919
3065
  CounterSyncSchema,
3066
+ CounterResponseSchema,
3067
+ CounterUpdateSchema,
3068
+ PingMessageSchema,
3069
+ PongMessageSchema,
3070
+ // --- Entry Processor ---
2920
3071
  EntryProcessRequestSchema,
2921
3072
  EntryProcessBatchRequestSchema,
2922
3073
  EntryProcessResponseSchema,
2923
3074
  EntryProcessBatchResponseSchema,
3075
+ // --- Journal ---
2924
3076
  JournalSubscribeRequestSchema,
2925
3077
  JournalUnsubscribeRequestSchema,
2926
3078
  JournalEventMessageSchema,
2927
3079
  JournalReadRequestSchema,
2928
3080
  JournalReadResponseSchema,
3081
+ // --- Conflict Resolver ---
2929
3082
  RegisterResolverRequestSchema,
2930
3083
  RegisterResolverResponseSchema,
2931
3084
  UnregisterResolverRequestSchema,
@@ -2933,15 +3086,16 @@ var MessageSchema = z10.discriminatedUnion("type", [
2933
3086
  MergeRejectedMessageSchema,
2934
3087
  ListResolversRequestSchema,
2935
3088
  ListResolversResponseSchema,
2936
- SearchMessageSchema,
2937
- SearchRespMessageSchema,
2938
- SearchSubMessageSchema,
2939
- SearchUpdateMessageSchema,
2940
- SearchUnsubMessageSchema,
2941
- ClusterSubRegisterMessageSchema,
2942
- ClusterSubAckMessageSchema,
2943
- ClusterSubUpdateMessageSchema,
2944
- ClusterSubUnregisterMessageSchema
3089
+ // --- Server-to-Client ---
3090
+ ServerEventMessageSchema,
3091
+ ServerBatchEventMessageSchema,
3092
+ GcPruneMessageSchema,
3093
+ AuthAckMessageSchema,
3094
+ AuthFailMessageSchema,
3095
+ ErrorMessageSchema,
3096
+ LockGrantedMessageSchema,
3097
+ LockReleasedMessageSchema,
3098
+ SyncResetRequiredMessageSchema
2945
3099
  ]);
2946
3100
 
2947
3101
  // src/types/WriteConcern.ts
@@ -3901,6 +4055,18 @@ function createPredicateMatcher(getAttribute) {
3901
4055
  }
3902
4056
 
3903
4057
  // src/query/QueryTypes.ts
4058
+ var COST_WEIGHTS = {
4059
+ CPU: 1,
4060
+ NETWORK: 10,
4061
+ // Network is expensive (latency, bandwidth)
4062
+ IO: 5,
4063
+ // Disk I/O is moderately expensive
4064
+ ROWS: 1e-3
4065
+ // Row count factor
4066
+ };
4067
+ function calculateTotalCost(cost) {
4068
+ return cost.rows * COST_WEIGHTS.ROWS + cost.cpu * COST_WEIGHTS.CPU + cost.network * COST_WEIGHTS.NETWORK + cost.io * COST_WEIGHTS.IO;
4069
+ }
3904
4070
  function isSimpleQuery(query) {
3905
4071
  return [
3906
4072
  "eq",
@@ -6222,13 +6388,22 @@ var QueryOptimizer = class {
6222
6388
  * Optimize a query and return an execution plan.
6223
6389
  *
6224
6390
  * Optimization order (by cost):
6225
- * 1. StandingQueryIndex (cost: 10) - pre-computed results
6226
- * 2. Other indexes via optimizeNode
6391
+ * 1. Point lookup (cost: 1) - direct primary key access
6392
+ * 2. StandingQueryIndex (cost: 10) - pre-computed results
6393
+ * 3. Other indexes via optimizeNode
6227
6394
  *
6228
6395
  * @param query - Query to optimize
6229
6396
  * @returns Query execution plan
6230
6397
  */
6231
6398
  optimize(query) {
6399
+ const pointLookupStep = this.tryPointLookup(query);
6400
+ if (pointLookupStep) {
6401
+ return {
6402
+ root: pointLookupStep,
6403
+ estimatedCost: this.estimateCost(pointLookupStep),
6404
+ usesIndexes: this.usesIndexes(pointLookupStep)
6405
+ };
6406
+ }
6232
6407
  if (this.standingQueryRegistry) {
6233
6408
  const standingIndex = this.standingQueryRegistry.getIndex(query);
6234
6409
  if (standingIndex) {
@@ -6252,16 +6427,68 @@ var QueryOptimizer = class {
6252
6427
  };
6253
6428
  }
6254
6429
  /**
6255
- * Optimize a query with sort/limit/offset options.
6430
+ * Optimize a query with sort/limit/offset options and index hints.
6431
+ *
6432
+ * Hint precedence: disableOptimization > useIndex > forceIndexScan.
6256
6433
  *
6257
6434
  * @param query - Query to optimize
6258
- * @param options - Query options (sort, limit, offset)
6435
+ * @param options - Query options (sort, limit, cursor, hints)
6259
6436
  * @returns Query execution plan with options
6260
6437
  */
6261
6438
  optimizeWithOptions(query, options) {
6439
+ if (options.disableOptimization) {
6440
+ return {
6441
+ root: { type: "full-scan", predicate: query },
6442
+ estimatedCost: Number.MAX_SAFE_INTEGER,
6443
+ usesIndexes: false
6444
+ };
6445
+ }
6446
+ if (options.useIndex) {
6447
+ const indexes = this.indexRegistry.getIndexes(options.useIndex);
6448
+ if (indexes.length === 0) {
6449
+ throw new Error(
6450
+ `Index hint: no index found for attribute "${options.useIndex}"`
6451
+ );
6452
+ }
6453
+ let best = indexes[0];
6454
+ for (let i = 1; i < indexes.length; i++) {
6455
+ if (indexes[i].getRetrievalCost() < best.getRetrievalCost()) {
6456
+ best = indexes[i];
6457
+ }
6458
+ }
6459
+ const step = {
6460
+ type: "index-scan",
6461
+ index: best,
6462
+ query: this.buildHintedIndexQuery(query, options.useIndex)
6463
+ };
6464
+ return this.applyPlanOptions(
6465
+ {
6466
+ root: step,
6467
+ estimatedCost: best.getRetrievalCost(),
6468
+ usesIndexes: true,
6469
+ hint: options.useIndex
6470
+ },
6471
+ options
6472
+ );
6473
+ }
6262
6474
  const basePlan = this.optimize(query);
6475
+ if (options.forceIndexScan && !basePlan.usesIndexes) {
6476
+ throw new Error(
6477
+ "No suitable index found and forceIndexScan is enabled"
6478
+ );
6479
+ }
6480
+ return this.applyPlanOptions(basePlan, options);
6481
+ }
6482
+ /**
6483
+ * Apply sort/limit/cursor options to a query plan.
6484
+ *
6485
+ * @param plan - Base query plan
6486
+ * @param options - Query options with sort/limit/cursor
6487
+ * @returns Plan with options applied
6488
+ */
6489
+ applyPlanOptions(plan, options) {
6263
6490
  if (!options.sort && options.limit === void 0 && options.cursor === void 0) {
6264
- return basePlan;
6491
+ return plan;
6265
6492
  }
6266
6493
  let indexedSort = false;
6267
6494
  let sortField;
@@ -6278,14 +6505,73 @@ var QueryOptimizer = class {
6278
6505
  }
6279
6506
  }
6280
6507
  return {
6281
- ...basePlan,
6508
+ ...plan,
6282
6509
  indexedSort,
6283
6510
  sort: sortField && sortDirection ? { field: sortField, direction: sortDirection } : void 0,
6284
6511
  limit: options.limit,
6285
6512
  cursor: options.cursor
6286
- // replaces offset
6287
6513
  };
6288
6514
  }
6515
+ /**
6516
+ * Extract the relevant index query for a hinted attribute from the query tree.
6517
+ * If the query directly references the attribute, build an index query from it.
6518
+ * For compound (logical) queries, extract the matching child predicate.
6519
+ * Falls back to { type: 'has' } when no matching predicate is found,
6520
+ * retrieving all entries from the index for post-filtering.
6521
+ * FTS query nodes also fall back to { type: 'has' } since full-text search
6522
+ * queries are not compatible with regular index lookups.
6523
+ *
6524
+ * @param query - Original query tree
6525
+ * @param attributeName - Hinted attribute name
6526
+ * @returns Index query for the hinted attribute
6527
+ */
6528
+ buildHintedIndexQuery(query, attributeName) {
6529
+ if (isSimpleQuery(query) && query.attribute === attributeName) {
6530
+ return this.buildIndexQuery(query);
6531
+ }
6532
+ if (isFTSQuery(query)) {
6533
+ return { type: "has" };
6534
+ }
6535
+ if (isLogicalQuery(query) && query.children) {
6536
+ for (const child of query.children) {
6537
+ if (isSimpleQuery(child) && child.attribute === attributeName) {
6538
+ return this.buildIndexQuery(child);
6539
+ }
6540
+ }
6541
+ }
6542
+ return { type: "has" };
6543
+ }
6544
+ /**
6545
+ * Try to optimize query as a point lookup.
6546
+ * Returns a point lookup step if query is an equality or IN query on primary key.
6547
+ *
6548
+ * @param query - Query to check
6549
+ * @returns Point lookup step or null
6550
+ */
6551
+ tryPointLookup(query) {
6552
+ if (!isSimpleQuery(query)) {
6553
+ return null;
6554
+ }
6555
+ const primaryKeyFields = ["_key", "key", "id"];
6556
+ if (!primaryKeyFields.includes(query.attribute)) {
6557
+ return null;
6558
+ }
6559
+ if (query.type === "eq") {
6560
+ return {
6561
+ type: "point-lookup",
6562
+ key: query.value,
6563
+ cost: 1
6564
+ };
6565
+ }
6566
+ if (query.type === "in" && query.values) {
6567
+ return {
6568
+ type: "multi-point-lookup",
6569
+ keys: query.values,
6570
+ cost: query.values.length
6571
+ };
6572
+ }
6573
+ return null;
6574
+ }
6289
6575
  /**
6290
6576
  * Optimize a single query node.
6291
6577
  */
@@ -6674,6 +6960,9 @@ var QueryOptimizer = class {
6674
6960
  */
6675
6961
  estimateCost(step) {
6676
6962
  switch (step.type) {
6963
+ case "point-lookup":
6964
+ case "multi-point-lookup":
6965
+ return step.cost;
6677
6966
  case "index-scan":
6678
6967
  return step.index.getRetrievalCost();
6679
6968
  case "full-scan":
@@ -6708,11 +6997,89 @@ var QueryOptimizer = class {
6708
6997
  return Number.MAX_SAFE_INTEGER;
6709
6998
  }
6710
6999
  }
7000
+ /**
7001
+ * Estimate distributed cost including network overhead.
7002
+ *
7003
+ * Network cost is assigned based on step type:
7004
+ * - full-scan: broadcast to all nodes (highest cost)
7005
+ * - index-scan: 0 if local partition, 5 if remote
7006
+ * - point-lookup: 0 if local key, 5 if remote
7007
+ * - intersection/union: aggregating results from multiple sources
7008
+ *
7009
+ * @param step - Plan step to estimate
7010
+ * @param context - Distributed query context (optional)
7011
+ * @returns Distributed cost breakdown
7012
+ */
7013
+ estimateDistributedCost(step, context) {
7014
+ const baseCost = this.estimateCost(step);
7015
+ if (!context?.isDistributed || context.nodeCount <= 1) {
7016
+ return {
7017
+ rows: baseCost,
7018
+ cpu: baseCost,
7019
+ network: 0,
7020
+ io: 0
7021
+ };
7022
+ }
7023
+ let networkCost = 0;
7024
+ switch (step.type) {
7025
+ case "full-scan":
7026
+ networkCost = context.nodeCount * 10;
7027
+ break;
7028
+ case "index-scan":
7029
+ networkCost = 5;
7030
+ break;
7031
+ case "point-lookup":
7032
+ networkCost = 5;
7033
+ break;
7034
+ case "multi-point-lookup":
7035
+ networkCost = Math.min(step.keys.length, context.nodeCount) * 5;
7036
+ break;
7037
+ case "intersection":
7038
+ case "union":
7039
+ networkCost = step.steps.length * 5;
7040
+ break;
7041
+ case "filter":
7042
+ return this.estimateDistributedCost(step.source, context);
7043
+ case "not":
7044
+ networkCost = context.nodeCount * 5;
7045
+ break;
7046
+ case "fts-scan":
7047
+ networkCost = Math.ceil(context.nodeCount / 2) * 5;
7048
+ break;
7049
+ case "fusion":
7050
+ networkCost = step.steps.reduce(
7051
+ (sum, s) => sum + this.estimateDistributedCost(s, context).network,
7052
+ 0
7053
+ );
7054
+ break;
7055
+ }
7056
+ return {
7057
+ rows: baseCost,
7058
+ cpu: baseCost,
7059
+ network: networkCost,
7060
+ io: context.usesStorage ? baseCost * 0.5 : 0
7061
+ };
7062
+ }
7063
+ /**
7064
+ * Get total distributed cost for a plan step.
7065
+ * Convenience method combining estimateDistributedCost and calculateTotalCost.
7066
+ *
7067
+ * @param step - Plan step to estimate
7068
+ * @param context - Distributed query context (optional)
7069
+ * @returns Weighted total cost
7070
+ */
7071
+ getTotalDistributedCost(step, context) {
7072
+ const distributedCost = this.estimateDistributedCost(step, context);
7073
+ return calculateTotalCost(distributedCost);
7074
+ }
6711
7075
  /**
6712
7076
  * Check if a plan step uses any indexes.
6713
7077
  */
6714
7078
  usesIndexes(step) {
6715
7079
  switch (step.type) {
7080
+ case "point-lookup":
7081
+ case "multi-point-lookup":
7082
+ return true;
6716
7083
  case "index-scan":
6717
7084
  return true;
6718
7085
  case "full-scan":
@@ -8893,6 +9260,24 @@ var IndexedLWWMap = class extends LWWMap {
8893
9260
  */
8894
9261
  executePlan(step) {
8895
9262
  switch (step.type) {
9263
+ case "point-lookup": {
9264
+ const key = step.key;
9265
+ const result = /* @__PURE__ */ new Set();
9266
+ if (this.get(key) !== void 0) {
9267
+ result.add(key);
9268
+ }
9269
+ return new SetResultSet(result, 1);
9270
+ }
9271
+ case "multi-point-lookup": {
9272
+ const result = /* @__PURE__ */ new Set();
9273
+ for (const key of step.keys) {
9274
+ const k = key;
9275
+ if (this.get(k) !== void 0) {
9276
+ result.add(k);
9277
+ }
9278
+ }
9279
+ return new SetResultSet(result, step.keys.length);
9280
+ }
8896
9281
  case "index-scan":
8897
9282
  return step.index.retrieve(step.query);
8898
9283
  case "full-scan": {
@@ -11951,14 +12336,517 @@ function getSearchDebugger() {
11951
12336
  function resetSearchDebugger() {
11952
12337
  globalSearchDebugger = null;
11953
12338
  }
12339
+
12340
+ // src/testing/VirtualClock.ts
12341
+ var RealClock = {
12342
+ now: () => Date.now()
12343
+ };
12344
+ var VirtualClock = class {
12345
+ /**
12346
+ * @param initialTime Starting timestamp in milliseconds (default: 0)
12347
+ */
12348
+ constructor(initialTime = 0) {
12349
+ if (!Number.isFinite(initialTime) || initialTime < 0) {
12350
+ throw new Error("Initial time must be a non-negative finite number");
12351
+ }
12352
+ this.currentTime = initialTime;
12353
+ }
12354
+ /**
12355
+ * Returns the current virtual time.
12356
+ * Time remains frozen until advance() or set() is called.
12357
+ */
12358
+ now() {
12359
+ return this.currentTime;
12360
+ }
12361
+ /**
12362
+ * Advances time forward by the specified milliseconds.
12363
+ * @param ms Milliseconds to advance (must be non-negative)
12364
+ */
12365
+ advance(ms) {
12366
+ if (!Number.isFinite(ms) || ms < 0) {
12367
+ throw new Error("Advance amount must be a non-negative finite number");
12368
+ }
12369
+ this.currentTime += ms;
12370
+ }
12371
+ /**
12372
+ * Sets the clock to a specific time.
12373
+ * Allows moving time forward or backward (useful for testing).
12374
+ * @param time Absolute timestamp in milliseconds
12375
+ */
12376
+ set(time) {
12377
+ if (!Number.isFinite(time) || time < 0) {
12378
+ throw new Error("Time must be a non-negative finite number");
12379
+ }
12380
+ this.currentTime = time;
12381
+ }
12382
+ /**
12383
+ * Resets the clock to zero.
12384
+ */
12385
+ reset() {
12386
+ this.currentTime = 0;
12387
+ }
12388
+ };
12389
+
12390
+ // src/testing/SeededRNG.ts
12391
+ var SeededRNG = class {
12392
+ /**
12393
+ * @param seed Integer seed value. Same seed = same sequence.
12394
+ */
12395
+ constructor(seed) {
12396
+ if (!Number.isInteger(seed)) {
12397
+ throw new Error("Seed must be an integer");
12398
+ }
12399
+ this.state = seed >>> 0;
12400
+ this.originalSeed = this.state;
12401
+ }
12402
+ /**
12403
+ * Returns the original seed used to construct this RNG.
12404
+ */
12405
+ getSeed() {
12406
+ return this.originalSeed;
12407
+ }
12408
+ /**
12409
+ * Generates the next random number in [0, 1).
12410
+ * Uses mulberry32 algorithm for deterministic, high-quality randomness.
12411
+ */
12412
+ random() {
12413
+ let t = this.state += 1831565813;
12414
+ t = Math.imul(t ^ t >>> 15, t | 1);
12415
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
12416
+ this.state = t;
12417
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
12418
+ }
12419
+ /**
12420
+ * Generates a random integer in [min, max] (inclusive).
12421
+ * @param min Minimum value (inclusive)
12422
+ * @param max Maximum value (inclusive)
12423
+ */
12424
+ randomInt(min, max) {
12425
+ if (!Number.isInteger(min) || !Number.isInteger(max)) {
12426
+ throw new Error("Min and max must be integers");
12427
+ }
12428
+ if (min > max) {
12429
+ throw new Error("Min must be less than or equal to max");
12430
+ }
12431
+ const range = max - min + 1;
12432
+ return Math.floor(this.random() * range) + min;
12433
+ }
12434
+ /**
12435
+ * Generates a random boolean value.
12436
+ * @param probability Probability of returning true (default: 0.5)
12437
+ */
12438
+ randomBool(probability = 0.5) {
12439
+ if (probability < 0 || probability > 1) {
12440
+ throw new Error("Probability must be between 0 and 1");
12441
+ }
12442
+ return this.random() < probability;
12443
+ }
12444
+ /**
12445
+ * Shuffles an array in place using Fisher-Yates algorithm.
12446
+ * Returns the shuffled array.
12447
+ * @param array Array to shuffle
12448
+ */
12449
+ shuffle(array) {
12450
+ for (let i = array.length - 1; i > 0; i--) {
12451
+ const j = this.randomInt(0, i);
12452
+ [array[i], array[j]] = [array[j], array[i]];
12453
+ }
12454
+ return array;
12455
+ }
12456
+ /**
12457
+ * Picks a random element from an array.
12458
+ * @param array Array to pick from
12459
+ * @returns Random element, or undefined if array is empty
12460
+ */
12461
+ pick(array) {
12462
+ if (array.length === 0) return void 0;
12463
+ return array[this.randomInt(0, array.length - 1)];
12464
+ }
12465
+ /**
12466
+ * Resets the RNG to its original seed.
12467
+ * Useful for reproducing a sequence from the start.
12468
+ */
12469
+ reset() {
12470
+ this.state = this.originalSeed;
12471
+ }
12472
+ };
12473
+
12474
+ // src/testing/VirtualNetwork.ts
12475
+ var VirtualNetwork = class {
12476
+ constructor(rng, clock) {
12477
+ this.pendingMessages = [];
12478
+ this.rng = rng;
12479
+ this.clock = clock;
12480
+ this.config = {
12481
+ latencyMs: { min: 0, max: 0 },
12482
+ packetLossRate: 0,
12483
+ partitions: []
12484
+ };
12485
+ }
12486
+ /**
12487
+ * Updates network configuration.
12488
+ * Partially updates existing config with provided values.
12489
+ */
12490
+ configure(config) {
12491
+ if (config.latencyMs !== void 0) {
12492
+ const { min, max } = config.latencyMs;
12493
+ if (min < 0 || max < 0 || min > max) {
12494
+ throw new Error("Invalid latency range");
12495
+ }
12496
+ this.config.latencyMs = config.latencyMs;
12497
+ }
12498
+ if (config.packetLossRate !== void 0) {
12499
+ if (config.packetLossRate < 0 || config.packetLossRate > 1) {
12500
+ throw new Error("Packet loss rate must be between 0 and 1");
12501
+ }
12502
+ this.config.packetLossRate = config.packetLossRate;
12503
+ }
12504
+ if (config.partitions !== void 0) {
12505
+ this.config.partitions = config.partitions;
12506
+ }
12507
+ }
12508
+ /**
12509
+ * Sends a message through the network.
12510
+ * Subject to packet loss, latency, and partition rules.
12511
+ */
12512
+ send(from, to, payload) {
12513
+ if (this.rng.random() < this.config.packetLossRate) {
12514
+ return;
12515
+ }
12516
+ if (this.isPartitioned(from, to)) {
12517
+ return;
12518
+ }
12519
+ const latency = this.rng.randomInt(
12520
+ this.config.latencyMs.min,
12521
+ this.config.latencyMs.max
12522
+ );
12523
+ const scheduledTime = this.clock.now() + latency;
12524
+ this.pendingMessages.push({
12525
+ from,
12526
+ to,
12527
+ payload,
12528
+ scheduledTime
12529
+ });
12530
+ }
12531
+ /**
12532
+ * Creates a network partition between two groups.
12533
+ * Nodes in groupA cannot communicate with nodes in groupB.
12534
+ */
12535
+ partition(groupA, groupB) {
12536
+ this.config.partitions.push(groupA, groupB);
12537
+ }
12538
+ /**
12539
+ * Removes all network partitions.
12540
+ */
12541
+ heal() {
12542
+ this.config.partitions = [];
12543
+ }
12544
+ /**
12545
+ * Delivers all messages scheduled at or before the current time.
12546
+ * @returns Array of delivered messages
12547
+ */
12548
+ tick() {
12549
+ const currentTime = this.clock.now();
12550
+ const delivered = [];
12551
+ const remaining = [];
12552
+ for (const msg of this.pendingMessages) {
12553
+ if (msg.scheduledTime <= currentTime) {
12554
+ delivered.push(msg);
12555
+ } else {
12556
+ remaining.push(msg);
12557
+ }
12558
+ }
12559
+ this.pendingMessages = remaining;
12560
+ return delivered;
12561
+ }
12562
+ /**
12563
+ * Returns the number of messages currently in flight.
12564
+ */
12565
+ getPendingCount() {
12566
+ return this.pendingMessages.length;
12567
+ }
12568
+ /**
12569
+ * Clears all pending messages.
12570
+ */
12571
+ clear() {
12572
+ this.pendingMessages = [];
12573
+ }
12574
+ /**
12575
+ * Checks if two nodes are partitioned from each other.
12576
+ */
12577
+ isPartitioned(from, to) {
12578
+ for (let i = 0; i < this.config.partitions.length; i += 2) {
12579
+ const groupA = this.config.partitions[i];
12580
+ const groupB = this.config.partitions[i + 1];
12581
+ if (groupA.includes(from) && groupB.includes(to) || groupB.includes(from) && groupA.includes(to)) {
12582
+ return true;
12583
+ }
12584
+ }
12585
+ return false;
12586
+ }
12587
+ /**
12588
+ * Returns all pending messages (useful for debugging).
12589
+ */
12590
+ getPendingMessages() {
12591
+ return [...this.pendingMessages];
12592
+ }
12593
+ };
12594
+
12595
+ // src/testing/InvariantChecker.ts
12596
+ var InvariantChecker = class {
12597
+ constructor() {
12598
+ this.invariants = /* @__PURE__ */ new Map();
12599
+ }
12600
+ /**
12601
+ * Adds an invariant to be checked.
12602
+ * @param name Unique name for this invariant
12603
+ * @param check Function that returns true if invariant holds
12604
+ */
12605
+ addInvariant(name, check) {
12606
+ if (this.invariants.has(name)) {
12607
+ throw new Error(`Invariant '${name}' already exists`);
12608
+ }
12609
+ this.invariants.set(name, check);
12610
+ }
12611
+ /**
12612
+ * Removes an invariant by name.
12613
+ */
12614
+ removeInvariant(name) {
12615
+ return this.invariants.delete(name);
12616
+ }
12617
+ /**
12618
+ * Verifies all invariants against the provided state.
12619
+ * @returns Result with pass/fail status and list of failed invariants
12620
+ */
12621
+ verify(state) {
12622
+ const failures = [];
12623
+ for (const [name, check] of this.invariants.entries()) {
12624
+ try {
12625
+ if (!check(state)) {
12626
+ failures.push(name);
12627
+ }
12628
+ } catch (error) {
12629
+ failures.push(`${name} (exception: ${error instanceof Error ? error.message : String(error)})`);
12630
+ }
12631
+ }
12632
+ return {
12633
+ passed: failures.length === 0,
12634
+ failures
12635
+ };
12636
+ }
12637
+ /**
12638
+ * Returns the number of registered invariants.
12639
+ */
12640
+ get count() {
12641
+ return this.invariants.size;
12642
+ }
12643
+ /**
12644
+ * Clears all invariants.
12645
+ */
12646
+ clear() {
12647
+ this.invariants.clear();
12648
+ }
12649
+ };
12650
+ var CRDTInvariants = {
12651
+ /**
12652
+ * Verifies LWW-Map convergence: all maps contain the same values for same keys.
12653
+ */
12654
+ lwwConvergence: (maps) => {
12655
+ if (maps.length < 2) return true;
12656
+ const reference = maps[0];
12657
+ const refKeys = new Set(reference.allKeys());
12658
+ for (let i = 1; i < maps.length; i++) {
12659
+ const other = maps[i];
12660
+ const otherKeys = new Set(other.allKeys());
12661
+ if (refKeys.size !== otherKeys.size) return false;
12662
+ for (const key of refKeys) {
12663
+ if (!otherKeys.has(key)) return false;
12664
+ }
12665
+ for (const key of refKeys) {
12666
+ const refRecord = reference.getRecord(key);
12667
+ const otherRecord = other.getRecord(key);
12668
+ if (!refRecord || !otherRecord) {
12669
+ if (refRecord !== otherRecord) return false;
12670
+ continue;
12671
+ }
12672
+ if (refRecord.value !== otherRecord.value) return false;
12673
+ if (HLC.compare(refRecord.timestamp, otherRecord.timestamp) !== 0) {
12674
+ return false;
12675
+ }
12676
+ }
12677
+ }
12678
+ return true;
12679
+ },
12680
+ /**
12681
+ * Verifies OR-Map convergence: all maps contain the same values for same keys.
12682
+ */
12683
+ orMapConvergence: (maps) => {
12684
+ if (maps.length < 2) return true;
12685
+ const reference = maps[0];
12686
+ const refKeys = reference.allKeys();
12687
+ for (let i = 1; i < maps.length; i++) {
12688
+ const other = maps[i];
12689
+ const otherKeys = new Set(other.allKeys());
12690
+ if (refKeys.length !== otherKeys.size) return false;
12691
+ for (const key of refKeys) {
12692
+ if (!otherKeys.has(key)) return false;
12693
+ }
12694
+ for (const key of refKeys) {
12695
+ const refRecords = reference.getRecords(key);
12696
+ const otherRecords = other.getRecords(key);
12697
+ if (refRecords.length !== otherRecords.length) return false;
12698
+ const refSorted = [...refRecords].sort((a, b) => a.tag.localeCompare(b.tag));
12699
+ const otherSorted = [...otherRecords].sort((a, b) => a.tag.localeCompare(b.tag));
12700
+ for (let j = 0; j < refSorted.length; j++) {
12701
+ if (refSorted[j].tag !== otherSorted[j].tag) return false;
12702
+ if (refSorted[j].value !== otherSorted[j].value) return false;
12703
+ if (HLC.compare(refSorted[j].timestamp, otherSorted[j].timestamp) !== 0) {
12704
+ return false;
12705
+ }
12706
+ }
12707
+ }
12708
+ const refTombstones = new Set(reference.getTombstones());
12709
+ const otherTombstones = new Set(other.getTombstones());
12710
+ if (refTombstones.size !== otherTombstones.size) return false;
12711
+ for (const tag of refTombstones) {
12712
+ if (!otherTombstones.has(tag)) return false;
12713
+ }
12714
+ }
12715
+ return true;
12716
+ },
12717
+ /**
12718
+ * Verifies HLC monotonicity: timestamps are strictly increasing.
12719
+ */
12720
+ hlcMonotonicity: (timestamps) => {
12721
+ for (let i = 1; i < timestamps.length; i++) {
12722
+ if (HLC.compare(timestamps[i - 1], timestamps[i]) >= 0) {
12723
+ return false;
12724
+ }
12725
+ }
12726
+ return true;
12727
+ },
12728
+ /**
12729
+ * Verifies Merkle tree consistency: trees with same data have same root hash.
12730
+ */
12731
+ merkleConsistency: (trees) => {
12732
+ if (trees.length < 2) return true;
12733
+ const referenceHash = trees[0].getRootHash();
12734
+ for (let i = 1; i < trees.length; i++) {
12735
+ if (trees[i].getRootHash() !== referenceHash) {
12736
+ return false;
12737
+ }
12738
+ }
12739
+ return true;
12740
+ }
12741
+ };
12742
+
12743
+ // src/testing/ScenarioRunner.ts
12744
+ var ScenarioRunner = class {
12745
+ constructor(config) {
12746
+ if (!config.nodes || config.nodes.length === 0) {
12747
+ throw new Error("Scenario must have at least one node");
12748
+ }
12749
+ if (config.duration <= 0) {
12750
+ throw new Error("Duration must be positive");
12751
+ }
12752
+ this.seed = config.seed ?? Math.floor(Math.random() * 2147483647);
12753
+ this.config = {
12754
+ ...config,
12755
+ seed: this.seed,
12756
+ tickInterval: config.tickInterval ?? 1
12757
+ };
12758
+ this.clock = new VirtualClock(0);
12759
+ this.rng = new SeededRNG(this.seed);
12760
+ this.network = new VirtualNetwork(this.rng, this.clock);
12761
+ }
12762
+ /**
12763
+ * Returns the seed used for this scenario.
12764
+ */
12765
+ getSeed() {
12766
+ return this.seed;
12767
+ }
12768
+ /**
12769
+ * Returns the virtual clock instance.
12770
+ */
12771
+ getClock() {
12772
+ return this.clock;
12773
+ }
12774
+ /**
12775
+ * Returns the seeded RNG instance.
12776
+ */
12777
+ getRNG() {
12778
+ return this.rng;
12779
+ }
12780
+ /**
12781
+ * Returns the virtual network instance.
12782
+ */
12783
+ getNetwork() {
12784
+ return this.network;
12785
+ }
12786
+ /**
12787
+ * Returns the list of nodes in this scenario.
12788
+ */
12789
+ getNodes() {
12790
+ return [...this.config.nodes];
12791
+ }
12792
+ /**
12793
+ * Executes the simulation scenario.
12794
+ *
12795
+ * @param setup Called once before simulation starts. Initialize state here.
12796
+ * @param step Called on each tick. Perform operations and message delivery.
12797
+ * @param invariants Checker for verifying correctness throughout execution.
12798
+ * @returns Result with pass/fail status and captured state
12799
+ */
12800
+ run(setup, step, invariants) {
12801
+ const finalStates = /* @__PURE__ */ new Map();
12802
+ const invariantFailures = [];
12803
+ setup(this);
12804
+ let tickCount = 0;
12805
+ const tickInterval = this.config.tickInterval;
12806
+ const endTime = this.config.duration;
12807
+ while (this.clock.now() < endTime) {
12808
+ this.clock.advance(tickInterval);
12809
+ tickCount++;
12810
+ step(this, tickCount);
12811
+ const delivered = this.network.tick();
12812
+ if (delivered.length > 0) {
12813
+ finalStates.set(`_tick_${tickCount}_delivered`, delivered.length);
12814
+ }
12815
+ }
12816
+ const result = invariants.verify(null);
12817
+ if (!result.passed) {
12818
+ invariantFailures.push(...result.failures);
12819
+ }
12820
+ return {
12821
+ seed: this.seed,
12822
+ passed: invariantFailures.length === 0,
12823
+ ticks: tickCount,
12824
+ invariantFailures,
12825
+ finalStates
12826
+ };
12827
+ }
12828
+ /**
12829
+ * Stores state for a node (useful for capturing final state).
12830
+ */
12831
+ setState(nodeId, state) {
12832
+ if (!this.config.nodes.includes(nodeId)) {
12833
+ throw new Error(`Unknown node: ${nodeId}`);
12834
+ }
12835
+ }
12836
+ };
11954
12837
  export {
12838
+ AuthAckMessageSchema,
11955
12839
  AuthFailMessageSchema,
11956
12840
  AuthMessageSchema,
12841
+ AuthRequiredMessageSchema,
11957
12842
  BM25Scorer,
11958
12843
  BatchMessageSchema,
11959
12844
  BuiltInProcessors,
11960
12845
  BuiltInResolvers,
12846
+ COST_WEIGHTS,
11961
12847
  CRDTDebugger,
12848
+ CRDTInvariants,
12849
+ ChangeEventTypeSchema,
11962
12850
  ClientOpMessageSchema,
11963
12851
  ClientOpSchema,
11964
12852
  ClusterSearchReqMessageSchema,
@@ -12000,6 +12888,7 @@ export {
12000
12888
  DEFAULT_RESOLVER_RATE_LIMITS,
12001
12889
  DEFAULT_STOP_WORDS,
12002
12890
  DEFAULT_WRITE_CONCERN_TIMEOUT,
12891
+ DeltaRecordSchema,
12003
12892
  ENGLISH_STOPWORDS,
12004
12893
  EntryProcessBatchRequestSchema,
12005
12894
  EntryProcessBatchResponseSchema,
@@ -12008,6 +12897,7 @@ export {
12008
12897
  EntryProcessResponseSchema,
12009
12898
  EntryProcessorDefSchema,
12010
12899
  EntryProcessorSchema,
12900
+ ErrorMessageSchema,
12011
12901
  EventJournalImpl,
12012
12902
  FORBIDDEN_PATTERNS,
12013
12903
  BM25InvertedIndex as FTSInvertedIndex,
@@ -12019,12 +12909,18 @@ export {
12019
12909
  GcPrunePayloadSchema,
12020
12910
  HLC,
12021
12911
  HashIndex,
12022
- HybridQueryDeltaPayloadSchema,
12023
- HybridQueryRespPayloadSchema,
12912
+ HttpQueryRequestSchema,
12913
+ HttpQueryResultSchema,
12914
+ HttpSearchRequestSchema,
12915
+ HttpSearchResultSchema,
12916
+ HttpSyncErrorSchema,
12917
+ HttpSyncRequestSchema,
12918
+ HttpSyncResponseSchema,
12024
12919
  IndexRegistry,
12025
12920
  IndexedLWWMap,
12026
12921
  IndexedORMap,
12027
12922
  IntersectionResultSet,
12923
+ InvariantChecker,
12028
12924
  InvertedIndex,
12029
12925
  JournalEventDataSchema,
12030
12926
  JournalEventMessageSchema,
@@ -12040,11 +12936,14 @@ export {
12040
12936
  ListResolversRequestSchema,
12041
12937
  ListResolversResponseSchema,
12042
12938
  LiveQueryManager,
12939
+ LockGrantedMessageSchema,
12043
12940
  LockGrantedPayloadSchema,
12044
12941
  LockReleaseSchema,
12942
+ LockReleasedMessageSchema,
12045
12943
  LockReleasedPayloadSchema,
12046
12944
  LockRequestSchema,
12047
12945
  LowercaseFilter,
12946
+ MapDeltaSchema,
12048
12947
  MaxLengthFilter,
12049
12948
  MergeRejectedMessageSchema,
12050
12949
  MerkleReqBucketMessageSchema,
@@ -12054,9 +12953,11 @@ export {
12054
12953
  MultiValueAttribute,
12055
12954
  NGramTokenizer,
12056
12955
  NavigableIndex,
12956
+ NodeInfoSchema,
12057
12957
  ORMap,
12058
12958
  ORMapDiffRequestSchema,
12059
12959
  ORMapDiffResponseSchema,
12960
+ ORMapEntrySchema,
12060
12961
  ORMapMerkleReqBucketSchema,
12061
12962
  ORMapMerkleTree,
12062
12963
  ORMapPushDiffSchema,
@@ -12072,6 +12973,9 @@ export {
12072
12973
  PARTITION_COUNT,
12073
12974
  PNCounterImpl,
12074
12975
  PNCounterStateObjectSchema,
12976
+ PartitionInfoSchema,
12977
+ PartitionMapMessageSchema,
12978
+ PartitionMapPayloadSchema,
12075
12979
  PartitionMapRequestSchema,
12076
12980
  PartitionState,
12077
12981
  PingMessageSchema,
@@ -12089,10 +12993,12 @@ export {
12089
12993
  QueryUpdateMessageSchema,
12090
12994
  QueryUpdatePayloadSchema,
12091
12995
  RESOLVER_FORBIDDEN_PATTERNS,
12996
+ RealClock,
12092
12997
  ReciprocalRankFusion,
12093
12998
  RegisterResolverRequestSchema,
12094
12999
  RegisterResolverResponseSchema,
12095
13000
  Ringbuffer,
13001
+ ScenarioRunner,
12096
13002
  SearchCursor,
12097
13003
  SearchDebugger,
12098
13004
  SearchMessageSchema,
@@ -12107,6 +13013,7 @@ export {
12107
13013
  SearchUpdateMessageSchema,
12108
13014
  SearchUpdatePayloadSchema,
12109
13015
  SearchUpdateTypeSchema,
13016
+ SeededRNG,
12110
13017
  ServerBatchEventMessageSchema,
12111
13018
  ServerEventMessageSchema,
12112
13019
  ServerEventPayloadSchema,
@@ -12118,6 +13025,8 @@ export {
12118
13025
  StandingQueryRegistry,
12119
13026
  StopWordFilter,
12120
13027
  SyncInitMessageSchema,
13028
+ SyncMapEntrySchema,
13029
+ SyncResetRequiredMessageSchema,
12121
13030
  SyncResetRequiredPayloadSchema,
12122
13031
  SyncRespBucketsMessageSchema,
12123
13032
  SyncRespLeafMessageSchema,
@@ -12133,11 +13042,14 @@ export {
12133
13042
  UniqueFilter,
12134
13043
  UnregisterResolverRequestSchema,
12135
13044
  UnregisterResolverResponseSchema,
13045
+ VirtualClock,
13046
+ VirtualNetwork,
12136
13047
  WRITE_CONCERN_ORDER,
12137
13048
  WhitespaceTokenizer,
12138
13049
  WordBoundaryTokenizer,
12139
13050
  WriteConcern,
12140
13051
  WriteConcernSchema,
13052
+ calculateTotalCost,
12141
13053
  combineHashes,
12142
13054
  compareHLCTimestamps,
12143
13055
  compareTimestamps,
@@ -12147,7 +13059,6 @@ export {
12147
13059
  decodeBase64Url,
12148
13060
  deepMerge,
12149
13061
  deserialize,
12150
- disableNativeHash,
12151
13062
  encodeBase64Url,
12152
13063
  evaluatePredicate,
12153
13064
  getCRDTDebugger,
@@ -12159,13 +13070,11 @@ export {
12159
13070
  hashString,
12160
13071
  isLogicalQuery,
12161
13072
  isSimpleQuery,
12162
- isUsingNativeHash,
12163
13073
  isWriteConcernAchieved,
12164
13074
  logger,
12165
13075
  multiAttribute,
12166
13076
  porterStem,
12167
13077
  resetCRDTDebugger,
12168
- resetNativeHash,
12169
13078
  resetSearchDebugger,
12170
13079
  serialize,
12171
13080
  simpleAttribute,