@topgunbuild/core 0.10.1 → 0.11.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 +755 -8
- package/dist/index.d.ts +755 -8
- package/dist/index.js +868 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +849 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -21,6 +21,7 @@ var HLC = class {
|
|
|
21
21
|
this.nodeId = nodeId;
|
|
22
22
|
this.strictMode = options.strictMode ?? false;
|
|
23
23
|
this.maxDriftMs = options.maxDriftMs ?? 6e4;
|
|
24
|
+
this.clockSource = options.clockSource ?? { now: () => Date.now() };
|
|
24
25
|
this.lastMillis = 0;
|
|
25
26
|
this.lastCounter = 0;
|
|
26
27
|
}
|
|
@@ -33,12 +34,19 @@ var HLC = class {
|
|
|
33
34
|
get getMaxDriftMs() {
|
|
34
35
|
return this.maxDriftMs;
|
|
35
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Returns the clock source used by this HLC instance.
|
|
39
|
+
* Useful for LWWMap/ORMap to access the same clock for TTL checks.
|
|
40
|
+
*/
|
|
41
|
+
getClockSource() {
|
|
42
|
+
return this.clockSource;
|
|
43
|
+
}
|
|
36
44
|
/**
|
|
37
45
|
* Generates a new unique timestamp for a local event.
|
|
38
46
|
* Ensures monotonicity: always greater than any previously generated or received timestamp.
|
|
39
47
|
*/
|
|
40
48
|
now() {
|
|
41
|
-
const systemTime =
|
|
49
|
+
const systemTime = this.clockSource.now();
|
|
42
50
|
if (systemTime > this.lastMillis) {
|
|
43
51
|
this.lastMillis = systemTime;
|
|
44
52
|
this.lastCounter = 0;
|
|
@@ -56,7 +64,7 @@ var HLC = class {
|
|
|
56
64
|
* Must be called whenever a message/event is received from another node.
|
|
57
65
|
*/
|
|
58
66
|
update(remote) {
|
|
59
|
-
const systemTime =
|
|
67
|
+
const systemTime = this.clockSource.now();
|
|
60
68
|
const drift = remote.millis - systemTime;
|
|
61
69
|
if (drift > this.maxDriftMs) {
|
|
62
70
|
if (this.strictMode) {
|
|
@@ -340,7 +348,7 @@ var LWWMap = class {
|
|
|
340
348
|
return void 0;
|
|
341
349
|
}
|
|
342
350
|
if (record.ttlMs) {
|
|
343
|
-
const now =
|
|
351
|
+
const now = this.hlc.getClockSource().now();
|
|
344
352
|
if (record.timestamp.millis + record.ttlMs < now) {
|
|
345
353
|
return void 0;
|
|
346
354
|
}
|
|
@@ -417,7 +425,7 @@ var LWWMap = class {
|
|
|
417
425
|
*/
|
|
418
426
|
entries() {
|
|
419
427
|
const iterator = this.data.entries();
|
|
420
|
-
const
|
|
428
|
+
const clockSource = this.hlc.getClockSource();
|
|
421
429
|
return {
|
|
422
430
|
[Symbol.iterator]() {
|
|
423
431
|
return this;
|
|
@@ -427,7 +435,7 @@ var LWWMap = class {
|
|
|
427
435
|
while (!result.done) {
|
|
428
436
|
const [key, record] = result.value;
|
|
429
437
|
if (record.value !== null) {
|
|
430
|
-
if (record.ttlMs && record.timestamp.millis + record.ttlMs < now) {
|
|
438
|
+
if (record.ttlMs && record.timestamp.millis + record.ttlMs < clockSource.now()) {
|
|
431
439
|
result = iterator.next();
|
|
432
440
|
continue;
|
|
433
441
|
}
|
|
@@ -764,7 +772,7 @@ var ORMap = class {
|
|
|
764
772
|
const keyMap = this.items.get(key);
|
|
765
773
|
if (!keyMap) return [];
|
|
766
774
|
const values = [];
|
|
767
|
-
const now =
|
|
775
|
+
const now = this.hlc.getClockSource().now();
|
|
768
776
|
for (const [tag, record] of keyMap.entries()) {
|
|
769
777
|
if (!this.tombstones.has(tag)) {
|
|
770
778
|
if (record.ttlMs && record.timestamp.millis + record.ttlMs < now) {
|
|
@@ -784,7 +792,7 @@ var ORMap = class {
|
|
|
784
792
|
const keyMap = this.items.get(key);
|
|
785
793
|
if (!keyMap) return [];
|
|
786
794
|
const records = [];
|
|
787
|
-
const now =
|
|
795
|
+
const now = this.hlc.getClockSource().now();
|
|
788
796
|
for (const [tag, record] of keyMap.entries()) {
|
|
789
797
|
if (!this.tombstones.has(tag)) {
|
|
790
798
|
if (record.ttlMs && record.timestamp.millis + record.ttlMs < now) {
|
|
@@ -2886,9 +2894,86 @@ var SyncResetRequiredPayloadSchema = z9.object({
|
|
|
2886
2894
|
reason: z9.string()
|
|
2887
2895
|
});
|
|
2888
2896
|
|
|
2889
|
-
// src/schemas/
|
|
2897
|
+
// src/schemas/http-sync-schemas.ts
|
|
2890
2898
|
import { z as z10 } from "zod";
|
|
2891
|
-
var
|
|
2899
|
+
var SyncMapEntrySchema = z10.object({
|
|
2900
|
+
mapName: z10.string(),
|
|
2901
|
+
lastSyncTimestamp: TimestampSchema
|
|
2902
|
+
});
|
|
2903
|
+
var HttpQueryRequestSchema = z10.object({
|
|
2904
|
+
queryId: z10.string(),
|
|
2905
|
+
mapName: z10.string(),
|
|
2906
|
+
filter: z10.any(),
|
|
2907
|
+
limit: z10.number().optional(),
|
|
2908
|
+
offset: z10.number().optional()
|
|
2909
|
+
});
|
|
2910
|
+
var HttpSearchRequestSchema = z10.object({
|
|
2911
|
+
searchId: z10.string(),
|
|
2912
|
+
mapName: z10.string(),
|
|
2913
|
+
query: z10.string(),
|
|
2914
|
+
options: z10.any().optional()
|
|
2915
|
+
});
|
|
2916
|
+
var HttpSyncRequestSchema = z10.object({
|
|
2917
|
+
// Client identification
|
|
2918
|
+
clientId: z10.string(),
|
|
2919
|
+
// Client's current HLC for causality tracking
|
|
2920
|
+
clientHlc: TimestampSchema,
|
|
2921
|
+
// Batch of operations to push (optional)
|
|
2922
|
+
operations: z10.array(ClientOpSchema).optional(),
|
|
2923
|
+
// Maps the client wants deltas for, with their last known sync HLC timestamp
|
|
2924
|
+
syncMaps: z10.array(SyncMapEntrySchema).optional(),
|
|
2925
|
+
// One-shot queries to execute (optional)
|
|
2926
|
+
queries: z10.array(HttpQueryRequestSchema).optional(),
|
|
2927
|
+
// One-shot search requests (optional)
|
|
2928
|
+
searches: z10.array(HttpSearchRequestSchema).optional()
|
|
2929
|
+
});
|
|
2930
|
+
var DeltaRecordSchema = z10.object({
|
|
2931
|
+
key: z10.string(),
|
|
2932
|
+
record: LWWRecordSchema,
|
|
2933
|
+
eventType: z10.enum(["PUT", "REMOVE"])
|
|
2934
|
+
});
|
|
2935
|
+
var MapDeltaSchema = z10.object({
|
|
2936
|
+
mapName: z10.string(),
|
|
2937
|
+
records: z10.array(DeltaRecordSchema),
|
|
2938
|
+
serverSyncTimestamp: TimestampSchema
|
|
2939
|
+
});
|
|
2940
|
+
var HttpQueryResultSchema = z10.object({
|
|
2941
|
+
queryId: z10.string(),
|
|
2942
|
+
results: z10.array(z10.any()),
|
|
2943
|
+
hasMore: z10.boolean().optional(),
|
|
2944
|
+
nextCursor: z10.string().optional()
|
|
2945
|
+
});
|
|
2946
|
+
var HttpSearchResultSchema = z10.object({
|
|
2947
|
+
searchId: z10.string(),
|
|
2948
|
+
results: z10.array(z10.any()),
|
|
2949
|
+
totalCount: z10.number().optional()
|
|
2950
|
+
});
|
|
2951
|
+
var HttpSyncErrorSchema = z10.object({
|
|
2952
|
+
code: z10.number(),
|
|
2953
|
+
message: z10.string(),
|
|
2954
|
+
context: z10.string().optional()
|
|
2955
|
+
});
|
|
2956
|
+
var HttpSyncResponseSchema = z10.object({
|
|
2957
|
+
// Server's current HLC
|
|
2958
|
+
serverHlc: TimestampSchema,
|
|
2959
|
+
// Acknowledgment of received operations
|
|
2960
|
+
ack: z10.object({
|
|
2961
|
+
lastId: z10.string(),
|
|
2962
|
+
results: z10.array(OpResultSchema).optional()
|
|
2963
|
+
}).optional(),
|
|
2964
|
+
// Delta records for requested maps (new/changed since lastSyncTimestamp)
|
|
2965
|
+
deltas: z10.array(MapDeltaSchema).optional(),
|
|
2966
|
+
// Query results
|
|
2967
|
+
queryResults: z10.array(HttpQueryResultSchema).optional(),
|
|
2968
|
+
// Search results
|
|
2969
|
+
searchResults: z10.array(HttpSearchResultSchema).optional(),
|
|
2970
|
+
// Errors for individual operations
|
|
2971
|
+
errors: z10.array(HttpSyncErrorSchema).optional()
|
|
2972
|
+
});
|
|
2973
|
+
|
|
2974
|
+
// src/schemas/index.ts
|
|
2975
|
+
import { z as z11 } from "zod";
|
|
2976
|
+
var MessageSchema = z11.discriminatedUnion("type", [
|
|
2892
2977
|
AuthMessageSchema,
|
|
2893
2978
|
QuerySubMessageSchema,
|
|
2894
2979
|
QueryUnsubMessageSchema,
|
|
@@ -3901,6 +3986,18 @@ function createPredicateMatcher(getAttribute) {
|
|
|
3901
3986
|
}
|
|
3902
3987
|
|
|
3903
3988
|
// src/query/QueryTypes.ts
|
|
3989
|
+
var COST_WEIGHTS = {
|
|
3990
|
+
CPU: 1,
|
|
3991
|
+
NETWORK: 10,
|
|
3992
|
+
// Network is expensive (latency, bandwidth)
|
|
3993
|
+
IO: 5,
|
|
3994
|
+
// Disk I/O is moderately expensive
|
|
3995
|
+
ROWS: 1e-3
|
|
3996
|
+
// Row count factor
|
|
3997
|
+
};
|
|
3998
|
+
function calculateTotalCost(cost) {
|
|
3999
|
+
return cost.rows * COST_WEIGHTS.ROWS + cost.cpu * COST_WEIGHTS.CPU + cost.network * COST_WEIGHTS.NETWORK + cost.io * COST_WEIGHTS.IO;
|
|
4000
|
+
}
|
|
3904
4001
|
function isSimpleQuery(query) {
|
|
3905
4002
|
return [
|
|
3906
4003
|
"eq",
|
|
@@ -6222,13 +6319,22 @@ var QueryOptimizer = class {
|
|
|
6222
6319
|
* Optimize a query and return an execution plan.
|
|
6223
6320
|
*
|
|
6224
6321
|
* Optimization order (by cost):
|
|
6225
|
-
* 1.
|
|
6226
|
-
* 2.
|
|
6322
|
+
* 1. Point lookup (cost: 1) - direct primary key access
|
|
6323
|
+
* 2. StandingQueryIndex (cost: 10) - pre-computed results
|
|
6324
|
+
* 3. Other indexes via optimizeNode
|
|
6227
6325
|
*
|
|
6228
6326
|
* @param query - Query to optimize
|
|
6229
6327
|
* @returns Query execution plan
|
|
6230
6328
|
*/
|
|
6231
6329
|
optimize(query) {
|
|
6330
|
+
const pointLookupStep = this.tryPointLookup(query);
|
|
6331
|
+
if (pointLookupStep) {
|
|
6332
|
+
return {
|
|
6333
|
+
root: pointLookupStep,
|
|
6334
|
+
estimatedCost: this.estimateCost(pointLookupStep),
|
|
6335
|
+
usesIndexes: this.usesIndexes(pointLookupStep)
|
|
6336
|
+
};
|
|
6337
|
+
}
|
|
6232
6338
|
if (this.standingQueryRegistry) {
|
|
6233
6339
|
const standingIndex = this.standingQueryRegistry.getIndex(query);
|
|
6234
6340
|
if (standingIndex) {
|
|
@@ -6252,16 +6358,68 @@ var QueryOptimizer = class {
|
|
|
6252
6358
|
};
|
|
6253
6359
|
}
|
|
6254
6360
|
/**
|
|
6255
|
-
* Optimize a query with sort/limit/offset options.
|
|
6361
|
+
* Optimize a query with sort/limit/offset options and index hints.
|
|
6362
|
+
*
|
|
6363
|
+
* Hint precedence: disableOptimization > useIndex > forceIndexScan.
|
|
6256
6364
|
*
|
|
6257
6365
|
* @param query - Query to optimize
|
|
6258
|
-
* @param options - Query options (sort, limit,
|
|
6366
|
+
* @param options - Query options (sort, limit, cursor, hints)
|
|
6259
6367
|
* @returns Query execution plan with options
|
|
6260
6368
|
*/
|
|
6261
6369
|
optimizeWithOptions(query, options) {
|
|
6370
|
+
if (options.disableOptimization) {
|
|
6371
|
+
return {
|
|
6372
|
+
root: { type: "full-scan", predicate: query },
|
|
6373
|
+
estimatedCost: Number.MAX_SAFE_INTEGER,
|
|
6374
|
+
usesIndexes: false
|
|
6375
|
+
};
|
|
6376
|
+
}
|
|
6377
|
+
if (options.useIndex) {
|
|
6378
|
+
const indexes = this.indexRegistry.getIndexes(options.useIndex);
|
|
6379
|
+
if (indexes.length === 0) {
|
|
6380
|
+
throw new Error(
|
|
6381
|
+
`Index hint: no index found for attribute "${options.useIndex}"`
|
|
6382
|
+
);
|
|
6383
|
+
}
|
|
6384
|
+
let best = indexes[0];
|
|
6385
|
+
for (let i = 1; i < indexes.length; i++) {
|
|
6386
|
+
if (indexes[i].getRetrievalCost() < best.getRetrievalCost()) {
|
|
6387
|
+
best = indexes[i];
|
|
6388
|
+
}
|
|
6389
|
+
}
|
|
6390
|
+
const step = {
|
|
6391
|
+
type: "index-scan",
|
|
6392
|
+
index: best,
|
|
6393
|
+
query: this.buildHintedIndexQuery(query, options.useIndex)
|
|
6394
|
+
};
|
|
6395
|
+
return this.applyPlanOptions(
|
|
6396
|
+
{
|
|
6397
|
+
root: step,
|
|
6398
|
+
estimatedCost: best.getRetrievalCost(),
|
|
6399
|
+
usesIndexes: true,
|
|
6400
|
+
hint: options.useIndex
|
|
6401
|
+
},
|
|
6402
|
+
options
|
|
6403
|
+
);
|
|
6404
|
+
}
|
|
6262
6405
|
const basePlan = this.optimize(query);
|
|
6406
|
+
if (options.forceIndexScan && !basePlan.usesIndexes) {
|
|
6407
|
+
throw new Error(
|
|
6408
|
+
"No suitable index found and forceIndexScan is enabled"
|
|
6409
|
+
);
|
|
6410
|
+
}
|
|
6411
|
+
return this.applyPlanOptions(basePlan, options);
|
|
6412
|
+
}
|
|
6413
|
+
/**
|
|
6414
|
+
* Apply sort/limit/cursor options to a query plan.
|
|
6415
|
+
*
|
|
6416
|
+
* @param plan - Base query plan
|
|
6417
|
+
* @param options - Query options with sort/limit/cursor
|
|
6418
|
+
* @returns Plan with options applied
|
|
6419
|
+
*/
|
|
6420
|
+
applyPlanOptions(plan, options) {
|
|
6263
6421
|
if (!options.sort && options.limit === void 0 && options.cursor === void 0) {
|
|
6264
|
-
return
|
|
6422
|
+
return plan;
|
|
6265
6423
|
}
|
|
6266
6424
|
let indexedSort = false;
|
|
6267
6425
|
let sortField;
|
|
@@ -6278,14 +6436,73 @@ var QueryOptimizer = class {
|
|
|
6278
6436
|
}
|
|
6279
6437
|
}
|
|
6280
6438
|
return {
|
|
6281
|
-
...
|
|
6439
|
+
...plan,
|
|
6282
6440
|
indexedSort,
|
|
6283
6441
|
sort: sortField && sortDirection ? { field: sortField, direction: sortDirection } : void 0,
|
|
6284
6442
|
limit: options.limit,
|
|
6285
6443
|
cursor: options.cursor
|
|
6286
|
-
// replaces offset
|
|
6287
6444
|
};
|
|
6288
6445
|
}
|
|
6446
|
+
/**
|
|
6447
|
+
* Extract the relevant index query for a hinted attribute from the query tree.
|
|
6448
|
+
* If the query directly references the attribute, build an index query from it.
|
|
6449
|
+
* For compound (logical) queries, extract the matching child predicate.
|
|
6450
|
+
* Falls back to { type: 'has' } when no matching predicate is found,
|
|
6451
|
+
* retrieving all entries from the index for post-filtering.
|
|
6452
|
+
* FTS query nodes also fall back to { type: 'has' } since full-text search
|
|
6453
|
+
* queries are not compatible with regular index lookups.
|
|
6454
|
+
*
|
|
6455
|
+
* @param query - Original query tree
|
|
6456
|
+
* @param attributeName - Hinted attribute name
|
|
6457
|
+
* @returns Index query for the hinted attribute
|
|
6458
|
+
*/
|
|
6459
|
+
buildHintedIndexQuery(query, attributeName) {
|
|
6460
|
+
if (isSimpleQuery(query) && query.attribute === attributeName) {
|
|
6461
|
+
return this.buildIndexQuery(query);
|
|
6462
|
+
}
|
|
6463
|
+
if (isFTSQuery(query)) {
|
|
6464
|
+
return { type: "has" };
|
|
6465
|
+
}
|
|
6466
|
+
if (isLogicalQuery(query) && query.children) {
|
|
6467
|
+
for (const child of query.children) {
|
|
6468
|
+
if (isSimpleQuery(child) && child.attribute === attributeName) {
|
|
6469
|
+
return this.buildIndexQuery(child);
|
|
6470
|
+
}
|
|
6471
|
+
}
|
|
6472
|
+
}
|
|
6473
|
+
return { type: "has" };
|
|
6474
|
+
}
|
|
6475
|
+
/**
|
|
6476
|
+
* Try to optimize query as a point lookup.
|
|
6477
|
+
* Returns a point lookup step if query is an equality or IN query on primary key.
|
|
6478
|
+
*
|
|
6479
|
+
* @param query - Query to check
|
|
6480
|
+
* @returns Point lookup step or null
|
|
6481
|
+
*/
|
|
6482
|
+
tryPointLookup(query) {
|
|
6483
|
+
if (!isSimpleQuery(query)) {
|
|
6484
|
+
return null;
|
|
6485
|
+
}
|
|
6486
|
+
const primaryKeyFields = ["_key", "key", "id"];
|
|
6487
|
+
if (!primaryKeyFields.includes(query.attribute)) {
|
|
6488
|
+
return null;
|
|
6489
|
+
}
|
|
6490
|
+
if (query.type === "eq") {
|
|
6491
|
+
return {
|
|
6492
|
+
type: "point-lookup",
|
|
6493
|
+
key: query.value,
|
|
6494
|
+
cost: 1
|
|
6495
|
+
};
|
|
6496
|
+
}
|
|
6497
|
+
if (query.type === "in" && query.values) {
|
|
6498
|
+
return {
|
|
6499
|
+
type: "multi-point-lookup",
|
|
6500
|
+
keys: query.values,
|
|
6501
|
+
cost: query.values.length
|
|
6502
|
+
};
|
|
6503
|
+
}
|
|
6504
|
+
return null;
|
|
6505
|
+
}
|
|
6289
6506
|
/**
|
|
6290
6507
|
* Optimize a single query node.
|
|
6291
6508
|
*/
|
|
@@ -6674,6 +6891,9 @@ var QueryOptimizer = class {
|
|
|
6674
6891
|
*/
|
|
6675
6892
|
estimateCost(step) {
|
|
6676
6893
|
switch (step.type) {
|
|
6894
|
+
case "point-lookup":
|
|
6895
|
+
case "multi-point-lookup":
|
|
6896
|
+
return step.cost;
|
|
6677
6897
|
case "index-scan":
|
|
6678
6898
|
return step.index.getRetrievalCost();
|
|
6679
6899
|
case "full-scan":
|
|
@@ -6708,11 +6928,89 @@ var QueryOptimizer = class {
|
|
|
6708
6928
|
return Number.MAX_SAFE_INTEGER;
|
|
6709
6929
|
}
|
|
6710
6930
|
}
|
|
6931
|
+
/**
|
|
6932
|
+
* Estimate distributed cost including network overhead.
|
|
6933
|
+
*
|
|
6934
|
+
* Network cost is assigned based on step type:
|
|
6935
|
+
* - full-scan: broadcast to all nodes (highest cost)
|
|
6936
|
+
* - index-scan: 0 if local partition, 5 if remote
|
|
6937
|
+
* - point-lookup: 0 if local key, 5 if remote
|
|
6938
|
+
* - intersection/union: aggregating results from multiple sources
|
|
6939
|
+
*
|
|
6940
|
+
* @param step - Plan step to estimate
|
|
6941
|
+
* @param context - Distributed query context (optional)
|
|
6942
|
+
* @returns Distributed cost breakdown
|
|
6943
|
+
*/
|
|
6944
|
+
estimateDistributedCost(step, context) {
|
|
6945
|
+
const baseCost = this.estimateCost(step);
|
|
6946
|
+
if (!context?.isDistributed || context.nodeCount <= 1) {
|
|
6947
|
+
return {
|
|
6948
|
+
rows: baseCost,
|
|
6949
|
+
cpu: baseCost,
|
|
6950
|
+
network: 0,
|
|
6951
|
+
io: 0
|
|
6952
|
+
};
|
|
6953
|
+
}
|
|
6954
|
+
let networkCost = 0;
|
|
6955
|
+
switch (step.type) {
|
|
6956
|
+
case "full-scan":
|
|
6957
|
+
networkCost = context.nodeCount * 10;
|
|
6958
|
+
break;
|
|
6959
|
+
case "index-scan":
|
|
6960
|
+
networkCost = 5;
|
|
6961
|
+
break;
|
|
6962
|
+
case "point-lookup":
|
|
6963
|
+
networkCost = 5;
|
|
6964
|
+
break;
|
|
6965
|
+
case "multi-point-lookup":
|
|
6966
|
+
networkCost = Math.min(step.keys.length, context.nodeCount) * 5;
|
|
6967
|
+
break;
|
|
6968
|
+
case "intersection":
|
|
6969
|
+
case "union":
|
|
6970
|
+
networkCost = step.steps.length * 5;
|
|
6971
|
+
break;
|
|
6972
|
+
case "filter":
|
|
6973
|
+
return this.estimateDistributedCost(step.source, context);
|
|
6974
|
+
case "not":
|
|
6975
|
+
networkCost = context.nodeCount * 5;
|
|
6976
|
+
break;
|
|
6977
|
+
case "fts-scan":
|
|
6978
|
+
networkCost = Math.ceil(context.nodeCount / 2) * 5;
|
|
6979
|
+
break;
|
|
6980
|
+
case "fusion":
|
|
6981
|
+
networkCost = step.steps.reduce(
|
|
6982
|
+
(sum, s) => sum + this.estimateDistributedCost(s, context).network,
|
|
6983
|
+
0
|
|
6984
|
+
);
|
|
6985
|
+
break;
|
|
6986
|
+
}
|
|
6987
|
+
return {
|
|
6988
|
+
rows: baseCost,
|
|
6989
|
+
cpu: baseCost,
|
|
6990
|
+
network: networkCost,
|
|
6991
|
+
io: context.usesStorage ? baseCost * 0.5 : 0
|
|
6992
|
+
};
|
|
6993
|
+
}
|
|
6994
|
+
/**
|
|
6995
|
+
* Get total distributed cost for a plan step.
|
|
6996
|
+
* Convenience method combining estimateDistributedCost and calculateTotalCost.
|
|
6997
|
+
*
|
|
6998
|
+
* @param step - Plan step to estimate
|
|
6999
|
+
* @param context - Distributed query context (optional)
|
|
7000
|
+
* @returns Weighted total cost
|
|
7001
|
+
*/
|
|
7002
|
+
getTotalDistributedCost(step, context) {
|
|
7003
|
+
const distributedCost = this.estimateDistributedCost(step, context);
|
|
7004
|
+
return calculateTotalCost(distributedCost);
|
|
7005
|
+
}
|
|
6711
7006
|
/**
|
|
6712
7007
|
* Check if a plan step uses any indexes.
|
|
6713
7008
|
*/
|
|
6714
7009
|
usesIndexes(step) {
|
|
6715
7010
|
switch (step.type) {
|
|
7011
|
+
case "point-lookup":
|
|
7012
|
+
case "multi-point-lookup":
|
|
7013
|
+
return true;
|
|
6716
7014
|
case "index-scan":
|
|
6717
7015
|
return true;
|
|
6718
7016
|
case "full-scan":
|
|
@@ -8893,6 +9191,24 @@ var IndexedLWWMap = class extends LWWMap {
|
|
|
8893
9191
|
*/
|
|
8894
9192
|
executePlan(step) {
|
|
8895
9193
|
switch (step.type) {
|
|
9194
|
+
case "point-lookup": {
|
|
9195
|
+
const key = step.key;
|
|
9196
|
+
const result = /* @__PURE__ */ new Set();
|
|
9197
|
+
if (this.get(key) !== void 0) {
|
|
9198
|
+
result.add(key);
|
|
9199
|
+
}
|
|
9200
|
+
return new SetResultSet(result, 1);
|
|
9201
|
+
}
|
|
9202
|
+
case "multi-point-lookup": {
|
|
9203
|
+
const result = /* @__PURE__ */ new Set();
|
|
9204
|
+
for (const key of step.keys) {
|
|
9205
|
+
const k = key;
|
|
9206
|
+
if (this.get(k) !== void 0) {
|
|
9207
|
+
result.add(k);
|
|
9208
|
+
}
|
|
9209
|
+
}
|
|
9210
|
+
return new SetResultSet(result, step.keys.length);
|
|
9211
|
+
}
|
|
8896
9212
|
case "index-scan":
|
|
8897
9213
|
return step.index.retrieve(step.query);
|
|
8898
9214
|
case "full-scan": {
|
|
@@ -11951,6 +12267,504 @@ function getSearchDebugger() {
|
|
|
11951
12267
|
function resetSearchDebugger() {
|
|
11952
12268
|
globalSearchDebugger = null;
|
|
11953
12269
|
}
|
|
12270
|
+
|
|
12271
|
+
// src/testing/VirtualClock.ts
|
|
12272
|
+
var RealClock = {
|
|
12273
|
+
now: () => Date.now()
|
|
12274
|
+
};
|
|
12275
|
+
var VirtualClock = class {
|
|
12276
|
+
/**
|
|
12277
|
+
* @param initialTime Starting timestamp in milliseconds (default: 0)
|
|
12278
|
+
*/
|
|
12279
|
+
constructor(initialTime = 0) {
|
|
12280
|
+
if (!Number.isFinite(initialTime) || initialTime < 0) {
|
|
12281
|
+
throw new Error("Initial time must be a non-negative finite number");
|
|
12282
|
+
}
|
|
12283
|
+
this.currentTime = initialTime;
|
|
12284
|
+
}
|
|
12285
|
+
/**
|
|
12286
|
+
* Returns the current virtual time.
|
|
12287
|
+
* Time remains frozen until advance() or set() is called.
|
|
12288
|
+
*/
|
|
12289
|
+
now() {
|
|
12290
|
+
return this.currentTime;
|
|
12291
|
+
}
|
|
12292
|
+
/**
|
|
12293
|
+
* Advances time forward by the specified milliseconds.
|
|
12294
|
+
* @param ms Milliseconds to advance (must be non-negative)
|
|
12295
|
+
*/
|
|
12296
|
+
advance(ms) {
|
|
12297
|
+
if (!Number.isFinite(ms) || ms < 0) {
|
|
12298
|
+
throw new Error("Advance amount must be a non-negative finite number");
|
|
12299
|
+
}
|
|
12300
|
+
this.currentTime += ms;
|
|
12301
|
+
}
|
|
12302
|
+
/**
|
|
12303
|
+
* Sets the clock to a specific time.
|
|
12304
|
+
* Allows moving time forward or backward (useful for testing).
|
|
12305
|
+
* @param time Absolute timestamp in milliseconds
|
|
12306
|
+
*/
|
|
12307
|
+
set(time) {
|
|
12308
|
+
if (!Number.isFinite(time) || time < 0) {
|
|
12309
|
+
throw new Error("Time must be a non-negative finite number");
|
|
12310
|
+
}
|
|
12311
|
+
this.currentTime = time;
|
|
12312
|
+
}
|
|
12313
|
+
/**
|
|
12314
|
+
* Resets the clock to zero.
|
|
12315
|
+
*/
|
|
12316
|
+
reset() {
|
|
12317
|
+
this.currentTime = 0;
|
|
12318
|
+
}
|
|
12319
|
+
};
|
|
12320
|
+
|
|
12321
|
+
// src/testing/SeededRNG.ts
|
|
12322
|
+
var SeededRNG = class {
|
|
12323
|
+
/**
|
|
12324
|
+
* @param seed Integer seed value. Same seed = same sequence.
|
|
12325
|
+
*/
|
|
12326
|
+
constructor(seed) {
|
|
12327
|
+
if (!Number.isInteger(seed)) {
|
|
12328
|
+
throw new Error("Seed must be an integer");
|
|
12329
|
+
}
|
|
12330
|
+
this.state = seed >>> 0;
|
|
12331
|
+
this.originalSeed = this.state;
|
|
12332
|
+
}
|
|
12333
|
+
/**
|
|
12334
|
+
* Returns the original seed used to construct this RNG.
|
|
12335
|
+
*/
|
|
12336
|
+
getSeed() {
|
|
12337
|
+
return this.originalSeed;
|
|
12338
|
+
}
|
|
12339
|
+
/**
|
|
12340
|
+
* Generates the next random number in [0, 1).
|
|
12341
|
+
* Uses mulberry32 algorithm for deterministic, high-quality randomness.
|
|
12342
|
+
*/
|
|
12343
|
+
random() {
|
|
12344
|
+
let t = this.state += 1831565813;
|
|
12345
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
12346
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
12347
|
+
this.state = t;
|
|
12348
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
12349
|
+
}
|
|
12350
|
+
/**
|
|
12351
|
+
* Generates a random integer in [min, max] (inclusive).
|
|
12352
|
+
* @param min Minimum value (inclusive)
|
|
12353
|
+
* @param max Maximum value (inclusive)
|
|
12354
|
+
*/
|
|
12355
|
+
randomInt(min, max) {
|
|
12356
|
+
if (!Number.isInteger(min) || !Number.isInteger(max)) {
|
|
12357
|
+
throw new Error("Min and max must be integers");
|
|
12358
|
+
}
|
|
12359
|
+
if (min > max) {
|
|
12360
|
+
throw new Error("Min must be less than or equal to max");
|
|
12361
|
+
}
|
|
12362
|
+
const range = max - min + 1;
|
|
12363
|
+
return Math.floor(this.random() * range) + min;
|
|
12364
|
+
}
|
|
12365
|
+
/**
|
|
12366
|
+
* Generates a random boolean value.
|
|
12367
|
+
* @param probability Probability of returning true (default: 0.5)
|
|
12368
|
+
*/
|
|
12369
|
+
randomBool(probability = 0.5) {
|
|
12370
|
+
if (probability < 0 || probability > 1) {
|
|
12371
|
+
throw new Error("Probability must be between 0 and 1");
|
|
12372
|
+
}
|
|
12373
|
+
return this.random() < probability;
|
|
12374
|
+
}
|
|
12375
|
+
/**
|
|
12376
|
+
* Shuffles an array in place using Fisher-Yates algorithm.
|
|
12377
|
+
* Returns the shuffled array.
|
|
12378
|
+
* @param array Array to shuffle
|
|
12379
|
+
*/
|
|
12380
|
+
shuffle(array) {
|
|
12381
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
12382
|
+
const j = this.randomInt(0, i);
|
|
12383
|
+
[array[i], array[j]] = [array[j], array[i]];
|
|
12384
|
+
}
|
|
12385
|
+
return array;
|
|
12386
|
+
}
|
|
12387
|
+
/**
|
|
12388
|
+
* Picks a random element from an array.
|
|
12389
|
+
* @param array Array to pick from
|
|
12390
|
+
* @returns Random element, or undefined if array is empty
|
|
12391
|
+
*/
|
|
12392
|
+
pick(array) {
|
|
12393
|
+
if (array.length === 0) return void 0;
|
|
12394
|
+
return array[this.randomInt(0, array.length - 1)];
|
|
12395
|
+
}
|
|
12396
|
+
/**
|
|
12397
|
+
* Resets the RNG to its original seed.
|
|
12398
|
+
* Useful for reproducing a sequence from the start.
|
|
12399
|
+
*/
|
|
12400
|
+
reset() {
|
|
12401
|
+
this.state = this.originalSeed;
|
|
12402
|
+
}
|
|
12403
|
+
};
|
|
12404
|
+
|
|
12405
|
+
// src/testing/VirtualNetwork.ts
|
|
12406
|
+
var VirtualNetwork = class {
|
|
12407
|
+
constructor(rng, clock) {
|
|
12408
|
+
this.pendingMessages = [];
|
|
12409
|
+
this.rng = rng;
|
|
12410
|
+
this.clock = clock;
|
|
12411
|
+
this.config = {
|
|
12412
|
+
latencyMs: { min: 0, max: 0 },
|
|
12413
|
+
packetLossRate: 0,
|
|
12414
|
+
partitions: []
|
|
12415
|
+
};
|
|
12416
|
+
}
|
|
12417
|
+
/**
|
|
12418
|
+
* Updates network configuration.
|
|
12419
|
+
* Partially updates existing config with provided values.
|
|
12420
|
+
*/
|
|
12421
|
+
configure(config) {
|
|
12422
|
+
if (config.latencyMs !== void 0) {
|
|
12423
|
+
const { min, max } = config.latencyMs;
|
|
12424
|
+
if (min < 0 || max < 0 || min > max) {
|
|
12425
|
+
throw new Error("Invalid latency range");
|
|
12426
|
+
}
|
|
12427
|
+
this.config.latencyMs = config.latencyMs;
|
|
12428
|
+
}
|
|
12429
|
+
if (config.packetLossRate !== void 0) {
|
|
12430
|
+
if (config.packetLossRate < 0 || config.packetLossRate > 1) {
|
|
12431
|
+
throw new Error("Packet loss rate must be between 0 and 1");
|
|
12432
|
+
}
|
|
12433
|
+
this.config.packetLossRate = config.packetLossRate;
|
|
12434
|
+
}
|
|
12435
|
+
if (config.partitions !== void 0) {
|
|
12436
|
+
this.config.partitions = config.partitions;
|
|
12437
|
+
}
|
|
12438
|
+
}
|
|
12439
|
+
/**
|
|
12440
|
+
* Sends a message through the network.
|
|
12441
|
+
* Subject to packet loss, latency, and partition rules.
|
|
12442
|
+
*/
|
|
12443
|
+
send(from, to, payload) {
|
|
12444
|
+
if (this.rng.random() < this.config.packetLossRate) {
|
|
12445
|
+
return;
|
|
12446
|
+
}
|
|
12447
|
+
if (this.isPartitioned(from, to)) {
|
|
12448
|
+
return;
|
|
12449
|
+
}
|
|
12450
|
+
const latency = this.rng.randomInt(
|
|
12451
|
+
this.config.latencyMs.min,
|
|
12452
|
+
this.config.latencyMs.max
|
|
12453
|
+
);
|
|
12454
|
+
const scheduledTime = this.clock.now() + latency;
|
|
12455
|
+
this.pendingMessages.push({
|
|
12456
|
+
from,
|
|
12457
|
+
to,
|
|
12458
|
+
payload,
|
|
12459
|
+
scheduledTime
|
|
12460
|
+
});
|
|
12461
|
+
}
|
|
12462
|
+
/**
|
|
12463
|
+
* Creates a network partition between two groups.
|
|
12464
|
+
* Nodes in groupA cannot communicate with nodes in groupB.
|
|
12465
|
+
*/
|
|
12466
|
+
partition(groupA, groupB) {
|
|
12467
|
+
this.config.partitions.push(groupA, groupB);
|
|
12468
|
+
}
|
|
12469
|
+
/**
|
|
12470
|
+
* Removes all network partitions.
|
|
12471
|
+
*/
|
|
12472
|
+
heal() {
|
|
12473
|
+
this.config.partitions = [];
|
|
12474
|
+
}
|
|
12475
|
+
/**
|
|
12476
|
+
* Delivers all messages scheduled at or before the current time.
|
|
12477
|
+
* @returns Array of delivered messages
|
|
12478
|
+
*/
|
|
12479
|
+
tick() {
|
|
12480
|
+
const currentTime = this.clock.now();
|
|
12481
|
+
const delivered = [];
|
|
12482
|
+
const remaining = [];
|
|
12483
|
+
for (const msg of this.pendingMessages) {
|
|
12484
|
+
if (msg.scheduledTime <= currentTime) {
|
|
12485
|
+
delivered.push(msg);
|
|
12486
|
+
} else {
|
|
12487
|
+
remaining.push(msg);
|
|
12488
|
+
}
|
|
12489
|
+
}
|
|
12490
|
+
this.pendingMessages = remaining;
|
|
12491
|
+
return delivered;
|
|
12492
|
+
}
|
|
12493
|
+
/**
|
|
12494
|
+
* Returns the number of messages currently in flight.
|
|
12495
|
+
*/
|
|
12496
|
+
getPendingCount() {
|
|
12497
|
+
return this.pendingMessages.length;
|
|
12498
|
+
}
|
|
12499
|
+
/**
|
|
12500
|
+
* Clears all pending messages.
|
|
12501
|
+
*/
|
|
12502
|
+
clear() {
|
|
12503
|
+
this.pendingMessages = [];
|
|
12504
|
+
}
|
|
12505
|
+
/**
|
|
12506
|
+
* Checks if two nodes are partitioned from each other.
|
|
12507
|
+
*/
|
|
12508
|
+
isPartitioned(from, to) {
|
|
12509
|
+
for (let i = 0; i < this.config.partitions.length; i += 2) {
|
|
12510
|
+
const groupA = this.config.partitions[i];
|
|
12511
|
+
const groupB = this.config.partitions[i + 1];
|
|
12512
|
+
if (groupA.includes(from) && groupB.includes(to) || groupB.includes(from) && groupA.includes(to)) {
|
|
12513
|
+
return true;
|
|
12514
|
+
}
|
|
12515
|
+
}
|
|
12516
|
+
return false;
|
|
12517
|
+
}
|
|
12518
|
+
/**
|
|
12519
|
+
* Returns all pending messages (useful for debugging).
|
|
12520
|
+
*/
|
|
12521
|
+
getPendingMessages() {
|
|
12522
|
+
return [...this.pendingMessages];
|
|
12523
|
+
}
|
|
12524
|
+
};
|
|
12525
|
+
|
|
12526
|
+
// src/testing/InvariantChecker.ts
|
|
12527
|
+
var InvariantChecker = class {
|
|
12528
|
+
constructor() {
|
|
12529
|
+
this.invariants = /* @__PURE__ */ new Map();
|
|
12530
|
+
}
|
|
12531
|
+
/**
|
|
12532
|
+
* Adds an invariant to be checked.
|
|
12533
|
+
* @param name Unique name for this invariant
|
|
12534
|
+
* @param check Function that returns true if invariant holds
|
|
12535
|
+
*/
|
|
12536
|
+
addInvariant(name, check) {
|
|
12537
|
+
if (this.invariants.has(name)) {
|
|
12538
|
+
throw new Error(`Invariant '${name}' already exists`);
|
|
12539
|
+
}
|
|
12540
|
+
this.invariants.set(name, check);
|
|
12541
|
+
}
|
|
12542
|
+
/**
|
|
12543
|
+
* Removes an invariant by name.
|
|
12544
|
+
*/
|
|
12545
|
+
removeInvariant(name) {
|
|
12546
|
+
return this.invariants.delete(name);
|
|
12547
|
+
}
|
|
12548
|
+
/**
|
|
12549
|
+
* Verifies all invariants against the provided state.
|
|
12550
|
+
* @returns Result with pass/fail status and list of failed invariants
|
|
12551
|
+
*/
|
|
12552
|
+
verify(state) {
|
|
12553
|
+
const failures = [];
|
|
12554
|
+
for (const [name, check] of this.invariants.entries()) {
|
|
12555
|
+
try {
|
|
12556
|
+
if (!check(state)) {
|
|
12557
|
+
failures.push(name);
|
|
12558
|
+
}
|
|
12559
|
+
} catch (error) {
|
|
12560
|
+
failures.push(`${name} (exception: ${error instanceof Error ? error.message : String(error)})`);
|
|
12561
|
+
}
|
|
12562
|
+
}
|
|
12563
|
+
return {
|
|
12564
|
+
passed: failures.length === 0,
|
|
12565
|
+
failures
|
|
12566
|
+
};
|
|
12567
|
+
}
|
|
12568
|
+
/**
|
|
12569
|
+
* Returns the number of registered invariants.
|
|
12570
|
+
*/
|
|
12571
|
+
get count() {
|
|
12572
|
+
return this.invariants.size;
|
|
12573
|
+
}
|
|
12574
|
+
/**
|
|
12575
|
+
* Clears all invariants.
|
|
12576
|
+
*/
|
|
12577
|
+
clear() {
|
|
12578
|
+
this.invariants.clear();
|
|
12579
|
+
}
|
|
12580
|
+
};
|
|
12581
|
+
var CRDTInvariants = {
|
|
12582
|
+
/**
|
|
12583
|
+
* Verifies LWW-Map convergence: all maps contain the same values for same keys.
|
|
12584
|
+
*/
|
|
12585
|
+
lwwConvergence: (maps) => {
|
|
12586
|
+
if (maps.length < 2) return true;
|
|
12587
|
+
const reference = maps[0];
|
|
12588
|
+
const refKeys = new Set(reference.allKeys());
|
|
12589
|
+
for (let i = 1; i < maps.length; i++) {
|
|
12590
|
+
const other = maps[i];
|
|
12591
|
+
const otherKeys = new Set(other.allKeys());
|
|
12592
|
+
if (refKeys.size !== otherKeys.size) return false;
|
|
12593
|
+
for (const key of refKeys) {
|
|
12594
|
+
if (!otherKeys.has(key)) return false;
|
|
12595
|
+
}
|
|
12596
|
+
for (const key of refKeys) {
|
|
12597
|
+
const refRecord = reference.getRecord(key);
|
|
12598
|
+
const otherRecord = other.getRecord(key);
|
|
12599
|
+
if (!refRecord || !otherRecord) {
|
|
12600
|
+
if (refRecord !== otherRecord) return false;
|
|
12601
|
+
continue;
|
|
12602
|
+
}
|
|
12603
|
+
if (refRecord.value !== otherRecord.value) return false;
|
|
12604
|
+
if (HLC.compare(refRecord.timestamp, otherRecord.timestamp) !== 0) {
|
|
12605
|
+
return false;
|
|
12606
|
+
}
|
|
12607
|
+
}
|
|
12608
|
+
}
|
|
12609
|
+
return true;
|
|
12610
|
+
},
|
|
12611
|
+
/**
|
|
12612
|
+
* Verifies OR-Map convergence: all maps contain the same values for same keys.
|
|
12613
|
+
*/
|
|
12614
|
+
orMapConvergence: (maps) => {
|
|
12615
|
+
if (maps.length < 2) return true;
|
|
12616
|
+
const reference = maps[0];
|
|
12617
|
+
const refKeys = reference.allKeys();
|
|
12618
|
+
for (let i = 1; i < maps.length; i++) {
|
|
12619
|
+
const other = maps[i];
|
|
12620
|
+
const otherKeys = new Set(other.allKeys());
|
|
12621
|
+
if (refKeys.length !== otherKeys.size) return false;
|
|
12622
|
+
for (const key of refKeys) {
|
|
12623
|
+
if (!otherKeys.has(key)) return false;
|
|
12624
|
+
}
|
|
12625
|
+
for (const key of refKeys) {
|
|
12626
|
+
const refRecords = reference.getRecords(key);
|
|
12627
|
+
const otherRecords = other.getRecords(key);
|
|
12628
|
+
if (refRecords.length !== otherRecords.length) return false;
|
|
12629
|
+
const refSorted = [...refRecords].sort((a, b) => a.tag.localeCompare(b.tag));
|
|
12630
|
+
const otherSorted = [...otherRecords].sort((a, b) => a.tag.localeCompare(b.tag));
|
|
12631
|
+
for (let j = 0; j < refSorted.length; j++) {
|
|
12632
|
+
if (refSorted[j].tag !== otherSorted[j].tag) return false;
|
|
12633
|
+
if (refSorted[j].value !== otherSorted[j].value) return false;
|
|
12634
|
+
if (HLC.compare(refSorted[j].timestamp, otherSorted[j].timestamp) !== 0) {
|
|
12635
|
+
return false;
|
|
12636
|
+
}
|
|
12637
|
+
}
|
|
12638
|
+
}
|
|
12639
|
+
const refTombstones = new Set(reference.getTombstones());
|
|
12640
|
+
const otherTombstones = new Set(other.getTombstones());
|
|
12641
|
+
if (refTombstones.size !== otherTombstones.size) return false;
|
|
12642
|
+
for (const tag of refTombstones) {
|
|
12643
|
+
if (!otherTombstones.has(tag)) return false;
|
|
12644
|
+
}
|
|
12645
|
+
}
|
|
12646
|
+
return true;
|
|
12647
|
+
},
|
|
12648
|
+
/**
|
|
12649
|
+
* Verifies HLC monotonicity: timestamps are strictly increasing.
|
|
12650
|
+
*/
|
|
12651
|
+
hlcMonotonicity: (timestamps) => {
|
|
12652
|
+
for (let i = 1; i < timestamps.length; i++) {
|
|
12653
|
+
if (HLC.compare(timestamps[i - 1], timestamps[i]) >= 0) {
|
|
12654
|
+
return false;
|
|
12655
|
+
}
|
|
12656
|
+
}
|
|
12657
|
+
return true;
|
|
12658
|
+
},
|
|
12659
|
+
/**
|
|
12660
|
+
* Verifies Merkle tree consistency: trees with same data have same root hash.
|
|
12661
|
+
*/
|
|
12662
|
+
merkleConsistency: (trees) => {
|
|
12663
|
+
if (trees.length < 2) return true;
|
|
12664
|
+
const referenceHash = trees[0].getRootHash();
|
|
12665
|
+
for (let i = 1; i < trees.length; i++) {
|
|
12666
|
+
if (trees[i].getRootHash() !== referenceHash) {
|
|
12667
|
+
return false;
|
|
12668
|
+
}
|
|
12669
|
+
}
|
|
12670
|
+
return true;
|
|
12671
|
+
}
|
|
12672
|
+
};
|
|
12673
|
+
|
|
12674
|
+
// src/testing/ScenarioRunner.ts
|
|
12675
|
+
var ScenarioRunner = class {
|
|
12676
|
+
constructor(config) {
|
|
12677
|
+
if (!config.nodes || config.nodes.length === 0) {
|
|
12678
|
+
throw new Error("Scenario must have at least one node");
|
|
12679
|
+
}
|
|
12680
|
+
if (config.duration <= 0) {
|
|
12681
|
+
throw new Error("Duration must be positive");
|
|
12682
|
+
}
|
|
12683
|
+
this.seed = config.seed ?? Math.floor(Math.random() * 2147483647);
|
|
12684
|
+
this.config = {
|
|
12685
|
+
...config,
|
|
12686
|
+
seed: this.seed,
|
|
12687
|
+
tickInterval: config.tickInterval ?? 1
|
|
12688
|
+
};
|
|
12689
|
+
this.clock = new VirtualClock(0);
|
|
12690
|
+
this.rng = new SeededRNG(this.seed);
|
|
12691
|
+
this.network = new VirtualNetwork(this.rng, this.clock);
|
|
12692
|
+
}
|
|
12693
|
+
/**
|
|
12694
|
+
* Returns the seed used for this scenario.
|
|
12695
|
+
*/
|
|
12696
|
+
getSeed() {
|
|
12697
|
+
return this.seed;
|
|
12698
|
+
}
|
|
12699
|
+
/**
|
|
12700
|
+
* Returns the virtual clock instance.
|
|
12701
|
+
*/
|
|
12702
|
+
getClock() {
|
|
12703
|
+
return this.clock;
|
|
12704
|
+
}
|
|
12705
|
+
/**
|
|
12706
|
+
* Returns the seeded RNG instance.
|
|
12707
|
+
*/
|
|
12708
|
+
getRNG() {
|
|
12709
|
+
return this.rng;
|
|
12710
|
+
}
|
|
12711
|
+
/**
|
|
12712
|
+
* Returns the virtual network instance.
|
|
12713
|
+
*/
|
|
12714
|
+
getNetwork() {
|
|
12715
|
+
return this.network;
|
|
12716
|
+
}
|
|
12717
|
+
/**
|
|
12718
|
+
* Returns the list of nodes in this scenario.
|
|
12719
|
+
*/
|
|
12720
|
+
getNodes() {
|
|
12721
|
+
return [...this.config.nodes];
|
|
12722
|
+
}
|
|
12723
|
+
/**
|
|
12724
|
+
* Executes the simulation scenario.
|
|
12725
|
+
*
|
|
12726
|
+
* @param setup Called once before simulation starts. Initialize state here.
|
|
12727
|
+
* @param step Called on each tick. Perform operations and message delivery.
|
|
12728
|
+
* @param invariants Checker for verifying correctness throughout execution.
|
|
12729
|
+
* @returns Result with pass/fail status and captured state
|
|
12730
|
+
*/
|
|
12731
|
+
run(setup, step, invariants) {
|
|
12732
|
+
const finalStates = /* @__PURE__ */ new Map();
|
|
12733
|
+
const invariantFailures = [];
|
|
12734
|
+
setup(this);
|
|
12735
|
+
let tickCount = 0;
|
|
12736
|
+
const tickInterval = this.config.tickInterval;
|
|
12737
|
+
const endTime = this.config.duration;
|
|
12738
|
+
while (this.clock.now() < endTime) {
|
|
12739
|
+
this.clock.advance(tickInterval);
|
|
12740
|
+
tickCount++;
|
|
12741
|
+
step(this, tickCount);
|
|
12742
|
+
const delivered = this.network.tick();
|
|
12743
|
+
if (delivered.length > 0) {
|
|
12744
|
+
finalStates.set(`_tick_${tickCount}_delivered`, delivered.length);
|
|
12745
|
+
}
|
|
12746
|
+
}
|
|
12747
|
+
const result = invariants.verify(null);
|
|
12748
|
+
if (!result.passed) {
|
|
12749
|
+
invariantFailures.push(...result.failures);
|
|
12750
|
+
}
|
|
12751
|
+
return {
|
|
12752
|
+
seed: this.seed,
|
|
12753
|
+
passed: invariantFailures.length === 0,
|
|
12754
|
+
ticks: tickCount,
|
|
12755
|
+
invariantFailures,
|
|
12756
|
+
finalStates
|
|
12757
|
+
};
|
|
12758
|
+
}
|
|
12759
|
+
/**
|
|
12760
|
+
* Stores state for a node (useful for capturing final state).
|
|
12761
|
+
*/
|
|
12762
|
+
setState(nodeId, state) {
|
|
12763
|
+
if (!this.config.nodes.includes(nodeId)) {
|
|
12764
|
+
throw new Error(`Unknown node: ${nodeId}`);
|
|
12765
|
+
}
|
|
12766
|
+
}
|
|
12767
|
+
};
|
|
11954
12768
|
export {
|
|
11955
12769
|
AuthFailMessageSchema,
|
|
11956
12770
|
AuthMessageSchema,
|
|
@@ -11958,7 +12772,9 @@ export {
|
|
|
11958
12772
|
BatchMessageSchema,
|
|
11959
12773
|
BuiltInProcessors,
|
|
11960
12774
|
BuiltInResolvers,
|
|
12775
|
+
COST_WEIGHTS,
|
|
11961
12776
|
CRDTDebugger,
|
|
12777
|
+
CRDTInvariants,
|
|
11962
12778
|
ClientOpMessageSchema,
|
|
11963
12779
|
ClientOpSchema,
|
|
11964
12780
|
ClusterSearchReqMessageSchema,
|
|
@@ -12000,6 +12816,7 @@ export {
|
|
|
12000
12816
|
DEFAULT_RESOLVER_RATE_LIMITS,
|
|
12001
12817
|
DEFAULT_STOP_WORDS,
|
|
12002
12818
|
DEFAULT_WRITE_CONCERN_TIMEOUT,
|
|
12819
|
+
DeltaRecordSchema,
|
|
12003
12820
|
ENGLISH_STOPWORDS,
|
|
12004
12821
|
EntryProcessBatchRequestSchema,
|
|
12005
12822
|
EntryProcessBatchResponseSchema,
|
|
@@ -12019,12 +12836,20 @@ export {
|
|
|
12019
12836
|
GcPrunePayloadSchema,
|
|
12020
12837
|
HLC,
|
|
12021
12838
|
HashIndex,
|
|
12839
|
+
HttpQueryRequestSchema,
|
|
12840
|
+
HttpQueryResultSchema,
|
|
12841
|
+
HttpSearchRequestSchema,
|
|
12842
|
+
HttpSearchResultSchema,
|
|
12843
|
+
HttpSyncErrorSchema,
|
|
12844
|
+
HttpSyncRequestSchema,
|
|
12845
|
+
HttpSyncResponseSchema,
|
|
12022
12846
|
HybridQueryDeltaPayloadSchema,
|
|
12023
12847
|
HybridQueryRespPayloadSchema,
|
|
12024
12848
|
IndexRegistry,
|
|
12025
12849
|
IndexedLWWMap,
|
|
12026
12850
|
IndexedORMap,
|
|
12027
12851
|
IntersectionResultSet,
|
|
12852
|
+
InvariantChecker,
|
|
12028
12853
|
InvertedIndex,
|
|
12029
12854
|
JournalEventDataSchema,
|
|
12030
12855
|
JournalEventMessageSchema,
|
|
@@ -12045,6 +12870,7 @@ export {
|
|
|
12045
12870
|
LockReleasedPayloadSchema,
|
|
12046
12871
|
LockRequestSchema,
|
|
12047
12872
|
LowercaseFilter,
|
|
12873
|
+
MapDeltaSchema,
|
|
12048
12874
|
MaxLengthFilter,
|
|
12049
12875
|
MergeRejectedMessageSchema,
|
|
12050
12876
|
MerkleReqBucketMessageSchema,
|
|
@@ -12089,10 +12915,12 @@ export {
|
|
|
12089
12915
|
QueryUpdateMessageSchema,
|
|
12090
12916
|
QueryUpdatePayloadSchema,
|
|
12091
12917
|
RESOLVER_FORBIDDEN_PATTERNS,
|
|
12918
|
+
RealClock,
|
|
12092
12919
|
ReciprocalRankFusion,
|
|
12093
12920
|
RegisterResolverRequestSchema,
|
|
12094
12921
|
RegisterResolverResponseSchema,
|
|
12095
12922
|
Ringbuffer,
|
|
12923
|
+
ScenarioRunner,
|
|
12096
12924
|
SearchCursor,
|
|
12097
12925
|
SearchDebugger,
|
|
12098
12926
|
SearchMessageSchema,
|
|
@@ -12107,6 +12935,7 @@ export {
|
|
|
12107
12935
|
SearchUpdateMessageSchema,
|
|
12108
12936
|
SearchUpdatePayloadSchema,
|
|
12109
12937
|
SearchUpdateTypeSchema,
|
|
12938
|
+
SeededRNG,
|
|
12110
12939
|
ServerBatchEventMessageSchema,
|
|
12111
12940
|
ServerEventMessageSchema,
|
|
12112
12941
|
ServerEventPayloadSchema,
|
|
@@ -12118,6 +12947,7 @@ export {
|
|
|
12118
12947
|
StandingQueryRegistry,
|
|
12119
12948
|
StopWordFilter,
|
|
12120
12949
|
SyncInitMessageSchema,
|
|
12950
|
+
SyncMapEntrySchema,
|
|
12121
12951
|
SyncResetRequiredPayloadSchema,
|
|
12122
12952
|
SyncRespBucketsMessageSchema,
|
|
12123
12953
|
SyncRespLeafMessageSchema,
|
|
@@ -12133,11 +12963,14 @@ export {
|
|
|
12133
12963
|
UniqueFilter,
|
|
12134
12964
|
UnregisterResolverRequestSchema,
|
|
12135
12965
|
UnregisterResolverResponseSchema,
|
|
12966
|
+
VirtualClock,
|
|
12967
|
+
VirtualNetwork,
|
|
12136
12968
|
WRITE_CONCERN_ORDER,
|
|
12137
12969
|
WhitespaceTokenizer,
|
|
12138
12970
|
WordBoundaryTokenizer,
|
|
12139
12971
|
WriteConcern,
|
|
12140
12972
|
WriteConcernSchema,
|
|
12973
|
+
calculateTotalCost,
|
|
12141
12974
|
combineHashes,
|
|
12142
12975
|
compareHLCTimestamps,
|
|
12143
12976
|
compareTimestamps,
|