@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.js
CHANGED
|
@@ -36,7 +36,9 @@ __export(index_exports, {
|
|
|
36
36
|
BatchMessageSchema: () => BatchMessageSchema,
|
|
37
37
|
BuiltInProcessors: () => BuiltInProcessors,
|
|
38
38
|
BuiltInResolvers: () => BuiltInResolvers,
|
|
39
|
+
COST_WEIGHTS: () => COST_WEIGHTS,
|
|
39
40
|
CRDTDebugger: () => CRDTDebugger,
|
|
41
|
+
CRDTInvariants: () => CRDTInvariants,
|
|
40
42
|
ClientOpMessageSchema: () => ClientOpMessageSchema,
|
|
41
43
|
ClientOpSchema: () => ClientOpSchema,
|
|
42
44
|
ClusterSearchReqMessageSchema: () => ClusterSearchReqMessageSchema,
|
|
@@ -78,6 +80,7 @@ __export(index_exports, {
|
|
|
78
80
|
DEFAULT_RESOLVER_RATE_LIMITS: () => DEFAULT_RESOLVER_RATE_LIMITS,
|
|
79
81
|
DEFAULT_STOP_WORDS: () => DEFAULT_STOP_WORDS,
|
|
80
82
|
DEFAULT_WRITE_CONCERN_TIMEOUT: () => DEFAULT_WRITE_CONCERN_TIMEOUT,
|
|
83
|
+
DeltaRecordSchema: () => DeltaRecordSchema,
|
|
81
84
|
ENGLISH_STOPWORDS: () => ENGLISH_STOPWORDS,
|
|
82
85
|
EntryProcessBatchRequestSchema: () => EntryProcessBatchRequestSchema,
|
|
83
86
|
EntryProcessBatchResponseSchema: () => EntryProcessBatchResponseSchema,
|
|
@@ -97,12 +100,20 @@ __export(index_exports, {
|
|
|
97
100
|
GcPrunePayloadSchema: () => GcPrunePayloadSchema,
|
|
98
101
|
HLC: () => HLC,
|
|
99
102
|
HashIndex: () => HashIndex,
|
|
103
|
+
HttpQueryRequestSchema: () => HttpQueryRequestSchema,
|
|
104
|
+
HttpQueryResultSchema: () => HttpQueryResultSchema,
|
|
105
|
+
HttpSearchRequestSchema: () => HttpSearchRequestSchema,
|
|
106
|
+
HttpSearchResultSchema: () => HttpSearchResultSchema,
|
|
107
|
+
HttpSyncErrorSchema: () => HttpSyncErrorSchema,
|
|
108
|
+
HttpSyncRequestSchema: () => HttpSyncRequestSchema,
|
|
109
|
+
HttpSyncResponseSchema: () => HttpSyncResponseSchema,
|
|
100
110
|
HybridQueryDeltaPayloadSchema: () => HybridQueryDeltaPayloadSchema,
|
|
101
111
|
HybridQueryRespPayloadSchema: () => HybridQueryRespPayloadSchema,
|
|
102
112
|
IndexRegistry: () => IndexRegistry,
|
|
103
113
|
IndexedLWWMap: () => IndexedLWWMap,
|
|
104
114
|
IndexedORMap: () => IndexedORMap,
|
|
105
115
|
IntersectionResultSet: () => IntersectionResultSet,
|
|
116
|
+
InvariantChecker: () => InvariantChecker,
|
|
106
117
|
InvertedIndex: () => InvertedIndex,
|
|
107
118
|
JournalEventDataSchema: () => JournalEventDataSchema,
|
|
108
119
|
JournalEventMessageSchema: () => JournalEventMessageSchema,
|
|
@@ -123,6 +134,7 @@ __export(index_exports, {
|
|
|
123
134
|
LockReleasedPayloadSchema: () => LockReleasedPayloadSchema,
|
|
124
135
|
LockRequestSchema: () => LockRequestSchema,
|
|
125
136
|
LowercaseFilter: () => LowercaseFilter,
|
|
137
|
+
MapDeltaSchema: () => MapDeltaSchema,
|
|
126
138
|
MaxLengthFilter: () => MaxLengthFilter,
|
|
127
139
|
MergeRejectedMessageSchema: () => MergeRejectedMessageSchema,
|
|
128
140
|
MerkleReqBucketMessageSchema: () => MerkleReqBucketMessageSchema,
|
|
@@ -167,10 +179,12 @@ __export(index_exports, {
|
|
|
167
179
|
QueryUpdateMessageSchema: () => QueryUpdateMessageSchema,
|
|
168
180
|
QueryUpdatePayloadSchema: () => QueryUpdatePayloadSchema,
|
|
169
181
|
RESOLVER_FORBIDDEN_PATTERNS: () => RESOLVER_FORBIDDEN_PATTERNS,
|
|
182
|
+
RealClock: () => RealClock,
|
|
170
183
|
ReciprocalRankFusion: () => ReciprocalRankFusion,
|
|
171
184
|
RegisterResolverRequestSchema: () => RegisterResolverRequestSchema,
|
|
172
185
|
RegisterResolverResponseSchema: () => RegisterResolverResponseSchema,
|
|
173
186
|
Ringbuffer: () => Ringbuffer,
|
|
187
|
+
ScenarioRunner: () => ScenarioRunner,
|
|
174
188
|
SearchCursor: () => SearchCursor,
|
|
175
189
|
SearchDebugger: () => SearchDebugger,
|
|
176
190
|
SearchMessageSchema: () => SearchMessageSchema,
|
|
@@ -185,6 +199,7 @@ __export(index_exports, {
|
|
|
185
199
|
SearchUpdateMessageSchema: () => SearchUpdateMessageSchema,
|
|
186
200
|
SearchUpdatePayloadSchema: () => SearchUpdatePayloadSchema,
|
|
187
201
|
SearchUpdateTypeSchema: () => SearchUpdateTypeSchema,
|
|
202
|
+
SeededRNG: () => SeededRNG,
|
|
188
203
|
ServerBatchEventMessageSchema: () => ServerBatchEventMessageSchema,
|
|
189
204
|
ServerEventMessageSchema: () => ServerEventMessageSchema,
|
|
190
205
|
ServerEventPayloadSchema: () => ServerEventPayloadSchema,
|
|
@@ -196,6 +211,7 @@ __export(index_exports, {
|
|
|
196
211
|
StandingQueryRegistry: () => StandingQueryRegistry,
|
|
197
212
|
StopWordFilter: () => StopWordFilter,
|
|
198
213
|
SyncInitMessageSchema: () => SyncInitMessageSchema,
|
|
214
|
+
SyncMapEntrySchema: () => SyncMapEntrySchema,
|
|
199
215
|
SyncResetRequiredPayloadSchema: () => SyncResetRequiredPayloadSchema,
|
|
200
216
|
SyncRespBucketsMessageSchema: () => SyncRespBucketsMessageSchema,
|
|
201
217
|
SyncRespLeafMessageSchema: () => SyncRespLeafMessageSchema,
|
|
@@ -211,11 +227,14 @@ __export(index_exports, {
|
|
|
211
227
|
UniqueFilter: () => UniqueFilter,
|
|
212
228
|
UnregisterResolverRequestSchema: () => UnregisterResolverRequestSchema,
|
|
213
229
|
UnregisterResolverResponseSchema: () => UnregisterResolverResponseSchema,
|
|
230
|
+
VirtualClock: () => VirtualClock,
|
|
231
|
+
VirtualNetwork: () => VirtualNetwork,
|
|
214
232
|
WRITE_CONCERN_ORDER: () => WRITE_CONCERN_ORDER,
|
|
215
233
|
WhitespaceTokenizer: () => WhitespaceTokenizer,
|
|
216
234
|
WordBoundaryTokenizer: () => WordBoundaryTokenizer,
|
|
217
235
|
WriteConcern: () => WriteConcern,
|
|
218
236
|
WriteConcernSchema: () => WriteConcernSchema,
|
|
237
|
+
calculateTotalCost: () => calculateTotalCost,
|
|
219
238
|
combineHashes: () => combineHashes,
|
|
220
239
|
compareHLCTimestamps: () => compareHLCTimestamps,
|
|
221
240
|
compareTimestamps: () => compareTimestamps,
|
|
@@ -269,6 +288,7 @@ var HLC = class {
|
|
|
269
288
|
this.nodeId = nodeId;
|
|
270
289
|
this.strictMode = options.strictMode ?? false;
|
|
271
290
|
this.maxDriftMs = options.maxDriftMs ?? 6e4;
|
|
291
|
+
this.clockSource = options.clockSource ?? { now: () => Date.now() };
|
|
272
292
|
this.lastMillis = 0;
|
|
273
293
|
this.lastCounter = 0;
|
|
274
294
|
}
|
|
@@ -281,12 +301,19 @@ var HLC = class {
|
|
|
281
301
|
get getMaxDriftMs() {
|
|
282
302
|
return this.maxDriftMs;
|
|
283
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* Returns the clock source used by this HLC instance.
|
|
306
|
+
* Useful for LWWMap/ORMap to access the same clock for TTL checks.
|
|
307
|
+
*/
|
|
308
|
+
getClockSource() {
|
|
309
|
+
return this.clockSource;
|
|
310
|
+
}
|
|
284
311
|
/**
|
|
285
312
|
* Generates a new unique timestamp for a local event.
|
|
286
313
|
* Ensures monotonicity: always greater than any previously generated or received timestamp.
|
|
287
314
|
*/
|
|
288
315
|
now() {
|
|
289
|
-
const systemTime =
|
|
316
|
+
const systemTime = this.clockSource.now();
|
|
290
317
|
if (systemTime > this.lastMillis) {
|
|
291
318
|
this.lastMillis = systemTime;
|
|
292
319
|
this.lastCounter = 0;
|
|
@@ -304,7 +331,7 @@ var HLC = class {
|
|
|
304
331
|
* Must be called whenever a message/event is received from another node.
|
|
305
332
|
*/
|
|
306
333
|
update(remote) {
|
|
307
|
-
const systemTime =
|
|
334
|
+
const systemTime = this.clockSource.now();
|
|
308
335
|
const drift = remote.millis - systemTime;
|
|
309
336
|
if (drift > this.maxDriftMs) {
|
|
310
337
|
if (this.strictMode) {
|
|
@@ -588,7 +615,7 @@ var LWWMap = class {
|
|
|
588
615
|
return void 0;
|
|
589
616
|
}
|
|
590
617
|
if (record.ttlMs) {
|
|
591
|
-
const now =
|
|
618
|
+
const now = this.hlc.getClockSource().now();
|
|
592
619
|
if (record.timestamp.millis + record.ttlMs < now) {
|
|
593
620
|
return void 0;
|
|
594
621
|
}
|
|
@@ -665,7 +692,7 @@ var LWWMap = class {
|
|
|
665
692
|
*/
|
|
666
693
|
entries() {
|
|
667
694
|
const iterator = this.data.entries();
|
|
668
|
-
const
|
|
695
|
+
const clockSource = this.hlc.getClockSource();
|
|
669
696
|
return {
|
|
670
697
|
[Symbol.iterator]() {
|
|
671
698
|
return this;
|
|
@@ -675,7 +702,7 @@ var LWWMap = class {
|
|
|
675
702
|
while (!result.done) {
|
|
676
703
|
const [key, record] = result.value;
|
|
677
704
|
if (record.value !== null) {
|
|
678
|
-
if (record.ttlMs && record.timestamp.millis + record.ttlMs < now) {
|
|
705
|
+
if (record.ttlMs && record.timestamp.millis + record.ttlMs < clockSource.now()) {
|
|
679
706
|
result = iterator.next();
|
|
680
707
|
continue;
|
|
681
708
|
}
|
|
@@ -1012,7 +1039,7 @@ var ORMap = class {
|
|
|
1012
1039
|
const keyMap = this.items.get(key);
|
|
1013
1040
|
if (!keyMap) return [];
|
|
1014
1041
|
const values = [];
|
|
1015
|
-
const now =
|
|
1042
|
+
const now = this.hlc.getClockSource().now();
|
|
1016
1043
|
for (const [tag, record] of keyMap.entries()) {
|
|
1017
1044
|
if (!this.tombstones.has(tag)) {
|
|
1018
1045
|
if (record.ttlMs && record.timestamp.millis + record.ttlMs < now) {
|
|
@@ -1032,7 +1059,7 @@ var ORMap = class {
|
|
|
1032
1059
|
const keyMap = this.items.get(key);
|
|
1033
1060
|
if (!keyMap) return [];
|
|
1034
1061
|
const records = [];
|
|
1035
|
-
const now =
|
|
1062
|
+
const now = this.hlc.getClockSource().now();
|
|
1036
1063
|
for (const [tag, record] of keyMap.entries()) {
|
|
1037
1064
|
if (!this.tombstones.has(tag)) {
|
|
1038
1065
|
if (record.ttlMs && record.timestamp.millis + record.ttlMs < now) {
|
|
@@ -3134,9 +3161,86 @@ var SyncResetRequiredPayloadSchema = import_zod9.z.object({
|
|
|
3134
3161
|
reason: import_zod9.z.string()
|
|
3135
3162
|
});
|
|
3136
3163
|
|
|
3137
|
-
// src/schemas/
|
|
3164
|
+
// src/schemas/http-sync-schemas.ts
|
|
3138
3165
|
var import_zod10 = require("zod");
|
|
3139
|
-
var
|
|
3166
|
+
var SyncMapEntrySchema = import_zod10.z.object({
|
|
3167
|
+
mapName: import_zod10.z.string(),
|
|
3168
|
+
lastSyncTimestamp: TimestampSchema
|
|
3169
|
+
});
|
|
3170
|
+
var HttpQueryRequestSchema = import_zod10.z.object({
|
|
3171
|
+
queryId: import_zod10.z.string(),
|
|
3172
|
+
mapName: import_zod10.z.string(),
|
|
3173
|
+
filter: import_zod10.z.any(),
|
|
3174
|
+
limit: import_zod10.z.number().optional(),
|
|
3175
|
+
offset: import_zod10.z.number().optional()
|
|
3176
|
+
});
|
|
3177
|
+
var HttpSearchRequestSchema = import_zod10.z.object({
|
|
3178
|
+
searchId: import_zod10.z.string(),
|
|
3179
|
+
mapName: import_zod10.z.string(),
|
|
3180
|
+
query: import_zod10.z.string(),
|
|
3181
|
+
options: import_zod10.z.any().optional()
|
|
3182
|
+
});
|
|
3183
|
+
var HttpSyncRequestSchema = import_zod10.z.object({
|
|
3184
|
+
// Client identification
|
|
3185
|
+
clientId: import_zod10.z.string(),
|
|
3186
|
+
// Client's current HLC for causality tracking
|
|
3187
|
+
clientHlc: TimestampSchema,
|
|
3188
|
+
// Batch of operations to push (optional)
|
|
3189
|
+
operations: import_zod10.z.array(ClientOpSchema).optional(),
|
|
3190
|
+
// Maps the client wants deltas for, with their last known sync HLC timestamp
|
|
3191
|
+
syncMaps: import_zod10.z.array(SyncMapEntrySchema).optional(),
|
|
3192
|
+
// One-shot queries to execute (optional)
|
|
3193
|
+
queries: import_zod10.z.array(HttpQueryRequestSchema).optional(),
|
|
3194
|
+
// One-shot search requests (optional)
|
|
3195
|
+
searches: import_zod10.z.array(HttpSearchRequestSchema).optional()
|
|
3196
|
+
});
|
|
3197
|
+
var DeltaRecordSchema = import_zod10.z.object({
|
|
3198
|
+
key: import_zod10.z.string(),
|
|
3199
|
+
record: LWWRecordSchema,
|
|
3200
|
+
eventType: import_zod10.z.enum(["PUT", "REMOVE"])
|
|
3201
|
+
});
|
|
3202
|
+
var MapDeltaSchema = import_zod10.z.object({
|
|
3203
|
+
mapName: import_zod10.z.string(),
|
|
3204
|
+
records: import_zod10.z.array(DeltaRecordSchema),
|
|
3205
|
+
serverSyncTimestamp: TimestampSchema
|
|
3206
|
+
});
|
|
3207
|
+
var HttpQueryResultSchema = import_zod10.z.object({
|
|
3208
|
+
queryId: import_zod10.z.string(),
|
|
3209
|
+
results: import_zod10.z.array(import_zod10.z.any()),
|
|
3210
|
+
hasMore: import_zod10.z.boolean().optional(),
|
|
3211
|
+
nextCursor: import_zod10.z.string().optional()
|
|
3212
|
+
});
|
|
3213
|
+
var HttpSearchResultSchema = import_zod10.z.object({
|
|
3214
|
+
searchId: import_zod10.z.string(),
|
|
3215
|
+
results: import_zod10.z.array(import_zod10.z.any()),
|
|
3216
|
+
totalCount: import_zod10.z.number().optional()
|
|
3217
|
+
});
|
|
3218
|
+
var HttpSyncErrorSchema = import_zod10.z.object({
|
|
3219
|
+
code: import_zod10.z.number(),
|
|
3220
|
+
message: import_zod10.z.string(),
|
|
3221
|
+
context: import_zod10.z.string().optional()
|
|
3222
|
+
});
|
|
3223
|
+
var HttpSyncResponseSchema = import_zod10.z.object({
|
|
3224
|
+
// Server's current HLC
|
|
3225
|
+
serverHlc: TimestampSchema,
|
|
3226
|
+
// Acknowledgment of received operations
|
|
3227
|
+
ack: import_zod10.z.object({
|
|
3228
|
+
lastId: import_zod10.z.string(),
|
|
3229
|
+
results: import_zod10.z.array(OpResultSchema).optional()
|
|
3230
|
+
}).optional(),
|
|
3231
|
+
// Delta records for requested maps (new/changed since lastSyncTimestamp)
|
|
3232
|
+
deltas: import_zod10.z.array(MapDeltaSchema).optional(),
|
|
3233
|
+
// Query results
|
|
3234
|
+
queryResults: import_zod10.z.array(HttpQueryResultSchema).optional(),
|
|
3235
|
+
// Search results
|
|
3236
|
+
searchResults: import_zod10.z.array(HttpSearchResultSchema).optional(),
|
|
3237
|
+
// Errors for individual operations
|
|
3238
|
+
errors: import_zod10.z.array(HttpSyncErrorSchema).optional()
|
|
3239
|
+
});
|
|
3240
|
+
|
|
3241
|
+
// src/schemas/index.ts
|
|
3242
|
+
var import_zod11 = require("zod");
|
|
3243
|
+
var MessageSchema = import_zod11.z.discriminatedUnion("type", [
|
|
3140
3244
|
AuthMessageSchema,
|
|
3141
3245
|
QuerySubMessageSchema,
|
|
3142
3246
|
QueryUnsubMessageSchema,
|
|
@@ -4149,6 +4253,18 @@ function createPredicateMatcher(getAttribute) {
|
|
|
4149
4253
|
}
|
|
4150
4254
|
|
|
4151
4255
|
// src/query/QueryTypes.ts
|
|
4256
|
+
var COST_WEIGHTS = {
|
|
4257
|
+
CPU: 1,
|
|
4258
|
+
NETWORK: 10,
|
|
4259
|
+
// Network is expensive (latency, bandwidth)
|
|
4260
|
+
IO: 5,
|
|
4261
|
+
// Disk I/O is moderately expensive
|
|
4262
|
+
ROWS: 1e-3
|
|
4263
|
+
// Row count factor
|
|
4264
|
+
};
|
|
4265
|
+
function calculateTotalCost(cost) {
|
|
4266
|
+
return cost.rows * COST_WEIGHTS.ROWS + cost.cpu * COST_WEIGHTS.CPU + cost.network * COST_WEIGHTS.NETWORK + cost.io * COST_WEIGHTS.IO;
|
|
4267
|
+
}
|
|
4152
4268
|
function isSimpleQuery(query) {
|
|
4153
4269
|
return [
|
|
4154
4270
|
"eq",
|
|
@@ -6470,13 +6586,22 @@ var QueryOptimizer = class {
|
|
|
6470
6586
|
* Optimize a query and return an execution plan.
|
|
6471
6587
|
*
|
|
6472
6588
|
* Optimization order (by cost):
|
|
6473
|
-
* 1.
|
|
6474
|
-
* 2.
|
|
6589
|
+
* 1. Point lookup (cost: 1) - direct primary key access
|
|
6590
|
+
* 2. StandingQueryIndex (cost: 10) - pre-computed results
|
|
6591
|
+
* 3. Other indexes via optimizeNode
|
|
6475
6592
|
*
|
|
6476
6593
|
* @param query - Query to optimize
|
|
6477
6594
|
* @returns Query execution plan
|
|
6478
6595
|
*/
|
|
6479
6596
|
optimize(query) {
|
|
6597
|
+
const pointLookupStep = this.tryPointLookup(query);
|
|
6598
|
+
if (pointLookupStep) {
|
|
6599
|
+
return {
|
|
6600
|
+
root: pointLookupStep,
|
|
6601
|
+
estimatedCost: this.estimateCost(pointLookupStep),
|
|
6602
|
+
usesIndexes: this.usesIndexes(pointLookupStep)
|
|
6603
|
+
};
|
|
6604
|
+
}
|
|
6480
6605
|
if (this.standingQueryRegistry) {
|
|
6481
6606
|
const standingIndex = this.standingQueryRegistry.getIndex(query);
|
|
6482
6607
|
if (standingIndex) {
|
|
@@ -6500,16 +6625,68 @@ var QueryOptimizer = class {
|
|
|
6500
6625
|
};
|
|
6501
6626
|
}
|
|
6502
6627
|
/**
|
|
6503
|
-
* Optimize a query with sort/limit/offset options.
|
|
6628
|
+
* Optimize a query with sort/limit/offset options and index hints.
|
|
6629
|
+
*
|
|
6630
|
+
* Hint precedence: disableOptimization > useIndex > forceIndexScan.
|
|
6504
6631
|
*
|
|
6505
6632
|
* @param query - Query to optimize
|
|
6506
|
-
* @param options - Query options (sort, limit,
|
|
6633
|
+
* @param options - Query options (sort, limit, cursor, hints)
|
|
6507
6634
|
* @returns Query execution plan with options
|
|
6508
6635
|
*/
|
|
6509
6636
|
optimizeWithOptions(query, options) {
|
|
6637
|
+
if (options.disableOptimization) {
|
|
6638
|
+
return {
|
|
6639
|
+
root: { type: "full-scan", predicate: query },
|
|
6640
|
+
estimatedCost: Number.MAX_SAFE_INTEGER,
|
|
6641
|
+
usesIndexes: false
|
|
6642
|
+
};
|
|
6643
|
+
}
|
|
6644
|
+
if (options.useIndex) {
|
|
6645
|
+
const indexes = this.indexRegistry.getIndexes(options.useIndex);
|
|
6646
|
+
if (indexes.length === 0) {
|
|
6647
|
+
throw new Error(
|
|
6648
|
+
`Index hint: no index found for attribute "${options.useIndex}"`
|
|
6649
|
+
);
|
|
6650
|
+
}
|
|
6651
|
+
let best = indexes[0];
|
|
6652
|
+
for (let i = 1; i < indexes.length; i++) {
|
|
6653
|
+
if (indexes[i].getRetrievalCost() < best.getRetrievalCost()) {
|
|
6654
|
+
best = indexes[i];
|
|
6655
|
+
}
|
|
6656
|
+
}
|
|
6657
|
+
const step = {
|
|
6658
|
+
type: "index-scan",
|
|
6659
|
+
index: best,
|
|
6660
|
+
query: this.buildHintedIndexQuery(query, options.useIndex)
|
|
6661
|
+
};
|
|
6662
|
+
return this.applyPlanOptions(
|
|
6663
|
+
{
|
|
6664
|
+
root: step,
|
|
6665
|
+
estimatedCost: best.getRetrievalCost(),
|
|
6666
|
+
usesIndexes: true,
|
|
6667
|
+
hint: options.useIndex
|
|
6668
|
+
},
|
|
6669
|
+
options
|
|
6670
|
+
);
|
|
6671
|
+
}
|
|
6510
6672
|
const basePlan = this.optimize(query);
|
|
6673
|
+
if (options.forceIndexScan && !basePlan.usesIndexes) {
|
|
6674
|
+
throw new Error(
|
|
6675
|
+
"No suitable index found and forceIndexScan is enabled"
|
|
6676
|
+
);
|
|
6677
|
+
}
|
|
6678
|
+
return this.applyPlanOptions(basePlan, options);
|
|
6679
|
+
}
|
|
6680
|
+
/**
|
|
6681
|
+
* Apply sort/limit/cursor options to a query plan.
|
|
6682
|
+
*
|
|
6683
|
+
* @param plan - Base query plan
|
|
6684
|
+
* @param options - Query options with sort/limit/cursor
|
|
6685
|
+
* @returns Plan with options applied
|
|
6686
|
+
*/
|
|
6687
|
+
applyPlanOptions(plan, options) {
|
|
6511
6688
|
if (!options.sort && options.limit === void 0 && options.cursor === void 0) {
|
|
6512
|
-
return
|
|
6689
|
+
return plan;
|
|
6513
6690
|
}
|
|
6514
6691
|
let indexedSort = false;
|
|
6515
6692
|
let sortField;
|
|
@@ -6526,14 +6703,73 @@ var QueryOptimizer = class {
|
|
|
6526
6703
|
}
|
|
6527
6704
|
}
|
|
6528
6705
|
return {
|
|
6529
|
-
...
|
|
6706
|
+
...plan,
|
|
6530
6707
|
indexedSort,
|
|
6531
6708
|
sort: sortField && sortDirection ? { field: sortField, direction: sortDirection } : void 0,
|
|
6532
6709
|
limit: options.limit,
|
|
6533
6710
|
cursor: options.cursor
|
|
6534
|
-
// replaces offset
|
|
6535
6711
|
};
|
|
6536
6712
|
}
|
|
6713
|
+
/**
|
|
6714
|
+
* Extract the relevant index query for a hinted attribute from the query tree.
|
|
6715
|
+
* If the query directly references the attribute, build an index query from it.
|
|
6716
|
+
* For compound (logical) queries, extract the matching child predicate.
|
|
6717
|
+
* Falls back to { type: 'has' } when no matching predicate is found,
|
|
6718
|
+
* retrieving all entries from the index for post-filtering.
|
|
6719
|
+
* FTS query nodes also fall back to { type: 'has' } since full-text search
|
|
6720
|
+
* queries are not compatible with regular index lookups.
|
|
6721
|
+
*
|
|
6722
|
+
* @param query - Original query tree
|
|
6723
|
+
* @param attributeName - Hinted attribute name
|
|
6724
|
+
* @returns Index query for the hinted attribute
|
|
6725
|
+
*/
|
|
6726
|
+
buildHintedIndexQuery(query, attributeName) {
|
|
6727
|
+
if (isSimpleQuery(query) && query.attribute === attributeName) {
|
|
6728
|
+
return this.buildIndexQuery(query);
|
|
6729
|
+
}
|
|
6730
|
+
if (isFTSQuery(query)) {
|
|
6731
|
+
return { type: "has" };
|
|
6732
|
+
}
|
|
6733
|
+
if (isLogicalQuery(query) && query.children) {
|
|
6734
|
+
for (const child of query.children) {
|
|
6735
|
+
if (isSimpleQuery(child) && child.attribute === attributeName) {
|
|
6736
|
+
return this.buildIndexQuery(child);
|
|
6737
|
+
}
|
|
6738
|
+
}
|
|
6739
|
+
}
|
|
6740
|
+
return { type: "has" };
|
|
6741
|
+
}
|
|
6742
|
+
/**
|
|
6743
|
+
* Try to optimize query as a point lookup.
|
|
6744
|
+
* Returns a point lookup step if query is an equality or IN query on primary key.
|
|
6745
|
+
*
|
|
6746
|
+
* @param query - Query to check
|
|
6747
|
+
* @returns Point lookup step or null
|
|
6748
|
+
*/
|
|
6749
|
+
tryPointLookup(query) {
|
|
6750
|
+
if (!isSimpleQuery(query)) {
|
|
6751
|
+
return null;
|
|
6752
|
+
}
|
|
6753
|
+
const primaryKeyFields = ["_key", "key", "id"];
|
|
6754
|
+
if (!primaryKeyFields.includes(query.attribute)) {
|
|
6755
|
+
return null;
|
|
6756
|
+
}
|
|
6757
|
+
if (query.type === "eq") {
|
|
6758
|
+
return {
|
|
6759
|
+
type: "point-lookup",
|
|
6760
|
+
key: query.value,
|
|
6761
|
+
cost: 1
|
|
6762
|
+
};
|
|
6763
|
+
}
|
|
6764
|
+
if (query.type === "in" && query.values) {
|
|
6765
|
+
return {
|
|
6766
|
+
type: "multi-point-lookup",
|
|
6767
|
+
keys: query.values,
|
|
6768
|
+
cost: query.values.length
|
|
6769
|
+
};
|
|
6770
|
+
}
|
|
6771
|
+
return null;
|
|
6772
|
+
}
|
|
6537
6773
|
/**
|
|
6538
6774
|
* Optimize a single query node.
|
|
6539
6775
|
*/
|
|
@@ -6922,6 +7158,9 @@ var QueryOptimizer = class {
|
|
|
6922
7158
|
*/
|
|
6923
7159
|
estimateCost(step) {
|
|
6924
7160
|
switch (step.type) {
|
|
7161
|
+
case "point-lookup":
|
|
7162
|
+
case "multi-point-lookup":
|
|
7163
|
+
return step.cost;
|
|
6925
7164
|
case "index-scan":
|
|
6926
7165
|
return step.index.getRetrievalCost();
|
|
6927
7166
|
case "full-scan":
|
|
@@ -6956,11 +7195,89 @@ var QueryOptimizer = class {
|
|
|
6956
7195
|
return Number.MAX_SAFE_INTEGER;
|
|
6957
7196
|
}
|
|
6958
7197
|
}
|
|
7198
|
+
/**
|
|
7199
|
+
* Estimate distributed cost including network overhead.
|
|
7200
|
+
*
|
|
7201
|
+
* Network cost is assigned based on step type:
|
|
7202
|
+
* - full-scan: broadcast to all nodes (highest cost)
|
|
7203
|
+
* - index-scan: 0 if local partition, 5 if remote
|
|
7204
|
+
* - point-lookup: 0 if local key, 5 if remote
|
|
7205
|
+
* - intersection/union: aggregating results from multiple sources
|
|
7206
|
+
*
|
|
7207
|
+
* @param step - Plan step to estimate
|
|
7208
|
+
* @param context - Distributed query context (optional)
|
|
7209
|
+
* @returns Distributed cost breakdown
|
|
7210
|
+
*/
|
|
7211
|
+
estimateDistributedCost(step, context) {
|
|
7212
|
+
const baseCost = this.estimateCost(step);
|
|
7213
|
+
if (!context?.isDistributed || context.nodeCount <= 1) {
|
|
7214
|
+
return {
|
|
7215
|
+
rows: baseCost,
|
|
7216
|
+
cpu: baseCost,
|
|
7217
|
+
network: 0,
|
|
7218
|
+
io: 0
|
|
7219
|
+
};
|
|
7220
|
+
}
|
|
7221
|
+
let networkCost = 0;
|
|
7222
|
+
switch (step.type) {
|
|
7223
|
+
case "full-scan":
|
|
7224
|
+
networkCost = context.nodeCount * 10;
|
|
7225
|
+
break;
|
|
7226
|
+
case "index-scan":
|
|
7227
|
+
networkCost = 5;
|
|
7228
|
+
break;
|
|
7229
|
+
case "point-lookup":
|
|
7230
|
+
networkCost = 5;
|
|
7231
|
+
break;
|
|
7232
|
+
case "multi-point-lookup":
|
|
7233
|
+
networkCost = Math.min(step.keys.length, context.nodeCount) * 5;
|
|
7234
|
+
break;
|
|
7235
|
+
case "intersection":
|
|
7236
|
+
case "union":
|
|
7237
|
+
networkCost = step.steps.length * 5;
|
|
7238
|
+
break;
|
|
7239
|
+
case "filter":
|
|
7240
|
+
return this.estimateDistributedCost(step.source, context);
|
|
7241
|
+
case "not":
|
|
7242
|
+
networkCost = context.nodeCount * 5;
|
|
7243
|
+
break;
|
|
7244
|
+
case "fts-scan":
|
|
7245
|
+
networkCost = Math.ceil(context.nodeCount / 2) * 5;
|
|
7246
|
+
break;
|
|
7247
|
+
case "fusion":
|
|
7248
|
+
networkCost = step.steps.reduce(
|
|
7249
|
+
(sum, s) => sum + this.estimateDistributedCost(s, context).network,
|
|
7250
|
+
0
|
|
7251
|
+
);
|
|
7252
|
+
break;
|
|
7253
|
+
}
|
|
7254
|
+
return {
|
|
7255
|
+
rows: baseCost,
|
|
7256
|
+
cpu: baseCost,
|
|
7257
|
+
network: networkCost,
|
|
7258
|
+
io: context.usesStorage ? baseCost * 0.5 : 0
|
|
7259
|
+
};
|
|
7260
|
+
}
|
|
7261
|
+
/**
|
|
7262
|
+
* Get total distributed cost for a plan step.
|
|
7263
|
+
* Convenience method combining estimateDistributedCost and calculateTotalCost.
|
|
7264
|
+
*
|
|
7265
|
+
* @param step - Plan step to estimate
|
|
7266
|
+
* @param context - Distributed query context (optional)
|
|
7267
|
+
* @returns Weighted total cost
|
|
7268
|
+
*/
|
|
7269
|
+
getTotalDistributedCost(step, context) {
|
|
7270
|
+
const distributedCost = this.estimateDistributedCost(step, context);
|
|
7271
|
+
return calculateTotalCost(distributedCost);
|
|
7272
|
+
}
|
|
6959
7273
|
/**
|
|
6960
7274
|
* Check if a plan step uses any indexes.
|
|
6961
7275
|
*/
|
|
6962
7276
|
usesIndexes(step) {
|
|
6963
7277
|
switch (step.type) {
|
|
7278
|
+
case "point-lookup":
|
|
7279
|
+
case "multi-point-lookup":
|
|
7280
|
+
return true;
|
|
6964
7281
|
case "index-scan":
|
|
6965
7282
|
return true;
|
|
6966
7283
|
case "full-scan":
|
|
@@ -9141,6 +9458,24 @@ var IndexedLWWMap = class extends LWWMap {
|
|
|
9141
9458
|
*/
|
|
9142
9459
|
executePlan(step) {
|
|
9143
9460
|
switch (step.type) {
|
|
9461
|
+
case "point-lookup": {
|
|
9462
|
+
const key = step.key;
|
|
9463
|
+
const result = /* @__PURE__ */ new Set();
|
|
9464
|
+
if (this.get(key) !== void 0) {
|
|
9465
|
+
result.add(key);
|
|
9466
|
+
}
|
|
9467
|
+
return new SetResultSet(result, 1);
|
|
9468
|
+
}
|
|
9469
|
+
case "multi-point-lookup": {
|
|
9470
|
+
const result = /* @__PURE__ */ new Set();
|
|
9471
|
+
for (const key of step.keys) {
|
|
9472
|
+
const k = key;
|
|
9473
|
+
if (this.get(k) !== void 0) {
|
|
9474
|
+
result.add(k);
|
|
9475
|
+
}
|
|
9476
|
+
}
|
|
9477
|
+
return new SetResultSet(result, step.keys.length);
|
|
9478
|
+
}
|
|
9144
9479
|
case "index-scan":
|
|
9145
9480
|
return step.index.retrieve(step.query);
|
|
9146
9481
|
case "full-scan": {
|
|
@@ -12199,6 +12534,504 @@ function getSearchDebugger() {
|
|
|
12199
12534
|
function resetSearchDebugger() {
|
|
12200
12535
|
globalSearchDebugger = null;
|
|
12201
12536
|
}
|
|
12537
|
+
|
|
12538
|
+
// src/testing/VirtualClock.ts
|
|
12539
|
+
var RealClock = {
|
|
12540
|
+
now: () => Date.now()
|
|
12541
|
+
};
|
|
12542
|
+
var VirtualClock = class {
|
|
12543
|
+
/**
|
|
12544
|
+
* @param initialTime Starting timestamp in milliseconds (default: 0)
|
|
12545
|
+
*/
|
|
12546
|
+
constructor(initialTime = 0) {
|
|
12547
|
+
if (!Number.isFinite(initialTime) || initialTime < 0) {
|
|
12548
|
+
throw new Error("Initial time must be a non-negative finite number");
|
|
12549
|
+
}
|
|
12550
|
+
this.currentTime = initialTime;
|
|
12551
|
+
}
|
|
12552
|
+
/**
|
|
12553
|
+
* Returns the current virtual time.
|
|
12554
|
+
* Time remains frozen until advance() or set() is called.
|
|
12555
|
+
*/
|
|
12556
|
+
now() {
|
|
12557
|
+
return this.currentTime;
|
|
12558
|
+
}
|
|
12559
|
+
/**
|
|
12560
|
+
* Advances time forward by the specified milliseconds.
|
|
12561
|
+
* @param ms Milliseconds to advance (must be non-negative)
|
|
12562
|
+
*/
|
|
12563
|
+
advance(ms) {
|
|
12564
|
+
if (!Number.isFinite(ms) || ms < 0) {
|
|
12565
|
+
throw new Error("Advance amount must be a non-negative finite number");
|
|
12566
|
+
}
|
|
12567
|
+
this.currentTime += ms;
|
|
12568
|
+
}
|
|
12569
|
+
/**
|
|
12570
|
+
* Sets the clock to a specific time.
|
|
12571
|
+
* Allows moving time forward or backward (useful for testing).
|
|
12572
|
+
* @param time Absolute timestamp in milliseconds
|
|
12573
|
+
*/
|
|
12574
|
+
set(time) {
|
|
12575
|
+
if (!Number.isFinite(time) || time < 0) {
|
|
12576
|
+
throw new Error("Time must be a non-negative finite number");
|
|
12577
|
+
}
|
|
12578
|
+
this.currentTime = time;
|
|
12579
|
+
}
|
|
12580
|
+
/**
|
|
12581
|
+
* Resets the clock to zero.
|
|
12582
|
+
*/
|
|
12583
|
+
reset() {
|
|
12584
|
+
this.currentTime = 0;
|
|
12585
|
+
}
|
|
12586
|
+
};
|
|
12587
|
+
|
|
12588
|
+
// src/testing/SeededRNG.ts
|
|
12589
|
+
var SeededRNG = class {
|
|
12590
|
+
/**
|
|
12591
|
+
* @param seed Integer seed value. Same seed = same sequence.
|
|
12592
|
+
*/
|
|
12593
|
+
constructor(seed) {
|
|
12594
|
+
if (!Number.isInteger(seed)) {
|
|
12595
|
+
throw new Error("Seed must be an integer");
|
|
12596
|
+
}
|
|
12597
|
+
this.state = seed >>> 0;
|
|
12598
|
+
this.originalSeed = this.state;
|
|
12599
|
+
}
|
|
12600
|
+
/**
|
|
12601
|
+
* Returns the original seed used to construct this RNG.
|
|
12602
|
+
*/
|
|
12603
|
+
getSeed() {
|
|
12604
|
+
return this.originalSeed;
|
|
12605
|
+
}
|
|
12606
|
+
/**
|
|
12607
|
+
* Generates the next random number in [0, 1).
|
|
12608
|
+
* Uses mulberry32 algorithm for deterministic, high-quality randomness.
|
|
12609
|
+
*/
|
|
12610
|
+
random() {
|
|
12611
|
+
let t = this.state += 1831565813;
|
|
12612
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
12613
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
12614
|
+
this.state = t;
|
|
12615
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
12616
|
+
}
|
|
12617
|
+
/**
|
|
12618
|
+
* Generates a random integer in [min, max] (inclusive).
|
|
12619
|
+
* @param min Minimum value (inclusive)
|
|
12620
|
+
* @param max Maximum value (inclusive)
|
|
12621
|
+
*/
|
|
12622
|
+
randomInt(min, max) {
|
|
12623
|
+
if (!Number.isInteger(min) || !Number.isInteger(max)) {
|
|
12624
|
+
throw new Error("Min and max must be integers");
|
|
12625
|
+
}
|
|
12626
|
+
if (min > max) {
|
|
12627
|
+
throw new Error("Min must be less than or equal to max");
|
|
12628
|
+
}
|
|
12629
|
+
const range = max - min + 1;
|
|
12630
|
+
return Math.floor(this.random() * range) + min;
|
|
12631
|
+
}
|
|
12632
|
+
/**
|
|
12633
|
+
* Generates a random boolean value.
|
|
12634
|
+
* @param probability Probability of returning true (default: 0.5)
|
|
12635
|
+
*/
|
|
12636
|
+
randomBool(probability = 0.5) {
|
|
12637
|
+
if (probability < 0 || probability > 1) {
|
|
12638
|
+
throw new Error("Probability must be between 0 and 1");
|
|
12639
|
+
}
|
|
12640
|
+
return this.random() < probability;
|
|
12641
|
+
}
|
|
12642
|
+
/**
|
|
12643
|
+
* Shuffles an array in place using Fisher-Yates algorithm.
|
|
12644
|
+
* Returns the shuffled array.
|
|
12645
|
+
* @param array Array to shuffle
|
|
12646
|
+
*/
|
|
12647
|
+
shuffle(array) {
|
|
12648
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
12649
|
+
const j = this.randomInt(0, i);
|
|
12650
|
+
[array[i], array[j]] = [array[j], array[i]];
|
|
12651
|
+
}
|
|
12652
|
+
return array;
|
|
12653
|
+
}
|
|
12654
|
+
/**
|
|
12655
|
+
* Picks a random element from an array.
|
|
12656
|
+
* @param array Array to pick from
|
|
12657
|
+
* @returns Random element, or undefined if array is empty
|
|
12658
|
+
*/
|
|
12659
|
+
pick(array) {
|
|
12660
|
+
if (array.length === 0) return void 0;
|
|
12661
|
+
return array[this.randomInt(0, array.length - 1)];
|
|
12662
|
+
}
|
|
12663
|
+
/**
|
|
12664
|
+
* Resets the RNG to its original seed.
|
|
12665
|
+
* Useful for reproducing a sequence from the start.
|
|
12666
|
+
*/
|
|
12667
|
+
reset() {
|
|
12668
|
+
this.state = this.originalSeed;
|
|
12669
|
+
}
|
|
12670
|
+
};
|
|
12671
|
+
|
|
12672
|
+
// src/testing/VirtualNetwork.ts
|
|
12673
|
+
var VirtualNetwork = class {
|
|
12674
|
+
constructor(rng, clock) {
|
|
12675
|
+
this.pendingMessages = [];
|
|
12676
|
+
this.rng = rng;
|
|
12677
|
+
this.clock = clock;
|
|
12678
|
+
this.config = {
|
|
12679
|
+
latencyMs: { min: 0, max: 0 },
|
|
12680
|
+
packetLossRate: 0,
|
|
12681
|
+
partitions: []
|
|
12682
|
+
};
|
|
12683
|
+
}
|
|
12684
|
+
/**
|
|
12685
|
+
* Updates network configuration.
|
|
12686
|
+
* Partially updates existing config with provided values.
|
|
12687
|
+
*/
|
|
12688
|
+
configure(config) {
|
|
12689
|
+
if (config.latencyMs !== void 0) {
|
|
12690
|
+
const { min, max } = config.latencyMs;
|
|
12691
|
+
if (min < 0 || max < 0 || min > max) {
|
|
12692
|
+
throw new Error("Invalid latency range");
|
|
12693
|
+
}
|
|
12694
|
+
this.config.latencyMs = config.latencyMs;
|
|
12695
|
+
}
|
|
12696
|
+
if (config.packetLossRate !== void 0) {
|
|
12697
|
+
if (config.packetLossRate < 0 || config.packetLossRate > 1) {
|
|
12698
|
+
throw new Error("Packet loss rate must be between 0 and 1");
|
|
12699
|
+
}
|
|
12700
|
+
this.config.packetLossRate = config.packetLossRate;
|
|
12701
|
+
}
|
|
12702
|
+
if (config.partitions !== void 0) {
|
|
12703
|
+
this.config.partitions = config.partitions;
|
|
12704
|
+
}
|
|
12705
|
+
}
|
|
12706
|
+
/**
|
|
12707
|
+
* Sends a message through the network.
|
|
12708
|
+
* Subject to packet loss, latency, and partition rules.
|
|
12709
|
+
*/
|
|
12710
|
+
send(from, to, payload) {
|
|
12711
|
+
if (this.rng.random() < this.config.packetLossRate) {
|
|
12712
|
+
return;
|
|
12713
|
+
}
|
|
12714
|
+
if (this.isPartitioned(from, to)) {
|
|
12715
|
+
return;
|
|
12716
|
+
}
|
|
12717
|
+
const latency = this.rng.randomInt(
|
|
12718
|
+
this.config.latencyMs.min,
|
|
12719
|
+
this.config.latencyMs.max
|
|
12720
|
+
);
|
|
12721
|
+
const scheduledTime = this.clock.now() + latency;
|
|
12722
|
+
this.pendingMessages.push({
|
|
12723
|
+
from,
|
|
12724
|
+
to,
|
|
12725
|
+
payload,
|
|
12726
|
+
scheduledTime
|
|
12727
|
+
});
|
|
12728
|
+
}
|
|
12729
|
+
/**
|
|
12730
|
+
* Creates a network partition between two groups.
|
|
12731
|
+
* Nodes in groupA cannot communicate with nodes in groupB.
|
|
12732
|
+
*/
|
|
12733
|
+
partition(groupA, groupB) {
|
|
12734
|
+
this.config.partitions.push(groupA, groupB);
|
|
12735
|
+
}
|
|
12736
|
+
/**
|
|
12737
|
+
* Removes all network partitions.
|
|
12738
|
+
*/
|
|
12739
|
+
heal() {
|
|
12740
|
+
this.config.partitions = [];
|
|
12741
|
+
}
|
|
12742
|
+
/**
|
|
12743
|
+
* Delivers all messages scheduled at or before the current time.
|
|
12744
|
+
* @returns Array of delivered messages
|
|
12745
|
+
*/
|
|
12746
|
+
tick() {
|
|
12747
|
+
const currentTime = this.clock.now();
|
|
12748
|
+
const delivered = [];
|
|
12749
|
+
const remaining = [];
|
|
12750
|
+
for (const msg of this.pendingMessages) {
|
|
12751
|
+
if (msg.scheduledTime <= currentTime) {
|
|
12752
|
+
delivered.push(msg);
|
|
12753
|
+
} else {
|
|
12754
|
+
remaining.push(msg);
|
|
12755
|
+
}
|
|
12756
|
+
}
|
|
12757
|
+
this.pendingMessages = remaining;
|
|
12758
|
+
return delivered;
|
|
12759
|
+
}
|
|
12760
|
+
/**
|
|
12761
|
+
* Returns the number of messages currently in flight.
|
|
12762
|
+
*/
|
|
12763
|
+
getPendingCount() {
|
|
12764
|
+
return this.pendingMessages.length;
|
|
12765
|
+
}
|
|
12766
|
+
/**
|
|
12767
|
+
* Clears all pending messages.
|
|
12768
|
+
*/
|
|
12769
|
+
clear() {
|
|
12770
|
+
this.pendingMessages = [];
|
|
12771
|
+
}
|
|
12772
|
+
/**
|
|
12773
|
+
* Checks if two nodes are partitioned from each other.
|
|
12774
|
+
*/
|
|
12775
|
+
isPartitioned(from, to) {
|
|
12776
|
+
for (let i = 0; i < this.config.partitions.length; i += 2) {
|
|
12777
|
+
const groupA = this.config.partitions[i];
|
|
12778
|
+
const groupB = this.config.partitions[i + 1];
|
|
12779
|
+
if (groupA.includes(from) && groupB.includes(to) || groupB.includes(from) && groupA.includes(to)) {
|
|
12780
|
+
return true;
|
|
12781
|
+
}
|
|
12782
|
+
}
|
|
12783
|
+
return false;
|
|
12784
|
+
}
|
|
12785
|
+
/**
|
|
12786
|
+
* Returns all pending messages (useful for debugging).
|
|
12787
|
+
*/
|
|
12788
|
+
getPendingMessages() {
|
|
12789
|
+
return [...this.pendingMessages];
|
|
12790
|
+
}
|
|
12791
|
+
};
|
|
12792
|
+
|
|
12793
|
+
// src/testing/InvariantChecker.ts
|
|
12794
|
+
var InvariantChecker = class {
|
|
12795
|
+
constructor() {
|
|
12796
|
+
this.invariants = /* @__PURE__ */ new Map();
|
|
12797
|
+
}
|
|
12798
|
+
/**
|
|
12799
|
+
* Adds an invariant to be checked.
|
|
12800
|
+
* @param name Unique name for this invariant
|
|
12801
|
+
* @param check Function that returns true if invariant holds
|
|
12802
|
+
*/
|
|
12803
|
+
addInvariant(name, check) {
|
|
12804
|
+
if (this.invariants.has(name)) {
|
|
12805
|
+
throw new Error(`Invariant '${name}' already exists`);
|
|
12806
|
+
}
|
|
12807
|
+
this.invariants.set(name, check);
|
|
12808
|
+
}
|
|
12809
|
+
/**
|
|
12810
|
+
* Removes an invariant by name.
|
|
12811
|
+
*/
|
|
12812
|
+
removeInvariant(name) {
|
|
12813
|
+
return this.invariants.delete(name);
|
|
12814
|
+
}
|
|
12815
|
+
/**
|
|
12816
|
+
* Verifies all invariants against the provided state.
|
|
12817
|
+
* @returns Result with pass/fail status and list of failed invariants
|
|
12818
|
+
*/
|
|
12819
|
+
verify(state) {
|
|
12820
|
+
const failures = [];
|
|
12821
|
+
for (const [name, check] of this.invariants.entries()) {
|
|
12822
|
+
try {
|
|
12823
|
+
if (!check(state)) {
|
|
12824
|
+
failures.push(name);
|
|
12825
|
+
}
|
|
12826
|
+
} catch (error) {
|
|
12827
|
+
failures.push(`${name} (exception: ${error instanceof Error ? error.message : String(error)})`);
|
|
12828
|
+
}
|
|
12829
|
+
}
|
|
12830
|
+
return {
|
|
12831
|
+
passed: failures.length === 0,
|
|
12832
|
+
failures
|
|
12833
|
+
};
|
|
12834
|
+
}
|
|
12835
|
+
/**
|
|
12836
|
+
* Returns the number of registered invariants.
|
|
12837
|
+
*/
|
|
12838
|
+
get count() {
|
|
12839
|
+
return this.invariants.size;
|
|
12840
|
+
}
|
|
12841
|
+
/**
|
|
12842
|
+
* Clears all invariants.
|
|
12843
|
+
*/
|
|
12844
|
+
clear() {
|
|
12845
|
+
this.invariants.clear();
|
|
12846
|
+
}
|
|
12847
|
+
};
|
|
12848
|
+
var CRDTInvariants = {
|
|
12849
|
+
/**
|
|
12850
|
+
* Verifies LWW-Map convergence: all maps contain the same values for same keys.
|
|
12851
|
+
*/
|
|
12852
|
+
lwwConvergence: (maps) => {
|
|
12853
|
+
if (maps.length < 2) return true;
|
|
12854
|
+
const reference = maps[0];
|
|
12855
|
+
const refKeys = new Set(reference.allKeys());
|
|
12856
|
+
for (let i = 1; i < maps.length; i++) {
|
|
12857
|
+
const other = maps[i];
|
|
12858
|
+
const otherKeys = new Set(other.allKeys());
|
|
12859
|
+
if (refKeys.size !== otherKeys.size) return false;
|
|
12860
|
+
for (const key of refKeys) {
|
|
12861
|
+
if (!otherKeys.has(key)) return false;
|
|
12862
|
+
}
|
|
12863
|
+
for (const key of refKeys) {
|
|
12864
|
+
const refRecord = reference.getRecord(key);
|
|
12865
|
+
const otherRecord = other.getRecord(key);
|
|
12866
|
+
if (!refRecord || !otherRecord) {
|
|
12867
|
+
if (refRecord !== otherRecord) return false;
|
|
12868
|
+
continue;
|
|
12869
|
+
}
|
|
12870
|
+
if (refRecord.value !== otherRecord.value) return false;
|
|
12871
|
+
if (HLC.compare(refRecord.timestamp, otherRecord.timestamp) !== 0) {
|
|
12872
|
+
return false;
|
|
12873
|
+
}
|
|
12874
|
+
}
|
|
12875
|
+
}
|
|
12876
|
+
return true;
|
|
12877
|
+
},
|
|
12878
|
+
/**
|
|
12879
|
+
* Verifies OR-Map convergence: all maps contain the same values for same keys.
|
|
12880
|
+
*/
|
|
12881
|
+
orMapConvergence: (maps) => {
|
|
12882
|
+
if (maps.length < 2) return true;
|
|
12883
|
+
const reference = maps[0];
|
|
12884
|
+
const refKeys = reference.allKeys();
|
|
12885
|
+
for (let i = 1; i < maps.length; i++) {
|
|
12886
|
+
const other = maps[i];
|
|
12887
|
+
const otherKeys = new Set(other.allKeys());
|
|
12888
|
+
if (refKeys.length !== otherKeys.size) return false;
|
|
12889
|
+
for (const key of refKeys) {
|
|
12890
|
+
if (!otherKeys.has(key)) return false;
|
|
12891
|
+
}
|
|
12892
|
+
for (const key of refKeys) {
|
|
12893
|
+
const refRecords = reference.getRecords(key);
|
|
12894
|
+
const otherRecords = other.getRecords(key);
|
|
12895
|
+
if (refRecords.length !== otherRecords.length) return false;
|
|
12896
|
+
const refSorted = [...refRecords].sort((a, b) => a.tag.localeCompare(b.tag));
|
|
12897
|
+
const otherSorted = [...otherRecords].sort((a, b) => a.tag.localeCompare(b.tag));
|
|
12898
|
+
for (let j = 0; j < refSorted.length; j++) {
|
|
12899
|
+
if (refSorted[j].tag !== otherSorted[j].tag) return false;
|
|
12900
|
+
if (refSorted[j].value !== otherSorted[j].value) return false;
|
|
12901
|
+
if (HLC.compare(refSorted[j].timestamp, otherSorted[j].timestamp) !== 0) {
|
|
12902
|
+
return false;
|
|
12903
|
+
}
|
|
12904
|
+
}
|
|
12905
|
+
}
|
|
12906
|
+
const refTombstones = new Set(reference.getTombstones());
|
|
12907
|
+
const otherTombstones = new Set(other.getTombstones());
|
|
12908
|
+
if (refTombstones.size !== otherTombstones.size) return false;
|
|
12909
|
+
for (const tag of refTombstones) {
|
|
12910
|
+
if (!otherTombstones.has(tag)) return false;
|
|
12911
|
+
}
|
|
12912
|
+
}
|
|
12913
|
+
return true;
|
|
12914
|
+
},
|
|
12915
|
+
/**
|
|
12916
|
+
* Verifies HLC monotonicity: timestamps are strictly increasing.
|
|
12917
|
+
*/
|
|
12918
|
+
hlcMonotonicity: (timestamps) => {
|
|
12919
|
+
for (let i = 1; i < timestamps.length; i++) {
|
|
12920
|
+
if (HLC.compare(timestamps[i - 1], timestamps[i]) >= 0) {
|
|
12921
|
+
return false;
|
|
12922
|
+
}
|
|
12923
|
+
}
|
|
12924
|
+
return true;
|
|
12925
|
+
},
|
|
12926
|
+
/**
|
|
12927
|
+
* Verifies Merkle tree consistency: trees with same data have same root hash.
|
|
12928
|
+
*/
|
|
12929
|
+
merkleConsistency: (trees) => {
|
|
12930
|
+
if (trees.length < 2) return true;
|
|
12931
|
+
const referenceHash = trees[0].getRootHash();
|
|
12932
|
+
for (let i = 1; i < trees.length; i++) {
|
|
12933
|
+
if (trees[i].getRootHash() !== referenceHash) {
|
|
12934
|
+
return false;
|
|
12935
|
+
}
|
|
12936
|
+
}
|
|
12937
|
+
return true;
|
|
12938
|
+
}
|
|
12939
|
+
};
|
|
12940
|
+
|
|
12941
|
+
// src/testing/ScenarioRunner.ts
|
|
12942
|
+
var ScenarioRunner = class {
|
|
12943
|
+
constructor(config) {
|
|
12944
|
+
if (!config.nodes || config.nodes.length === 0) {
|
|
12945
|
+
throw new Error("Scenario must have at least one node");
|
|
12946
|
+
}
|
|
12947
|
+
if (config.duration <= 0) {
|
|
12948
|
+
throw new Error("Duration must be positive");
|
|
12949
|
+
}
|
|
12950
|
+
this.seed = config.seed ?? Math.floor(Math.random() * 2147483647);
|
|
12951
|
+
this.config = {
|
|
12952
|
+
...config,
|
|
12953
|
+
seed: this.seed,
|
|
12954
|
+
tickInterval: config.tickInterval ?? 1
|
|
12955
|
+
};
|
|
12956
|
+
this.clock = new VirtualClock(0);
|
|
12957
|
+
this.rng = new SeededRNG(this.seed);
|
|
12958
|
+
this.network = new VirtualNetwork(this.rng, this.clock);
|
|
12959
|
+
}
|
|
12960
|
+
/**
|
|
12961
|
+
* Returns the seed used for this scenario.
|
|
12962
|
+
*/
|
|
12963
|
+
getSeed() {
|
|
12964
|
+
return this.seed;
|
|
12965
|
+
}
|
|
12966
|
+
/**
|
|
12967
|
+
* Returns the virtual clock instance.
|
|
12968
|
+
*/
|
|
12969
|
+
getClock() {
|
|
12970
|
+
return this.clock;
|
|
12971
|
+
}
|
|
12972
|
+
/**
|
|
12973
|
+
* Returns the seeded RNG instance.
|
|
12974
|
+
*/
|
|
12975
|
+
getRNG() {
|
|
12976
|
+
return this.rng;
|
|
12977
|
+
}
|
|
12978
|
+
/**
|
|
12979
|
+
* Returns the virtual network instance.
|
|
12980
|
+
*/
|
|
12981
|
+
getNetwork() {
|
|
12982
|
+
return this.network;
|
|
12983
|
+
}
|
|
12984
|
+
/**
|
|
12985
|
+
* Returns the list of nodes in this scenario.
|
|
12986
|
+
*/
|
|
12987
|
+
getNodes() {
|
|
12988
|
+
return [...this.config.nodes];
|
|
12989
|
+
}
|
|
12990
|
+
/**
|
|
12991
|
+
* Executes the simulation scenario.
|
|
12992
|
+
*
|
|
12993
|
+
* @param setup Called once before simulation starts. Initialize state here.
|
|
12994
|
+
* @param step Called on each tick. Perform operations and message delivery.
|
|
12995
|
+
* @param invariants Checker for verifying correctness throughout execution.
|
|
12996
|
+
* @returns Result with pass/fail status and captured state
|
|
12997
|
+
*/
|
|
12998
|
+
run(setup, step, invariants) {
|
|
12999
|
+
const finalStates = /* @__PURE__ */ new Map();
|
|
13000
|
+
const invariantFailures = [];
|
|
13001
|
+
setup(this);
|
|
13002
|
+
let tickCount = 0;
|
|
13003
|
+
const tickInterval = this.config.tickInterval;
|
|
13004
|
+
const endTime = this.config.duration;
|
|
13005
|
+
while (this.clock.now() < endTime) {
|
|
13006
|
+
this.clock.advance(tickInterval);
|
|
13007
|
+
tickCount++;
|
|
13008
|
+
step(this, tickCount);
|
|
13009
|
+
const delivered = this.network.tick();
|
|
13010
|
+
if (delivered.length > 0) {
|
|
13011
|
+
finalStates.set(`_tick_${tickCount}_delivered`, delivered.length);
|
|
13012
|
+
}
|
|
13013
|
+
}
|
|
13014
|
+
const result = invariants.verify(null);
|
|
13015
|
+
if (!result.passed) {
|
|
13016
|
+
invariantFailures.push(...result.failures);
|
|
13017
|
+
}
|
|
13018
|
+
return {
|
|
13019
|
+
seed: this.seed,
|
|
13020
|
+
passed: invariantFailures.length === 0,
|
|
13021
|
+
ticks: tickCount,
|
|
13022
|
+
invariantFailures,
|
|
13023
|
+
finalStates
|
|
13024
|
+
};
|
|
13025
|
+
}
|
|
13026
|
+
/**
|
|
13027
|
+
* Stores state for a node (useful for capturing final state).
|
|
13028
|
+
*/
|
|
13029
|
+
setState(nodeId, state) {
|
|
13030
|
+
if (!this.config.nodes.includes(nodeId)) {
|
|
13031
|
+
throw new Error(`Unknown node: ${nodeId}`);
|
|
13032
|
+
}
|
|
13033
|
+
}
|
|
13034
|
+
};
|
|
12202
13035
|
// Annotate the CommonJS export names for ESM import in node:
|
|
12203
13036
|
0 && (module.exports = {
|
|
12204
13037
|
AuthFailMessageSchema,
|
|
@@ -12207,7 +13040,9 @@ function resetSearchDebugger() {
|
|
|
12207
13040
|
BatchMessageSchema,
|
|
12208
13041
|
BuiltInProcessors,
|
|
12209
13042
|
BuiltInResolvers,
|
|
13043
|
+
COST_WEIGHTS,
|
|
12210
13044
|
CRDTDebugger,
|
|
13045
|
+
CRDTInvariants,
|
|
12211
13046
|
ClientOpMessageSchema,
|
|
12212
13047
|
ClientOpSchema,
|
|
12213
13048
|
ClusterSearchReqMessageSchema,
|
|
@@ -12249,6 +13084,7 @@ function resetSearchDebugger() {
|
|
|
12249
13084
|
DEFAULT_RESOLVER_RATE_LIMITS,
|
|
12250
13085
|
DEFAULT_STOP_WORDS,
|
|
12251
13086
|
DEFAULT_WRITE_CONCERN_TIMEOUT,
|
|
13087
|
+
DeltaRecordSchema,
|
|
12252
13088
|
ENGLISH_STOPWORDS,
|
|
12253
13089
|
EntryProcessBatchRequestSchema,
|
|
12254
13090
|
EntryProcessBatchResponseSchema,
|
|
@@ -12268,12 +13104,20 @@ function resetSearchDebugger() {
|
|
|
12268
13104
|
GcPrunePayloadSchema,
|
|
12269
13105
|
HLC,
|
|
12270
13106
|
HashIndex,
|
|
13107
|
+
HttpQueryRequestSchema,
|
|
13108
|
+
HttpQueryResultSchema,
|
|
13109
|
+
HttpSearchRequestSchema,
|
|
13110
|
+
HttpSearchResultSchema,
|
|
13111
|
+
HttpSyncErrorSchema,
|
|
13112
|
+
HttpSyncRequestSchema,
|
|
13113
|
+
HttpSyncResponseSchema,
|
|
12271
13114
|
HybridQueryDeltaPayloadSchema,
|
|
12272
13115
|
HybridQueryRespPayloadSchema,
|
|
12273
13116
|
IndexRegistry,
|
|
12274
13117
|
IndexedLWWMap,
|
|
12275
13118
|
IndexedORMap,
|
|
12276
13119
|
IntersectionResultSet,
|
|
13120
|
+
InvariantChecker,
|
|
12277
13121
|
InvertedIndex,
|
|
12278
13122
|
JournalEventDataSchema,
|
|
12279
13123
|
JournalEventMessageSchema,
|
|
@@ -12294,6 +13138,7 @@ function resetSearchDebugger() {
|
|
|
12294
13138
|
LockReleasedPayloadSchema,
|
|
12295
13139
|
LockRequestSchema,
|
|
12296
13140
|
LowercaseFilter,
|
|
13141
|
+
MapDeltaSchema,
|
|
12297
13142
|
MaxLengthFilter,
|
|
12298
13143
|
MergeRejectedMessageSchema,
|
|
12299
13144
|
MerkleReqBucketMessageSchema,
|
|
@@ -12338,10 +13183,12 @@ function resetSearchDebugger() {
|
|
|
12338
13183
|
QueryUpdateMessageSchema,
|
|
12339
13184
|
QueryUpdatePayloadSchema,
|
|
12340
13185
|
RESOLVER_FORBIDDEN_PATTERNS,
|
|
13186
|
+
RealClock,
|
|
12341
13187
|
ReciprocalRankFusion,
|
|
12342
13188
|
RegisterResolverRequestSchema,
|
|
12343
13189
|
RegisterResolverResponseSchema,
|
|
12344
13190
|
Ringbuffer,
|
|
13191
|
+
ScenarioRunner,
|
|
12345
13192
|
SearchCursor,
|
|
12346
13193
|
SearchDebugger,
|
|
12347
13194
|
SearchMessageSchema,
|
|
@@ -12356,6 +13203,7 @@ function resetSearchDebugger() {
|
|
|
12356
13203
|
SearchUpdateMessageSchema,
|
|
12357
13204
|
SearchUpdatePayloadSchema,
|
|
12358
13205
|
SearchUpdateTypeSchema,
|
|
13206
|
+
SeededRNG,
|
|
12359
13207
|
ServerBatchEventMessageSchema,
|
|
12360
13208
|
ServerEventMessageSchema,
|
|
12361
13209
|
ServerEventPayloadSchema,
|
|
@@ -12367,6 +13215,7 @@ function resetSearchDebugger() {
|
|
|
12367
13215
|
StandingQueryRegistry,
|
|
12368
13216
|
StopWordFilter,
|
|
12369
13217
|
SyncInitMessageSchema,
|
|
13218
|
+
SyncMapEntrySchema,
|
|
12370
13219
|
SyncResetRequiredPayloadSchema,
|
|
12371
13220
|
SyncRespBucketsMessageSchema,
|
|
12372
13221
|
SyncRespLeafMessageSchema,
|
|
@@ -12382,11 +13231,14 @@ function resetSearchDebugger() {
|
|
|
12382
13231
|
UniqueFilter,
|
|
12383
13232
|
UnregisterResolverRequestSchema,
|
|
12384
13233
|
UnregisterResolverResponseSchema,
|
|
13234
|
+
VirtualClock,
|
|
13235
|
+
VirtualNetwork,
|
|
12385
13236
|
WRITE_CONCERN_ORDER,
|
|
12386
13237
|
WhitespaceTokenizer,
|
|
12387
13238
|
WordBoundaryTokenizer,
|
|
12388
13239
|
WriteConcern,
|
|
12389
13240
|
WriteConcernSchema,
|
|
13241
|
+
calculateTotalCost,
|
|
12390
13242
|
combineHashes,
|
|
12391
13243
|
compareHLCTimestamps,
|
|
12392
13244
|
compareTimestamps,
|