@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.d.mts +303 -2
- package/dist/index.d.ts +303 -2
- package/dist/index.js +843 -74
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +770 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -12
- package/LICENSE +0 -97
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
|
|
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
|
|
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
|
-
...
|
|
9427
|
-
defaultConsistency: config.defaultConsistency ??
|
|
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 ??
|
|
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,
|
|
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,
|
|
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,
|
|
10543
|
+
ws.send((0, import_core20.serialize)({ type: "AUTH_REQUIRED" }));
|
|
9882
10544
|
}
|
|
9883
10545
|
async handleMessage(client, rawMessage) {
|
|
9884
|
-
const parseResult =
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
12589
|
+
const oldRecord = map instanceof import_core20.LWWMap ? map.getRecord(key) : null;
|
|
11822
12590
|
if (this.partitionService.isRelated(key)) {
|
|
11823
|
-
if (map instanceof
|
|
12591
|
+
if (map instanceof import_core20.LWWMap && payload.record) {
|
|
11824
12592
|
map.merge(key, payload.record);
|
|
11825
|
-
} else if (map instanceof
|
|
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
|
|
12612
|
+
map = new import_core20.ORMap(this.hlc);
|
|
11845
12613
|
} else {
|
|
11846
|
-
map = new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
12718
|
+
if (isOR && currentMap instanceof import_core20.LWWMap) {
|
|
11951
12719
|
logger.info({ mapName: name }, "Map auto-detected as ORMap. Switching type.");
|
|
11952
|
-
targetMap = new
|
|
12720
|
+
targetMap = new import_core20.ORMap(this.hlc);
|
|
11953
12721
|
this.maps.set(name, targetMap);
|
|
11954
|
-
} else if (!isOR && currentMap instanceof
|
|
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
|
|
12724
|
+
targetMap = new import_core20.LWWMap(this.hlc);
|
|
11957
12725
|
this.maps.set(name, targetMap);
|
|
11958
12726
|
}
|
|
11959
|
-
if (targetMap instanceof
|
|
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
|
|
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
|
|
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 (
|
|
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 ||
|
|
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
|
|
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
|
|
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
|
|
13044
|
+
return import_core20.WriteConcern.FIRE_AND_FORGET;
|
|
12277
13045
|
case "MEMORY":
|
|
12278
|
-
return
|
|
13046
|
+
return import_core20.WriteConcern.MEMORY;
|
|
12279
13047
|
case "APPLIED":
|
|
12280
|
-
return
|
|
13048
|
+
return import_core20.WriteConcern.APPLIED;
|
|
12281
13049
|
case "REPLICATED":
|
|
12282
|
-
return
|
|
13050
|
+
return import_core20.WriteConcern.REPLICATED;
|
|
12283
13051
|
case "PERSISTED":
|
|
12284
|
-
return
|
|
13052
|
+
return import_core20.WriteConcern.PERSISTED;
|
|
12285
13053
|
default:
|
|
12286
|
-
return
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
13546
|
+
var import_core21 = require("@topgunbuild/core");
|
|
12779
13547
|
function getNativeModuleStatus() {
|
|
12780
13548
|
return {
|
|
12781
|
-
nativeHash: (0,
|
|
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
|
|
13583
|
+
var import_core22 = require("@topgunbuild/core");
|
|
12816
13584
|
var DEFAULT_CLUSTER_COORDINATOR_CONFIG = {
|
|
12817
13585
|
gradualRebalancing: true,
|
|
12818
|
-
migration:
|
|
12819
|
-
replication:
|
|
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
|
|
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
|
|
13192
|
-
this.map = new
|
|
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
|
|
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
|
|
14240
|
+
return new import_core24.LWWMap(hlc);
|
|
13473
14241
|
}
|
|
13474
|
-
const map = new
|
|
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
|
|
14258
|
+
return new import_core24.ORMap(hlc);
|
|
13491
14259
|
}
|
|
13492
|
-
const map = new
|
|
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,
|
|
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,
|