@topgunbuild/core 0.8.1 → 0.10.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/LICENSE +97 -0
- package/dist/index.d.mts +873 -9
- package/dist/index.d.ts +873 -9
- package/dist/index.js +691 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +662 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -11
package/dist/index.mjs
CHANGED
|
@@ -138,6 +138,18 @@ function resetNativeHash() {
|
|
|
138
138
|
nativeHash = null;
|
|
139
139
|
nativeLoadAttempted = false;
|
|
140
140
|
}
|
|
141
|
+
function hashObject(obj) {
|
|
142
|
+
const json = JSON.stringify(obj, (_, value) => {
|
|
143
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
144
|
+
return Object.keys(value).sort().reduce((sorted, key) => {
|
|
145
|
+
sorted[key] = value[key];
|
|
146
|
+
return sorted;
|
|
147
|
+
}, {});
|
|
148
|
+
}
|
|
149
|
+
return value;
|
|
150
|
+
});
|
|
151
|
+
return hashString(json);
|
|
152
|
+
}
|
|
141
153
|
|
|
142
154
|
// src/MerkleTree.ts
|
|
143
155
|
var MerkleTree = class {
|
|
@@ -2166,7 +2178,24 @@ var QuerySchema = z3.object({
|
|
|
2166
2178
|
predicate: PredicateNodeSchema.optional(),
|
|
2167
2179
|
sort: z3.record(z3.string(), z3.enum(["asc", "desc"])).optional(),
|
|
2168
2180
|
limit: z3.number().optional(),
|
|
2169
|
-
|
|
2181
|
+
cursor: z3.string().optional()
|
|
2182
|
+
// Phase 14.1: replaces offset
|
|
2183
|
+
});
|
|
2184
|
+
var CursorStatusSchema = z3.enum(["valid", "expired", "invalid", "none"]);
|
|
2185
|
+
var QueryRespPayloadSchema = z3.object({
|
|
2186
|
+
queryId: z3.string(),
|
|
2187
|
+
results: z3.array(z3.object({
|
|
2188
|
+
key: z3.string(),
|
|
2189
|
+
value: z3.unknown()
|
|
2190
|
+
})),
|
|
2191
|
+
nextCursor: z3.string().optional(),
|
|
2192
|
+
hasMore: z3.boolean().optional(),
|
|
2193
|
+
/** Debug info: status of input cursor processing */
|
|
2194
|
+
cursorStatus: CursorStatusSchema.optional()
|
|
2195
|
+
});
|
|
2196
|
+
var QueryRespMessageSchema = z3.object({
|
|
2197
|
+
type: z3.literal("QUERY_RESP"),
|
|
2198
|
+
payload: QueryRespPayloadSchema
|
|
2170
2199
|
});
|
|
2171
2200
|
var ClientOpSchema = z3.object({
|
|
2172
2201
|
id: z3.string().optional(),
|
|
@@ -2646,6 +2675,87 @@ var OpRejectedMessageSchema = z3.object({
|
|
|
2646
2675
|
code: z3.number().optional()
|
|
2647
2676
|
})
|
|
2648
2677
|
});
|
|
2678
|
+
var ClusterSubRegisterPayloadSchema = z3.object({
|
|
2679
|
+
/** Unique subscription ID */
|
|
2680
|
+
subscriptionId: z3.string(),
|
|
2681
|
+
/** Node ID of the coordinator (receives updates) */
|
|
2682
|
+
coordinatorNodeId: z3.string(),
|
|
2683
|
+
/** Map name to subscribe to */
|
|
2684
|
+
mapName: z3.string(),
|
|
2685
|
+
/** Subscription type */
|
|
2686
|
+
type: z3.enum(["SEARCH", "QUERY"]),
|
|
2687
|
+
// For FTS subscriptions
|
|
2688
|
+
/** Search query string (for SEARCH type) */
|
|
2689
|
+
searchQuery: z3.string().optional(),
|
|
2690
|
+
/** Search options (for SEARCH type) */
|
|
2691
|
+
searchOptions: z3.object({
|
|
2692
|
+
limit: z3.number().int().positive().optional(),
|
|
2693
|
+
minScore: z3.number().optional(),
|
|
2694
|
+
boost: z3.record(z3.string(), z3.number()).optional()
|
|
2695
|
+
}).optional(),
|
|
2696
|
+
// For Query subscriptions
|
|
2697
|
+
/** Query predicate (for QUERY type) */
|
|
2698
|
+
queryPredicate: z3.any().optional(),
|
|
2699
|
+
/** Query sort order (for QUERY type) */
|
|
2700
|
+
querySort: z3.record(z3.string(), z3.enum(["asc", "desc"])).optional()
|
|
2701
|
+
});
|
|
2702
|
+
var ClusterSubRegisterMessageSchema = z3.object({
|
|
2703
|
+
type: z3.literal("CLUSTER_SUB_REGISTER"),
|
|
2704
|
+
payload: ClusterSubRegisterPayloadSchema
|
|
2705
|
+
});
|
|
2706
|
+
var ClusterSubAckPayloadSchema = z3.object({
|
|
2707
|
+
/** Correlates to subscriptionId */
|
|
2708
|
+
subscriptionId: z3.string(),
|
|
2709
|
+
/** Node that registered this subscription */
|
|
2710
|
+
nodeId: z3.string(),
|
|
2711
|
+
/** Registration success status */
|
|
2712
|
+
success: z3.boolean(),
|
|
2713
|
+
/** Error message if failed */
|
|
2714
|
+
error: z3.string().optional(),
|
|
2715
|
+
/** Initial results from this node */
|
|
2716
|
+
initialResults: z3.array(z3.object({
|
|
2717
|
+
key: z3.string(),
|
|
2718
|
+
value: z3.unknown(),
|
|
2719
|
+
score: z3.number().optional(),
|
|
2720
|
+
matchedTerms: z3.array(z3.string()).optional()
|
|
2721
|
+
})).optional(),
|
|
2722
|
+
/** Total matching documents on this node */
|
|
2723
|
+
totalHits: z3.number().int().nonnegative().optional()
|
|
2724
|
+
});
|
|
2725
|
+
var ClusterSubAckMessageSchema = z3.object({
|
|
2726
|
+
type: z3.literal("CLUSTER_SUB_ACK"),
|
|
2727
|
+
payload: ClusterSubAckPayloadSchema
|
|
2728
|
+
});
|
|
2729
|
+
var ClusterSubUpdatePayloadSchema = z3.object({
|
|
2730
|
+
/** Subscription ID this update belongs to */
|
|
2731
|
+
subscriptionId: z3.string(),
|
|
2732
|
+
/** Node that produced this update */
|
|
2733
|
+
sourceNodeId: z3.string(),
|
|
2734
|
+
/** Document key */
|
|
2735
|
+
key: z3.string(),
|
|
2736
|
+
/** Document value (null for LEAVE) */
|
|
2737
|
+
value: z3.unknown(),
|
|
2738
|
+
/** Search score (for SEARCH type) */
|
|
2739
|
+
score: z3.number().optional(),
|
|
2740
|
+
/** Matched terms (for SEARCH type) */
|
|
2741
|
+
matchedTerms: z3.array(z3.string()).optional(),
|
|
2742
|
+
/** Change type: ENTER (new match), UPDATE (still matches), LEAVE (no longer matches) */
|
|
2743
|
+
changeType: z3.enum(["ENTER", "UPDATE", "LEAVE"]),
|
|
2744
|
+
/** Timestamp for ordering */
|
|
2745
|
+
timestamp: z3.number()
|
|
2746
|
+
});
|
|
2747
|
+
var ClusterSubUpdateMessageSchema = z3.object({
|
|
2748
|
+
type: z3.literal("CLUSTER_SUB_UPDATE"),
|
|
2749
|
+
payload: ClusterSubUpdatePayloadSchema
|
|
2750
|
+
});
|
|
2751
|
+
var ClusterSubUnregisterPayloadSchema = z3.object({
|
|
2752
|
+
/** Subscription ID to remove */
|
|
2753
|
+
subscriptionId: z3.string()
|
|
2754
|
+
});
|
|
2755
|
+
var ClusterSubUnregisterMessageSchema = z3.object({
|
|
2756
|
+
type: z3.literal("CLUSTER_SUB_UNREGISTER"),
|
|
2757
|
+
payload: ClusterSubUnregisterPayloadSchema
|
|
2758
|
+
});
|
|
2649
2759
|
var MessageSchema = z3.discriminatedUnion("type", [
|
|
2650
2760
|
AuthMessageSchema,
|
|
2651
2761
|
QuerySubMessageSchema,
|
|
@@ -2703,8 +2813,95 @@ var MessageSchema = z3.discriminatedUnion("type", [
|
|
|
2703
2813
|
// Phase 11.1b: Live Search Subscriptions
|
|
2704
2814
|
SearchSubMessageSchema,
|
|
2705
2815
|
SearchUpdateMessageSchema,
|
|
2706
|
-
SearchUnsubMessageSchema
|
|
2816
|
+
SearchUnsubMessageSchema,
|
|
2817
|
+
// Phase 14.2: Distributed Live Subscriptions (cluster-internal)
|
|
2818
|
+
ClusterSubRegisterMessageSchema,
|
|
2819
|
+
ClusterSubAckMessageSchema,
|
|
2820
|
+
ClusterSubUpdateMessageSchema,
|
|
2821
|
+
ClusterSubUnregisterMessageSchema
|
|
2707
2822
|
]);
|
|
2823
|
+
var ClusterSearchReqPayloadSchema = z3.object({
|
|
2824
|
+
/** Unique request ID for correlation */
|
|
2825
|
+
requestId: z3.string(),
|
|
2826
|
+
/** Map name to search */
|
|
2827
|
+
mapName: z3.string(),
|
|
2828
|
+
/** Search query text */
|
|
2829
|
+
query: z3.string(),
|
|
2830
|
+
/** Search options */
|
|
2831
|
+
options: z3.object({
|
|
2832
|
+
/** Maximum results per node (for Top-K) */
|
|
2833
|
+
limit: z3.number().int().positive().max(1e3),
|
|
2834
|
+
/** Minimum score threshold */
|
|
2835
|
+
minScore: z3.number().optional(),
|
|
2836
|
+
/** Fields to boost */
|
|
2837
|
+
boost: z3.record(z3.string(), z3.number()).optional(),
|
|
2838
|
+
/** Include matched terms in response */
|
|
2839
|
+
includeMatchedTerms: z3.boolean().optional(),
|
|
2840
|
+
/** For cursor-based pagination: return results after this score */
|
|
2841
|
+
afterScore: z3.number().optional(),
|
|
2842
|
+
/** For cursor-based pagination: return results after this key (tie-breaking) */
|
|
2843
|
+
afterKey: z3.string().optional()
|
|
2844
|
+
}),
|
|
2845
|
+
/** Timeout in milliseconds */
|
|
2846
|
+
timeoutMs: z3.number().int().positive().optional()
|
|
2847
|
+
});
|
|
2848
|
+
var ClusterSearchReqMessageSchema = z3.object({
|
|
2849
|
+
type: z3.literal("CLUSTER_SEARCH_REQ"),
|
|
2850
|
+
payload: ClusterSearchReqPayloadSchema
|
|
2851
|
+
});
|
|
2852
|
+
var ClusterSearchRespPayloadSchema = z3.object({
|
|
2853
|
+
/** Correlates to requestId */
|
|
2854
|
+
requestId: z3.string(),
|
|
2855
|
+
/** Node that produced these results */
|
|
2856
|
+
nodeId: z3.string(),
|
|
2857
|
+
/** Search results with scores */
|
|
2858
|
+
results: z3.array(z3.object({
|
|
2859
|
+
key: z3.string(),
|
|
2860
|
+
value: z3.unknown(),
|
|
2861
|
+
score: z3.number(),
|
|
2862
|
+
matchedTerms: z3.array(z3.string()).optional()
|
|
2863
|
+
})),
|
|
2864
|
+
/** Total matching documents on this node (for pagination) */
|
|
2865
|
+
totalHits: z3.number().int().nonnegative(),
|
|
2866
|
+
/** Execution time on this node in ms (for monitoring) */
|
|
2867
|
+
executionTimeMs: z3.number().nonnegative(),
|
|
2868
|
+
/** Error if search failed on this node */
|
|
2869
|
+
error: z3.string().optional()
|
|
2870
|
+
});
|
|
2871
|
+
var ClusterSearchRespMessageSchema = z3.object({
|
|
2872
|
+
type: z3.literal("CLUSTER_SEARCH_RESP"),
|
|
2873
|
+
payload: ClusterSearchRespPayloadSchema
|
|
2874
|
+
});
|
|
2875
|
+
var ClusterSearchSubscribePayloadSchema = z3.object({
|
|
2876
|
+
subscriptionId: z3.string(),
|
|
2877
|
+
mapName: z3.string(),
|
|
2878
|
+
query: z3.string(),
|
|
2879
|
+
options: SearchOptionsSchema.optional()
|
|
2880
|
+
});
|
|
2881
|
+
var ClusterSearchSubscribeMessageSchema = z3.object({
|
|
2882
|
+
type: z3.literal("CLUSTER_SEARCH_SUBSCRIBE"),
|
|
2883
|
+
payload: ClusterSearchSubscribePayloadSchema
|
|
2884
|
+
});
|
|
2885
|
+
var ClusterSearchUnsubscribePayloadSchema = z3.object({
|
|
2886
|
+
subscriptionId: z3.string()
|
|
2887
|
+
});
|
|
2888
|
+
var ClusterSearchUnsubscribeMessageSchema = z3.object({
|
|
2889
|
+
type: z3.literal("CLUSTER_SEARCH_UNSUBSCRIBE"),
|
|
2890
|
+
payload: ClusterSearchUnsubscribePayloadSchema
|
|
2891
|
+
});
|
|
2892
|
+
var ClusterSearchUpdatePayloadSchema = z3.object({
|
|
2893
|
+
subscriptionId: z3.string(),
|
|
2894
|
+
nodeId: z3.string(),
|
|
2895
|
+
key: z3.string(),
|
|
2896
|
+
value: z3.unknown(),
|
|
2897
|
+
score: z3.number(),
|
|
2898
|
+
matchedTerms: z3.array(z3.string()).optional(),
|
|
2899
|
+
type: SearchUpdateTypeSchema
|
|
2900
|
+
});
|
|
2901
|
+
var ClusterSearchUpdateMessageSchema = z3.object({
|
|
2902
|
+
type: z3.literal("CLUSTER_SEARCH_UPDATE"),
|
|
2903
|
+
payload: ClusterSearchUpdatePayloadSchema
|
|
2904
|
+
});
|
|
2708
2905
|
|
|
2709
2906
|
// src/types/WriteConcern.ts
|
|
2710
2907
|
var WriteConcern = /* @__PURE__ */ ((WriteConcern2) => {
|
|
@@ -6029,7 +6226,7 @@ var QueryOptimizer = class {
|
|
|
6029
6226
|
*/
|
|
6030
6227
|
optimizeWithOptions(query, options) {
|
|
6031
6228
|
const basePlan = this.optimize(query);
|
|
6032
|
-
if (!options.sort && options.limit === void 0 && options.
|
|
6229
|
+
if (!options.sort && options.limit === void 0 && options.cursor === void 0) {
|
|
6033
6230
|
return basePlan;
|
|
6034
6231
|
}
|
|
6035
6232
|
let indexedSort = false;
|
|
@@ -6051,7 +6248,8 @@ var QueryOptimizer = class {
|
|
|
6051
6248
|
indexedSort,
|
|
6052
6249
|
sort: sortField && sortDirection ? { field: sortField, direction: sortDirection } : void 0,
|
|
6053
6250
|
limit: options.limit,
|
|
6054
|
-
|
|
6251
|
+
cursor: options.cursor
|
|
6252
|
+
// Phase 14.1: replaces offset
|
|
6055
6253
|
};
|
|
6056
6254
|
}
|
|
6057
6255
|
/**
|
|
@@ -8171,6 +8369,264 @@ var ReciprocalRankFusion = class {
|
|
|
8171
8369
|
}
|
|
8172
8370
|
};
|
|
8173
8371
|
|
|
8372
|
+
// src/utils/base64url.ts
|
|
8373
|
+
function encodeBase64Url(str) {
|
|
8374
|
+
if (typeof Buffer !== "undefined") {
|
|
8375
|
+
return Buffer.from(str, "utf8").toString("base64url");
|
|
8376
|
+
}
|
|
8377
|
+
const base64 = btoa(unescape(encodeURIComponent(str)));
|
|
8378
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
8379
|
+
}
|
|
8380
|
+
function decodeBase64Url(encoded) {
|
|
8381
|
+
if (typeof Buffer !== "undefined") {
|
|
8382
|
+
return Buffer.from(encoded, "base64url").toString("utf8");
|
|
8383
|
+
}
|
|
8384
|
+
let base64 = encoded.replace(/-/g, "+").replace(/_/g, "/");
|
|
8385
|
+
const pad = base64.length % 4;
|
|
8386
|
+
if (pad) {
|
|
8387
|
+
base64 += "=".repeat(4 - pad);
|
|
8388
|
+
}
|
|
8389
|
+
return decodeURIComponent(escape(atob(base64)));
|
|
8390
|
+
}
|
|
8391
|
+
|
|
8392
|
+
// src/utils/compare.ts
|
|
8393
|
+
function compareValues(a, b) {
|
|
8394
|
+
if (a == null && b == null) return 0;
|
|
8395
|
+
if (a == null) return -1;
|
|
8396
|
+
if (b == null) return 1;
|
|
8397
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
8398
|
+
return a - b;
|
|
8399
|
+
}
|
|
8400
|
+
if (a instanceof Date && b instanceof Date) {
|
|
8401
|
+
return a.getTime() - b.getTime();
|
|
8402
|
+
}
|
|
8403
|
+
if (typeof a === "string" && typeof b === "string") {
|
|
8404
|
+
const dateA = Date.parse(a);
|
|
8405
|
+
const dateB = Date.parse(b);
|
|
8406
|
+
if (!isNaN(dateA) && !isNaN(dateB)) {
|
|
8407
|
+
return dateA - dateB;
|
|
8408
|
+
}
|
|
8409
|
+
return a.localeCompare(b);
|
|
8410
|
+
}
|
|
8411
|
+
if (typeof a === "boolean" && typeof b === "boolean") {
|
|
8412
|
+
return a === b ? 0 : a ? 1 : -1;
|
|
8413
|
+
}
|
|
8414
|
+
return String(a).localeCompare(String(b));
|
|
8415
|
+
}
|
|
8416
|
+
|
|
8417
|
+
// src/query/QueryCursor.ts
|
|
8418
|
+
var DEFAULT_QUERY_CURSOR_MAX_AGE_MS = 10 * 60 * 1e3;
|
|
8419
|
+
var QueryCursor = class {
|
|
8420
|
+
/**
|
|
8421
|
+
* Encode cursor data to an opaque base64url string.
|
|
8422
|
+
*
|
|
8423
|
+
* @param data - Cursor data to encode
|
|
8424
|
+
* @returns Opaque cursor string
|
|
8425
|
+
*/
|
|
8426
|
+
static encode(data) {
|
|
8427
|
+
const json = JSON.stringify(data);
|
|
8428
|
+
return encodeBase64Url(json);
|
|
8429
|
+
}
|
|
8430
|
+
/**
|
|
8431
|
+
* Decode cursor string back to data.
|
|
8432
|
+
*
|
|
8433
|
+
* @param cursor - Opaque cursor string
|
|
8434
|
+
* @returns Decoded cursor data, or null if invalid
|
|
8435
|
+
*/
|
|
8436
|
+
static decode(cursor) {
|
|
8437
|
+
try {
|
|
8438
|
+
const json = decodeBase64Url(cursor);
|
|
8439
|
+
const data = JSON.parse(json);
|
|
8440
|
+
if (typeof data !== "object" || typeof data.nodeValues !== "object" || typeof data.nodeKeys !== "object" || typeof data.sortField !== "string" || data.sortDirection !== "asc" && data.sortDirection !== "desc" || typeof data.predicateHash !== "number" || typeof data.sortHash !== "number" || typeof data.timestamp !== "number") {
|
|
8441
|
+
return null;
|
|
8442
|
+
}
|
|
8443
|
+
return data;
|
|
8444
|
+
} catch {
|
|
8445
|
+
return null;
|
|
8446
|
+
}
|
|
8447
|
+
}
|
|
8448
|
+
/**
|
|
8449
|
+
* Create a cursor from query results.
|
|
8450
|
+
*
|
|
8451
|
+
* The cursor captures the last seen position for each node that contributed
|
|
8452
|
+
* results, enabling efficient resumption in the next page request.
|
|
8453
|
+
*
|
|
8454
|
+
* @param results - Array of results with sort values and optional node tracking
|
|
8455
|
+
* @param sort - Sort configuration (single field only for v1)
|
|
8456
|
+
* @param predicate - Query predicate (for validation)
|
|
8457
|
+
* @returns Encoded cursor string
|
|
8458
|
+
*/
|
|
8459
|
+
static fromResults(results, sort, predicate) {
|
|
8460
|
+
const nodeValues = {};
|
|
8461
|
+
const nodeKeys = {};
|
|
8462
|
+
const sortEntries = Object.entries(sort);
|
|
8463
|
+
if (sortEntries.length === 0) {
|
|
8464
|
+
throw new Error("Sort configuration required for cursor pagination");
|
|
8465
|
+
}
|
|
8466
|
+
const [sortField, sortDirection] = sortEntries[0];
|
|
8467
|
+
for (const result of results) {
|
|
8468
|
+
const nodeId = result.nodeId ?? "local";
|
|
8469
|
+
nodeValues[nodeId] = result.sortValue;
|
|
8470
|
+
nodeKeys[nodeId] = result.key;
|
|
8471
|
+
}
|
|
8472
|
+
const data = {
|
|
8473
|
+
nodeValues,
|
|
8474
|
+
nodeKeys,
|
|
8475
|
+
sortField,
|
|
8476
|
+
sortDirection,
|
|
8477
|
+
predicateHash: hashObject(predicate ?? null),
|
|
8478
|
+
sortHash: hashObject(sort),
|
|
8479
|
+
timestamp: Date.now()
|
|
8480
|
+
};
|
|
8481
|
+
return this.encode(data);
|
|
8482
|
+
}
|
|
8483
|
+
/**
|
|
8484
|
+
* Create a cursor from the last result only.
|
|
8485
|
+
* Useful for local-only queries.
|
|
8486
|
+
*
|
|
8487
|
+
* @param lastResult - The last result in the current page
|
|
8488
|
+
* @param sort - Sort configuration
|
|
8489
|
+
* @param predicate - Query predicate
|
|
8490
|
+
* @returns Encoded cursor string
|
|
8491
|
+
*/
|
|
8492
|
+
static fromLastResult(lastResult, sort, predicate) {
|
|
8493
|
+
return this.fromResults([lastResult], sort, predicate);
|
|
8494
|
+
}
|
|
8495
|
+
/**
|
|
8496
|
+
* Validate that a cursor is valid for the given query.
|
|
8497
|
+
*
|
|
8498
|
+
* Checks:
|
|
8499
|
+
* 1. Predicate hash matches (cursor was created for this query)
|
|
8500
|
+
* 2. Sort hash matches (sort configuration unchanged)
|
|
8501
|
+
* 3. Cursor is not expired
|
|
8502
|
+
*
|
|
8503
|
+
* @param cursor - Decoded cursor data
|
|
8504
|
+
* @param predicate - Query predicate to validate against
|
|
8505
|
+
* @param sort - Sort configuration to validate against
|
|
8506
|
+
* @param options - Validation options
|
|
8507
|
+
* @returns true if cursor is valid
|
|
8508
|
+
*/
|
|
8509
|
+
static isValid(cursor, predicate, sort, options) {
|
|
8510
|
+
const maxAge = options?.maxAgeMs ?? DEFAULT_QUERY_CURSOR_MAX_AGE_MS;
|
|
8511
|
+
if (cursor.predicateHash !== hashObject(predicate ?? null)) {
|
|
8512
|
+
return false;
|
|
8513
|
+
}
|
|
8514
|
+
if (cursor.sortHash !== hashObject(sort)) {
|
|
8515
|
+
return false;
|
|
8516
|
+
}
|
|
8517
|
+
if (Date.now() - cursor.timestamp > maxAge) {
|
|
8518
|
+
return false;
|
|
8519
|
+
}
|
|
8520
|
+
return true;
|
|
8521
|
+
}
|
|
8522
|
+
/**
|
|
8523
|
+
* Get the cursor position for a specific node.
|
|
8524
|
+
*
|
|
8525
|
+
* @param cursor - Decoded cursor data
|
|
8526
|
+
* @param nodeId - Node ID to get position for (defaults to 'local')
|
|
8527
|
+
* @returns Position info or null if node not in cursor
|
|
8528
|
+
*/
|
|
8529
|
+
static getNodePosition(cursor, nodeId = "local") {
|
|
8530
|
+
if (!(nodeId in cursor.nodeValues)) {
|
|
8531
|
+
return null;
|
|
8532
|
+
}
|
|
8533
|
+
return {
|
|
8534
|
+
afterValue: cursor.nodeValues[nodeId],
|
|
8535
|
+
afterKey: cursor.nodeKeys[nodeId]
|
|
8536
|
+
};
|
|
8537
|
+
}
|
|
8538
|
+
/**
|
|
8539
|
+
* Check if a result should be included based on cursor position.
|
|
8540
|
+
*
|
|
8541
|
+
* For ASC sort: value > cursorValue OR (value === cursorValue AND key > cursorKey)
|
|
8542
|
+
* For DESC sort: value < cursorValue OR (value === cursorValue AND key > cursorKey)
|
|
8543
|
+
*
|
|
8544
|
+
* @param result - Result to check
|
|
8545
|
+
* @param cursor - Decoded cursor data
|
|
8546
|
+
* @returns true if result should be included (is after cursor)
|
|
8547
|
+
*/
|
|
8548
|
+
static isAfterCursor(result, cursor) {
|
|
8549
|
+
const nodeId = result.nodeId ?? "local";
|
|
8550
|
+
const position = this.getNodePosition(cursor, nodeId);
|
|
8551
|
+
if (!position) {
|
|
8552
|
+
return true;
|
|
8553
|
+
}
|
|
8554
|
+
const cmp = this.compareValues(result.sortValue, position.afterValue);
|
|
8555
|
+
if (cursor.sortDirection === "asc") {
|
|
8556
|
+
if (cmp > 0) return true;
|
|
8557
|
+
if (cmp === 0 && result.key > position.afterKey) return true;
|
|
8558
|
+
} else {
|
|
8559
|
+
if (cmp < 0) return true;
|
|
8560
|
+
if (cmp === 0 && result.key > position.afterKey) return true;
|
|
8561
|
+
}
|
|
8562
|
+
return false;
|
|
8563
|
+
}
|
|
8564
|
+
/**
|
|
8565
|
+
* Compare two values with type-aware comparison.
|
|
8566
|
+
* Delegates to shared compareValues utility.
|
|
8567
|
+
*
|
|
8568
|
+
* @param a - First value
|
|
8569
|
+
* @param b - Second value
|
|
8570
|
+
* @returns Negative if a < b, 0 if equal, positive if a > b
|
|
8571
|
+
*/
|
|
8572
|
+
static compareValues(a, b) {
|
|
8573
|
+
return compareValues(a, b);
|
|
8574
|
+
}
|
|
8575
|
+
/**
|
|
8576
|
+
* Merge multiple cursors into one.
|
|
8577
|
+
* Useful when combining results from multiple sub-queries or nodes.
|
|
8578
|
+
*
|
|
8579
|
+
* Keeps the furthest position for each node.
|
|
8580
|
+
*
|
|
8581
|
+
* @param cursors - Array of decoded cursor data
|
|
8582
|
+
* @param sort - Sort configuration
|
|
8583
|
+
* @param predicate - Query predicate
|
|
8584
|
+
* @returns New merged cursor
|
|
8585
|
+
*/
|
|
8586
|
+
static merge(cursors, sort, predicate) {
|
|
8587
|
+
const nodeValues = {};
|
|
8588
|
+
const nodeKeys = {};
|
|
8589
|
+
const sortEntries = Object.entries(sort);
|
|
8590
|
+
const [sortField, sortDirection] = sortEntries[0];
|
|
8591
|
+
for (const cursor of cursors) {
|
|
8592
|
+
for (const nodeId of Object.keys(cursor.nodeValues)) {
|
|
8593
|
+
const existingValue = nodeValues[nodeId];
|
|
8594
|
+
const newValue = cursor.nodeValues[nodeId];
|
|
8595
|
+
if (existingValue === void 0) {
|
|
8596
|
+
nodeValues[nodeId] = newValue;
|
|
8597
|
+
nodeKeys[nodeId] = cursor.nodeKeys[nodeId];
|
|
8598
|
+
} else {
|
|
8599
|
+
const cmp = this.compareValues(newValue, existingValue);
|
|
8600
|
+
const isFurther = sortDirection === "asc" ? cmp > 0 || cmp === 0 && cursor.nodeKeys[nodeId] > nodeKeys[nodeId] : cmp < 0 || cmp === 0 && cursor.nodeKeys[nodeId] > nodeKeys[nodeId];
|
|
8601
|
+
if (isFurther) {
|
|
8602
|
+
nodeValues[nodeId] = newValue;
|
|
8603
|
+
nodeKeys[nodeId] = cursor.nodeKeys[nodeId];
|
|
8604
|
+
}
|
|
8605
|
+
}
|
|
8606
|
+
}
|
|
8607
|
+
}
|
|
8608
|
+
return {
|
|
8609
|
+
nodeValues,
|
|
8610
|
+
nodeKeys,
|
|
8611
|
+
sortField,
|
|
8612
|
+
sortDirection,
|
|
8613
|
+
predicateHash: hashObject(predicate ?? null),
|
|
8614
|
+
sortHash: hashObject(sort),
|
|
8615
|
+
timestamp: Date.now()
|
|
8616
|
+
};
|
|
8617
|
+
}
|
|
8618
|
+
/**
|
|
8619
|
+
* Extract sort value from a record for cursor generation.
|
|
8620
|
+
*
|
|
8621
|
+
* @param record - Record to extract sort value from
|
|
8622
|
+
* @param sortField - Field to extract
|
|
8623
|
+
* @returns Sort value
|
|
8624
|
+
*/
|
|
8625
|
+
static extractSortValue(record, sortField) {
|
|
8626
|
+
return record[sortField];
|
|
8627
|
+
}
|
|
8628
|
+
};
|
|
8629
|
+
|
|
8174
8630
|
// src/IndexedLWWMap.ts
|
|
8175
8631
|
var IndexedLWWMap = class extends LWWMap {
|
|
8176
8632
|
constructor(hlc, options = {}) {
|
|
@@ -10746,6 +11202,179 @@ var IndexedORMap = class extends ORMap {
|
|
|
10746
11202
|
return null;
|
|
10747
11203
|
}
|
|
10748
11204
|
};
|
|
11205
|
+
|
|
11206
|
+
// src/search/SearchCursor.ts
|
|
11207
|
+
var DEFAULT_CURSOR_MAX_AGE_MS = 5 * 60 * 1e3;
|
|
11208
|
+
var SearchCursor = class {
|
|
11209
|
+
/**
|
|
11210
|
+
* Encode cursor data to an opaque base64url string.
|
|
11211
|
+
*
|
|
11212
|
+
* @param data - Cursor data to encode
|
|
11213
|
+
* @returns Opaque cursor string
|
|
11214
|
+
*/
|
|
11215
|
+
static encode(data) {
|
|
11216
|
+
const json = JSON.stringify(data);
|
|
11217
|
+
return encodeBase64Url(json);
|
|
11218
|
+
}
|
|
11219
|
+
/**
|
|
11220
|
+
* Decode cursor string back to data.
|
|
11221
|
+
*
|
|
11222
|
+
* @param cursor - Opaque cursor string
|
|
11223
|
+
* @returns Decoded cursor data, or null if invalid
|
|
11224
|
+
*/
|
|
11225
|
+
static decode(cursor) {
|
|
11226
|
+
try {
|
|
11227
|
+
const json = decodeBase64Url(cursor);
|
|
11228
|
+
const data = JSON.parse(json);
|
|
11229
|
+
if (typeof data !== "object" || typeof data.nodeScores !== "object" || typeof data.nodeKeys !== "object" || typeof data.queryHash !== "number" || typeof data.timestamp !== "number") {
|
|
11230
|
+
return null;
|
|
11231
|
+
}
|
|
11232
|
+
return data;
|
|
11233
|
+
} catch {
|
|
11234
|
+
return null;
|
|
11235
|
+
}
|
|
11236
|
+
}
|
|
11237
|
+
/**
|
|
11238
|
+
* Create a cursor from the last results of a search.
|
|
11239
|
+
*
|
|
11240
|
+
* The cursor captures the last seen position for each node that contributed
|
|
11241
|
+
* results, enabling efficient resumption in the next page request.
|
|
11242
|
+
*
|
|
11243
|
+
* @param results - Array of results with node tracking
|
|
11244
|
+
* @param query - Original query string (for validation)
|
|
11245
|
+
* @returns Encoded cursor string
|
|
11246
|
+
*/
|
|
11247
|
+
static fromResults(results, query) {
|
|
11248
|
+
const nodeScores = {};
|
|
11249
|
+
const nodeKeys = {};
|
|
11250
|
+
for (const result of results) {
|
|
11251
|
+
nodeScores[result.nodeId] = result.score;
|
|
11252
|
+
nodeKeys[result.nodeId] = result.key;
|
|
11253
|
+
}
|
|
11254
|
+
const data = {
|
|
11255
|
+
nodeScores,
|
|
11256
|
+
nodeKeys,
|
|
11257
|
+
queryHash: hashString(query),
|
|
11258
|
+
timestamp: Date.now()
|
|
11259
|
+
};
|
|
11260
|
+
return this.encode(data);
|
|
11261
|
+
}
|
|
11262
|
+
/**
|
|
11263
|
+
* Create a cursor from the last result only.
|
|
11264
|
+
* Useful when you only have the final merged result.
|
|
11265
|
+
*
|
|
11266
|
+
* @param lastResult - The last result in the current page
|
|
11267
|
+
* @param query - Original query string
|
|
11268
|
+
* @returns Encoded cursor string
|
|
11269
|
+
*/
|
|
11270
|
+
static fromLastResult(lastResult, query) {
|
|
11271
|
+
return this.fromResults([lastResult], query);
|
|
11272
|
+
}
|
|
11273
|
+
/**
|
|
11274
|
+
* Generate a hash for a query string.
|
|
11275
|
+
* Used to validate that cursor matches the current query.
|
|
11276
|
+
*
|
|
11277
|
+
* @param query - Query string to hash
|
|
11278
|
+
* @returns Numeric hash
|
|
11279
|
+
*/
|
|
11280
|
+
static hashQuery(query) {
|
|
11281
|
+
return hashString(query);
|
|
11282
|
+
}
|
|
11283
|
+
/**
|
|
11284
|
+
* Validate that a cursor is valid for the given query.
|
|
11285
|
+
*
|
|
11286
|
+
* Checks:
|
|
11287
|
+
* 1. Query hash matches (cursor was created for this query)
|
|
11288
|
+
* 2. Cursor is not expired
|
|
11289
|
+
*
|
|
11290
|
+
* @param cursor - Decoded cursor data
|
|
11291
|
+
* @param query - Query string to validate against
|
|
11292
|
+
* @param maxAgeMs - Maximum cursor age in milliseconds (default: 5 minutes)
|
|
11293
|
+
* @returns true if cursor is valid
|
|
11294
|
+
*/
|
|
11295
|
+
static isValid(cursor, query, maxAgeMs = DEFAULT_CURSOR_MAX_AGE_MS) {
|
|
11296
|
+
if (cursor.queryHash !== hashString(query)) {
|
|
11297
|
+
return false;
|
|
11298
|
+
}
|
|
11299
|
+
if (Date.now() - cursor.timestamp > maxAgeMs) {
|
|
11300
|
+
return false;
|
|
11301
|
+
}
|
|
11302
|
+
return true;
|
|
11303
|
+
}
|
|
11304
|
+
/**
|
|
11305
|
+
* Get the cursor position for a specific node.
|
|
11306
|
+
*
|
|
11307
|
+
* @param cursor - Decoded cursor data
|
|
11308
|
+
* @param nodeId - Node ID to get position for
|
|
11309
|
+
* @returns Position info or null if node not in cursor
|
|
11310
|
+
*/
|
|
11311
|
+
static getNodePosition(cursor, nodeId) {
|
|
11312
|
+
if (!(nodeId in cursor.nodeScores)) {
|
|
11313
|
+
return null;
|
|
11314
|
+
}
|
|
11315
|
+
return {
|
|
11316
|
+
afterScore: cursor.nodeScores[nodeId],
|
|
11317
|
+
afterKey: cursor.nodeKeys[nodeId]
|
|
11318
|
+
};
|
|
11319
|
+
}
|
|
11320
|
+
/**
|
|
11321
|
+
* Check if a result should be included based on cursor position.
|
|
11322
|
+
*
|
|
11323
|
+
* Results are ordered by score descending, then key ascending for tie-breaking.
|
|
11324
|
+
* A result should be included if it comes AFTER the cursor position:
|
|
11325
|
+
* - score < cursorScore, OR
|
|
11326
|
+
* - score === cursorScore AND key > cursorKey
|
|
11327
|
+
*
|
|
11328
|
+
* @param result - Result to check
|
|
11329
|
+
* @param cursor - Decoded cursor data
|
|
11330
|
+
* @returns true if result should be included (is after cursor)
|
|
11331
|
+
*/
|
|
11332
|
+
static isAfterCursor(result, cursor) {
|
|
11333
|
+
const position = this.getNodePosition(cursor, result.nodeId);
|
|
11334
|
+
if (!position) {
|
|
11335
|
+
return true;
|
|
11336
|
+
}
|
|
11337
|
+
if (result.score < position.afterScore) {
|
|
11338
|
+
return true;
|
|
11339
|
+
}
|
|
11340
|
+
if (result.score === position.afterScore && result.key > position.afterKey) {
|
|
11341
|
+
return true;
|
|
11342
|
+
}
|
|
11343
|
+
return false;
|
|
11344
|
+
}
|
|
11345
|
+
/**
|
|
11346
|
+
* Merge multiple cursors into one.
|
|
11347
|
+
* Useful when combining results from multiple sub-queries.
|
|
11348
|
+
*
|
|
11349
|
+
* @param cursors - Array of decoded cursor data
|
|
11350
|
+
* @param query - Original query string
|
|
11351
|
+
* @returns New merged cursor
|
|
11352
|
+
*/
|
|
11353
|
+
static merge(cursors, query) {
|
|
11354
|
+
const nodeScores = {};
|
|
11355
|
+
const nodeKeys = {};
|
|
11356
|
+
for (const cursor of cursors) {
|
|
11357
|
+
for (const nodeId of Object.keys(cursor.nodeScores)) {
|
|
11358
|
+
const existingScore = nodeScores[nodeId];
|
|
11359
|
+
const newScore = cursor.nodeScores[nodeId];
|
|
11360
|
+
if (existingScore === void 0 || newScore < existingScore) {
|
|
11361
|
+
nodeScores[nodeId] = newScore;
|
|
11362
|
+
nodeKeys[nodeId] = cursor.nodeKeys[nodeId];
|
|
11363
|
+
} else if (newScore === existingScore) {
|
|
11364
|
+
if (cursor.nodeKeys[nodeId] > nodeKeys[nodeId]) {
|
|
11365
|
+
nodeKeys[nodeId] = cursor.nodeKeys[nodeId];
|
|
11366
|
+
}
|
|
11367
|
+
}
|
|
11368
|
+
}
|
|
11369
|
+
}
|
|
11370
|
+
return {
|
|
11371
|
+
nodeScores,
|
|
11372
|
+
nodeKeys,
|
|
11373
|
+
queryHash: hashString(query),
|
|
11374
|
+
timestamp: Date.now()
|
|
11375
|
+
};
|
|
11376
|
+
}
|
|
11377
|
+
};
|
|
10749
11378
|
export {
|
|
10750
11379
|
AuthMessageSchema,
|
|
10751
11380
|
BM25Scorer,
|
|
@@ -10754,6 +11383,24 @@ export {
|
|
|
10754
11383
|
BuiltInResolvers,
|
|
10755
11384
|
ClientOpMessageSchema,
|
|
10756
11385
|
ClientOpSchema,
|
|
11386
|
+
ClusterSearchReqMessageSchema,
|
|
11387
|
+
ClusterSearchReqPayloadSchema,
|
|
11388
|
+
ClusterSearchRespMessageSchema,
|
|
11389
|
+
ClusterSearchRespPayloadSchema,
|
|
11390
|
+
ClusterSearchSubscribeMessageSchema,
|
|
11391
|
+
ClusterSearchSubscribePayloadSchema,
|
|
11392
|
+
ClusterSearchUnsubscribeMessageSchema,
|
|
11393
|
+
ClusterSearchUnsubscribePayloadSchema,
|
|
11394
|
+
ClusterSearchUpdateMessageSchema,
|
|
11395
|
+
ClusterSearchUpdatePayloadSchema,
|
|
11396
|
+
ClusterSubAckMessageSchema,
|
|
11397
|
+
ClusterSubAckPayloadSchema,
|
|
11398
|
+
ClusterSubRegisterMessageSchema,
|
|
11399
|
+
ClusterSubRegisterPayloadSchema,
|
|
11400
|
+
ClusterSubUnregisterMessageSchema,
|
|
11401
|
+
ClusterSubUnregisterPayloadSchema,
|
|
11402
|
+
ClusterSubUpdateMessageSchema,
|
|
11403
|
+
ClusterSubUpdatePayloadSchema,
|
|
10757
11404
|
ConflictResolverDefSchema,
|
|
10758
11405
|
ConflictResolverSchema,
|
|
10759
11406
|
ConsistencyLevel,
|
|
@@ -10761,13 +11408,16 @@ export {
|
|
|
10761
11408
|
CounterResponseSchema,
|
|
10762
11409
|
CounterSyncSchema,
|
|
10763
11410
|
CounterUpdateSchema,
|
|
11411
|
+
CursorStatusSchema,
|
|
10764
11412
|
DEFAULT_BACKUP_COUNT,
|
|
10765
11413
|
DEFAULT_CIRCUIT_BREAKER_CONFIG,
|
|
10766
11414
|
DEFAULT_CONNECTION_POOL_CONFIG,
|
|
11415
|
+
DEFAULT_CURSOR_MAX_AGE_MS,
|
|
10767
11416
|
DEFAULT_EVENT_JOURNAL_CONFIG,
|
|
10768
11417
|
DEFAULT_MIGRATION_CONFIG,
|
|
10769
11418
|
DEFAULT_PARTITION_ROUTER_CONFIG,
|
|
10770
11419
|
DEFAULT_PROCESSOR_RATE_LIMITS,
|
|
11420
|
+
DEFAULT_QUERY_CURSOR_MAX_AGE_MS,
|
|
10771
11421
|
DEFAULT_REPLICATION_CONFIG,
|
|
10772
11422
|
DEFAULT_RESOLVER_RATE_LIMITS,
|
|
10773
11423
|
DEFAULT_STOP_WORDS,
|
|
@@ -10845,7 +11495,10 @@ export {
|
|
|
10845
11495
|
PredicateNodeSchema,
|
|
10846
11496
|
PredicateOpSchema,
|
|
10847
11497
|
Predicates,
|
|
11498
|
+
QueryCursor,
|
|
10848
11499
|
QueryOptimizer,
|
|
11500
|
+
QueryRespMessageSchema,
|
|
11501
|
+
QueryRespPayloadSchema,
|
|
10849
11502
|
QuerySchema,
|
|
10850
11503
|
QuerySubMessageSchema,
|
|
10851
11504
|
QueryUnsubMessageSchema,
|
|
@@ -10854,6 +11507,7 @@ export {
|
|
|
10854
11507
|
RegisterResolverRequestSchema,
|
|
10855
11508
|
RegisterResolverResponseSchema,
|
|
10856
11509
|
Ringbuffer,
|
|
11510
|
+
SearchCursor,
|
|
10857
11511
|
SearchMessageSchema,
|
|
10858
11512
|
SearchOptionsSchema,
|
|
10859
11513
|
SearchPayloadSchema,
|
|
@@ -10896,15 +11550,19 @@ export {
|
|
|
10896
11550
|
combineHashes,
|
|
10897
11551
|
compareHLCTimestamps,
|
|
10898
11552
|
compareTimestamps,
|
|
11553
|
+
compareValues,
|
|
10899
11554
|
createFieldComparator,
|
|
10900
11555
|
createPredicateMatcher,
|
|
11556
|
+
decodeBase64Url,
|
|
10901
11557
|
deepMerge,
|
|
10902
11558
|
deserialize,
|
|
10903
11559
|
disableNativeHash,
|
|
11560
|
+
encodeBase64Url,
|
|
10904
11561
|
evaluatePredicate,
|
|
10905
11562
|
getHighestWriteConcernLevel,
|
|
10906
11563
|
hashORMapEntry,
|
|
10907
11564
|
hashORMapRecord,
|
|
11565
|
+
hashObject,
|
|
10908
11566
|
hashString,
|
|
10909
11567
|
isLogicalQuery,
|
|
10910
11568
|
isSimpleQuery,
|