@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.d.mts +1552 -377
- package/dist/index.d.ts +1552 -377
- package/dist/index.js +1074 -132
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1043 -134
- package/dist/index.mjs.map +1 -1
- package/package.json +14 -12
- package/LICENSE +0 -97
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 =
|
|
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
|
|
60
|
-
const
|
|
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 ${
|
|
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
|
|
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,
|
|
74
|
-
if (maxMillis === this.lastMillis && maxMillis ===
|
|
75
|
-
this.lastCounter = Math.max(this.lastCounter,
|
|
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 ===
|
|
79
|
-
this.lastCounter =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
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/
|
|
2931
|
+
// src/schemas/http-sync-schemas.ts
|
|
2890
2932
|
import { z as z10 } from "zod";
|
|
2891
|
-
var
|
|
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
|
-
|
|
2894
|
-
|
|
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
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
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
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
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.
|
|
6226
|
-
* 2.
|
|
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,
|
|
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
|
|
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
|
-
...
|
|
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
|
-
|
|
12023
|
-
|
|
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,
|