@topgunbuild/server 0.7.0 → 0.8.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.js CHANGED
@@ -71,6 +71,7 @@ __export(index_exports, {
71
71
  ReduceTasklet: () => ReduceTasklet,
72
72
  RepairScheduler: () => RepairScheduler,
73
73
  ReplicationPipeline: () => ReplicationPipeline,
74
+ SearchCoordinator: () => SearchCoordinator,
74
75
  SecurityManager: () => SecurityManager,
75
76
  ServerCoordinator: () => ServerCoordinator,
76
77
  TaskletScheduler: () => TaskletScheduler,
@@ -105,7 +106,7 @@ var import_http = require("http");
105
106
  var import_https = require("https");
106
107
  var import_fs2 = require("fs");
107
108
  var import_ws3 = require("ws");
108
- var import_core19 = require("@topgunbuild/core");
109
+ var import_core20 = require("@topgunbuild/core");
109
110
  var jwt = __toESM(require("jsonwebtoken"));
110
111
  var crypto = __toESM(require("crypto"));
111
112
 
@@ -9255,6 +9256,554 @@ var EventJournalService = class extends import_core18.EventJournalImpl {
9255
9256
  }
9256
9257
  };
9257
9258
 
9259
+ // src/search/SearchCoordinator.ts
9260
+ var import_core19 = require("@topgunbuild/core");
9261
+ var SearchCoordinator = class {
9262
+ constructor() {
9263
+ /** Map name → FullTextIndex */
9264
+ this.indexes = /* @__PURE__ */ new Map();
9265
+ /** Map name → FullTextIndexConfig (for reference) */
9266
+ this.configs = /* @__PURE__ */ new Map();
9267
+ // ============================================
9268
+ // Phase 11.1b: Live Search Subscription tracking
9269
+ // ============================================
9270
+ /** Subscription ID → SearchSubscription */
9271
+ this.subscriptions = /* @__PURE__ */ new Map();
9272
+ /** Map name → Set of subscription IDs */
9273
+ this.subscriptionsByMap = /* @__PURE__ */ new Map();
9274
+ /** Client ID → Set of subscription IDs */
9275
+ this.subscriptionsByClient = /* @__PURE__ */ new Map();
9276
+ // ============================================
9277
+ // Phase 11.2: Notification Batching
9278
+ // ============================================
9279
+ /** Queue of pending notifications per map */
9280
+ this.pendingNotifications = /* @__PURE__ */ new Map();
9281
+ /** Timer for batching notifications */
9282
+ this.notificationTimer = null;
9283
+ /** Batch interval in milliseconds (~1 frame at 60fps) */
9284
+ this.BATCH_INTERVAL = 16;
9285
+ logger.debug("SearchCoordinator initialized");
9286
+ }
9287
+ /**
9288
+ * Set the callback for sending updates to clients.
9289
+ * Called by ServerCoordinator during initialization.
9290
+ */
9291
+ setSendUpdateCallback(callback) {
9292
+ this.sendUpdate = callback;
9293
+ }
9294
+ /**
9295
+ * Set the callback for sending batched updates to clients.
9296
+ * When set, notifications are batched within BATCH_INTERVAL (16ms) window.
9297
+ * Called by ServerCoordinator during initialization.
9298
+ *
9299
+ * @param callback - Function to call with batched updates
9300
+ */
9301
+ setSendBatchUpdateCallback(callback) {
9302
+ this.sendBatchUpdate = callback;
9303
+ }
9304
+ /**
9305
+ * Set the callback for retrieving document values.
9306
+ * Called by ServerCoordinator during initialization.
9307
+ */
9308
+ setDocumentValueGetter(getter) {
9309
+ this.getDocumentValue = getter;
9310
+ }
9311
+ /**
9312
+ * Enable full-text search for a map.
9313
+ *
9314
+ * @param mapName - Name of the map to enable FTS for
9315
+ * @param config - FTS configuration (fields, tokenizer, bm25 options)
9316
+ */
9317
+ enableSearch(mapName, config) {
9318
+ if (this.indexes.has(mapName)) {
9319
+ logger.warn({ mapName }, "FTS already enabled for map, replacing index");
9320
+ this.indexes.delete(mapName);
9321
+ }
9322
+ const index = new import_core19.FullTextIndex(config);
9323
+ this.indexes.set(mapName, index);
9324
+ this.configs.set(mapName, config);
9325
+ logger.info({ mapName, fields: config.fields }, "FTS enabled for map");
9326
+ }
9327
+ /**
9328
+ * Disable full-text search for a map.
9329
+ *
9330
+ * @param mapName - Name of the map to disable FTS for
9331
+ */
9332
+ disableSearch(mapName) {
9333
+ if (!this.indexes.has(mapName)) {
9334
+ logger.warn({ mapName }, "FTS not enabled for map, nothing to disable");
9335
+ return;
9336
+ }
9337
+ this.indexes.delete(mapName);
9338
+ this.configs.delete(mapName);
9339
+ logger.info({ mapName }, "FTS disabled for map");
9340
+ }
9341
+ /**
9342
+ * Check if FTS is enabled for a map.
9343
+ */
9344
+ isSearchEnabled(mapName) {
9345
+ return this.indexes.has(mapName);
9346
+ }
9347
+ /**
9348
+ * Get enabled map names.
9349
+ */
9350
+ getEnabledMaps() {
9351
+ return Array.from(this.indexes.keys());
9352
+ }
9353
+ /**
9354
+ * Execute a one-shot search query.
9355
+ *
9356
+ * @param mapName - Name of the map to search
9357
+ * @param query - Search query text
9358
+ * @param options - Search options (limit, minScore, boost)
9359
+ * @returns Search response payload
9360
+ */
9361
+ search(mapName, query, options) {
9362
+ const index = this.indexes.get(mapName);
9363
+ if (!index) {
9364
+ logger.warn({ mapName }, "Search requested for map without FTS enabled");
9365
+ return {
9366
+ requestId: "",
9367
+ results: [],
9368
+ totalCount: 0,
9369
+ error: `Full-text search not enabled for map: ${mapName}`
9370
+ };
9371
+ }
9372
+ try {
9373
+ const searchResults = index.search(query, options);
9374
+ const results = searchResults.map((result) => {
9375
+ const value = this.getDocumentValue ? this.getDocumentValue(mapName, result.docId) : void 0;
9376
+ return {
9377
+ key: result.docId,
9378
+ value,
9379
+ score: result.score,
9380
+ matchedTerms: result.matchedTerms || []
9381
+ };
9382
+ });
9383
+ logger.debug(
9384
+ { mapName, query, resultCount: results.length },
9385
+ "Search executed"
9386
+ );
9387
+ return {
9388
+ requestId: "",
9389
+ results,
9390
+ totalCount: searchResults.length
9391
+ };
9392
+ } catch (err) {
9393
+ logger.error({ mapName, query, err }, "Search failed");
9394
+ return {
9395
+ requestId: "",
9396
+ results: [],
9397
+ totalCount: 0,
9398
+ error: `Search failed: ${err.message}`
9399
+ };
9400
+ }
9401
+ }
9402
+ /**
9403
+ * Handle document set/update.
9404
+ * Called by ServerCoordinator when data changes.
9405
+ *
9406
+ * @param mapName - Name of the map
9407
+ * @param key - Document key
9408
+ * @param value - Document value
9409
+ */
9410
+ onDataChange(mapName, key, value, changeType) {
9411
+ const index = this.indexes.get(mapName);
9412
+ if (!index) {
9413
+ return;
9414
+ }
9415
+ if (changeType === "remove" || value === null || value === void 0) {
9416
+ index.onRemove(key);
9417
+ } else {
9418
+ index.onSet(key, value);
9419
+ }
9420
+ this.notifySubscribers(mapName, key, value ?? null, changeType);
9421
+ }
9422
+ /**
9423
+ * Build index from existing map entries.
9424
+ * Called when FTS is enabled for a map that already has data.
9425
+ *
9426
+ * @param mapName - Name of the map
9427
+ * @param entries - Iterator of [key, value] tuples
9428
+ */
9429
+ buildIndexFromEntries(mapName, entries) {
9430
+ const index = this.indexes.get(mapName);
9431
+ if (!index) {
9432
+ logger.warn({ mapName }, "Cannot build index: FTS not enabled for map");
9433
+ return;
9434
+ }
9435
+ let count = 0;
9436
+ for (const [key, value] of entries) {
9437
+ if (value !== null) {
9438
+ index.onSet(key, value);
9439
+ count++;
9440
+ }
9441
+ }
9442
+ logger.info({ mapName, documentCount: count }, "Index built from entries");
9443
+ }
9444
+ /**
9445
+ * Get index statistics for monitoring.
9446
+ */
9447
+ getIndexStats(mapName) {
9448
+ const index = this.indexes.get(mapName);
9449
+ const config = this.configs.get(mapName);
9450
+ if (!index || !config) {
9451
+ return null;
9452
+ }
9453
+ return {
9454
+ documentCount: index.getSize(),
9455
+ fields: config.fields
9456
+ };
9457
+ }
9458
+ /**
9459
+ * Clear all indexes (for testing or shutdown).
9460
+ */
9461
+ clear() {
9462
+ for (const index of this.indexes.values()) {
9463
+ index.clear();
9464
+ }
9465
+ this.indexes.clear();
9466
+ this.configs.clear();
9467
+ this.subscriptions.clear();
9468
+ this.subscriptionsByMap.clear();
9469
+ this.subscriptionsByClient.clear();
9470
+ this.pendingNotifications.clear();
9471
+ if (this.notificationTimer) {
9472
+ clearTimeout(this.notificationTimer);
9473
+ this.notificationTimer = null;
9474
+ }
9475
+ logger.debug("SearchCoordinator cleared");
9476
+ }
9477
+ // ============================================
9478
+ // Phase 11.1b: Live Search Subscription Methods
9479
+ // ============================================
9480
+ /**
9481
+ * Subscribe to live search results.
9482
+ * Returns initial results and tracks the subscription for delta updates.
9483
+ *
9484
+ * @param clientId - ID of the subscribing client
9485
+ * @param subscriptionId - Unique subscription identifier
9486
+ * @param mapName - Name of the map to search
9487
+ * @param query - Search query text
9488
+ * @param options - Search options (limit, minScore, boost)
9489
+ * @returns Initial search results
9490
+ */
9491
+ subscribe(clientId, subscriptionId, mapName, query, options) {
9492
+ const index = this.indexes.get(mapName);
9493
+ if (!index) {
9494
+ logger.warn({ mapName }, "Subscribe requested for map without FTS enabled");
9495
+ return [];
9496
+ }
9497
+ const queryTerms = index.tokenizeQuery(query);
9498
+ const searchResults = index.search(query, options);
9499
+ const currentResults = /* @__PURE__ */ new Map();
9500
+ const results = [];
9501
+ for (const result of searchResults) {
9502
+ const value = this.getDocumentValue ? this.getDocumentValue(mapName, result.docId) : void 0;
9503
+ currentResults.set(result.docId, {
9504
+ score: result.score,
9505
+ matchedTerms: result.matchedTerms || []
9506
+ });
9507
+ results.push({
9508
+ key: result.docId,
9509
+ value,
9510
+ score: result.score,
9511
+ matchedTerms: result.matchedTerms || []
9512
+ });
9513
+ }
9514
+ const subscription = {
9515
+ id: subscriptionId,
9516
+ clientId,
9517
+ mapName,
9518
+ query,
9519
+ queryTerms,
9520
+ options: options || {},
9521
+ currentResults
9522
+ };
9523
+ this.subscriptions.set(subscriptionId, subscription);
9524
+ if (!this.subscriptionsByMap.has(mapName)) {
9525
+ this.subscriptionsByMap.set(mapName, /* @__PURE__ */ new Set());
9526
+ }
9527
+ this.subscriptionsByMap.get(mapName).add(subscriptionId);
9528
+ if (!this.subscriptionsByClient.has(clientId)) {
9529
+ this.subscriptionsByClient.set(clientId, /* @__PURE__ */ new Set());
9530
+ }
9531
+ this.subscriptionsByClient.get(clientId).add(subscriptionId);
9532
+ logger.debug(
9533
+ { subscriptionId, clientId, mapName, query, resultCount: results.length },
9534
+ "Search subscription created"
9535
+ );
9536
+ return results;
9537
+ }
9538
+ /**
9539
+ * Unsubscribe from a live search.
9540
+ *
9541
+ * @param subscriptionId - Subscription to remove
9542
+ */
9543
+ unsubscribe(subscriptionId) {
9544
+ const subscription = this.subscriptions.get(subscriptionId);
9545
+ if (!subscription) {
9546
+ return;
9547
+ }
9548
+ this.subscriptions.delete(subscriptionId);
9549
+ const mapSubs = this.subscriptionsByMap.get(subscription.mapName);
9550
+ if (mapSubs) {
9551
+ mapSubs.delete(subscriptionId);
9552
+ if (mapSubs.size === 0) {
9553
+ this.subscriptionsByMap.delete(subscription.mapName);
9554
+ }
9555
+ }
9556
+ const clientSubs = this.subscriptionsByClient.get(subscription.clientId);
9557
+ if (clientSubs) {
9558
+ clientSubs.delete(subscriptionId);
9559
+ if (clientSubs.size === 0) {
9560
+ this.subscriptionsByClient.delete(subscription.clientId);
9561
+ }
9562
+ }
9563
+ logger.debug({ subscriptionId }, "Search subscription removed");
9564
+ }
9565
+ /**
9566
+ * Unsubscribe all subscriptions for a client.
9567
+ * Called when a client disconnects.
9568
+ *
9569
+ * @param clientId - ID of the disconnected client
9570
+ */
9571
+ unsubscribeClient(clientId) {
9572
+ const clientSubs = this.subscriptionsByClient.get(clientId);
9573
+ if (!clientSubs) {
9574
+ return;
9575
+ }
9576
+ const subscriptionIds = Array.from(clientSubs);
9577
+ for (const subscriptionId of subscriptionIds) {
9578
+ this.unsubscribe(subscriptionId);
9579
+ }
9580
+ logger.debug({ clientId, count: subscriptionIds.length }, "Client subscriptions cleared");
9581
+ }
9582
+ /**
9583
+ * Get the number of active subscriptions.
9584
+ */
9585
+ getSubscriptionCount() {
9586
+ return this.subscriptions.size;
9587
+ }
9588
+ /**
9589
+ * Notify subscribers about a document change.
9590
+ * Computes delta (ENTER/UPDATE/LEAVE) for each affected subscription.
9591
+ *
9592
+ * @param mapName - Name of the map that changed
9593
+ * @param key - Document key that changed
9594
+ * @param value - New document value (null if removed)
9595
+ * @param changeType - Type of change
9596
+ */
9597
+ notifySubscribers(mapName, key, value, changeType) {
9598
+ if (!this.sendUpdate) {
9599
+ return;
9600
+ }
9601
+ const subscriptionIds = this.subscriptionsByMap.get(mapName);
9602
+ if (!subscriptionIds || subscriptionIds.size === 0) {
9603
+ return;
9604
+ }
9605
+ const index = this.indexes.get(mapName);
9606
+ if (!index) {
9607
+ return;
9608
+ }
9609
+ for (const subId of subscriptionIds) {
9610
+ const sub = this.subscriptions.get(subId);
9611
+ if (!sub) continue;
9612
+ const wasInResults = sub.currentResults.has(key);
9613
+ let isInResults = false;
9614
+ let newScore = 0;
9615
+ let matchedTerms = [];
9616
+ logger.debug({ subId, key, wasInResults, changeType }, "Processing subscription update");
9617
+ if (changeType !== "remove" && value !== null) {
9618
+ const result = this.scoreDocument(sub, key, value, index);
9619
+ if (result && result.score >= (sub.options.minScore ?? 0)) {
9620
+ isInResults = true;
9621
+ newScore = result.score;
9622
+ matchedTerms = result.matchedTerms;
9623
+ }
9624
+ }
9625
+ let updateType = null;
9626
+ if (!wasInResults && isInResults) {
9627
+ updateType = "ENTER";
9628
+ sub.currentResults.set(key, { score: newScore, matchedTerms });
9629
+ } else if (wasInResults && !isInResults) {
9630
+ updateType = "LEAVE";
9631
+ sub.currentResults.delete(key);
9632
+ } else if (wasInResults && isInResults) {
9633
+ const old = sub.currentResults.get(key);
9634
+ if (Math.abs(old.score - newScore) > 1e-4 || changeType === "update") {
9635
+ updateType = "UPDATE";
9636
+ sub.currentResults.set(key, { score: newScore, matchedTerms });
9637
+ }
9638
+ }
9639
+ logger.debug({ subId, key, wasInResults, isInResults, updateType, newScore }, "Update decision");
9640
+ if (updateType) {
9641
+ this.sendUpdate(
9642
+ sub.clientId,
9643
+ subId,
9644
+ key,
9645
+ value,
9646
+ newScore,
9647
+ matchedTerms,
9648
+ updateType
9649
+ );
9650
+ }
9651
+ }
9652
+ }
9653
+ /**
9654
+ * Score a single document against a subscription's query.
9655
+ *
9656
+ * OPTIMIZED: O(Q × D) complexity instead of O(N) full index scan.
9657
+ * Uses pre-tokenized queryTerms and FullTextIndex.scoreSingleDocument().
9658
+ *
9659
+ * @param subscription - The subscription containing query and cached queryTerms
9660
+ * @param key - Document key
9661
+ * @param value - Document value
9662
+ * @param index - The FullTextIndex for this map
9663
+ * @returns Scored result or null if document doesn't match
9664
+ */
9665
+ scoreDocument(subscription, key, value, index) {
9666
+ const result = index.scoreSingleDocument(key, subscription.queryTerms, value);
9667
+ if (!result) {
9668
+ return null;
9669
+ }
9670
+ return {
9671
+ score: result.score,
9672
+ matchedTerms: result.matchedTerms || []
9673
+ };
9674
+ }
9675
+ // ============================================
9676
+ // Phase 11.2: Notification Batching Methods
9677
+ // ============================================
9678
+ /**
9679
+ * Queue a notification for batched processing.
9680
+ * Notifications are collected and processed together after BATCH_INTERVAL.
9681
+ *
9682
+ * @param mapName - Name of the map that changed
9683
+ * @param key - Document key that changed
9684
+ * @param value - New document value (null if removed)
9685
+ * @param changeType - Type of change
9686
+ */
9687
+ queueNotification(mapName, key, value, changeType) {
9688
+ if (!this.sendBatchUpdate) {
9689
+ this.notifySubscribers(mapName, key, value, changeType);
9690
+ return;
9691
+ }
9692
+ const notification = { key, value, changeType };
9693
+ if (!this.pendingNotifications.has(mapName)) {
9694
+ this.pendingNotifications.set(mapName, []);
9695
+ }
9696
+ this.pendingNotifications.get(mapName).push(notification);
9697
+ this.scheduleNotificationFlush();
9698
+ }
9699
+ /**
9700
+ * Schedule a flush of pending notifications.
9701
+ * Uses setTimeout to batch notifications within BATCH_INTERVAL window.
9702
+ */
9703
+ scheduleNotificationFlush() {
9704
+ if (this.notificationTimer) {
9705
+ return;
9706
+ }
9707
+ this.notificationTimer = setTimeout(() => {
9708
+ this.flushNotifications();
9709
+ this.notificationTimer = null;
9710
+ }, this.BATCH_INTERVAL);
9711
+ }
9712
+ /**
9713
+ * Flush all pending notifications.
9714
+ * Processes each map's notifications and sends batched updates.
9715
+ */
9716
+ flushNotifications() {
9717
+ if (this.pendingNotifications.size === 0) {
9718
+ return;
9719
+ }
9720
+ for (const [mapName, notifications] of this.pendingNotifications) {
9721
+ this.processBatchedNotifications(mapName, notifications);
9722
+ }
9723
+ this.pendingNotifications.clear();
9724
+ }
9725
+ /**
9726
+ * Process batched notifications for a single map.
9727
+ * Computes updates for each subscription and sends as a batch.
9728
+ *
9729
+ * @param mapName - Name of the map
9730
+ * @param notifications - Array of pending notifications
9731
+ */
9732
+ processBatchedNotifications(mapName, notifications) {
9733
+ const subscriptionIds = this.subscriptionsByMap.get(mapName);
9734
+ if (!subscriptionIds || subscriptionIds.size === 0) {
9735
+ return;
9736
+ }
9737
+ const index = this.indexes.get(mapName);
9738
+ if (!index) {
9739
+ return;
9740
+ }
9741
+ for (const subId of subscriptionIds) {
9742
+ const sub = this.subscriptions.get(subId);
9743
+ if (!sub) continue;
9744
+ const updates = [];
9745
+ for (const { key, value, changeType } of notifications) {
9746
+ const update = this.computeSubscriptionUpdate(sub, key, value, changeType, index);
9747
+ if (update) {
9748
+ updates.push(update);
9749
+ }
9750
+ }
9751
+ if (updates.length > 0 && this.sendBatchUpdate) {
9752
+ this.sendBatchUpdate(sub.clientId, subId, updates);
9753
+ }
9754
+ }
9755
+ }
9756
+ /**
9757
+ * Compute the update for a single document change against a subscription.
9758
+ * Returns null if no update is needed.
9759
+ *
9760
+ * @param subscription - The subscription to check
9761
+ * @param key - Document key
9762
+ * @param value - Document value (null if removed)
9763
+ * @param changeType - Type of change
9764
+ * @param index - The FullTextIndex for this map
9765
+ * @returns BatchedUpdate or null
9766
+ */
9767
+ computeSubscriptionUpdate(subscription, key, value, changeType, index) {
9768
+ const wasInResults = subscription.currentResults.has(key);
9769
+ let isInResults = false;
9770
+ let newScore = 0;
9771
+ let matchedTerms = [];
9772
+ if (changeType !== "remove" && value !== null) {
9773
+ const result = this.scoreDocument(subscription, key, value, index);
9774
+ if (result && result.score >= (subscription.options.minScore ?? 0)) {
9775
+ isInResults = true;
9776
+ newScore = result.score;
9777
+ matchedTerms = result.matchedTerms;
9778
+ }
9779
+ }
9780
+ let updateType = null;
9781
+ if (!wasInResults && isInResults) {
9782
+ updateType = "ENTER";
9783
+ subscription.currentResults.set(key, { score: newScore, matchedTerms });
9784
+ } else if (wasInResults && !isInResults) {
9785
+ updateType = "LEAVE";
9786
+ subscription.currentResults.delete(key);
9787
+ } else if (wasInResults && isInResults) {
9788
+ const old = subscription.currentResults.get(key);
9789
+ if (Math.abs(old.score - newScore) > 1e-4 || changeType === "update") {
9790
+ updateType = "UPDATE";
9791
+ subscription.currentResults.set(key, { score: newScore, matchedTerms });
9792
+ }
9793
+ }
9794
+ if (!updateType) {
9795
+ return null;
9796
+ }
9797
+ return {
9798
+ key,
9799
+ value,
9800
+ score: newScore,
9801
+ matchedTerms,
9802
+ type: updateType
9803
+ };
9804
+ }
9805
+ };
9806
+
9258
9807
  // src/ServerCoordinator.ts
9259
9808
  var GC_INTERVAL_MS = 60 * 60 * 1e3;
9260
9809
  var GC_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
@@ -9281,7 +9830,7 @@ var ServerCoordinator = class {
9281
9830
  this._readyResolve = resolve;
9282
9831
  });
9283
9832
  this._nodeId = config.nodeId;
9284
- this.hlc = new import_core19.HLC(config.nodeId);
9833
+ this.hlc = new import_core20.HLC(config.nodeId);
9285
9834
  this.storage = config.storage;
9286
9835
  const rawSecret = config.jwtSecret || process.env.JWT_SECRET || "topgun-secret-dev";
9287
9836
  this.jwtSecret = rawSecret.replace(/\\n/g, "\n");
@@ -9423,8 +9972,8 @@ var ServerCoordinator = class {
9423
9972
  this.cluster,
9424
9973
  this.partitionService,
9425
9974
  {
9426
- ...import_core19.DEFAULT_REPLICATION_CONFIG,
9427
- defaultConsistency: config.defaultConsistency ?? import_core19.ConsistencyLevel.EVENTUAL,
9975
+ ...import_core20.DEFAULT_REPLICATION_CONFIG,
9976
+ defaultConsistency: config.defaultConsistency ?? import_core20.ConsistencyLevel.EVENTUAL,
9428
9977
  ...config.replicationConfig
9429
9978
  }
9430
9979
  );
@@ -9487,7 +10036,7 @@ var ServerCoordinator = class {
9487
10036
  void 0,
9488
10037
  // LagTracker - can be added later
9489
10038
  {
9490
- defaultConsistency: config.defaultConsistency ?? import_core19.ConsistencyLevel.STRONG,
10039
+ defaultConsistency: config.defaultConsistency ?? import_core20.ConsistencyLevel.STRONG,
9491
10040
  preferLocalReplica: true,
9492
10041
  loadBalancing: "latency-based"
9493
10042
  }
@@ -9512,6 +10061,34 @@ var ServerCoordinator = class {
9512
10061
  );
9513
10062
  this.repairScheduler.start();
9514
10063
  logger.info("MerkleTreeManager and RepairScheduler initialized");
10064
+ this.searchCoordinator = new SearchCoordinator();
10065
+ this.searchCoordinator.setDocumentValueGetter((mapName, key) => {
10066
+ const map = this.maps.get(mapName);
10067
+ if (!map) return void 0;
10068
+ return map.get(key);
10069
+ });
10070
+ this.searchCoordinator.setSendUpdateCallback((clientId, subscriptionId, key, value, score, matchedTerms, type) => {
10071
+ const client = this.clients.get(clientId);
10072
+ if (client) {
10073
+ client.writer.write({
10074
+ type: "SEARCH_UPDATE",
10075
+ payload: {
10076
+ subscriptionId,
10077
+ key,
10078
+ value,
10079
+ score,
10080
+ matchedTerms,
10081
+ type
10082
+ }
10083
+ });
10084
+ }
10085
+ });
10086
+ if (config.fullTextSearch) {
10087
+ for (const [mapName, ftsConfig] of Object.entries(config.fullTextSearch)) {
10088
+ this.searchCoordinator.enableSearch(mapName, ftsConfig);
10089
+ logger.info({ mapName, fields: ftsConfig.fields }, "FTS enabled for map");
10090
+ }
10091
+ }
9515
10092
  this.systemManager = new SystemManager(
9516
10093
  this.cluster,
9517
10094
  this.metricsService,
@@ -9535,6 +10112,7 @@ var ServerCoordinator = class {
9535
10112
  if (this.storage) {
9536
10113
  this.storage.initialize().then(() => {
9537
10114
  logger.info("Storage adapter initialized");
10115
+ this.backfillSearchIndexes();
9538
10116
  }).catch((err) => {
9539
10117
  logger.error({ err }, "Failed to initialize storage");
9540
10118
  });
@@ -9542,6 +10120,36 @@ var ServerCoordinator = class {
9542
10120
  this.startGarbageCollection();
9543
10121
  this.startHeartbeatCheck();
9544
10122
  }
10123
+ /**
10124
+ * Populate FTS indexes from existing map data.
10125
+ * Called after storage initialization.
10126
+ */
10127
+ async backfillSearchIndexes() {
10128
+ const enabledMaps = this.searchCoordinator.getEnabledMaps();
10129
+ const promises2 = enabledMaps.map(async (mapName) => {
10130
+ try {
10131
+ await this.getMapAsync(mapName);
10132
+ const map = this.maps.get(mapName);
10133
+ if (!map) return;
10134
+ if (map instanceof import_core20.LWWMap) {
10135
+ const entries = Array.from(map.entries());
10136
+ if (entries.length > 0) {
10137
+ logger.info({ mapName, count: entries.length }, "Backfilling FTS index");
10138
+ this.searchCoordinator.buildIndexFromEntries(
10139
+ mapName,
10140
+ map.entries()
10141
+ );
10142
+ }
10143
+ } else {
10144
+ logger.warn({ mapName }, "FTS backfill skipped: Map type not supported (only LWWMap)");
10145
+ }
10146
+ } catch (err) {
10147
+ logger.error({ mapName, err }, "Failed to backfill FTS index");
10148
+ }
10149
+ });
10150
+ await Promise.all(promises2);
10151
+ logger.info("FTS backfill completed");
10152
+ }
9545
10153
  /** Wait for server to be fully ready (ports assigned) */
9546
10154
  ready() {
9547
10155
  return this._readyPromise;
@@ -9608,6 +10216,59 @@ var ServerCoordinator = class {
9608
10216
  getTaskletScheduler() {
9609
10217
  return this.taskletScheduler;
9610
10218
  }
10219
+ // === Phase 11.1: Full-Text Search Public API ===
10220
+ /**
10221
+ * Enable full-text search for a map.
10222
+ * Can be called at runtime to enable FTS dynamically.
10223
+ *
10224
+ * @param mapName - Name of the map to enable FTS for
10225
+ * @param config - FTS configuration (fields, tokenizer, bm25 options)
10226
+ */
10227
+ enableFullTextSearch(mapName, config) {
10228
+ this.searchCoordinator.enableSearch(mapName, config);
10229
+ const map = this.maps.get(mapName);
10230
+ if (map) {
10231
+ const entries = [];
10232
+ if (map instanceof import_core20.LWWMap) {
10233
+ for (const [key, value] of map.entries()) {
10234
+ entries.push([key, value]);
10235
+ }
10236
+ } else if (map instanceof import_core20.ORMap) {
10237
+ for (const key of map.allKeys()) {
10238
+ const values = map.get(key);
10239
+ const value = values.length > 0 ? values[0] : null;
10240
+ entries.push([key, value]);
10241
+ }
10242
+ }
10243
+ this.searchCoordinator.buildIndexFromEntries(mapName, entries);
10244
+ }
10245
+ }
10246
+ /**
10247
+ * Disable full-text search for a map.
10248
+ *
10249
+ * @param mapName - Name of the map to disable FTS for
10250
+ */
10251
+ disableFullTextSearch(mapName) {
10252
+ this.searchCoordinator.disableSearch(mapName);
10253
+ }
10254
+ /**
10255
+ * Check if full-text search is enabled for a map.
10256
+ *
10257
+ * @param mapName - Name of the map to check
10258
+ * @returns True if FTS is enabled
10259
+ */
10260
+ isFullTextSearchEnabled(mapName) {
10261
+ return this.searchCoordinator.isSearchEnabled(mapName);
10262
+ }
10263
+ /**
10264
+ * Get FTS index statistics for a map.
10265
+ *
10266
+ * @param mapName - Name of the map
10267
+ * @returns Index stats or null if FTS not enabled
10268
+ */
10269
+ getFullTextSearchStats(mapName) {
10270
+ return this.searchCoordinator.getIndexStats(mapName);
10271
+ }
9611
10272
  /**
9612
10273
  * Phase 10.02: Graceful cluster departure
9613
10274
  *
@@ -9693,7 +10354,7 @@ var ServerCoordinator = class {
9693
10354
  this.metricsService.destroy();
9694
10355
  this.wss.close();
9695
10356
  logger.info(`Closing ${this.clients.size} client connections...`);
9696
- const shutdownMsg = (0, import_core19.serialize)({ type: "SHUTDOWN_PENDING", retryAfter: 5e3 });
10357
+ const shutdownMsg = (0, import_core20.serialize)({ type: "SHUTDOWN_PENDING", retryAfter: 5e3 });
9697
10358
  for (const client of this.clients.values()) {
9698
10359
  try {
9699
10360
  if (client.socket.readyState === import_ws3.WebSocket.OPEN) {
@@ -9826,7 +10487,7 @@ var ServerCoordinator = class {
9826
10487
  buf = Buffer.from(message);
9827
10488
  }
9828
10489
  try {
9829
- data = (0, import_core19.deserialize)(buf);
10490
+ data = (0, import_core20.deserialize)(buf);
9830
10491
  } catch (e) {
9831
10492
  try {
9832
10493
  const text = Buffer.isBuffer(buf) ? buf.toString() : new TextDecoder().decode(buf);
@@ -9866,6 +10527,7 @@ var ServerCoordinator = class {
9866
10527
  this.lockManager.handleClientDisconnect(clientId);
9867
10528
  this.topicManager.unsubscribeAll(clientId);
9868
10529
  this.counterHandler.unsubscribeAll(clientId);
10530
+ this.searchCoordinator.unsubscribeClient(clientId);
9869
10531
  const members = this.cluster.getMembers();
9870
10532
  for (const memberId of members) {
9871
10533
  if (!this.cluster.isLocal(memberId)) {
@@ -9878,10 +10540,10 @@ var ServerCoordinator = class {
9878
10540
  this.clients.delete(clientId);
9879
10541
  this.metricsService.setConnectedClients(this.clients.size);
9880
10542
  });
9881
- ws.send((0, import_core19.serialize)({ type: "AUTH_REQUIRED" }));
10543
+ ws.send((0, import_core20.serialize)({ type: "AUTH_REQUIRED" }));
9882
10544
  }
9883
10545
  async handleMessage(client, rawMessage) {
9884
- const parseResult = import_core19.MessageSchema.safeParse(rawMessage);
10546
+ const parseResult = import_core20.MessageSchema.safeParse(rawMessage);
9885
10547
  if (!parseResult.success) {
9886
10548
  logger.error({ clientId: client.id, error: parseResult.error }, "Invalid message format from client");
9887
10549
  client.writer.write({
@@ -9951,7 +10613,7 @@ var ServerCoordinator = class {
9951
10613
  options: {
9952
10614
  // Default to EVENTUAL for read scaling unless specified otherwise
9953
10615
  // In future, we could extract consistency from query options if available
9954
- consistency: import_core19.ConsistencyLevel.EVENTUAL
10616
+ consistency: import_core20.ConsistencyLevel.EVENTUAL
9955
10617
  }
9956
10618
  });
9957
10619
  if (targetNode) {
@@ -10146,7 +10808,7 @@ var ServerCoordinator = class {
10146
10808
  this.metricsService.incOp("GET", message.mapName);
10147
10809
  try {
10148
10810
  const mapForSync = await this.getMapAsync(message.mapName);
10149
- if (mapForSync instanceof import_core19.LWWMap) {
10811
+ if (mapForSync instanceof import_core20.LWWMap) {
10150
10812
  const tree = mapForSync.getMerkleTree();
10151
10813
  const rootHash = tree.getRootHash();
10152
10814
  client.writer.write({
@@ -10184,7 +10846,7 @@ var ServerCoordinator = class {
10184
10846
  const { mapName, path } = message.payload;
10185
10847
  try {
10186
10848
  const mapForBucket = await this.getMapAsync(mapName);
10187
- if (mapForBucket instanceof import_core19.LWWMap) {
10849
+ if (mapForBucket instanceof import_core20.LWWMap) {
10188
10850
  const treeForBucket = mapForBucket.getMerkleTree();
10189
10851
  const buckets = treeForBucket.getBuckets(path);
10190
10852
  const node = treeForBucket.getNode(path);
@@ -10566,7 +11228,7 @@ var ServerCoordinator = class {
10566
11228
  this.metricsService.incOp("GET", message.mapName);
10567
11229
  try {
10568
11230
  const mapForSync = await this.getMapAsync(message.mapName, "OR");
10569
- if (mapForSync instanceof import_core19.ORMap) {
11231
+ if (mapForSync instanceof import_core20.ORMap) {
10570
11232
  const tree = mapForSync.getMerkleTree();
10571
11233
  const rootHash = tree.getRootHash();
10572
11234
  client.writer.write({
@@ -10603,7 +11265,7 @@ var ServerCoordinator = class {
10603
11265
  const { mapName, path } = message.payload;
10604
11266
  try {
10605
11267
  const mapForBucket = await this.getMapAsync(mapName, "OR");
10606
- if (mapForBucket instanceof import_core19.ORMap) {
11268
+ if (mapForBucket instanceof import_core20.ORMap) {
10607
11269
  const tree = mapForBucket.getMerkleTree();
10608
11270
  const buckets = tree.getBuckets(path);
10609
11271
  const isLeaf = tree.isLeaf(path);
@@ -10647,7 +11309,7 @@ var ServerCoordinator = class {
10647
11309
  const { mapName: diffMapName, keys } = message.payload;
10648
11310
  try {
10649
11311
  const mapForDiff = await this.getMapAsync(diffMapName, "OR");
10650
- if (mapForDiff instanceof import_core19.ORMap) {
11312
+ if (mapForDiff instanceof import_core20.ORMap) {
10651
11313
  const entries = [];
10652
11314
  const allTombstones = mapForDiff.getTombstones();
10653
11315
  for (const key of keys) {
@@ -10679,7 +11341,7 @@ var ServerCoordinator = class {
10679
11341
  const { mapName: pushMapName, entries: pushEntries } = message.payload;
10680
11342
  try {
10681
11343
  const mapForPush = await this.getMapAsync(pushMapName, "OR");
10682
- if (mapForPush instanceof import_core19.ORMap) {
11344
+ if (mapForPush instanceof import_core20.ORMap) {
10683
11345
  let totalAdded = 0;
10684
11346
  let totalUpdated = 0;
10685
11347
  for (const entry of pushEntries) {
@@ -10807,6 +11469,106 @@ var ServerCoordinator = class {
10807
11469
  });
10808
11470
  break;
10809
11471
  }
11472
+ // Phase 11.1: Full-Text Search
11473
+ case "SEARCH": {
11474
+ const { requestId: searchReqId, mapName: searchMapName, query: searchQuery, options: searchOptions } = message.payload;
11475
+ if (!this.securityManager.checkPermission(client.principal, searchMapName, "READ")) {
11476
+ logger.warn({ clientId: client.id, mapName: searchMapName }, "Access Denied: SEARCH");
11477
+ client.writer.write({
11478
+ type: "SEARCH_RESP",
11479
+ payload: {
11480
+ requestId: searchReqId,
11481
+ results: [],
11482
+ totalCount: 0,
11483
+ error: `Access denied for map: ${searchMapName}`
11484
+ }
11485
+ });
11486
+ break;
11487
+ }
11488
+ if (!this.searchCoordinator.isSearchEnabled(searchMapName)) {
11489
+ client.writer.write({
11490
+ type: "SEARCH_RESP",
11491
+ payload: {
11492
+ requestId: searchReqId,
11493
+ results: [],
11494
+ totalCount: 0,
11495
+ error: `Full-text search not enabled for map: ${searchMapName}`
11496
+ }
11497
+ });
11498
+ break;
11499
+ }
11500
+ const searchResult = this.searchCoordinator.search(searchMapName, searchQuery, searchOptions);
11501
+ searchResult.requestId = searchReqId;
11502
+ logger.debug({
11503
+ clientId: client.id,
11504
+ mapName: searchMapName,
11505
+ query: searchQuery,
11506
+ resultCount: searchResult.results.length
11507
+ }, "Search executed");
11508
+ client.writer.write({
11509
+ type: "SEARCH_RESP",
11510
+ payload: searchResult
11511
+ });
11512
+ break;
11513
+ }
11514
+ // Phase 11.1b: Live Search Subscriptions
11515
+ case "SEARCH_SUB": {
11516
+ const { subscriptionId, mapName: subMapName, query: subQuery, options: subOptions } = message.payload;
11517
+ if (!this.securityManager.checkPermission(client.principal, subMapName, "READ")) {
11518
+ logger.warn({ clientId: client.id, mapName: subMapName }, "Access Denied: SEARCH_SUB");
11519
+ client.writer.write({
11520
+ type: "SEARCH_RESP",
11521
+ payload: {
11522
+ requestId: subscriptionId,
11523
+ results: [],
11524
+ totalCount: 0,
11525
+ error: `Access denied for map: ${subMapName}`
11526
+ }
11527
+ });
11528
+ break;
11529
+ }
11530
+ if (!this.searchCoordinator.isSearchEnabled(subMapName)) {
11531
+ client.writer.write({
11532
+ type: "SEARCH_RESP",
11533
+ payload: {
11534
+ requestId: subscriptionId,
11535
+ results: [],
11536
+ totalCount: 0,
11537
+ error: `Full-text search not enabled for map: ${subMapName}`
11538
+ }
11539
+ });
11540
+ break;
11541
+ }
11542
+ const initialResults = this.searchCoordinator.subscribe(
11543
+ client.id,
11544
+ subscriptionId,
11545
+ subMapName,
11546
+ subQuery,
11547
+ subOptions
11548
+ );
11549
+ logger.debug({
11550
+ clientId: client.id,
11551
+ subscriptionId,
11552
+ mapName: subMapName,
11553
+ query: subQuery,
11554
+ resultCount: initialResults.length
11555
+ }, "Search subscription created");
11556
+ client.writer.write({
11557
+ type: "SEARCH_RESP",
11558
+ payload: {
11559
+ requestId: subscriptionId,
11560
+ results: initialResults,
11561
+ totalCount: initialResults.length
11562
+ }
11563
+ });
11564
+ break;
11565
+ }
11566
+ case "SEARCH_UNSUB": {
11567
+ const { subscriptionId: unsubId } = message.payload;
11568
+ this.searchCoordinator.unsubscribe(unsubId);
11569
+ logger.debug({ clientId: client.id, subscriptionId: unsubId }, "Search unsubscription");
11570
+ break;
11571
+ }
10810
11572
  default:
10811
11573
  logger.warn({ type: message.type }, "Unknown message type");
10812
11574
  }
@@ -10820,7 +11582,7 @@ var ServerCoordinator = class {
10820
11582
  } else if (op.orRecord && op.orRecord.timestamp) {
10821
11583
  } else if (op.orTag) {
10822
11584
  try {
10823
- ts = import_core19.HLC.parse(op.orTag);
11585
+ ts = import_core20.HLC.parse(op.orTag);
10824
11586
  } catch (e) {
10825
11587
  }
10826
11588
  }
@@ -10917,7 +11679,7 @@ var ServerCoordinator = class {
10917
11679
  client.writer.write({ ...message, payload: newPayload });
10918
11680
  }
10919
11681
  } else {
10920
- const msgData = (0, import_core19.serialize)(message);
11682
+ const msgData = (0, import_core20.serialize)(message);
10921
11683
  for (const [id, client] of this.clients) {
10922
11684
  if (id !== excludeClientId && client.socket.readyState === 1) {
10923
11685
  client.writer.writeRaw(msgData);
@@ -10995,7 +11757,7 @@ var ServerCoordinator = class {
10995
11757
  payload: { events: filteredEvents },
10996
11758
  timestamp: this.hlc.now()
10997
11759
  };
10998
- const serializedBatch = (0, import_core19.serialize)(batchMessage);
11760
+ const serializedBatch = (0, import_core20.serialize)(batchMessage);
10999
11761
  for (const client of clients) {
11000
11762
  try {
11001
11763
  client.writer.writeRaw(serializedBatch);
@@ -11080,7 +11842,7 @@ var ServerCoordinator = class {
11080
11842
  payload: { events: filteredEvents },
11081
11843
  timestamp: this.hlc.now()
11082
11844
  };
11083
- const serializedBatch = (0, import_core19.serialize)(batchMessage);
11845
+ const serializedBatch = (0, import_core20.serialize)(batchMessage);
11084
11846
  for (const client of clients) {
11085
11847
  sendPromises.push(new Promise((resolve, reject) => {
11086
11848
  try {
@@ -11278,7 +12040,7 @@ var ServerCoordinator = class {
11278
12040
  const localQuery = { ...query };
11279
12041
  delete localQuery.offset;
11280
12042
  delete localQuery.limit;
11281
- if (map instanceof import_core19.IndexedLWWMap) {
12043
+ if (map instanceof import_core20.IndexedLWWMap) {
11282
12044
  const coreQuery = this.convertToCoreQuery(localQuery);
11283
12045
  if (coreQuery) {
11284
12046
  const entries = map.queryEntries(coreQuery);
@@ -11288,7 +12050,7 @@ var ServerCoordinator = class {
11288
12050
  });
11289
12051
  }
11290
12052
  }
11291
- if (map instanceof import_core19.IndexedORMap) {
12053
+ if (map instanceof import_core20.IndexedORMap) {
11292
12054
  const coreQuery = this.convertToCoreQuery(localQuery);
11293
12055
  if (coreQuery) {
11294
12056
  const results = map.query(coreQuery);
@@ -11296,14 +12058,14 @@ var ServerCoordinator = class {
11296
12058
  }
11297
12059
  }
11298
12060
  const records = /* @__PURE__ */ new Map();
11299
- if (map instanceof import_core19.LWWMap) {
12061
+ if (map instanceof import_core20.LWWMap) {
11300
12062
  for (const key of map.allKeys()) {
11301
12063
  const rec = map.getRecord(key);
11302
12064
  if (rec && rec.value !== null) {
11303
12065
  records.set(key, rec);
11304
12066
  }
11305
12067
  }
11306
- } else if (map instanceof import_core19.ORMap) {
12068
+ } else if (map instanceof import_core20.ORMap) {
11307
12069
  const items = map.items;
11308
12070
  for (const key of items.keys()) {
11309
12071
  const values = map.get(key);
@@ -11451,11 +12213,11 @@ var ServerCoordinator = class {
11451
12213
  async applyOpToMap(op, remoteNodeId) {
11452
12214
  const typeHint = op.opType === "OR_ADD" || op.opType === "OR_REMOVE" ? "OR" : "LWW";
11453
12215
  const map = this.getMap(op.mapName, typeHint);
11454
- if (typeHint === "OR" && map instanceof import_core19.LWWMap) {
12216
+ if (typeHint === "OR" && map instanceof import_core20.LWWMap) {
11455
12217
  logger.error({ mapName: op.mapName }, "Map type mismatch: LWWMap but received OR op");
11456
12218
  throw new Error("Map type mismatch: LWWMap but received OR op");
11457
12219
  }
11458
- if (typeHint === "LWW" && map instanceof import_core19.ORMap) {
12220
+ if (typeHint === "LWW" && map instanceof import_core20.ORMap) {
11459
12221
  logger.error({ mapName: op.mapName }, "Map type mismatch: ORMap but received LWW op");
11460
12222
  throw new Error("Map type mismatch: ORMap but received LWW op");
11461
12223
  }
@@ -11466,7 +12228,7 @@ var ServerCoordinator = class {
11466
12228
  mapName: op.mapName,
11467
12229
  key: op.key
11468
12230
  };
11469
- if (map instanceof import_core19.LWWMap) {
12231
+ if (map instanceof import_core20.LWWMap) {
11470
12232
  oldRecord = map.getRecord(op.key);
11471
12233
  if (this.conflictResolverHandler.hasResolvers(op.mapName)) {
11472
12234
  const mergeResult = await this.conflictResolverHandler.mergeWithResolver(
@@ -11494,7 +12256,7 @@ var ServerCoordinator = class {
11494
12256
  eventPayload.eventType = "UPDATED";
11495
12257
  eventPayload.record = op.record;
11496
12258
  }
11497
- } else if (map instanceof import_core19.ORMap) {
12259
+ } else if (map instanceof import_core20.ORMap) {
11498
12260
  oldRecord = map.getRecords(op.key);
11499
12261
  if (op.opType === "OR_ADD") {
11500
12262
  map.apply(op.key, op.orRecord);
@@ -11510,7 +12272,7 @@ var ServerCoordinator = class {
11510
12272
  }
11511
12273
  }
11512
12274
  this.queryRegistry.processChange(op.mapName, map, op.key, op.record || op.orRecord, oldRecord);
11513
- const mapSize = map instanceof import_core19.ORMap ? map.totalRecords : map.size;
12275
+ const mapSize = map instanceof import_core20.ORMap ? map.totalRecords : map.size;
11514
12276
  this.metricsService.setMapSize(op.mapName, mapSize);
11515
12277
  if (this.storage) {
11516
12278
  if (recordToStore) {
@@ -11543,6 +12305,12 @@ var ServerCoordinator = class {
11543
12305
  const partitionId = this.partitionService.getPartitionId(op.key);
11544
12306
  this.merkleTreeManager.updateRecord(partitionId, op.key, recordToStore);
11545
12307
  }
12308
+ if (this.searchCoordinator.isSearchEnabled(op.mapName)) {
12309
+ const isRemove = op.opType === "REMOVE" || op.record && op.record.value === null;
12310
+ const value = isRemove ? null : op.record?.value ?? op.orRecord?.value;
12311
+ const changeType = isRemove ? "remove" : oldRecord ? "update" : "add";
12312
+ this.searchCoordinator.onDataChange(op.mapName, op.key, value, changeType);
12313
+ }
11546
12314
  return { eventPayload, oldRecord };
11547
12315
  }
11548
12316
  /**
@@ -11818,11 +12586,11 @@ var ServerCoordinator = class {
11818
12586
  return;
11819
12587
  }
11820
12588
  const map = this.getMap(mapName, eventType === "OR_ADD" || eventType === "OR_REMOVE" ? "OR" : "LWW");
11821
- const oldRecord = map instanceof import_core19.LWWMap ? map.getRecord(key) : null;
12589
+ const oldRecord = map instanceof import_core20.LWWMap ? map.getRecord(key) : null;
11822
12590
  if (this.partitionService.isRelated(key)) {
11823
- if (map instanceof import_core19.LWWMap && payload.record) {
12591
+ if (map instanceof import_core20.LWWMap && payload.record) {
11824
12592
  map.merge(key, payload.record);
11825
- } else if (map instanceof import_core19.ORMap) {
12593
+ } else if (map instanceof import_core20.ORMap) {
11826
12594
  if (eventType === "OR_ADD" && payload.orRecord) {
11827
12595
  map.apply(key, payload.orRecord);
11828
12596
  } else if (eventType === "OR_REMOVE" && payload.orTag) {
@@ -11841,9 +12609,9 @@ var ServerCoordinator = class {
11841
12609
  if (!this.maps.has(name)) {
11842
12610
  let map;
11843
12611
  if (typeHint === "OR") {
11844
- map = new import_core19.ORMap(this.hlc);
12612
+ map = new import_core20.ORMap(this.hlc);
11845
12613
  } else {
11846
- map = new import_core19.LWWMap(this.hlc);
12614
+ map = new import_core20.LWWMap(this.hlc);
11847
12615
  }
11848
12616
  this.maps.set(name, map);
11849
12617
  if (this.storage) {
@@ -11866,7 +12634,7 @@ var ServerCoordinator = class {
11866
12634
  this.getMap(name, typeHint);
11867
12635
  const loadingPromise = this.mapLoadingPromises.get(name);
11868
12636
  const map = this.maps.get(name);
11869
- const mapSize = map instanceof import_core19.LWWMap ? Array.from(map.entries()).length : map instanceof import_core19.ORMap ? map.size : 0;
12637
+ const mapSize = map instanceof import_core20.LWWMap ? Array.from(map.entries()).length : map instanceof import_core20.ORMap ? map.size : 0;
11870
12638
  logger.info({
11871
12639
  mapName: name,
11872
12640
  mapExisted,
@@ -11876,7 +12644,7 @@ var ServerCoordinator = class {
11876
12644
  if (loadingPromise) {
11877
12645
  logger.info({ mapName: name }, "[getMapAsync] Waiting for loadMapFromStorage...");
11878
12646
  await loadingPromise;
11879
- const newMapSize = map instanceof import_core19.LWWMap ? Array.from(map.entries()).length : map instanceof import_core19.ORMap ? map.size : 0;
12647
+ const newMapSize = map instanceof import_core20.LWWMap ? Array.from(map.entries()).length : map instanceof import_core20.ORMap ? map.size : 0;
11880
12648
  logger.info({ mapName: name, mapSizeAfterLoad: newMapSize }, "[getMapAsync] Load completed");
11881
12649
  }
11882
12650
  return this.maps.get(name);
@@ -11893,7 +12661,7 @@ var ServerCoordinator = class {
11893
12661
  const mapName = key.substring(0, separatorIndex);
11894
12662
  const actualKey = key.substring(separatorIndex + 1);
11895
12663
  const map = this.maps.get(mapName);
11896
- if (!map || !(map instanceof import_core19.LWWMap)) {
12664
+ if (!map || !(map instanceof import_core20.LWWMap)) {
11897
12665
  return null;
11898
12666
  }
11899
12667
  return map.getRecord(actualKey) ?? null;
@@ -11947,16 +12715,16 @@ var ServerCoordinator = class {
11947
12715
  const currentMap = this.maps.get(name);
11948
12716
  if (!currentMap) return;
11949
12717
  let targetMap = currentMap;
11950
- if (isOR && currentMap instanceof import_core19.LWWMap) {
12718
+ if (isOR && currentMap instanceof import_core20.LWWMap) {
11951
12719
  logger.info({ mapName: name }, "Map auto-detected as ORMap. Switching type.");
11952
- targetMap = new import_core19.ORMap(this.hlc);
12720
+ targetMap = new import_core20.ORMap(this.hlc);
11953
12721
  this.maps.set(name, targetMap);
11954
- } else if (!isOR && currentMap instanceof import_core19.ORMap && typeHint !== "OR") {
12722
+ } else if (!isOR && currentMap instanceof import_core20.ORMap && typeHint !== "OR") {
11955
12723
  logger.info({ mapName: name }, "Map auto-detected as LWWMap. Switching type.");
11956
- targetMap = new import_core19.LWWMap(this.hlc);
12724
+ targetMap = new import_core20.LWWMap(this.hlc);
11957
12725
  this.maps.set(name, targetMap);
11958
12726
  }
11959
- if (targetMap instanceof import_core19.ORMap) {
12727
+ if (targetMap instanceof import_core20.ORMap) {
11960
12728
  for (const [key, record] of records) {
11961
12729
  if (key === "__tombstones__") {
11962
12730
  const t = record;
@@ -11969,7 +12737,7 @@ var ServerCoordinator = class {
11969
12737
  }
11970
12738
  }
11971
12739
  }
11972
- } else if (targetMap instanceof import_core19.LWWMap) {
12740
+ } else if (targetMap instanceof import_core20.LWWMap) {
11973
12741
  for (const [key, record] of records) {
11974
12742
  if (!record.type) {
11975
12743
  targetMap.merge(key, record);
@@ -11980,7 +12748,7 @@ var ServerCoordinator = class {
11980
12748
  if (count > 0) {
11981
12749
  logger.info({ mapName: name, count }, "Loaded records for map");
11982
12750
  this.queryRegistry.refreshSubscriptions(name, targetMap);
11983
- const mapSize = targetMap instanceof import_core19.ORMap ? targetMap.totalRecords : targetMap.size;
12751
+ const mapSize = targetMap instanceof import_core20.ORMap ? targetMap.totalRecords : targetMap.size;
11984
12752
  this.metricsService.setMapSize(name, mapSize);
11985
12753
  }
11986
12754
  } catch (err) {
@@ -12062,7 +12830,7 @@ var ServerCoordinator = class {
12062
12830
  reportLocalHlc() {
12063
12831
  let minHlc = this.hlc.now();
12064
12832
  for (const client of this.clients.values()) {
12065
- if (import_core19.HLC.compare(client.lastActiveHlc, minHlc) < 0) {
12833
+ if (import_core20.HLC.compare(client.lastActiveHlc, minHlc) < 0) {
12066
12834
  minHlc = client.lastActiveHlc;
12067
12835
  }
12068
12836
  }
@@ -12083,7 +12851,7 @@ var ServerCoordinator = class {
12083
12851
  let globalSafe = this.hlc.now();
12084
12852
  let initialized = false;
12085
12853
  for (const ts of this.gcReports.values()) {
12086
- if (!initialized || import_core19.HLC.compare(ts, globalSafe) < 0) {
12854
+ if (!initialized || import_core20.HLC.compare(ts, globalSafe) < 0) {
12087
12855
  globalSafe = ts;
12088
12856
  initialized = true;
12089
12857
  }
@@ -12118,7 +12886,7 @@ var ServerCoordinator = class {
12118
12886
  logger.info({ olderThanMillis: olderThan.millis }, "Performing Garbage Collection");
12119
12887
  const now = Date.now();
12120
12888
  for (const [name, map] of this.maps) {
12121
- if (map instanceof import_core19.LWWMap) {
12889
+ if (map instanceof import_core20.LWWMap) {
12122
12890
  for (const key of map.allKeys()) {
12123
12891
  const record = map.getRecord(key);
12124
12892
  if (record && record.value !== null && record.ttlMs) {
@@ -12170,7 +12938,7 @@ var ServerCoordinator = class {
12170
12938
  });
12171
12939
  }
12172
12940
  }
12173
- } else if (map instanceof import_core19.ORMap) {
12941
+ } else if (map instanceof import_core20.ORMap) {
12174
12942
  const items = map.items;
12175
12943
  const tombstonesSet = map.tombstones;
12176
12944
  const tagsToExpire = [];
@@ -12273,17 +13041,17 @@ var ServerCoordinator = class {
12273
13041
  stringToWriteConcern(value) {
12274
13042
  switch (value) {
12275
13043
  case "FIRE_AND_FORGET":
12276
- return import_core19.WriteConcern.FIRE_AND_FORGET;
13044
+ return import_core20.WriteConcern.FIRE_AND_FORGET;
12277
13045
  case "MEMORY":
12278
- return import_core19.WriteConcern.MEMORY;
13046
+ return import_core20.WriteConcern.MEMORY;
12279
13047
  case "APPLIED":
12280
- return import_core19.WriteConcern.APPLIED;
13048
+ return import_core20.WriteConcern.APPLIED;
12281
13049
  case "REPLICATED":
12282
- return import_core19.WriteConcern.REPLICATED;
13050
+ return import_core20.WriteConcern.REPLICATED;
12283
13051
  case "PERSISTED":
12284
- return import_core19.WriteConcern.PERSISTED;
13052
+ return import_core20.WriteConcern.PERSISTED;
12285
13053
  default:
12286
- return import_core19.WriteConcern.MEMORY;
13054
+ return import_core20.WriteConcern.MEMORY;
12287
13055
  }
12288
13056
  }
12289
13057
  /**
@@ -12340,7 +13108,7 @@ var ServerCoordinator = class {
12340
13108
  }
12341
13109
  });
12342
13110
  if (op.id) {
12343
- this.writeAckManager.notifyLevel(op.id, import_core19.WriteConcern.REPLICATED);
13111
+ this.writeAckManager.notifyLevel(op.id, import_core20.WriteConcern.REPLICATED);
12344
13112
  }
12345
13113
  }
12346
13114
  }
@@ -12348,7 +13116,7 @@ var ServerCoordinator = class {
12348
13116
  this.broadcastBatch(batchedEvents, clientId);
12349
13117
  for (const op of ops) {
12350
13118
  if (op.id && this.partitionService.isLocalOwner(op.key)) {
12351
- this.writeAckManager.notifyLevel(op.id, import_core19.WriteConcern.REPLICATED);
13119
+ this.writeAckManager.notifyLevel(op.id, import_core20.WriteConcern.REPLICATED);
12352
13120
  }
12353
13121
  }
12354
13122
  }
@@ -12376,7 +13144,7 @@ var ServerCoordinator = class {
12376
13144
  const owner = this.partitionService.getOwner(op.key);
12377
13145
  await this.forwardOpAndWait(op, owner);
12378
13146
  if (op.id) {
12379
- this.writeAckManager.notifyLevel(op.id, import_core19.WriteConcern.REPLICATED);
13147
+ this.writeAckManager.notifyLevel(op.id, import_core20.WriteConcern.REPLICATED);
12380
13148
  }
12381
13149
  }
12382
13150
  }
@@ -12384,7 +13152,7 @@ var ServerCoordinator = class {
12384
13152
  await this.broadcastBatchSync(batchedEvents, clientId);
12385
13153
  for (const op of ops) {
12386
13154
  if (op.id && this.partitionService.isLocalOwner(op.key)) {
12387
- this.writeAckManager.notifyLevel(op.id, import_core19.WriteConcern.REPLICATED);
13155
+ this.writeAckManager.notifyLevel(op.id, import_core20.WriteConcern.REPLICATED);
12388
13156
  }
12389
13157
  }
12390
13158
  }
@@ -12418,7 +13186,7 @@ var ServerCoordinator = class {
12418
13186
  return;
12419
13187
  }
12420
13188
  if (op.id) {
12421
- this.writeAckManager.notifyLevel(op.id, import_core19.WriteConcern.APPLIED);
13189
+ this.writeAckManager.notifyLevel(op.id, import_core20.WriteConcern.APPLIED);
12422
13190
  }
12423
13191
  if (eventPayload) {
12424
13192
  batchedEvents.push({
@@ -12432,7 +13200,7 @@ var ServerCoordinator = class {
12432
13200
  try {
12433
13201
  await this.persistOpSync(op);
12434
13202
  if (op.id) {
12435
- this.writeAckManager.notifyLevel(op.id, import_core19.WriteConcern.PERSISTED);
13203
+ this.writeAckManager.notifyLevel(op.id, import_core20.WriteConcern.PERSISTED);
12436
13204
  }
12437
13205
  } catch (err) {
12438
13206
  logger.error({ opId: op.id, err }, "Persistence failed");
@@ -12775,10 +13543,10 @@ var RateLimitInterceptor = class {
12775
13543
  };
12776
13544
 
12777
13545
  // src/utils/nativeStats.ts
12778
- var import_core20 = require("@topgunbuild/core");
13546
+ var import_core21 = require("@topgunbuild/core");
12779
13547
  function getNativeModuleStatus() {
12780
13548
  return {
12781
- nativeHash: (0, import_core20.isUsingNativeHash)(),
13549
+ nativeHash: (0, import_core21.isUsingNativeHash)(),
12782
13550
  sharedArrayBuffer: SharedMemoryManager.isAvailable()
12783
13551
  };
12784
13552
  }
@@ -12812,11 +13580,11 @@ function logNativeStatus() {
12812
13580
 
12813
13581
  // src/cluster/ClusterCoordinator.ts
12814
13582
  var import_events13 = require("events");
12815
- var import_core21 = require("@topgunbuild/core");
13583
+ var import_core22 = require("@topgunbuild/core");
12816
13584
  var DEFAULT_CLUSTER_COORDINATOR_CONFIG = {
12817
13585
  gradualRebalancing: true,
12818
- migration: import_core21.DEFAULT_MIGRATION_CONFIG,
12819
- replication: import_core21.DEFAULT_REPLICATION_CONFIG,
13586
+ migration: import_core22.DEFAULT_MIGRATION_CONFIG,
13587
+ replication: import_core22.DEFAULT_REPLICATION_CONFIG,
12820
13588
  replicationEnabled: true
12821
13589
  };
12822
13590
  var ClusterCoordinator = class extends import_events13.EventEmitter {
@@ -13184,12 +13952,12 @@ var ClusterCoordinator = class extends import_events13.EventEmitter {
13184
13952
  };
13185
13953
 
13186
13954
  // src/MapWithResolver.ts
13187
- var import_core22 = require("@topgunbuild/core");
13955
+ var import_core23 = require("@topgunbuild/core");
13188
13956
  var MapWithResolver = class {
13189
13957
  constructor(config) {
13190
13958
  this.mapName = config.name;
13191
- this.hlc = new import_core22.HLC(config.nodeId);
13192
- this.map = new import_core22.LWWMap(this.hlc);
13959
+ this.hlc = new import_core23.HLC(config.nodeId);
13960
+ this.map = new import_core23.LWWMap(this.hlc);
13193
13961
  this.resolverService = config.resolverService;
13194
13962
  this.onRejection = config.onRejection;
13195
13963
  }
@@ -13445,7 +14213,7 @@ function mergeWithDefaults(userConfig) {
13445
14213
  }
13446
14214
 
13447
14215
  // src/config/MapFactory.ts
13448
- var import_core23 = require("@topgunbuild/core");
14216
+ var import_core24 = require("@topgunbuild/core");
13449
14217
  var MapFactory = class {
13450
14218
  /**
13451
14219
  * Create a MapFactory.
@@ -13469,9 +14237,9 @@ var MapFactory = class {
13469
14237
  createLWWMap(mapName, hlc) {
13470
14238
  const mapConfig = this.mapConfigs.get(mapName);
13471
14239
  if (!mapConfig || mapConfig.indexes.length === 0) {
13472
- return new import_core23.LWWMap(hlc);
14240
+ return new import_core24.LWWMap(hlc);
13473
14241
  }
13474
- const map = new import_core23.IndexedLWWMap(hlc);
14242
+ const map = new import_core24.IndexedLWWMap(hlc);
13475
14243
  for (const indexDef of mapConfig.indexes) {
13476
14244
  this.addIndexToLWWMap(map, indexDef);
13477
14245
  }
@@ -13487,9 +14255,9 @@ var MapFactory = class {
13487
14255
  createORMap(mapName, hlc) {
13488
14256
  const mapConfig = this.mapConfigs.get(mapName);
13489
14257
  if (!mapConfig || mapConfig.indexes.length === 0) {
13490
- return new import_core23.ORMap(hlc);
14258
+ return new import_core24.ORMap(hlc);
13491
14259
  }
13492
- const map = new import_core23.IndexedORMap(hlc);
14260
+ const map = new import_core24.IndexedORMap(hlc);
13493
14261
  for (const indexDef of mapConfig.indexes) {
13494
14262
  this.addIndexToORMap(map, indexDef);
13495
14263
  }
@@ -13526,7 +14294,7 @@ var MapFactory = class {
13526
14294
  * Supports dot notation for nested paths.
13527
14295
  */
13528
14296
  createAttribute(path) {
13529
- return (0, import_core23.simpleAttribute)(path, (record) => {
14297
+ return (0, import_core24.simpleAttribute)(path, (record) => {
13530
14298
  return this.getNestedValue(record, path);
13531
14299
  });
13532
14300
  }
@@ -13658,6 +14426,7 @@ var MapFactory = class {
13658
14426
  ReduceTasklet,
13659
14427
  RepairScheduler,
13660
14428
  ReplicationPipeline,
14429
+ SearchCoordinator,
13661
14430
  SecurityManager,
13662
14431
  ServerCoordinator,
13663
14432
  TaskletScheduler,