@topgunbuild/core 0.9.0 → 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/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
- offset: z3.number().optional()
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.offset === void 0) {
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
- offset: options.offset
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,