@topgunbuild/server 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -10,7 +10,7 @@ import { createServer as createHttpServer } from "http";
10
10
  import { createServer as createHttpsServer } from "https";
11
11
  import { readFileSync as readFileSync2 } from "fs";
12
12
  import { WebSocketServer as WebSocketServer2, WebSocket as WebSocket3 } from "ws";
13
- import { HLC, LWWMap as LWWMap2, ORMap as ORMap2, serialize as serialize4, deserialize, MessageSchema, WriteConcern as WriteConcern2, ConsistencyLevel as ConsistencyLevel2, DEFAULT_REPLICATION_CONFIG as DEFAULT_REPLICATION_CONFIG2 } from "@topgunbuild/core";
13
+ import { HLC as HLC2, LWWMap as LWWMap3, ORMap as ORMap2, serialize as serialize4, deserialize, MessageSchema, WriteConcern as WriteConcern2, ConsistencyLevel as ConsistencyLevel2, DEFAULT_REPLICATION_CONFIG as DEFAULT_REPLICATION_CONFIG2, IndexedLWWMap as IndexedLWWMap2, IndexedORMap as IndexedORMap2 } from "@topgunbuild/core";
14
14
  import * as jwt from "jsonwebtoken";
15
15
  import * as crypto from "crypto";
16
16
 
@@ -101,7 +101,7 @@ function executeQuery(records, query) {
101
101
  }
102
102
 
103
103
  // src/query/QueryRegistry.ts
104
- import { serialize } from "@topgunbuild/core";
104
+ import { serialize, IndexedLWWMap } from "@topgunbuild/core";
105
105
 
106
106
  // src/utils/logger.ts
107
107
  import pino from "pino";
@@ -362,14 +362,70 @@ var QueryRegistry = class {
362
362
  /**
363
363
  * Processes a record change for all relevant subscriptions.
364
364
  * Calculates diffs and sends updates.
365
+ *
366
+ * For IndexedLWWMap: Uses StandingQueryRegistry for O(1) affected query detection.
367
+ * For regular maps: Falls back to ReverseQueryIndex.
365
368
  */
366
369
  processChange(mapName, map, changeKey, changeRecord, oldRecord) {
367
370
  const index = this.indexes.get(mapName);
368
371
  if (!index) return;
369
372
  const newVal = this.extractValue(changeRecord);
370
373
  const oldVal = this.extractValue(oldRecord);
374
+ if (map instanceof IndexedLWWMap) {
375
+ this.processChangeWithStandingQuery(mapName, map, changeKey, newVal, oldVal);
376
+ return;
377
+ }
378
+ this.processChangeWithReverseIndex(mapName, map, changeKey, newVal, oldVal, index);
379
+ }
380
+ /**
381
+ * Process change using IndexedLWWMap's StandingQueryRegistry.
382
+ * O(1) detection of affected queries.
383
+ */
384
+ processChangeWithStandingQuery(mapName, map, changeKey, newVal, oldVal) {
385
+ const subs = this.subscriptions.get(mapName);
386
+ if (!subs || subs.size === 0) return;
387
+ const subsByQueryId = /* @__PURE__ */ new Map();
388
+ for (const sub of subs) {
389
+ subsByQueryId.set(sub.id, sub);
390
+ }
391
+ const standingRegistry = map.getStandingQueryRegistry();
392
+ let changes;
393
+ if (oldVal === null || oldVal === void 0) {
394
+ if (newVal !== null && newVal !== void 0) {
395
+ changes = standingRegistry.onRecordAdded(changeKey, newVal);
396
+ } else {
397
+ return;
398
+ }
399
+ } else if (newVal === null || newVal === void 0) {
400
+ changes = standingRegistry.onRecordRemoved(changeKey, oldVal);
401
+ } else {
402
+ changes = standingRegistry.onRecordUpdated(changeKey, oldVal, newVal);
403
+ }
404
+ for (const sub of subs) {
405
+ const coreQuery = this.convertToCoreQuery(sub.query);
406
+ if (!coreQuery) {
407
+ this.processSubscriptionFallback(sub, map, changeKey, newVal);
408
+ continue;
409
+ }
410
+ const queryHash = this.hashCoreQuery(coreQuery);
411
+ const change = changes.get(queryHash);
412
+ if (change === "added") {
413
+ sub.previousResultKeys.add(changeKey);
414
+ this.sendUpdate(sub, changeKey, newVal, "UPDATE");
415
+ } else if (change === "removed") {
416
+ sub.previousResultKeys.delete(changeKey);
417
+ this.sendUpdate(sub, changeKey, null, "REMOVE");
418
+ } else if (change === "updated") {
419
+ this.sendUpdate(sub, changeKey, newVal, "UPDATE");
420
+ }
421
+ }
422
+ }
423
+ /**
424
+ * Process change using legacy ReverseQueryIndex.
425
+ */
426
+ processChangeWithReverseIndex(mapName, map, changeKey, newVal, oldVal, index) {
371
427
  const changedFields = this.getChangedFields(oldVal, newVal);
372
- if (changedFields !== "ALL" && changedFields.size === 0 && oldRecord && changeRecord) {
428
+ if (changedFields !== "ALL" && changedFields.size === 0 && oldVal && newVal) {
373
429
  return;
374
430
  }
375
431
  const candidates = index.getCandidates(changedFields, oldVal, newVal);
@@ -411,6 +467,103 @@ var QueryRegistry = class {
411
467
  sub.previousResultKeys = newResultKeys;
412
468
  }
413
469
  }
470
+ /**
471
+ * Fallback processing for subscriptions that can't use StandingQueryRegistry.
472
+ */
473
+ processSubscriptionFallback(sub, map, changeKey, newVal) {
474
+ const dummyRecord = {
475
+ value: newVal,
476
+ timestamp: { millis: 0, counter: 0, nodeId: "" }
477
+ };
478
+ const isMatch = newVal !== null && matchesQuery(dummyRecord, sub.query);
479
+ const wasInResult = sub.previousResultKeys.has(changeKey);
480
+ if (isMatch && !wasInResult) {
481
+ sub.previousResultKeys.add(changeKey);
482
+ this.sendUpdate(sub, changeKey, newVal, "UPDATE");
483
+ } else if (!isMatch && wasInResult) {
484
+ sub.previousResultKeys.delete(changeKey);
485
+ this.sendUpdate(sub, changeKey, null, "REMOVE");
486
+ } else if (isMatch && wasInResult) {
487
+ this.sendUpdate(sub, changeKey, newVal, "UPDATE");
488
+ }
489
+ }
490
+ /**
491
+ * Convert server Query format to core Query format.
492
+ */
493
+ convertToCoreQuery(query) {
494
+ if (query.predicate) {
495
+ return this.predicateToCoreQuery(query.predicate);
496
+ }
497
+ if (query.where) {
498
+ const conditions = [];
499
+ for (const [attribute, condition] of Object.entries(query.where)) {
500
+ if (typeof condition !== "object" || condition === null) {
501
+ conditions.push({ type: "eq", attribute, value: condition });
502
+ } else {
503
+ for (const [op, value] of Object.entries(condition)) {
504
+ const coreOp = this.convertOperator(op);
505
+ if (coreOp) {
506
+ conditions.push({ type: coreOp, attribute, value });
507
+ }
508
+ }
509
+ }
510
+ }
511
+ if (conditions.length === 0) return null;
512
+ if (conditions.length === 1) return conditions[0];
513
+ return { type: "and", children: conditions };
514
+ }
515
+ return null;
516
+ }
517
+ predicateToCoreQuery(predicate) {
518
+ if (!predicate || !predicate.op) return null;
519
+ switch (predicate.op) {
520
+ case "eq":
521
+ case "neq":
522
+ case "gt":
523
+ case "gte":
524
+ case "lt":
525
+ case "lte":
526
+ return {
527
+ type: predicate.op,
528
+ attribute: predicate.attribute,
529
+ value: predicate.value
530
+ };
531
+ case "and":
532
+ case "or":
533
+ if (predicate.children && Array.isArray(predicate.children)) {
534
+ const children = predicate.children.map((c) => this.predicateToCoreQuery(c)).filter((c) => c !== null);
535
+ if (children.length === 0) return null;
536
+ if (children.length === 1) return children[0];
537
+ return { type: predicate.op, children };
538
+ }
539
+ return null;
540
+ case "not":
541
+ if (predicate.children && predicate.children[0]) {
542
+ const child = this.predicateToCoreQuery(predicate.children[0]);
543
+ if (child) {
544
+ return { type: "not", child };
545
+ }
546
+ }
547
+ return null;
548
+ default:
549
+ return null;
550
+ }
551
+ }
552
+ convertOperator(op) {
553
+ const mapping = {
554
+ "$eq": "eq",
555
+ "$ne": "neq",
556
+ "$neq": "neq",
557
+ "$gt": "gt",
558
+ "$gte": "gte",
559
+ "$lt": "lt",
560
+ "$lte": "lte"
561
+ };
562
+ return mapping[op] || null;
563
+ }
564
+ hashCoreQuery(query) {
565
+ return JSON.stringify(query);
566
+ }
414
567
  extractValue(record) {
415
568
  if (!record) return null;
416
569
  if (Array.isArray(record)) {
@@ -6412,118 +6565,1498 @@ var ReplicationPipeline = class extends EventEmitter8 {
6412
6565
  }
6413
6566
  };
6414
6567
 
6415
- // src/ServerCoordinator.ts
6416
- var GC_INTERVAL_MS = 60 * 60 * 1e3;
6417
- var GC_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
6418
- var CLIENT_HEARTBEAT_TIMEOUT_MS = 2e4;
6419
- var CLIENT_HEARTBEAT_CHECK_INTERVAL_MS = 5e3;
6420
- var ServerCoordinator = class {
6421
- constructor(config) {
6422
- this.clients = /* @__PURE__ */ new Map();
6423
- // Interceptors
6424
- this.interceptors = [];
6425
- // In-memory storage (partitioned later)
6426
- this.maps = /* @__PURE__ */ new Map();
6427
- this.pendingClusterQueries = /* @__PURE__ */ new Map();
6428
- // GC Consensus State
6429
- this.gcReports = /* @__PURE__ */ new Map();
6430
- // Track map loading state to avoid returning empty results during async load
6431
- this.mapLoadingPromises = /* @__PURE__ */ new Map();
6432
- // Track pending batch operations for testing purposes
6433
- this.pendingBatchOperations = /* @__PURE__ */ new Set();
6434
- this._actualPort = 0;
6435
- this._actualClusterPort = 0;
6436
- this._readyPromise = new Promise((resolve) => {
6437
- this._readyResolve = resolve;
6438
- });
6439
- this.hlc = new HLC(config.nodeId);
6440
- this.storage = config.storage;
6441
- const rawSecret = config.jwtSecret || process.env.JWT_SECRET || "topgun-secret-dev";
6442
- this.jwtSecret = rawSecret.replace(/\\n/g, "\n");
6443
- this.queryRegistry = new QueryRegistry();
6444
- this.securityManager = new SecurityManager(config.securityPolicies || []);
6445
- this.interceptors = config.interceptors || [];
6446
- this.metricsService = new MetricsService();
6447
- this.eventExecutor = new StripedEventExecutor({
6448
- stripeCount: config.eventStripeCount ?? 4,
6449
- queueCapacity: config.eventQueueCapacity ?? 1e4,
6450
- name: `${config.nodeId}-event-executor`,
6451
- onReject: (task) => {
6452
- logger.warn({ nodeId: config.nodeId, key: task.key }, "Event task rejected due to queue capacity");
6453
- this.metricsService.incEventQueueRejected();
6568
+ // src/handlers/CounterHandler.ts
6569
+ import { PNCounterImpl } from "@topgunbuild/core";
6570
+ var CounterHandler = class {
6571
+ // counterName -> Set<clientId>
6572
+ constructor(nodeId = "server") {
6573
+ this.nodeId = nodeId;
6574
+ this.counters = /* @__PURE__ */ new Map();
6575
+ this.subscriptions = /* @__PURE__ */ new Map();
6576
+ }
6577
+ /**
6578
+ * Get or create a counter by name.
6579
+ */
6580
+ getOrCreateCounter(name) {
6581
+ let counter = this.counters.get(name);
6582
+ if (!counter) {
6583
+ counter = new PNCounterImpl({ nodeId: this.nodeId });
6584
+ this.counters.set(name, counter);
6585
+ logger.debug({ name }, "Created new counter");
6586
+ }
6587
+ return counter;
6588
+ }
6589
+ /**
6590
+ * Handle COUNTER_REQUEST - client wants initial state.
6591
+ * @returns Response message to send back to client
6592
+ */
6593
+ handleCounterRequest(clientId, name) {
6594
+ const counter = this.getOrCreateCounter(name);
6595
+ this.subscribe(clientId, name);
6596
+ const state = counter.getState();
6597
+ logger.debug({ clientId, name, value: counter.get() }, "Counter request handled");
6598
+ return {
6599
+ type: "COUNTER_RESPONSE",
6600
+ payload: {
6601
+ name,
6602
+ state: this.stateToObject(state)
6454
6603
  }
6455
- });
6456
- this.backpressure = new BackpressureRegulator({
6457
- syncFrequency: config.backpressureSyncFrequency ?? 100,
6458
- maxPendingOps: config.backpressureMaxPending ?? 1e3,
6459
- backoffTimeoutMs: config.backpressureBackoffMs ?? 5e3,
6460
- enabled: config.backpressureEnabled ?? true
6461
- });
6462
- this.writeCoalescingEnabled = config.writeCoalescingEnabled ?? true;
6463
- const preset = coalescingPresets[config.writeCoalescingPreset ?? "highThroughput"];
6464
- this.writeCoalescingOptions = {
6465
- maxBatchSize: config.writeCoalescingMaxBatch ?? preset.maxBatchSize,
6466
- maxDelayMs: config.writeCoalescingMaxDelayMs ?? preset.maxDelayMs,
6467
- maxBatchBytes: config.writeCoalescingMaxBytes ?? preset.maxBatchBytes
6468
6604
  };
6469
- this.eventPayloadPool = createEventPayloadPool({
6470
- maxSize: 4096,
6471
- initialSize: 128
6472
- });
6473
- this.taskletScheduler = new TaskletScheduler({
6474
- defaultTimeBudgetMs: 5,
6475
- maxConcurrent: 20
6476
- });
6477
- this.writeAckManager = new WriteAckManager({
6478
- defaultTimeout: config.writeAckTimeout ?? 5e3
6479
- });
6480
- this.rateLimitingEnabled = config.rateLimitingEnabled ?? true;
6481
- this.rateLimiter = new ConnectionRateLimiter({
6482
- maxConnectionsPerSecond: config.maxConnectionsPerSecond ?? 100,
6483
- maxPendingConnections: config.maxPendingConnections ?? 1e3,
6484
- cooldownMs: 1e3
6485
- });
6486
- if (config.workerPoolEnabled) {
6487
- this.workerPool = new WorkerPool({
6488
- minWorkers: config.workerPoolConfig?.minWorkers ?? 2,
6489
- maxWorkers: config.workerPoolConfig?.maxWorkers,
6490
- taskTimeout: config.workerPoolConfig?.taskTimeout ?? 5e3,
6491
- idleTimeout: config.workerPoolConfig?.idleTimeout ?? 3e4,
6492
- autoRestart: config.workerPoolConfig?.autoRestart ?? true
6493
- });
6494
- this.merkleWorker = new MerkleWorker(this.workerPool);
6495
- this.crdtMergeWorker = new CRDTMergeWorker(this.workerPool);
6496
- this.serializationWorker = new SerializationWorker(this.workerPool);
6497
- logger.info({
6498
- minWorkers: config.workerPoolConfig?.minWorkers ?? 2,
6499
- maxWorkers: config.workerPoolConfig?.maxWorkers ?? "auto"
6500
- }, "Worker pool initialized for CPU-bound operations");
6605
+ }
6606
+ /**
6607
+ * Handle COUNTER_SYNC - client sends their state to merge.
6608
+ * @returns Merged state and list of clients to broadcast to
6609
+ */
6610
+ handleCounterSync(clientId, name, stateObj) {
6611
+ const counter = this.getOrCreateCounter(name);
6612
+ const incomingState = this.objectToState(stateObj);
6613
+ counter.merge(incomingState);
6614
+ const mergedState = counter.getState();
6615
+ const mergedStateObj = this.stateToObject(mergedState);
6616
+ logger.debug(
6617
+ { clientId, name, value: counter.get() },
6618
+ "Counter sync handled"
6619
+ );
6620
+ this.subscribe(clientId, name);
6621
+ const subscribers = this.subscriptions.get(name) || /* @__PURE__ */ new Set();
6622
+ const broadcastTo = Array.from(subscribers).filter((id) => id !== clientId);
6623
+ return {
6624
+ // Response to the sending client
6625
+ response: {
6626
+ type: "COUNTER_UPDATE",
6627
+ payload: {
6628
+ name,
6629
+ state: mergedStateObj
6630
+ }
6631
+ },
6632
+ // Broadcast to other clients
6633
+ broadcastTo,
6634
+ broadcastMessage: {
6635
+ type: "COUNTER_UPDATE",
6636
+ payload: {
6637
+ name,
6638
+ state: mergedStateObj
6639
+ }
6640
+ }
6641
+ };
6642
+ }
6643
+ /**
6644
+ * Subscribe a client to counter updates.
6645
+ */
6646
+ subscribe(clientId, counterName) {
6647
+ if (!this.subscriptions.has(counterName)) {
6648
+ this.subscriptions.set(counterName, /* @__PURE__ */ new Set());
6501
6649
  }
6502
- if (config.tls?.enabled) {
6503
- const tlsOptions = this.buildTLSOptions(config.tls);
6504
- this.httpServer = createHttpsServer(tlsOptions, (_req, res) => {
6505
- res.writeHead(200);
6506
- res.end("TopGun Server Running (Secure)");
6507
- });
6508
- logger.info("TLS enabled for client connections");
6650
+ this.subscriptions.get(counterName).add(clientId);
6651
+ logger.debug({ clientId, counterName }, "Client subscribed to counter");
6652
+ }
6653
+ /**
6654
+ * Unsubscribe a client from counter updates.
6655
+ */
6656
+ unsubscribe(clientId, counterName) {
6657
+ const subs = this.subscriptions.get(counterName);
6658
+ if (subs) {
6659
+ subs.delete(clientId);
6660
+ if (subs.size === 0) {
6661
+ this.subscriptions.delete(counterName);
6662
+ }
6663
+ }
6664
+ }
6665
+ /**
6666
+ * Unsubscribe a client from all counters (e.g., on disconnect).
6667
+ */
6668
+ unsubscribeAll(clientId) {
6669
+ for (const [counterName, subs] of this.subscriptions) {
6670
+ subs.delete(clientId);
6671
+ if (subs.size === 0) {
6672
+ this.subscriptions.delete(counterName);
6673
+ }
6674
+ }
6675
+ logger.debug({ clientId }, "Client unsubscribed from all counters");
6676
+ }
6677
+ /**
6678
+ * Get current counter value (for monitoring/debugging).
6679
+ */
6680
+ getCounterValue(name) {
6681
+ const counter = this.counters.get(name);
6682
+ return counter ? counter.get() : 0;
6683
+ }
6684
+ /**
6685
+ * Get all counter names.
6686
+ */
6687
+ getCounterNames() {
6688
+ return Array.from(this.counters.keys());
6689
+ }
6690
+ /**
6691
+ * Get number of subscribers for a counter.
6692
+ */
6693
+ getSubscriberCount(name) {
6694
+ return this.subscriptions.get(name)?.size || 0;
6695
+ }
6696
+ /**
6697
+ * Convert Map-based state to plain object for serialization.
6698
+ */
6699
+ stateToObject(state) {
6700
+ return {
6701
+ p: Object.fromEntries(state.positive),
6702
+ n: Object.fromEntries(state.negative)
6703
+ };
6704
+ }
6705
+ /**
6706
+ * Convert plain object to Map-based state.
6707
+ */
6708
+ objectToState(obj) {
6709
+ return {
6710
+ positive: new Map(Object.entries(obj.p || {})),
6711
+ negative: new Map(Object.entries(obj.n || {}))
6712
+ };
6713
+ }
6714
+ };
6715
+
6716
+ // src/handlers/EntryProcessorHandler.ts
6717
+ import {
6718
+ EntryProcessorDefSchema
6719
+ } from "@topgunbuild/core";
6720
+
6721
+ // src/ProcessorSandbox.ts
6722
+ import {
6723
+ validateProcessorCode
6724
+ } from "@topgunbuild/core";
6725
+ var ivm = null;
6726
+ try {
6727
+ ivm = __require("isolated-vm");
6728
+ } catch {
6729
+ const isProduction = process.env.NODE_ENV === "production";
6730
+ if (isProduction) {
6731
+ logger.error(
6732
+ "SECURITY WARNING: isolated-vm not available in production! Entry processors will run in less secure fallback mode. Install isolated-vm for production environments: pnpm add isolated-vm"
6733
+ );
6734
+ } else {
6735
+ logger.warn("isolated-vm not available, falling back to less secure VM");
6736
+ }
6737
+ }
6738
+ var DEFAULT_SANDBOX_CONFIG = {
6739
+ memoryLimitMb: 8,
6740
+ timeoutMs: 100,
6741
+ maxCachedIsolates: 100,
6742
+ strictValidation: true
6743
+ };
6744
+ var ProcessorSandbox = class {
6745
+ constructor(config = {}) {
6746
+ this.isolateCache = /* @__PURE__ */ new Map();
6747
+ this.scriptCache = /* @__PURE__ */ new Map();
6748
+ this.fallbackScriptCache = /* @__PURE__ */ new Map();
6749
+ this.disposed = false;
6750
+ this.config = { ...DEFAULT_SANDBOX_CONFIG, ...config };
6751
+ }
6752
+ /**
6753
+ * Execute an entry processor in the sandbox.
6754
+ *
6755
+ * @param processor The processor definition (name, code, args)
6756
+ * @param value The current value for the key (or undefined)
6757
+ * @param key The key being processed
6758
+ * @returns Result containing success status, result, and new value
6759
+ */
6760
+ async execute(processor, value, key) {
6761
+ if (this.disposed) {
6762
+ return {
6763
+ success: false,
6764
+ error: "Sandbox has been disposed"
6765
+ };
6766
+ }
6767
+ if (this.config.strictValidation) {
6768
+ const validation = validateProcessorCode(processor.code);
6769
+ if (!validation.valid) {
6770
+ return {
6771
+ success: false,
6772
+ error: validation.error
6773
+ };
6774
+ }
6775
+ }
6776
+ if (ivm) {
6777
+ return this.executeInIsolate(processor, value, key);
6509
6778
  } else {
6510
- this.httpServer = createHttpServer((_req, res) => {
6511
- res.writeHead(200);
6512
- res.end("TopGun Server Running");
6779
+ return this.executeInFallback(processor, value, key);
6780
+ }
6781
+ }
6782
+ /**
6783
+ * Execute processor in isolated-vm (secure production mode).
6784
+ */
6785
+ async executeInIsolate(processor, value, key) {
6786
+ if (!ivm) {
6787
+ return { success: false, error: "isolated-vm not available" };
6788
+ }
6789
+ const isolate = this.getOrCreateIsolate(processor.name);
6790
+ try {
6791
+ const context = await isolate.createContext();
6792
+ const jail = context.global;
6793
+ await jail.set("global", jail.derefInto());
6794
+ await context.eval(`
6795
+ var value = ${JSON.stringify(value)};
6796
+ var key = ${JSON.stringify(key)};
6797
+ var args = ${JSON.stringify(processor.args)};
6798
+ `);
6799
+ const wrappedCode = `
6800
+ (function() {
6801
+ ${processor.code}
6802
+ })()
6803
+ `;
6804
+ const script = await this.getOrCompileScript(
6805
+ processor.name,
6806
+ wrappedCode,
6807
+ isolate
6808
+ );
6809
+ const result = await script.run(context, {
6810
+ timeout: this.config.timeoutMs
6513
6811
  });
6514
- if (process.env.NODE_ENV === "production") {
6515
- logger.warn("\u26A0\uFE0F TLS is disabled! Client connections are NOT encrypted.");
6812
+ const parsed = result;
6813
+ if (typeof parsed !== "object" || parsed === null) {
6814
+ return {
6815
+ success: false,
6816
+ error: "Processor must return { value, result? } object"
6817
+ };
6516
6818
  }
6819
+ return {
6820
+ success: true,
6821
+ result: parsed.result,
6822
+ newValue: parsed.value
6823
+ };
6824
+ } catch (error) {
6825
+ const message = error instanceof Error ? error.message : String(error);
6826
+ if (message.includes("Script execution timed out")) {
6827
+ return {
6828
+ success: false,
6829
+ error: "Processor execution timed out"
6830
+ };
6831
+ }
6832
+ return {
6833
+ success: false,
6834
+ error: message
6835
+ };
6517
6836
  }
6518
- const metricsPort = config.metricsPort !== void 0 ? config.metricsPort : 9090;
6519
- this.metricsServer = createHttpServer(async (req, res) => {
6520
- if (req.url === "/metrics") {
6521
- try {
6522
- res.setHeader("Content-Type", this.metricsService.getContentType());
6523
- res.end(await this.metricsService.getMetrics());
6524
- } catch (err) {
6525
- res.statusCode = 500;
6526
- res.end("Internal Server Error");
6837
+ }
6838
+ /**
6839
+ * Execute processor in fallback VM (less secure, for development).
6840
+ */
6841
+ async executeInFallback(processor, value, key) {
6842
+ try {
6843
+ const isResolver = processor.name.startsWith("resolver:");
6844
+ let fn = isResolver ? void 0 : this.fallbackScriptCache.get(processor.name);
6845
+ if (!fn) {
6846
+ const wrappedCode = `
6847
+ return (function(value, key, args) {
6848
+ ${processor.code}
6849
+ })
6850
+ `;
6851
+ fn = new Function(wrappedCode)();
6852
+ if (!isResolver) {
6853
+ this.fallbackScriptCache.set(processor.name, fn);
6854
+ }
6855
+ }
6856
+ const timeoutPromise = new Promise((_, reject) => {
6857
+ setTimeout(() => reject(new Error("Processor execution timed out")), this.config.timeoutMs);
6858
+ });
6859
+ const executionPromise = Promise.resolve().then(() => fn(value, key, processor.args));
6860
+ const result = await Promise.race([executionPromise, timeoutPromise]);
6861
+ if (typeof result !== "object" || result === null) {
6862
+ return {
6863
+ success: false,
6864
+ error: "Processor must return { value, result? } object"
6865
+ };
6866
+ }
6867
+ return {
6868
+ success: true,
6869
+ result: result.result,
6870
+ newValue: result.value
6871
+ };
6872
+ } catch (error) {
6873
+ const message = error instanceof Error ? error.message : String(error);
6874
+ return {
6875
+ success: false,
6876
+ error: message
6877
+ };
6878
+ }
6879
+ }
6880
+ /**
6881
+ * Get or create an isolate for a processor.
6882
+ */
6883
+ getOrCreateIsolate(name) {
6884
+ if (!ivm) {
6885
+ throw new Error("isolated-vm not available");
6886
+ }
6887
+ let isolate = this.isolateCache.get(name);
6888
+ if (!isolate || isolate.isDisposed) {
6889
+ if (this.isolateCache.size >= this.config.maxCachedIsolates) {
6890
+ const oldest = this.isolateCache.keys().next().value;
6891
+ if (oldest) {
6892
+ const oldIsolate = this.isolateCache.get(oldest);
6893
+ if (oldIsolate && !oldIsolate.isDisposed) {
6894
+ oldIsolate.dispose();
6895
+ }
6896
+ this.isolateCache.delete(oldest);
6897
+ this.scriptCache.delete(oldest);
6898
+ }
6899
+ }
6900
+ isolate = new ivm.Isolate({
6901
+ memoryLimit: this.config.memoryLimitMb
6902
+ });
6903
+ this.isolateCache.set(name, isolate);
6904
+ }
6905
+ return isolate;
6906
+ }
6907
+ /**
6908
+ * Get or compile a script for a processor.
6909
+ */
6910
+ async getOrCompileScript(name, code, isolate) {
6911
+ let script = this.scriptCache.get(name);
6912
+ if (!script) {
6913
+ script = await isolate.compileScript(code);
6914
+ this.scriptCache.set(name, script);
6915
+ }
6916
+ return script;
6917
+ }
6918
+ /**
6919
+ * Clear script cache for a specific processor (e.g., when code changes).
6920
+ */
6921
+ clearCache(processorName) {
6922
+ if (processorName) {
6923
+ const isolate = this.isolateCache.get(processorName);
6924
+ if (isolate && !isolate.isDisposed) {
6925
+ isolate.dispose();
6926
+ }
6927
+ this.isolateCache.delete(processorName);
6928
+ this.scriptCache.delete(processorName);
6929
+ this.fallbackScriptCache.delete(processorName);
6930
+ } else {
6931
+ for (const isolate of this.isolateCache.values()) {
6932
+ if (!isolate.isDisposed) {
6933
+ isolate.dispose();
6934
+ }
6935
+ }
6936
+ this.isolateCache.clear();
6937
+ this.scriptCache.clear();
6938
+ this.fallbackScriptCache.clear();
6939
+ }
6940
+ }
6941
+ /**
6942
+ * Check if using secure isolated-vm mode.
6943
+ */
6944
+ isSecureMode() {
6945
+ return ivm !== null;
6946
+ }
6947
+ /**
6948
+ * Get current cache sizes.
6949
+ */
6950
+ getCacheStats() {
6951
+ return {
6952
+ isolates: this.isolateCache.size,
6953
+ scripts: this.scriptCache.size,
6954
+ fallbackScripts: this.fallbackScriptCache.size
6955
+ };
6956
+ }
6957
+ /**
6958
+ * Dispose of all isolates and clear caches.
6959
+ */
6960
+ dispose() {
6961
+ if (this.disposed) return;
6962
+ this.disposed = true;
6963
+ this.clearCache();
6964
+ logger.debug("ProcessorSandbox disposed");
6965
+ }
6966
+ };
6967
+
6968
+ // src/handlers/EntryProcessorHandler.ts
6969
+ var EntryProcessorHandler = class {
6970
+ constructor(config) {
6971
+ this.hlc = config.hlc;
6972
+ this.sandbox = new ProcessorSandbox(config.sandboxConfig);
6973
+ }
6974
+ /**
6975
+ * Execute a processor on a single key atomically.
6976
+ *
6977
+ * @param map The LWWMap to operate on
6978
+ * @param key The key to process
6979
+ * @param processorDef The processor definition (will be validated)
6980
+ * @returns Result with success status, processor result, and new value
6981
+ */
6982
+ async executeOnKey(map, key, processorDef) {
6983
+ const parseResult = EntryProcessorDefSchema.safeParse(processorDef);
6984
+ if (!parseResult.success) {
6985
+ logger.warn(
6986
+ { key, error: parseResult.error.message },
6987
+ "Invalid processor definition"
6988
+ );
6989
+ return {
6990
+ result: {
6991
+ success: false,
6992
+ error: `Invalid processor: ${parseResult.error.message}`
6993
+ }
6994
+ };
6995
+ }
6996
+ const processor = parseResult.data;
6997
+ const currentValue = map.get(key);
6998
+ logger.debug(
6999
+ { key, processor: processor.name, hasValue: currentValue !== void 0 },
7000
+ "Executing entry processor"
7001
+ );
7002
+ const sandboxResult = await this.sandbox.execute(
7003
+ processor,
7004
+ currentValue,
7005
+ key
7006
+ );
7007
+ if (!sandboxResult.success) {
7008
+ logger.warn(
7009
+ { key, processor: processor.name, error: sandboxResult.error },
7010
+ "Processor execution failed"
7011
+ );
7012
+ return { result: sandboxResult };
7013
+ }
7014
+ let timestamp;
7015
+ if (sandboxResult.newValue !== void 0) {
7016
+ const record = map.set(key, sandboxResult.newValue);
7017
+ timestamp = record.timestamp;
7018
+ logger.debug(
7019
+ { key, processor: processor.name, timestamp },
7020
+ "Processor updated value"
7021
+ );
7022
+ } else if (currentValue !== void 0) {
7023
+ const tombstone = map.remove(key);
7024
+ timestamp = tombstone.timestamp;
7025
+ logger.debug(
7026
+ { key, processor: processor.name, timestamp },
7027
+ "Processor deleted value"
7028
+ );
7029
+ }
7030
+ return {
7031
+ result: sandboxResult,
7032
+ timestamp
7033
+ };
7034
+ }
7035
+ /**
7036
+ * Execute a processor on multiple keys.
7037
+ *
7038
+ * Each key is processed sequentially to ensure atomicity per-key.
7039
+ * For parallel execution across keys, use multiple calls.
7040
+ *
7041
+ * @param map The LWWMap to operate on
7042
+ * @param keys The keys to process
7043
+ * @param processorDef The processor definition
7044
+ * @returns Map of key -> result
7045
+ */
7046
+ async executeOnKeys(map, keys, processorDef) {
7047
+ const results = /* @__PURE__ */ new Map();
7048
+ const timestamps = /* @__PURE__ */ new Map();
7049
+ const parseResult = EntryProcessorDefSchema.safeParse(processorDef);
7050
+ if (!parseResult.success) {
7051
+ const errorResult = {
7052
+ success: false,
7053
+ error: `Invalid processor: ${parseResult.error.message}`
7054
+ };
7055
+ for (const key of keys) {
7056
+ results.set(key, errorResult);
7057
+ }
7058
+ return { results, timestamps };
7059
+ }
7060
+ for (const key of keys) {
7061
+ const { result, timestamp } = await this.executeOnKey(
7062
+ map,
7063
+ key,
7064
+ processorDef
7065
+ );
7066
+ results.set(key, result);
7067
+ if (timestamp) {
7068
+ timestamps.set(key, timestamp);
7069
+ }
7070
+ }
7071
+ return { results, timestamps };
7072
+ }
7073
+ /**
7074
+ * Execute a processor on all entries matching a predicate.
7075
+ *
7076
+ * WARNING: This can be expensive for large maps.
7077
+ *
7078
+ * @param map The LWWMap to operate on
7079
+ * @param processorDef The processor definition
7080
+ * @param predicateCode Optional predicate code to filter entries
7081
+ * @returns Map of key -> result for processed entries
7082
+ */
7083
+ async executeOnEntries(map, processorDef, predicateCode) {
7084
+ const results = /* @__PURE__ */ new Map();
7085
+ const timestamps = /* @__PURE__ */ new Map();
7086
+ const parseResult = EntryProcessorDefSchema.safeParse(processorDef);
7087
+ if (!parseResult.success) {
7088
+ return { results, timestamps };
7089
+ }
7090
+ const entries = map.entries();
7091
+ for (const [key, value] of entries) {
7092
+ if (predicateCode) {
7093
+ const predicateResult = await this.sandbox.execute(
7094
+ {
7095
+ name: "_predicate",
7096
+ code: `return { value, result: (function() { ${predicateCode} })() };`
7097
+ },
7098
+ value,
7099
+ key
7100
+ );
7101
+ if (!predicateResult.success || !predicateResult.result) {
7102
+ continue;
7103
+ }
7104
+ }
7105
+ const { result, timestamp } = await this.executeOnKey(
7106
+ map,
7107
+ key,
7108
+ processorDef
7109
+ );
7110
+ results.set(key, result);
7111
+ if (timestamp) {
7112
+ timestamps.set(key, timestamp);
7113
+ }
7114
+ }
7115
+ return { results, timestamps };
7116
+ }
7117
+ /**
7118
+ * Check if sandbox is in secure mode (using isolated-vm).
7119
+ */
7120
+ isSecureMode() {
7121
+ return this.sandbox.isSecureMode();
7122
+ }
7123
+ /**
7124
+ * Get sandbox cache statistics.
7125
+ */
7126
+ getCacheStats() {
7127
+ return this.sandbox.getCacheStats();
7128
+ }
7129
+ /**
7130
+ * Clear sandbox cache.
7131
+ */
7132
+ clearCache(processorName) {
7133
+ this.sandbox.clearCache(processorName);
7134
+ }
7135
+ /**
7136
+ * Dispose of the handler and its sandbox.
7137
+ */
7138
+ dispose() {
7139
+ this.sandbox.dispose();
7140
+ logger.debug("EntryProcessorHandler disposed");
7141
+ }
7142
+ };
7143
+
7144
+ // src/ConflictResolverService.ts
7145
+ import {
7146
+ BuiltInResolvers,
7147
+ ConflictResolverDefSchema,
7148
+ validateResolverCode
7149
+ } from "@topgunbuild/core";
7150
+ var DEFAULT_CONFLICT_RESOLVER_CONFIG = {
7151
+ maxResolversPerMap: 100,
7152
+ enableSandboxedResolvers: true,
7153
+ resolverTimeoutMs: 100
7154
+ };
7155
+ var ConflictResolverService = class {
7156
+ constructor(sandbox, config = {}) {
7157
+ this.resolvers = /* @__PURE__ */ new Map();
7158
+ this.disposed = false;
7159
+ this.sandbox = sandbox;
7160
+ this.config = { ...DEFAULT_CONFLICT_RESOLVER_CONFIG, ...config };
7161
+ }
7162
+ /**
7163
+ * Set callback for merge rejections.
7164
+ */
7165
+ onRejection(callback) {
7166
+ this.onRejectionCallback = callback;
7167
+ }
7168
+ /**
7169
+ * Register a resolver for a map.
7170
+ *
7171
+ * @param mapName The map this resolver applies to
7172
+ * @param resolver The resolver definition
7173
+ * @param registeredBy Optional client ID that registered this resolver
7174
+ */
7175
+ register(mapName, resolver, registeredBy) {
7176
+ if (this.disposed) {
7177
+ throw new Error("ConflictResolverService has been disposed");
7178
+ }
7179
+ if (resolver.code) {
7180
+ const parsed = ConflictResolverDefSchema.safeParse({
7181
+ name: resolver.name,
7182
+ code: resolver.code,
7183
+ priority: resolver.priority,
7184
+ keyPattern: resolver.keyPattern
7185
+ });
7186
+ if (!parsed.success) {
7187
+ throw new Error(`Invalid resolver definition: ${parsed.error.message}`);
7188
+ }
7189
+ const validation = validateResolverCode(resolver.code);
7190
+ if (!validation.valid) {
7191
+ throw new Error(`Invalid resolver code: ${validation.error}`);
7192
+ }
7193
+ }
7194
+ const entries = this.resolvers.get(mapName) ?? [];
7195
+ if (entries.length >= this.config.maxResolversPerMap) {
7196
+ throw new Error(
7197
+ `Maximum resolvers per map (${this.config.maxResolversPerMap}) exceeded`
7198
+ );
7199
+ }
7200
+ const filtered = entries.filter((e) => e.resolver.name !== resolver.name);
7201
+ const entry = {
7202
+ resolver,
7203
+ registeredBy
7204
+ };
7205
+ if (resolver.code && !resolver.fn && this.config.enableSandboxedResolvers) {
7206
+ entry.compiledFn = this.compileSandboxed(resolver.name, resolver.code);
7207
+ }
7208
+ filtered.push(entry);
7209
+ filtered.sort(
7210
+ (a, b) => (b.resolver.priority ?? 50) - (a.resolver.priority ?? 50)
7211
+ );
7212
+ this.resolvers.set(mapName, filtered);
7213
+ logger.debug(
7214
+ `Registered resolver '${resolver.name}' for map '${mapName}' with priority ${resolver.priority ?? 50}`
7215
+ );
7216
+ }
7217
+ /**
7218
+ * Unregister a resolver.
7219
+ *
7220
+ * @param mapName The map name
7221
+ * @param resolverName The resolver name to unregister
7222
+ * @param clientId Optional - only unregister if registered by this client
7223
+ */
7224
+ unregister(mapName, resolverName, clientId) {
7225
+ const entries = this.resolvers.get(mapName);
7226
+ if (!entries) return false;
7227
+ const entryIndex = entries.findIndex(
7228
+ (e) => e.resolver.name === resolverName && (!clientId || e.registeredBy === clientId)
7229
+ );
7230
+ if (entryIndex === -1) return false;
7231
+ entries.splice(entryIndex, 1);
7232
+ if (entries.length === 0) {
7233
+ this.resolvers.delete(mapName);
7234
+ }
7235
+ logger.debug(`Unregistered resolver '${resolverName}' from map '${mapName}'`);
7236
+ return true;
7237
+ }
7238
+ /**
7239
+ * Resolve a merge conflict using registered resolvers.
7240
+ *
7241
+ * @param context The merge context
7242
+ * @returns The merge result
7243
+ */
7244
+ async resolve(context) {
7245
+ if (this.disposed) {
7246
+ return { action: "accept", value: context.remoteValue };
7247
+ }
7248
+ const entries = this.resolvers.get(context.mapName) ?? [];
7249
+ const allEntries = [
7250
+ ...entries,
7251
+ { resolver: BuiltInResolvers.LWW() }
7252
+ ];
7253
+ for (const entry of allEntries) {
7254
+ const { resolver } = entry;
7255
+ if (resolver.keyPattern && !this.matchKeyPattern(context.key, resolver.keyPattern)) {
7256
+ continue;
7257
+ }
7258
+ try {
7259
+ let result;
7260
+ if (resolver.fn) {
7261
+ const fn = resolver.fn;
7262
+ const maybePromise = fn(context);
7263
+ result = maybePromise instanceof Promise ? await maybePromise : maybePromise;
7264
+ } else if (entry.compiledFn) {
7265
+ const compiledFn = entry.compiledFn;
7266
+ result = await compiledFn(context);
7267
+ } else {
7268
+ continue;
7269
+ }
7270
+ if (result.action !== "local") {
7271
+ if (result.action === "reject") {
7272
+ logger.debug(
7273
+ `Resolver '${resolver.name}' rejected merge for key '${context.key}' in map '${context.mapName}': ${result.reason}`
7274
+ );
7275
+ if (this.onRejectionCallback) {
7276
+ this.onRejectionCallback({
7277
+ mapName: context.mapName,
7278
+ key: context.key,
7279
+ attemptedValue: context.remoteValue,
7280
+ reason: result.reason,
7281
+ timestamp: context.remoteTimestamp,
7282
+ nodeId: context.remoteNodeId
7283
+ });
7284
+ }
7285
+ }
7286
+ return result;
7287
+ }
7288
+ } catch (error) {
7289
+ const message = error instanceof Error ? error.message : String(error);
7290
+ logger.error(`Resolver '${resolver.name}' threw error: ${message}`);
7291
+ }
7292
+ }
7293
+ return { action: "accept", value: context.remoteValue };
7294
+ }
7295
+ /**
7296
+ * List registered resolvers.
7297
+ *
7298
+ * @param mapName Optional - filter by map name
7299
+ */
7300
+ list(mapName) {
7301
+ const result = [];
7302
+ if (mapName) {
7303
+ const entries = this.resolvers.get(mapName) ?? [];
7304
+ for (const entry of entries) {
7305
+ result.push({
7306
+ mapName,
7307
+ name: entry.resolver.name,
7308
+ priority: entry.resolver.priority,
7309
+ keyPattern: entry.resolver.keyPattern,
7310
+ registeredBy: entry.registeredBy
7311
+ });
7312
+ }
7313
+ } else {
7314
+ for (const [map, entries] of this.resolvers.entries()) {
7315
+ for (const entry of entries) {
7316
+ result.push({
7317
+ mapName: map,
7318
+ name: entry.resolver.name,
7319
+ priority: entry.resolver.priority,
7320
+ keyPattern: entry.resolver.keyPattern,
7321
+ registeredBy: entry.registeredBy
7322
+ });
7323
+ }
7324
+ }
7325
+ }
7326
+ return result;
7327
+ }
7328
+ /**
7329
+ * Check if a map has any registered resolvers.
7330
+ */
7331
+ hasResolvers(mapName) {
7332
+ const entries = this.resolvers.get(mapName);
7333
+ return entries !== void 0 && entries.length > 0;
7334
+ }
7335
+ /**
7336
+ * Get the number of registered resolvers.
7337
+ */
7338
+ get size() {
7339
+ let count = 0;
7340
+ for (const entries of this.resolvers.values()) {
7341
+ count += entries.length;
7342
+ }
7343
+ return count;
7344
+ }
7345
+ /**
7346
+ * Clear all registered resolvers.
7347
+ *
7348
+ * @param mapName Optional - only clear resolvers for specific map
7349
+ */
7350
+ clear(mapName) {
7351
+ if (mapName) {
7352
+ this.resolvers.delete(mapName);
7353
+ } else {
7354
+ this.resolvers.clear();
7355
+ }
7356
+ }
7357
+ /**
7358
+ * Clear resolvers registered by a specific client.
7359
+ */
7360
+ clearByClient(clientId) {
7361
+ let removed = 0;
7362
+ for (const [mapName, entries] of this.resolvers.entries()) {
7363
+ const before = entries.length;
7364
+ const filtered = entries.filter((e) => e.registeredBy !== clientId);
7365
+ removed += before - filtered.length;
7366
+ if (filtered.length === 0) {
7367
+ this.resolvers.delete(mapName);
7368
+ } else if (filtered.length !== before) {
7369
+ this.resolvers.set(mapName, filtered);
7370
+ }
7371
+ }
7372
+ return removed;
7373
+ }
7374
+ /**
7375
+ * Dispose the service.
7376
+ */
7377
+ dispose() {
7378
+ if (this.disposed) return;
7379
+ this.disposed = true;
7380
+ this.resolvers.clear();
7381
+ logger.debug("ConflictResolverService disposed");
7382
+ }
7383
+ /**
7384
+ * Match a key against a glob-like pattern.
7385
+ * Supports * (any chars) and ? (single char).
7386
+ */
7387
+ matchKeyPattern(key, pattern) {
7388
+ const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
7389
+ const regex = new RegExp(`^${regexPattern}$`);
7390
+ return regex.test(key);
7391
+ }
7392
+ /**
7393
+ * Compile sandboxed resolver code.
7394
+ */
7395
+ compileSandboxed(name, code) {
7396
+ return async (ctx) => {
7397
+ const wrappedCode = `
7398
+ const context = {
7399
+ mapName: ${JSON.stringify(ctx.mapName)},
7400
+ key: ${JSON.stringify(ctx.key)},
7401
+ localValue: ${JSON.stringify(ctx.localValue)},
7402
+ remoteValue: ${JSON.stringify(ctx.remoteValue)},
7403
+ localTimestamp: ${JSON.stringify(ctx.localTimestamp)},
7404
+ remoteTimestamp: ${JSON.stringify(ctx.remoteTimestamp)},
7405
+ remoteNodeId: ${JSON.stringify(ctx.remoteNodeId)},
7406
+ auth: ${JSON.stringify(ctx.auth)},
7407
+ };
7408
+
7409
+ function resolve(context) {
7410
+ ${code}
7411
+ }
7412
+
7413
+ const result = resolve(context);
7414
+ return { value: result, result };
7415
+ `;
7416
+ const result = await this.sandbox.execute(
7417
+ {
7418
+ name: `resolver:${name}`,
7419
+ code: wrappedCode
7420
+ },
7421
+ null,
7422
+ // value parameter unused for resolvers
7423
+ "resolver"
7424
+ );
7425
+ if (!result.success) {
7426
+ throw new Error(result.error || "Resolver execution failed");
7427
+ }
7428
+ const resolverResult = result.result;
7429
+ if (!resolverResult || typeof resolverResult !== "object") {
7430
+ throw new Error("Resolver must return a result object");
7431
+ }
7432
+ const action = resolverResult.action;
7433
+ if (!["accept", "reject", "merge", "local"].includes(action)) {
7434
+ throw new Error(`Invalid resolver action: ${action}`);
7435
+ }
7436
+ return resolverResult;
7437
+ };
7438
+ }
7439
+ };
7440
+
7441
+ // src/handlers/ConflictResolverHandler.ts
7442
+ var ConflictResolverHandler = class {
7443
+ constructor(config) {
7444
+ this.rejectionListeners = /* @__PURE__ */ new Set();
7445
+ this.nodeId = config.nodeId;
7446
+ this.sandbox = new ProcessorSandbox(config.sandboxConfig);
7447
+ this.resolverService = new ConflictResolverService(
7448
+ this.sandbox,
7449
+ config.resolverConfig
7450
+ );
7451
+ this.resolverService.onRejection((rejection) => {
7452
+ for (const listener of this.rejectionListeners) {
7453
+ try {
7454
+ listener(rejection);
7455
+ } catch (e) {
7456
+ logger.error({ error: e }, "Error in rejection listener");
7457
+ }
7458
+ }
7459
+ });
7460
+ }
7461
+ /**
7462
+ * Register a conflict resolver for a map.
7463
+ *
7464
+ * @param mapName The map name
7465
+ * @param resolver The resolver definition
7466
+ * @param clientId Optional client ID that registered this resolver
7467
+ */
7468
+ registerResolver(mapName, resolver, clientId) {
7469
+ this.resolverService.register(mapName, resolver, clientId);
7470
+ logger.info(
7471
+ {
7472
+ mapName,
7473
+ resolverName: resolver.name,
7474
+ priority: resolver.priority,
7475
+ clientId
7476
+ },
7477
+ "Resolver registered"
7478
+ );
7479
+ }
7480
+ /**
7481
+ * Unregister a conflict resolver.
7482
+ *
7483
+ * @param mapName The map name
7484
+ * @param resolverName The resolver name
7485
+ * @param clientId Optional - only unregister if registered by this client
7486
+ */
7487
+ unregisterResolver(mapName, resolverName, clientId) {
7488
+ const removed = this.resolverService.unregister(
7489
+ mapName,
7490
+ resolverName,
7491
+ clientId
7492
+ );
7493
+ if (removed) {
7494
+ logger.info({ mapName, resolverName, clientId }, "Resolver unregistered");
7495
+ }
7496
+ return removed;
7497
+ }
7498
+ /**
7499
+ * List registered resolvers.
7500
+ *
7501
+ * @param mapName Optional - filter by map name
7502
+ */
7503
+ listResolvers(mapName) {
7504
+ return this.resolverService.list(mapName);
7505
+ }
7506
+ /**
7507
+ * Apply a merge with conflict resolution.
7508
+ *
7509
+ * Deletions (tombstones) are also passed through resolvers to allow
7510
+ * protection via IMMUTABLE, OWNER_ONLY, or similar resolvers.
7511
+ * If no custom resolvers are registered, deletions use standard LWW.
7512
+ *
7513
+ * @param map The LWWMap to merge into
7514
+ * @param mapName The map name (for resolver lookup)
7515
+ * @param key The key being merged
7516
+ * @param record The incoming record
7517
+ * @param remoteNodeId The source node ID
7518
+ * @param auth Optional authentication context
7519
+ */
7520
+ async mergeWithResolver(map, mapName, key, record, remoteNodeId, auth) {
7521
+ const isDeletion = record.value === null;
7522
+ const localRecord = map.getRecord(key);
7523
+ const context = {
7524
+ mapName,
7525
+ key,
7526
+ localValue: localRecord?.value ?? void 0,
7527
+ // For deletions, remoteValue is null - resolvers can check this
7528
+ remoteValue: record.value,
7529
+ localTimestamp: localRecord?.timestamp,
7530
+ remoteTimestamp: record.timestamp,
7531
+ remoteNodeId,
7532
+ auth,
7533
+ readEntry: (k) => map.get(k)
7534
+ };
7535
+ const result = await this.resolverService.resolve(context);
7536
+ switch (result.action) {
7537
+ case "accept":
7538
+ case "merge": {
7539
+ const finalValue = isDeletion ? null : result.value;
7540
+ const finalRecord = {
7541
+ value: finalValue,
7542
+ timestamp: record.timestamp,
7543
+ ttlMs: record.ttlMs
7544
+ };
7545
+ map.merge(key, finalRecord);
7546
+ return { applied: true, result, record: finalRecord };
7547
+ }
7548
+ case "reject": {
7549
+ const rejection = {
7550
+ mapName,
7551
+ key,
7552
+ attemptedValue: record.value,
7553
+ reason: result.reason,
7554
+ timestamp: record.timestamp,
7555
+ nodeId: remoteNodeId
7556
+ };
7557
+ return { applied: false, result, rejection };
7558
+ }
7559
+ case "local":
7560
+ default:
7561
+ return { applied: false, result };
7562
+ }
7563
+ }
7564
+ /**
7565
+ * Check if a map has custom resolvers registered.
7566
+ */
7567
+ hasResolvers(mapName) {
7568
+ return this.resolverService.hasResolvers(mapName);
7569
+ }
7570
+ /**
7571
+ * Add a listener for merge rejections.
7572
+ */
7573
+ onRejection(listener) {
7574
+ this.rejectionListeners.add(listener);
7575
+ return () => this.rejectionListeners.delete(listener);
7576
+ }
7577
+ /**
7578
+ * Clear resolvers registered by a specific client.
7579
+ */
7580
+ clearByClient(clientId) {
7581
+ return this.resolverService.clearByClient(clientId);
7582
+ }
7583
+ /**
7584
+ * Get the number of registered resolvers.
7585
+ */
7586
+ get resolverCount() {
7587
+ return this.resolverService.size;
7588
+ }
7589
+ /**
7590
+ * Check if sandbox is in secure mode.
7591
+ */
7592
+ isSecureMode() {
7593
+ return this.sandbox.isSecureMode();
7594
+ }
7595
+ /**
7596
+ * Dispose of the handler.
7597
+ */
7598
+ dispose() {
7599
+ this.resolverService.dispose();
7600
+ this.sandbox.dispose();
7601
+ this.rejectionListeners.clear();
7602
+ logger.debug("ConflictResolverHandler disposed");
7603
+ }
7604
+ };
7605
+
7606
+ // src/EventJournalService.ts
7607
+ import {
7608
+ EventJournalImpl,
7609
+ DEFAULT_EVENT_JOURNAL_CONFIG
7610
+ } from "@topgunbuild/core";
7611
+ var DEFAULT_JOURNAL_SERVICE_CONFIG = {
7612
+ ...DEFAULT_EVENT_JOURNAL_CONFIG,
7613
+ tableName: "event_journal",
7614
+ persistBatchSize: 100,
7615
+ persistIntervalMs: 1e3
7616
+ };
7617
+ var TABLE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
7618
+ function validateTableName(name) {
7619
+ if (!TABLE_NAME_REGEX.test(name)) {
7620
+ throw new Error(
7621
+ `Invalid table name "${name}". Table name must start with a letter or underscore and contain only alphanumeric characters and underscores.`
7622
+ );
7623
+ }
7624
+ }
7625
+ var EventJournalService = class extends EventJournalImpl {
7626
+ constructor(config) {
7627
+ super(config);
7628
+ this.pendingPersist = [];
7629
+ this.isPersisting = false;
7630
+ this.isInitialized = false;
7631
+ this.isLoadingFromStorage = false;
7632
+ this.pool = config.pool;
7633
+ this.tableName = config.tableName ?? DEFAULT_JOURNAL_SERVICE_CONFIG.tableName;
7634
+ this.persistBatchSize = config.persistBatchSize ?? DEFAULT_JOURNAL_SERVICE_CONFIG.persistBatchSize;
7635
+ this.persistIntervalMs = config.persistIntervalMs ?? DEFAULT_JOURNAL_SERVICE_CONFIG.persistIntervalMs;
7636
+ validateTableName(this.tableName);
7637
+ this.subscribe((event) => {
7638
+ if (this.isLoadingFromStorage) return;
7639
+ if (event.sequence >= 0n && this.getConfig().persistent) {
7640
+ this.pendingPersist.push(event);
7641
+ if (this.pendingPersist.length >= this.persistBatchSize) {
7642
+ this.persistToStorage().catch((err) => {
7643
+ logger.error({ err }, "Failed to persist journal events");
7644
+ });
7645
+ }
7646
+ }
7647
+ });
7648
+ this.startPersistTimer();
7649
+ }
7650
+ /**
7651
+ * Initialize the journal service, creating table if needed.
7652
+ */
7653
+ async initialize() {
7654
+ if (this.isInitialized) return;
7655
+ const client = await this.pool.connect();
7656
+ try {
7657
+ await client.query(`
7658
+ CREATE TABLE IF NOT EXISTS ${this.tableName} (
7659
+ sequence BIGINT PRIMARY KEY,
7660
+ type VARCHAR(10) NOT NULL CHECK (type IN ('PUT', 'UPDATE', 'DELETE')),
7661
+ map_name VARCHAR(255) NOT NULL,
7662
+ key VARCHAR(1024) NOT NULL,
7663
+ value JSONB,
7664
+ previous_value JSONB,
7665
+ timestamp JSONB NOT NULL,
7666
+ node_id VARCHAR(64) NOT NULL,
7667
+ metadata JSONB,
7668
+ created_at TIMESTAMPTZ DEFAULT NOW()
7669
+ );
7670
+ `);
7671
+ await client.query(`
7672
+ CREATE INDEX IF NOT EXISTS idx_${this.tableName}_map_name
7673
+ ON ${this.tableName}(map_name);
7674
+ `);
7675
+ await client.query(`
7676
+ CREATE INDEX IF NOT EXISTS idx_${this.tableName}_key
7677
+ ON ${this.tableName}(map_name, key);
7678
+ `);
7679
+ await client.query(`
7680
+ CREATE INDEX IF NOT EXISTS idx_${this.tableName}_created_at
7681
+ ON ${this.tableName}(created_at);
7682
+ `);
7683
+ await client.query(`
7684
+ CREATE INDEX IF NOT EXISTS idx_${this.tableName}_node_id
7685
+ ON ${this.tableName}(node_id);
7686
+ `);
7687
+ this.isInitialized = true;
7688
+ logger.info({ tableName: this.tableName }, "EventJournalService initialized");
7689
+ } finally {
7690
+ client.release();
7691
+ }
7692
+ }
7693
+ /**
7694
+ * Persist pending events to PostgreSQL.
7695
+ */
7696
+ async persistToStorage() {
7697
+ if (this.pendingPersist.length === 0 || this.isPersisting) return;
7698
+ this.isPersisting = true;
7699
+ const batch = this.pendingPersist.splice(0, this.persistBatchSize);
7700
+ try {
7701
+ if (batch.length === 0) return;
7702
+ const values = [];
7703
+ const placeholders = [];
7704
+ batch.forEach((e, i) => {
7705
+ const offset = i * 9;
7706
+ placeholders.push(
7707
+ `($${offset + 1}, $${offset + 2}, $${offset + 3}, $${offset + 4}, $${offset + 5}, $${offset + 6}, $${offset + 7}, $${offset + 8}, $${offset + 9})`
7708
+ );
7709
+ values.push(
7710
+ e.sequence.toString(),
7711
+ e.type,
7712
+ e.mapName,
7713
+ e.key,
7714
+ e.value !== void 0 ? JSON.stringify(e.value) : null,
7715
+ e.previousValue !== void 0 ? JSON.stringify(e.previousValue) : null,
7716
+ JSON.stringify(e.timestamp),
7717
+ e.nodeId,
7718
+ e.metadata ? JSON.stringify(e.metadata) : null
7719
+ );
7720
+ });
7721
+ await this.pool.query(
7722
+ `INSERT INTO ${this.tableName}
7723
+ (sequence, type, map_name, key, value, previous_value, timestamp, node_id, metadata)
7724
+ VALUES ${placeholders.join(", ")}
7725
+ ON CONFLICT (sequence) DO NOTHING`,
7726
+ values
7727
+ );
7728
+ logger.debug({ count: batch.length }, "Persisted journal events");
7729
+ } catch (error) {
7730
+ this.pendingPersist.unshift(...batch);
7731
+ throw error;
7732
+ } finally {
7733
+ this.isPersisting = false;
7734
+ }
7735
+ }
7736
+ /**
7737
+ * Load journal events from PostgreSQL on startup.
7738
+ */
7739
+ async loadFromStorage() {
7740
+ const config = this.getConfig();
7741
+ const result = await this.pool.query(
7742
+ `SELECT sequence, type, map_name, key, value, previous_value, timestamp, node_id, metadata
7743
+ FROM ${this.tableName}
7744
+ ORDER BY sequence DESC
7745
+ LIMIT $1`,
7746
+ [config.capacity]
7747
+ );
7748
+ const events = result.rows.reverse();
7749
+ this.isLoadingFromStorage = true;
7750
+ try {
7751
+ for (const row of events) {
7752
+ this.append({
7753
+ type: row.type,
7754
+ mapName: row.map_name,
7755
+ key: row.key,
7756
+ value: row.value,
7757
+ previousValue: row.previous_value,
7758
+ timestamp: typeof row.timestamp === "string" ? JSON.parse(row.timestamp) : row.timestamp,
7759
+ nodeId: row.node_id,
7760
+ metadata: row.metadata
7761
+ });
7762
+ }
7763
+ } finally {
7764
+ this.isLoadingFromStorage = false;
7765
+ }
7766
+ logger.info({ count: events.length }, "Loaded journal events from storage");
7767
+ }
7768
+ /**
7769
+ * Export events as NDJSON stream.
7770
+ */
7771
+ exportStream(options = {}) {
7772
+ const self = this;
7773
+ return new ReadableStream({
7774
+ start(controller) {
7775
+ const startSeq = options.fromSequence ?? self.getOldestSequence();
7776
+ const endSeq = options.toSequence ?? self.getLatestSequence();
7777
+ for (let seq = startSeq; seq <= endSeq; seq++) {
7778
+ const events = self.readFrom(seq, 1);
7779
+ if (events.length > 0) {
7780
+ const event = events[0];
7781
+ if (options.mapName && event.mapName !== options.mapName) continue;
7782
+ if (options.types && !options.types.includes(event.type)) continue;
7783
+ const serializable = {
7784
+ ...event,
7785
+ sequence: event.sequence.toString()
7786
+ };
7787
+ controller.enqueue(JSON.stringify(serializable) + "\n");
7788
+ }
7789
+ }
7790
+ controller.close();
7791
+ }
7792
+ });
7793
+ }
7794
+ /**
7795
+ * Get events for a specific map.
7796
+ */
7797
+ getMapEvents(mapName, fromSeq) {
7798
+ const events = this.readFrom(fromSeq ?? this.getOldestSequence(), this.getConfig().capacity);
7799
+ return events.filter((e) => e.mapName === mapName);
7800
+ }
7801
+ /**
7802
+ * Query events from PostgreSQL with filters.
7803
+ */
7804
+ async queryFromStorage(options = {}) {
7805
+ const conditions = [];
7806
+ const params = [];
7807
+ let paramIndex = 1;
7808
+ if (options.mapName) {
7809
+ conditions.push(`map_name = $${paramIndex++}`);
7810
+ params.push(options.mapName);
7811
+ }
7812
+ if (options.key) {
7813
+ conditions.push(`key = $${paramIndex++}`);
7814
+ params.push(options.key);
7815
+ }
7816
+ if (options.types && options.types.length > 0) {
7817
+ conditions.push(`type = ANY($${paramIndex++})`);
7818
+ params.push(options.types);
7819
+ }
7820
+ if (options.fromSequence !== void 0) {
7821
+ conditions.push(`sequence >= $${paramIndex++}`);
7822
+ params.push(options.fromSequence.toString());
7823
+ }
7824
+ if (options.toSequence !== void 0) {
7825
+ conditions.push(`sequence <= $${paramIndex++}`);
7826
+ params.push(options.toSequence.toString());
7827
+ }
7828
+ if (options.fromDate) {
7829
+ conditions.push(`created_at >= $${paramIndex++}`);
7830
+ params.push(options.fromDate);
7831
+ }
7832
+ if (options.toDate) {
7833
+ conditions.push(`created_at <= $${paramIndex++}`);
7834
+ params.push(options.toDate);
7835
+ }
7836
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
7837
+ const limit = options.limit ?? 100;
7838
+ const offset = options.offset ?? 0;
7839
+ const result = await this.pool.query(
7840
+ `SELECT sequence, type, map_name, key, value, previous_value, timestamp, node_id, metadata
7841
+ FROM ${this.tableName}
7842
+ ${whereClause}
7843
+ ORDER BY sequence ASC
7844
+ LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
7845
+ [...params, limit, offset]
7846
+ );
7847
+ return result.rows.map((row) => ({
7848
+ sequence: BigInt(row.sequence),
7849
+ type: row.type,
7850
+ mapName: row.map_name,
7851
+ key: row.key,
7852
+ value: row.value,
7853
+ previousValue: row.previous_value,
7854
+ timestamp: typeof row.timestamp === "string" ? JSON.parse(row.timestamp) : row.timestamp,
7855
+ nodeId: row.node_id,
7856
+ metadata: row.metadata
7857
+ }));
7858
+ }
7859
+ /**
7860
+ * Count events matching filters.
7861
+ */
7862
+ async countFromStorage(options = {}) {
7863
+ const conditions = [];
7864
+ const params = [];
7865
+ let paramIndex = 1;
7866
+ if (options.mapName) {
7867
+ conditions.push(`map_name = $${paramIndex++}`);
7868
+ params.push(options.mapName);
7869
+ }
7870
+ if (options.types && options.types.length > 0) {
7871
+ conditions.push(`type = ANY($${paramIndex++})`);
7872
+ params.push(options.types);
7873
+ }
7874
+ if (options.fromDate) {
7875
+ conditions.push(`created_at >= $${paramIndex++}`);
7876
+ params.push(options.fromDate);
7877
+ }
7878
+ if (options.toDate) {
7879
+ conditions.push(`created_at <= $${paramIndex++}`);
7880
+ params.push(options.toDate);
7881
+ }
7882
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
7883
+ const result = await this.pool.query(
7884
+ `SELECT COUNT(*) as count FROM ${this.tableName} ${whereClause}`,
7885
+ params
7886
+ );
7887
+ return parseInt(result.rows[0].count, 10);
7888
+ }
7889
+ /**
7890
+ * Cleanup old events based on retention policy.
7891
+ */
7892
+ async cleanupOldEvents(retentionDays) {
7893
+ const result = await this.pool.query(
7894
+ `DELETE FROM ${this.tableName}
7895
+ WHERE created_at < NOW() - ($1 || ' days')::INTERVAL
7896
+ RETURNING sequence`,
7897
+ [retentionDays]
7898
+ );
7899
+ const count = result.rowCount ?? 0;
7900
+ if (count > 0) {
7901
+ logger.info({ deletedCount: count, retentionDays }, "Cleaned up old journal events");
7902
+ }
7903
+ return count;
7904
+ }
7905
+ /**
7906
+ * Start the periodic persistence timer.
7907
+ */
7908
+ startPersistTimer() {
7909
+ this.persistTimer = setInterval(() => {
7910
+ if (this.pendingPersist.length > 0) {
7911
+ this.persistToStorage().catch((err) => {
7912
+ logger.error({ err }, "Periodic persist failed");
7913
+ });
7914
+ }
7915
+ }, this.persistIntervalMs);
7916
+ }
7917
+ /**
7918
+ * Stop the periodic persistence timer.
7919
+ */
7920
+ stopPersistTimer() {
7921
+ if (this.persistTimer) {
7922
+ clearInterval(this.persistTimer);
7923
+ this.persistTimer = void 0;
7924
+ }
7925
+ }
7926
+ /**
7927
+ * Dispose resources and persist remaining events.
7928
+ */
7929
+ dispose() {
7930
+ this.stopPersistTimer();
7931
+ if (this.pendingPersist.length > 0) {
7932
+ this.persistToStorage().catch((err) => {
7933
+ logger.error({ err }, "Final persist failed on dispose");
7934
+ });
7935
+ }
7936
+ super.dispose();
7937
+ }
7938
+ /**
7939
+ * Get pending persist count (for monitoring).
7940
+ */
7941
+ getPendingPersistCount() {
7942
+ return this.pendingPersist.length;
7943
+ }
7944
+ };
7945
+
7946
+ // src/ServerCoordinator.ts
7947
+ var GC_INTERVAL_MS = 60 * 60 * 1e3;
7948
+ var GC_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
7949
+ var CLIENT_HEARTBEAT_TIMEOUT_MS = 2e4;
7950
+ var CLIENT_HEARTBEAT_CHECK_INTERVAL_MS = 5e3;
7951
+ var ServerCoordinator = class {
7952
+ constructor(config) {
7953
+ this.clients = /* @__PURE__ */ new Map();
7954
+ // Interceptors
7955
+ this.interceptors = [];
7956
+ // In-memory storage (partitioned later)
7957
+ this.maps = /* @__PURE__ */ new Map();
7958
+ this.pendingClusterQueries = /* @__PURE__ */ new Map();
7959
+ // GC Consensus State
7960
+ this.gcReports = /* @__PURE__ */ new Map();
7961
+ // Track map loading state to avoid returning empty results during async load
7962
+ this.mapLoadingPromises = /* @__PURE__ */ new Map();
7963
+ // Track pending batch operations for testing purposes
7964
+ this.pendingBatchOperations = /* @__PURE__ */ new Set();
7965
+ this.journalSubscriptions = /* @__PURE__ */ new Map();
7966
+ this._actualPort = 0;
7967
+ this._actualClusterPort = 0;
7968
+ this._readyPromise = new Promise((resolve) => {
7969
+ this._readyResolve = resolve;
7970
+ });
7971
+ this._nodeId = config.nodeId;
7972
+ this.hlc = new HLC2(config.nodeId);
7973
+ this.storage = config.storage;
7974
+ const rawSecret = config.jwtSecret || process.env.JWT_SECRET || "topgun-secret-dev";
7975
+ this.jwtSecret = rawSecret.replace(/\\n/g, "\n");
7976
+ this.queryRegistry = new QueryRegistry();
7977
+ this.securityManager = new SecurityManager(config.securityPolicies || []);
7978
+ this.interceptors = config.interceptors || [];
7979
+ this.metricsService = new MetricsService();
7980
+ this.eventExecutor = new StripedEventExecutor({
7981
+ stripeCount: config.eventStripeCount ?? 4,
7982
+ queueCapacity: config.eventQueueCapacity ?? 1e4,
7983
+ name: `${config.nodeId}-event-executor`,
7984
+ onReject: (task) => {
7985
+ logger.warn({ nodeId: config.nodeId, key: task.key }, "Event task rejected due to queue capacity");
7986
+ this.metricsService.incEventQueueRejected();
7987
+ }
7988
+ });
7989
+ this.backpressure = new BackpressureRegulator({
7990
+ syncFrequency: config.backpressureSyncFrequency ?? 100,
7991
+ maxPendingOps: config.backpressureMaxPending ?? 1e3,
7992
+ backoffTimeoutMs: config.backpressureBackoffMs ?? 5e3,
7993
+ enabled: config.backpressureEnabled ?? true
7994
+ });
7995
+ this.writeCoalescingEnabled = config.writeCoalescingEnabled ?? true;
7996
+ const preset = coalescingPresets[config.writeCoalescingPreset ?? "highThroughput"];
7997
+ this.writeCoalescingOptions = {
7998
+ maxBatchSize: config.writeCoalescingMaxBatch ?? preset.maxBatchSize,
7999
+ maxDelayMs: config.writeCoalescingMaxDelayMs ?? preset.maxDelayMs,
8000
+ maxBatchBytes: config.writeCoalescingMaxBytes ?? preset.maxBatchBytes
8001
+ };
8002
+ this.eventPayloadPool = createEventPayloadPool({
8003
+ maxSize: 4096,
8004
+ initialSize: 128
8005
+ });
8006
+ this.taskletScheduler = new TaskletScheduler({
8007
+ defaultTimeBudgetMs: 5,
8008
+ maxConcurrent: 20
8009
+ });
8010
+ this.writeAckManager = new WriteAckManager({
8011
+ defaultTimeout: config.writeAckTimeout ?? 5e3
8012
+ });
8013
+ this.rateLimitingEnabled = config.rateLimitingEnabled ?? true;
8014
+ this.rateLimiter = new ConnectionRateLimiter({
8015
+ maxConnectionsPerSecond: config.maxConnectionsPerSecond ?? 100,
8016
+ maxPendingConnections: config.maxPendingConnections ?? 1e3,
8017
+ cooldownMs: 1e3
8018
+ });
8019
+ if (config.workerPoolEnabled) {
8020
+ this.workerPool = new WorkerPool({
8021
+ minWorkers: config.workerPoolConfig?.minWorkers ?? 2,
8022
+ maxWorkers: config.workerPoolConfig?.maxWorkers,
8023
+ taskTimeout: config.workerPoolConfig?.taskTimeout ?? 5e3,
8024
+ idleTimeout: config.workerPoolConfig?.idleTimeout ?? 3e4,
8025
+ autoRestart: config.workerPoolConfig?.autoRestart ?? true
8026
+ });
8027
+ this.merkleWorker = new MerkleWorker(this.workerPool);
8028
+ this.crdtMergeWorker = new CRDTMergeWorker(this.workerPool);
8029
+ this.serializationWorker = new SerializationWorker(this.workerPool);
8030
+ logger.info({
8031
+ minWorkers: config.workerPoolConfig?.minWorkers ?? 2,
8032
+ maxWorkers: config.workerPoolConfig?.maxWorkers ?? "auto"
8033
+ }, "Worker pool initialized for CPU-bound operations");
8034
+ }
8035
+ if (config.tls?.enabled) {
8036
+ const tlsOptions = this.buildTLSOptions(config.tls);
8037
+ this.httpServer = createHttpsServer(tlsOptions, (_req, res) => {
8038
+ res.writeHead(200);
8039
+ res.end("TopGun Server Running (Secure)");
8040
+ });
8041
+ logger.info("TLS enabled for client connections");
8042
+ } else {
8043
+ this.httpServer = createHttpServer((_req, res) => {
8044
+ res.writeHead(200);
8045
+ res.end("TopGun Server Running");
8046
+ });
8047
+ if (process.env.NODE_ENV === "production") {
8048
+ logger.warn("\u26A0\uFE0F TLS is disabled! Client connections are NOT encrypted.");
8049
+ }
8050
+ }
8051
+ const metricsPort = config.metricsPort !== void 0 ? config.metricsPort : 9090;
8052
+ this.metricsServer = createHttpServer(async (req, res) => {
8053
+ if (req.url === "/metrics") {
8054
+ try {
8055
+ res.setHeader("Content-Type", this.metricsService.getContentType());
8056
+ res.end(await this.metricsService.getMetrics());
8057
+ } catch (err) {
8058
+ res.statusCode = 500;
8059
+ res.end("Internal Server Error");
6527
8060
  }
6528
8061
  } else {
6529
8062
  res.statusCode = 404;
@@ -6600,6 +8133,27 @@ var ServerCoordinator = class {
6600
8133
  }
6601
8134
  }
6602
8135
  });
8136
+ this.counterHandler = new CounterHandler(this._nodeId);
8137
+ this.entryProcessorHandler = new EntryProcessorHandler({ hlc: this.hlc });
8138
+ this.conflictResolverHandler = new ConflictResolverHandler({ nodeId: this._nodeId });
8139
+ this.conflictResolverHandler.onRejection((rejection) => {
8140
+ this.notifyMergeRejection(rejection);
8141
+ });
8142
+ if (config.eventJournalEnabled && this.storage && "pool" in this.storage) {
8143
+ const pool = this.storage.pool;
8144
+ this.eventJournalService = new EventJournalService({
8145
+ capacity: 1e4,
8146
+ ttlMs: 0,
8147
+ persistent: true,
8148
+ pool,
8149
+ ...config.eventJournalConfig
8150
+ });
8151
+ this.eventJournalService.initialize().then(() => {
8152
+ logger.info("EventJournalService initialized");
8153
+ }).catch((err) => {
8154
+ logger.error({ err }, "Failed to initialize EventJournalService");
8155
+ });
8156
+ }
6603
8157
  this.systemManager = new SystemManager(
6604
8158
  this.cluster,
6605
8159
  this.metricsService,
@@ -6759,6 +8313,10 @@ var ServerCoordinator = class {
6759
8313
  this.eventPayloadPool.clear();
6760
8314
  this.taskletScheduler.shutdown();
6761
8315
  this.writeAckManager.shutdown();
8316
+ this.entryProcessorHandler.dispose();
8317
+ if (this.eventJournalService) {
8318
+ this.eventJournalService.dispose();
8319
+ }
6762
8320
  logger.info("Server Coordinator shutdown complete.");
6763
8321
  }
6764
8322
  async handleConnection(ws) {
@@ -6865,6 +8423,7 @@ var ServerCoordinator = class {
6865
8423
  }
6866
8424
  this.lockManager.handleClientDisconnect(clientId);
6867
8425
  this.topicManager.unsubscribeAll(clientId);
8426
+ this.counterHandler.unsubscribeAll(clientId);
6868
8427
  const members = this.cluster.getMembers();
6869
8428
  for (const memberId of members) {
6870
8429
  if (!this.cluster.isLocal(memberId)) {
@@ -7120,7 +8679,7 @@ var ServerCoordinator = class {
7120
8679
  this.metricsService.incOp("GET", message.mapName);
7121
8680
  try {
7122
8681
  const mapForSync = await this.getMapAsync(message.mapName);
7123
- if (mapForSync instanceof LWWMap2) {
8682
+ if (mapForSync instanceof LWWMap3) {
7124
8683
  const tree = mapForSync.getMerkleTree();
7125
8684
  const rootHash = tree.getRootHash();
7126
8685
  client.writer.write({
@@ -7158,7 +8717,7 @@ var ServerCoordinator = class {
7158
8717
  const { mapName, path } = message.payload;
7159
8718
  try {
7160
8719
  const mapForBucket = await this.getMapAsync(mapName);
7161
- if (mapForBucket instanceof LWWMap2) {
8720
+ if (mapForBucket instanceof LWWMap3) {
7162
8721
  const treeForBucket = mapForBucket.getMerkleTree();
7163
8722
  const buckets = treeForBucket.getBuckets(path);
7164
8723
  const node = treeForBucket.getNode(path);
@@ -7287,6 +8846,219 @@ var ServerCoordinator = class {
7287
8846
  }
7288
8847
  break;
7289
8848
  }
8849
+ // ============ Phase 5.2: PN Counter Handlers ============
8850
+ case "COUNTER_REQUEST": {
8851
+ const { name } = message.payload;
8852
+ const response = this.counterHandler.handleCounterRequest(client.id, name);
8853
+ client.writer.write(response);
8854
+ logger.debug({ clientId: client.id, name }, "Counter request handled");
8855
+ break;
8856
+ }
8857
+ case "COUNTER_SYNC": {
8858
+ const { name, state } = message.payload;
8859
+ const result = this.counterHandler.handleCounterSync(client.id, name, state);
8860
+ client.writer.write(result.response);
8861
+ for (const targetClientId of result.broadcastTo) {
8862
+ const targetClient = this.clients.get(targetClientId);
8863
+ if (targetClient && targetClient.socket.readyState === WebSocket3.OPEN) {
8864
+ targetClient.writer.write(result.broadcastMessage);
8865
+ }
8866
+ }
8867
+ logger.debug({ clientId: client.id, name, broadcastCount: result.broadcastTo.length }, "Counter sync handled");
8868
+ break;
8869
+ }
8870
+ // ============ Phase 5.03: Entry Processor Handlers ============
8871
+ case "ENTRY_PROCESS": {
8872
+ const { requestId, mapName, key, processor } = message;
8873
+ if (!this.securityManager.checkPermission(client.principal, mapName, "PUT")) {
8874
+ client.writer.write({
8875
+ type: "ENTRY_PROCESS_RESPONSE",
8876
+ requestId,
8877
+ success: false,
8878
+ error: `Access Denied for map ${mapName}`
8879
+ }, true);
8880
+ break;
8881
+ }
8882
+ const entryMap = this.getMap(mapName);
8883
+ const { result, timestamp } = await this.entryProcessorHandler.executeOnKey(
8884
+ entryMap,
8885
+ key,
8886
+ processor
8887
+ );
8888
+ client.writer.write({
8889
+ type: "ENTRY_PROCESS_RESPONSE",
8890
+ requestId,
8891
+ success: result.success,
8892
+ result: result.result,
8893
+ newValue: result.newValue,
8894
+ error: result.error
8895
+ });
8896
+ if (result.success && timestamp) {
8897
+ const record = entryMap.getRecord(key);
8898
+ if (record) {
8899
+ this.queryRegistry.processChange(mapName, entryMap, key, record, void 0);
8900
+ }
8901
+ }
8902
+ logger.debug({
8903
+ clientId: client.id,
8904
+ mapName,
8905
+ key,
8906
+ processor: processor.name,
8907
+ success: result.success
8908
+ }, "Entry processor executed");
8909
+ break;
8910
+ }
8911
+ case "ENTRY_PROCESS_BATCH": {
8912
+ const { requestId, mapName, keys, processor } = message;
8913
+ if (!this.securityManager.checkPermission(client.principal, mapName, "PUT")) {
8914
+ const errorResults = {};
8915
+ for (const key of keys) {
8916
+ errorResults[key] = {
8917
+ success: false,
8918
+ error: `Access Denied for map ${mapName}`
8919
+ };
8920
+ }
8921
+ client.writer.write({
8922
+ type: "ENTRY_PROCESS_BATCH_RESPONSE",
8923
+ requestId,
8924
+ results: errorResults
8925
+ }, true);
8926
+ break;
8927
+ }
8928
+ const batchMap = this.getMap(mapName);
8929
+ const { results, timestamps } = await this.entryProcessorHandler.executeOnKeys(
8930
+ batchMap,
8931
+ keys,
8932
+ processor
8933
+ );
8934
+ const resultsRecord = {};
8935
+ for (const [key, keyResult] of results) {
8936
+ resultsRecord[key] = {
8937
+ success: keyResult.success,
8938
+ result: keyResult.result,
8939
+ newValue: keyResult.newValue,
8940
+ error: keyResult.error
8941
+ };
8942
+ }
8943
+ client.writer.write({
8944
+ type: "ENTRY_PROCESS_BATCH_RESPONSE",
8945
+ requestId,
8946
+ results: resultsRecord
8947
+ });
8948
+ for (const [key] of timestamps) {
8949
+ const record = batchMap.getRecord(key);
8950
+ if (record) {
8951
+ this.queryRegistry.processChange(mapName, batchMap, key, record, void 0);
8952
+ }
8953
+ }
8954
+ logger.debug({
8955
+ clientId: client.id,
8956
+ mapName,
8957
+ keyCount: keys.length,
8958
+ processor: processor.name,
8959
+ successCount: Array.from(results.values()).filter((r) => r.success).length
8960
+ }, "Entry processor batch executed");
8961
+ break;
8962
+ }
8963
+ // ============ Phase 5.05: Conflict Resolver Handlers ============
8964
+ case "REGISTER_RESOLVER": {
8965
+ const { requestId, mapName, resolver } = message;
8966
+ if (!this.securityManager.checkPermission(client.principal, mapName, "PUT")) {
8967
+ client.writer.write({
8968
+ type: "REGISTER_RESOLVER_RESPONSE",
8969
+ requestId,
8970
+ success: false,
8971
+ error: `Access Denied for map ${mapName}`
8972
+ }, true);
8973
+ break;
8974
+ }
8975
+ try {
8976
+ this.conflictResolverHandler.registerResolver(
8977
+ mapName,
8978
+ {
8979
+ name: resolver.name,
8980
+ code: resolver.code,
8981
+ priority: resolver.priority,
8982
+ keyPattern: resolver.keyPattern
8983
+ },
8984
+ client.id
8985
+ );
8986
+ client.writer.write({
8987
+ type: "REGISTER_RESOLVER_RESPONSE",
8988
+ requestId,
8989
+ success: true
8990
+ });
8991
+ logger.info({
8992
+ clientId: client.id,
8993
+ mapName,
8994
+ resolverName: resolver.name,
8995
+ priority: resolver.priority
8996
+ }, "Conflict resolver registered");
8997
+ } catch (err) {
8998
+ const errorMessage = err instanceof Error ? err.message : String(err);
8999
+ client.writer.write({
9000
+ type: "REGISTER_RESOLVER_RESPONSE",
9001
+ requestId,
9002
+ success: false,
9003
+ error: errorMessage
9004
+ }, true);
9005
+ logger.warn({
9006
+ clientId: client.id,
9007
+ mapName,
9008
+ error: errorMessage
9009
+ }, "Failed to register conflict resolver");
9010
+ }
9011
+ break;
9012
+ }
9013
+ case "UNREGISTER_RESOLVER": {
9014
+ const { requestId, mapName, resolverName } = message;
9015
+ if (!this.securityManager.checkPermission(client.principal, mapName, "PUT")) {
9016
+ client.writer.write({
9017
+ type: "UNREGISTER_RESOLVER_RESPONSE",
9018
+ requestId,
9019
+ success: false,
9020
+ error: `Access Denied for map ${mapName}`
9021
+ }, true);
9022
+ break;
9023
+ }
9024
+ const removed = this.conflictResolverHandler.unregisterResolver(
9025
+ mapName,
9026
+ resolverName,
9027
+ client.id
9028
+ );
9029
+ client.writer.write({
9030
+ type: "UNREGISTER_RESOLVER_RESPONSE",
9031
+ requestId,
9032
+ success: removed,
9033
+ error: removed ? void 0 : "Resolver not found or not owned by this client"
9034
+ });
9035
+ if (removed) {
9036
+ logger.info({
9037
+ clientId: client.id,
9038
+ mapName,
9039
+ resolverName
9040
+ }, "Conflict resolver unregistered");
9041
+ }
9042
+ break;
9043
+ }
9044
+ case "LIST_RESOLVERS": {
9045
+ const { requestId, mapName } = message;
9046
+ if (mapName && !this.securityManager.checkPermission(client.principal, mapName, "READ")) {
9047
+ client.writer.write({
9048
+ type: "LIST_RESOLVERS_RESPONSE",
9049
+ requestId,
9050
+ resolvers: []
9051
+ });
9052
+ break;
9053
+ }
9054
+ const resolvers = this.conflictResolverHandler.listResolvers(mapName);
9055
+ client.writer.write({
9056
+ type: "LIST_RESOLVERS_RESPONSE",
9057
+ requestId,
9058
+ resolvers
9059
+ });
9060
+ break;
9061
+ }
7290
9062
  // ============ Phase 4: Partition Map Request Handler ============
7291
9063
  case "PARTITION_MAP_REQUEST": {
7292
9064
  const clientVersion = message.payload?.currentVersion ?? 0;
@@ -7482,6 +9254,92 @@ var ServerCoordinator = class {
7482
9254
  }
7483
9255
  break;
7484
9256
  }
9257
+ // === Event Journal Messages (Phase 5.04) ===
9258
+ case "JOURNAL_SUBSCRIBE": {
9259
+ if (!this.eventJournalService) {
9260
+ client.writer.write({
9261
+ type: "ERROR",
9262
+ payload: { code: 503, message: "Event journal not enabled" }
9263
+ }, true);
9264
+ break;
9265
+ }
9266
+ const { requestId, fromSequence, mapName, types } = message;
9267
+ const subscriptionId = requestId;
9268
+ this.journalSubscriptions.set(subscriptionId, {
9269
+ clientId: client.id,
9270
+ mapName,
9271
+ types
9272
+ });
9273
+ const unsubscribe = this.eventJournalService.subscribe(
9274
+ (event) => {
9275
+ if (mapName && event.mapName !== mapName) return;
9276
+ if (types && types.length > 0 && !types.includes(event.type)) return;
9277
+ const clientConn = this.clients.get(client.id);
9278
+ if (!clientConn) {
9279
+ unsubscribe();
9280
+ this.journalSubscriptions.delete(subscriptionId);
9281
+ return;
9282
+ }
9283
+ clientConn.writer.write({
9284
+ type: "JOURNAL_EVENT",
9285
+ event: {
9286
+ sequence: event.sequence.toString(),
9287
+ type: event.type,
9288
+ mapName: event.mapName,
9289
+ key: event.key,
9290
+ value: event.value,
9291
+ previousValue: event.previousValue,
9292
+ timestamp: event.timestamp,
9293
+ nodeId: event.nodeId,
9294
+ metadata: event.metadata
9295
+ }
9296
+ });
9297
+ },
9298
+ fromSequence ? BigInt(fromSequence) : void 0
9299
+ );
9300
+ logger.info({ clientId: client.id, subscriptionId, mapName }, "Journal subscription created");
9301
+ break;
9302
+ }
9303
+ case "JOURNAL_UNSUBSCRIBE": {
9304
+ const { subscriptionId } = message;
9305
+ this.journalSubscriptions.delete(subscriptionId);
9306
+ logger.info({ clientId: client.id, subscriptionId }, "Journal subscription removed");
9307
+ break;
9308
+ }
9309
+ case "JOURNAL_READ": {
9310
+ if (!this.eventJournalService) {
9311
+ client.writer.write({
9312
+ type: "ERROR",
9313
+ payload: { code: 503, message: "Event journal not enabled" }
9314
+ }, true);
9315
+ break;
9316
+ }
9317
+ const { requestId: readReqId, fromSequence: readFromSeq, limit, mapName: readMapName } = message;
9318
+ const startSeq = BigInt(readFromSeq);
9319
+ const eventLimit = limit ?? 100;
9320
+ let events = this.eventJournalService.readFrom(startSeq, eventLimit);
9321
+ if (readMapName) {
9322
+ events = events.filter((e) => e.mapName === readMapName);
9323
+ }
9324
+ const serializedEvents = events.map((e) => ({
9325
+ sequence: e.sequence.toString(),
9326
+ type: e.type,
9327
+ mapName: e.mapName,
9328
+ key: e.key,
9329
+ value: e.value,
9330
+ previousValue: e.previousValue,
9331
+ timestamp: e.timestamp,
9332
+ nodeId: e.nodeId,
9333
+ metadata: e.metadata
9334
+ }));
9335
+ client.writer.write({
9336
+ type: "JOURNAL_READ_RESPONSE",
9337
+ requestId: readReqId,
9338
+ events: serializedEvents,
9339
+ hasMore: events.length === eventLimit
9340
+ });
9341
+ break;
9342
+ }
7485
9343
  default:
7486
9344
  logger.warn({ type: message.type }, "Unknown message type");
7487
9345
  }
@@ -7495,7 +9353,7 @@ var ServerCoordinator = class {
7495
9353
  } else if (op.orRecord && op.orRecord.timestamp) {
7496
9354
  } else if (op.orTag) {
7497
9355
  try {
7498
- ts = HLC.parse(op.orTag);
9356
+ ts = HLC2.parse(op.orTag);
7499
9357
  } catch (e) {
7500
9358
  }
7501
9359
  }
@@ -7529,6 +9387,39 @@ var ServerCoordinator = class {
7529
9387
  clientCount: broadcastCount
7530
9388
  }, "Broadcast partition map to clients");
7531
9389
  }
9390
+ /**
9391
+ * Notify a client about a merge rejection (Phase 5.05).
9392
+ * Finds the client by node ID and sends MERGE_REJECTED message.
9393
+ */
9394
+ notifyMergeRejection(rejection) {
9395
+ for (const [clientId, client] of this.clients) {
9396
+ if (clientId === rejection.nodeId || rejection.nodeId.includes(clientId)) {
9397
+ client.writer.write({
9398
+ type: "MERGE_REJECTED",
9399
+ mapName: rejection.mapName,
9400
+ key: rejection.key,
9401
+ attemptedValue: rejection.attemptedValue,
9402
+ reason: rejection.reason,
9403
+ timestamp: rejection.timestamp
9404
+ }, true);
9405
+ return;
9406
+ }
9407
+ }
9408
+ const subscribedClientIds = this.queryRegistry.getSubscribedClientIds(rejection.mapName);
9409
+ for (const clientId of subscribedClientIds) {
9410
+ const client = this.clients.get(clientId);
9411
+ if (client) {
9412
+ client.writer.write({
9413
+ type: "MERGE_REJECTED",
9414
+ mapName: rejection.mapName,
9415
+ key: rejection.key,
9416
+ attemptedValue: rejection.attemptedValue,
9417
+ reason: rejection.reason,
9418
+ timestamp: rejection.timestamp
9419
+ });
9420
+ }
9421
+ }
9422
+ }
7532
9423
  broadcast(message, excludeClientId) {
7533
9424
  const isServerEvent = message.type === "SERVER_EVENT";
7534
9425
  if (isServerEvent) {
@@ -7865,8 +9756,28 @@ var ServerCoordinator = class {
7865
9756
  }
7866
9757
  async executeLocalQuery(mapName, query) {
7867
9758
  const map = await this.getMapAsync(mapName);
9759
+ const localQuery = { ...query };
9760
+ delete localQuery.offset;
9761
+ delete localQuery.limit;
9762
+ if (map instanceof IndexedLWWMap2) {
9763
+ const coreQuery = this.convertToCoreQuery(localQuery);
9764
+ if (coreQuery) {
9765
+ const entries = map.queryEntries(coreQuery);
9766
+ return entries.map(([key, value]) => {
9767
+ const record = map.getRecord(key);
9768
+ return { key, value, timestamp: record?.timestamp };
9769
+ });
9770
+ }
9771
+ }
9772
+ if (map instanceof IndexedORMap2) {
9773
+ const coreQuery = this.convertToCoreQuery(localQuery);
9774
+ if (coreQuery) {
9775
+ const results = map.query(coreQuery);
9776
+ return results.map(({ key, value }) => ({ key, value }));
9777
+ }
9778
+ }
7868
9779
  const records = /* @__PURE__ */ new Map();
7869
- if (map instanceof LWWMap2) {
9780
+ if (map instanceof LWWMap3) {
7870
9781
  for (const key of map.allKeys()) {
7871
9782
  const rec = map.getRecord(key);
7872
9783
  if (rec && rec.value !== null) {
@@ -7882,11 +9793,89 @@ var ServerCoordinator = class {
7882
9793
  }
7883
9794
  }
7884
9795
  }
7885
- const localQuery = { ...query };
7886
- delete localQuery.offset;
7887
- delete localQuery.limit;
7888
9796
  return executeQuery(records, localQuery);
7889
9797
  }
9798
+ /**
9799
+ * Convert server Query format to core Query format for indexed execution.
9800
+ * Returns null if conversion is not possible (complex queries).
9801
+ */
9802
+ convertToCoreQuery(query) {
9803
+ if (query.predicate) {
9804
+ return this.predicateToCoreQuery(query.predicate);
9805
+ }
9806
+ if (query.where) {
9807
+ const conditions = [];
9808
+ for (const [attribute, condition] of Object.entries(query.where)) {
9809
+ if (typeof condition !== "object" || condition === null) {
9810
+ conditions.push({ type: "eq", attribute, value: condition });
9811
+ } else {
9812
+ for (const [op, value] of Object.entries(condition)) {
9813
+ const coreOp = this.convertOperator(op);
9814
+ if (coreOp) {
9815
+ conditions.push({ type: coreOp, attribute, value });
9816
+ }
9817
+ }
9818
+ }
9819
+ }
9820
+ if (conditions.length === 0) return null;
9821
+ if (conditions.length === 1) return conditions[0];
9822
+ return { type: "and", children: conditions };
9823
+ }
9824
+ return null;
9825
+ }
9826
+ /**
9827
+ * Convert predicate node to core Query format.
9828
+ */
9829
+ predicateToCoreQuery(predicate) {
9830
+ if (!predicate || !predicate.op) return null;
9831
+ switch (predicate.op) {
9832
+ case "eq":
9833
+ case "neq":
9834
+ case "gt":
9835
+ case "gte":
9836
+ case "lt":
9837
+ case "lte":
9838
+ return {
9839
+ type: predicate.op,
9840
+ attribute: predicate.attribute,
9841
+ value: predicate.value
9842
+ };
9843
+ case "and":
9844
+ case "or":
9845
+ if (predicate.children && Array.isArray(predicate.children)) {
9846
+ const children = predicate.children.map((c) => this.predicateToCoreQuery(c)).filter((c) => c !== null);
9847
+ if (children.length === 0) return null;
9848
+ if (children.length === 1) return children[0];
9849
+ return { type: predicate.op, children };
9850
+ }
9851
+ return null;
9852
+ case "not":
9853
+ if (predicate.children && predicate.children[0]) {
9854
+ const child = this.predicateToCoreQuery(predicate.children[0]);
9855
+ if (child) {
9856
+ return { type: "not", child };
9857
+ }
9858
+ }
9859
+ return null;
9860
+ default:
9861
+ return null;
9862
+ }
9863
+ }
9864
+ /**
9865
+ * Convert server operator to core query type.
9866
+ */
9867
+ convertOperator(op) {
9868
+ const mapping = {
9869
+ "$eq": "eq",
9870
+ "$ne": "neq",
9871
+ "$neq": "neq",
9872
+ "$gt": "gt",
9873
+ "$gte": "gte",
9874
+ "$lt": "lt",
9875
+ "$lte": "lte"
9876
+ };
9877
+ return mapping[op] || null;
9878
+ }
7890
9879
  finalizeClusterQuery(requestId, timeout = false) {
7891
9880
  const pending = this.pendingClusterQueries.get(requestId);
7892
9881
  if (!pending) return;
@@ -7940,10 +9929,10 @@ var ServerCoordinator = class {
7940
9929
  *
7941
9930
  * @returns Event payload for broadcasting (or null if operation failed)
7942
9931
  */
7943
- applyOpToMap(op) {
9932
+ async applyOpToMap(op, remoteNodeId) {
7944
9933
  const typeHint = op.opType === "OR_ADD" || op.opType === "OR_REMOVE" ? "OR" : "LWW";
7945
9934
  const map = this.getMap(op.mapName, typeHint);
7946
- if (typeHint === "OR" && map instanceof LWWMap2) {
9935
+ if (typeHint === "OR" && map instanceof LWWMap3) {
7947
9936
  logger.error({ mapName: op.mapName }, "Map type mismatch: LWWMap but received OR op");
7948
9937
  throw new Error("Map type mismatch: LWWMap but received OR op");
7949
9938
  }
@@ -7958,12 +9947,34 @@ var ServerCoordinator = class {
7958
9947
  mapName: op.mapName,
7959
9948
  key: op.key
7960
9949
  };
7961
- if (map instanceof LWWMap2) {
9950
+ if (map instanceof LWWMap3) {
7962
9951
  oldRecord = map.getRecord(op.key);
7963
- map.merge(op.key, op.record);
7964
- recordToStore = op.record;
7965
- eventPayload.eventType = "UPDATED";
7966
- eventPayload.record = op.record;
9952
+ if (this.conflictResolverHandler.hasResolvers(op.mapName)) {
9953
+ const mergeResult = await this.conflictResolverHandler.mergeWithResolver(
9954
+ map,
9955
+ op.mapName,
9956
+ op.key,
9957
+ op.record,
9958
+ remoteNodeId || this._nodeId
9959
+ );
9960
+ if (!mergeResult.applied) {
9961
+ if (mergeResult.rejection) {
9962
+ logger.debug(
9963
+ { mapName: op.mapName, key: op.key, reason: mergeResult.rejection.reason },
9964
+ "Merge rejected by resolver"
9965
+ );
9966
+ }
9967
+ return { eventPayload: null, oldRecord, rejected: true };
9968
+ }
9969
+ recordToStore = mergeResult.record;
9970
+ eventPayload.eventType = "UPDATED";
9971
+ eventPayload.record = mergeResult.record;
9972
+ } else {
9973
+ map.merge(op.key, op.record);
9974
+ recordToStore = op.record;
9975
+ eventPayload.eventType = "UPDATED";
9976
+ eventPayload.record = op.record;
9977
+ }
7967
9978
  } else if (map instanceof ORMap2) {
7968
9979
  oldRecord = map.getRecords(op.key);
7969
9980
  if (op.opType === "OR_ADD") {
@@ -7994,6 +10005,21 @@ var ServerCoordinator = class {
7994
10005
  });
7995
10006
  }
7996
10007
  }
10008
+ if (this.eventJournalService) {
10009
+ const isDelete = op.opType === "REMOVE" || op.opType === "OR_REMOVE" || op.record && op.record.value === null;
10010
+ const isNew = !oldRecord || Array.isArray(oldRecord) && oldRecord.length === 0;
10011
+ const journalEventType = isDelete ? "DELETE" : isNew ? "PUT" : "UPDATE";
10012
+ const timestamp = op.record?.timestamp || op.orRecord?.timestamp || this.hlc.now();
10013
+ this.eventJournalService.append({
10014
+ type: journalEventType,
10015
+ mapName: op.mapName,
10016
+ key: op.key,
10017
+ value: op.record?.value ?? op.orRecord?.value,
10018
+ previousValue: oldRecord?.value ?? (Array.isArray(oldRecord) ? oldRecord[0]?.value : void 0),
10019
+ timestamp,
10020
+ nodeId: this._nodeId
10021
+ });
10022
+ }
7997
10023
  return { eventPayload, oldRecord };
7998
10024
  }
7999
10025
  /**
@@ -8015,7 +10041,10 @@ var ServerCoordinator = class {
8015
10041
  try {
8016
10042
  const op = operation;
8017
10043
  logger.debug({ sourceNode, opId, mapName: op.mapName, key: op.key }, "Applying replicated operation");
8018
- const { eventPayload } = this.applyOpToMap(op);
10044
+ const { eventPayload, rejected } = await this.applyOpToMap(op, sourceNode);
10045
+ if (rejected || !eventPayload) {
10046
+ return true;
10047
+ }
8019
10048
  this.broadcast({
8020
10049
  type: "SERVER_EVENT",
8021
10050
  payload: eventPayload,
@@ -8114,7 +10143,10 @@ var ServerCoordinator = class {
8114
10143
  logger.warn({ err, opId: op.id }, "Interceptor rejected op");
8115
10144
  throw err;
8116
10145
  }
8117
- const { eventPayload } = this.applyOpToMap(op);
10146
+ const { eventPayload, rejected } = await this.applyOpToMap(op, originalSenderId);
10147
+ if (rejected || !eventPayload) {
10148
+ return;
10149
+ }
8118
10150
  if (this.replicationPipeline && !fromCluster) {
8119
10151
  const opId = op.id || `${op.mapName}:${op.key}:${Date.now()}`;
8120
10152
  this.replicationPipeline.replicate(op, opId, op.key).catch((err) => {
@@ -8242,7 +10274,10 @@ var ServerCoordinator = class {
8242
10274
  logger.warn({ err, opId: op.id }, "Interceptor rejected op in batch");
8243
10275
  throw err;
8244
10276
  }
8245
- const { eventPayload } = this.applyOpToMap(op);
10277
+ const { eventPayload, rejected } = await this.applyOpToMap(op, clientId);
10278
+ if (rejected || !eventPayload) {
10279
+ return;
10280
+ }
8246
10281
  if (this.replicationPipeline) {
8247
10282
  const opId = op.id || `${op.mapName}:${op.key}:${Date.now()}`;
8248
10283
  this.replicationPipeline.replicate(op, opId, op.key).catch((err) => {
@@ -8256,9 +10291,9 @@ var ServerCoordinator = class {
8256
10291
  handleClusterEvent(payload) {
8257
10292
  const { mapName, key, eventType } = payload;
8258
10293
  const map = this.getMap(mapName, eventType === "OR_ADD" || eventType === "OR_REMOVE" ? "OR" : "LWW");
8259
- const oldRecord = map instanceof LWWMap2 ? map.getRecord(key) : null;
10294
+ const oldRecord = map instanceof LWWMap3 ? map.getRecord(key) : null;
8260
10295
  if (this.partitionService.isRelated(key)) {
8261
- if (map instanceof LWWMap2 && payload.record) {
10296
+ if (map instanceof LWWMap3 && payload.record) {
8262
10297
  map.merge(key, payload.record);
8263
10298
  } else if (map instanceof ORMap2) {
8264
10299
  if (eventType === "OR_ADD" && payload.orRecord) {
@@ -8281,7 +10316,7 @@ var ServerCoordinator = class {
8281
10316
  if (typeHint === "OR") {
8282
10317
  map = new ORMap2(this.hlc);
8283
10318
  } else {
8284
- map = new LWWMap2(this.hlc);
10319
+ map = new LWWMap3(this.hlc);
8285
10320
  }
8286
10321
  this.maps.set(name, map);
8287
10322
  if (this.storage) {
@@ -8304,7 +10339,7 @@ var ServerCoordinator = class {
8304
10339
  this.getMap(name, typeHint);
8305
10340
  const loadingPromise = this.mapLoadingPromises.get(name);
8306
10341
  const map = this.maps.get(name);
8307
- const mapSize = map instanceof LWWMap2 ? Array.from(map.entries()).length : map instanceof ORMap2 ? map.size : 0;
10342
+ const mapSize = map instanceof LWWMap3 ? Array.from(map.entries()).length : map instanceof ORMap2 ? map.size : 0;
8308
10343
  logger.info({
8309
10344
  mapName: name,
8310
10345
  mapExisted,
@@ -8314,7 +10349,7 @@ var ServerCoordinator = class {
8314
10349
  if (loadingPromise) {
8315
10350
  logger.info({ mapName: name }, "[getMapAsync] Waiting for loadMapFromStorage...");
8316
10351
  await loadingPromise;
8317
- const newMapSize = map instanceof LWWMap2 ? Array.from(map.entries()).length : map instanceof ORMap2 ? map.size : 0;
10352
+ const newMapSize = map instanceof LWWMap3 ? Array.from(map.entries()).length : map instanceof ORMap2 ? map.size : 0;
8318
10353
  logger.info({ mapName: name, mapSizeAfterLoad: newMapSize }, "[getMapAsync] Load completed");
8319
10354
  }
8320
10355
  return this.maps.get(name);
@@ -8340,13 +10375,13 @@ var ServerCoordinator = class {
8340
10375
  const currentMap = this.maps.get(name);
8341
10376
  if (!currentMap) return;
8342
10377
  let targetMap = currentMap;
8343
- if (isOR && currentMap instanceof LWWMap2) {
10378
+ if (isOR && currentMap instanceof LWWMap3) {
8344
10379
  logger.info({ mapName: name }, "Map auto-detected as ORMap. Switching type.");
8345
10380
  targetMap = new ORMap2(this.hlc);
8346
10381
  this.maps.set(name, targetMap);
8347
10382
  } else if (!isOR && currentMap instanceof ORMap2 && typeHint !== "OR") {
8348
10383
  logger.info({ mapName: name }, "Map auto-detected as LWWMap. Switching type.");
8349
- targetMap = new LWWMap2(this.hlc);
10384
+ targetMap = new LWWMap3(this.hlc);
8350
10385
  this.maps.set(name, targetMap);
8351
10386
  }
8352
10387
  if (targetMap instanceof ORMap2) {
@@ -8362,7 +10397,7 @@ var ServerCoordinator = class {
8362
10397
  }
8363
10398
  }
8364
10399
  }
8365
- } else if (targetMap instanceof LWWMap2) {
10400
+ } else if (targetMap instanceof LWWMap3) {
8366
10401
  for (const [key, record] of records) {
8367
10402
  if (!record.type) {
8368
10403
  targetMap.merge(key, record);
@@ -8455,7 +10490,7 @@ var ServerCoordinator = class {
8455
10490
  reportLocalHlc() {
8456
10491
  let minHlc = this.hlc.now();
8457
10492
  for (const client of this.clients.values()) {
8458
- if (HLC.compare(client.lastActiveHlc, minHlc) < 0) {
10493
+ if (HLC2.compare(client.lastActiveHlc, minHlc) < 0) {
8459
10494
  minHlc = client.lastActiveHlc;
8460
10495
  }
8461
10496
  }
@@ -8476,7 +10511,7 @@ var ServerCoordinator = class {
8476
10511
  let globalSafe = this.hlc.now();
8477
10512
  let initialized = false;
8478
10513
  for (const ts of this.gcReports.values()) {
8479
- if (!initialized || HLC.compare(ts, globalSafe) < 0) {
10514
+ if (!initialized || HLC2.compare(ts, globalSafe) < 0) {
8480
10515
  globalSafe = ts;
8481
10516
  initialized = true;
8482
10517
  }
@@ -8511,7 +10546,7 @@ var ServerCoordinator = class {
8511
10546
  logger.info({ olderThanMillis: olderThan.millis }, "Performing Garbage Collection");
8512
10547
  const now = Date.now();
8513
10548
  for (const [name, map] of this.maps) {
8514
- if (map instanceof LWWMap2) {
10549
+ if (map instanceof LWWMap3) {
8515
10550
  for (const key of map.allKeys()) {
8516
10551
  const record = map.getRecord(key);
8517
10552
  if (record && record.value !== null && record.ttlMs) {
@@ -8803,7 +10838,13 @@ var ServerCoordinator = class {
8803
10838
  }
8804
10839
  return;
8805
10840
  }
8806
- const { eventPayload } = this.applyOpToMap(op);
10841
+ const { eventPayload, rejected } = await this.applyOpToMap(op, clientId);
10842
+ if (rejected) {
10843
+ if (op.id) {
10844
+ this.writeAckManager.failPending(op.id, "Rejected by conflict resolver");
10845
+ }
10846
+ return;
10847
+ }
8807
10848
  if (op.id) {
8808
10849
  this.writeAckManager.notifyLevel(op.id, WriteConcern2.APPLIED);
8809
10850
  }
@@ -8885,9 +10926,9 @@ var ServerCoordinator = class {
8885
10926
  // src/storage/PostgresAdapter.ts
8886
10927
  import { Pool } from "pg";
8887
10928
  var DEFAULT_TABLE_NAME = "topgun_maps";
8888
- var TABLE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
8889
- function validateTableName(name) {
8890
- if (!TABLE_NAME_REGEX.test(name)) {
10929
+ var TABLE_NAME_REGEX2 = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
10930
+ function validateTableName2(name) {
10931
+ if (!TABLE_NAME_REGEX2.test(name)) {
8891
10932
  throw new Error(
8892
10933
  `Invalid table name "${name}". Table name must start with a letter or underscore and contain only alphanumeric characters and underscores.`
8893
10934
  );
@@ -8901,7 +10942,7 @@ var PostgresAdapter = class {
8901
10942
  this.pool = new Pool(configOrPool);
8902
10943
  }
8903
10944
  const tableName = options?.tableName ?? DEFAULT_TABLE_NAME;
8904
- validateTableName(tableName);
10945
+ validateTableName2(tableName);
8905
10946
  this.tableName = tableName;
8906
10947
  }
8907
10948
  async initialize() {
@@ -9572,24 +11613,477 @@ var ClusterCoordinator = class extends EventEmitter9 {
9572
11613
  }
9573
11614
  }
9574
11615
  };
11616
+
11617
+ // src/MapWithResolver.ts
11618
+ import {
11619
+ LWWMap as LWWMap4,
11620
+ HLC as HLC3
11621
+ } from "@topgunbuild/core";
11622
+ var MapWithResolver = class {
11623
+ constructor(config) {
11624
+ this.mapName = config.name;
11625
+ this.hlc = new HLC3(config.nodeId);
11626
+ this.map = new LWWMap4(this.hlc);
11627
+ this.resolverService = config.resolverService;
11628
+ this.onRejection = config.onRejection;
11629
+ }
11630
+ /**
11631
+ * Get the map name.
11632
+ */
11633
+ get name() {
11634
+ return this.mapName;
11635
+ }
11636
+ /**
11637
+ * Get the underlying LWWMap.
11638
+ */
11639
+ get rawMap() {
11640
+ return this.map;
11641
+ }
11642
+ /**
11643
+ * Get a value by key.
11644
+ */
11645
+ get(key) {
11646
+ return this.map.get(key);
11647
+ }
11648
+ /**
11649
+ * Get the full record for a key.
11650
+ */
11651
+ getRecord(key) {
11652
+ return this.map.getRecord(key);
11653
+ }
11654
+ /**
11655
+ * Get the timestamp for a key.
11656
+ */
11657
+ getTimestamp(key) {
11658
+ return this.map.getRecord(key)?.timestamp;
11659
+ }
11660
+ /**
11661
+ * Set a value locally (no resolver).
11662
+ * Use for server-initiated writes.
11663
+ */
11664
+ set(key, value, ttlMs) {
11665
+ return this.map.set(key, value, ttlMs);
11666
+ }
11667
+ /**
11668
+ * Set a value with conflict resolution.
11669
+ * Use for client-initiated writes.
11670
+ *
11671
+ * @param key The key to set
11672
+ * @param value The new value
11673
+ * @param timestamp The client's timestamp
11674
+ * @param remoteNodeId The client's node ID
11675
+ * @param auth Optional authentication context
11676
+ * @returns Result containing applied status and merge result
11677
+ */
11678
+ async setWithResolver(key, value, timestamp, remoteNodeId, auth) {
11679
+ const context = {
11680
+ mapName: this.mapName,
11681
+ key,
11682
+ localValue: this.map.get(key),
11683
+ remoteValue: value,
11684
+ localTimestamp: this.getTimestamp(key),
11685
+ remoteTimestamp: timestamp,
11686
+ remoteNodeId,
11687
+ auth,
11688
+ readEntry: (k) => this.map.get(k)
11689
+ };
11690
+ const result = await this.resolverService.resolve(context);
11691
+ switch (result.action) {
11692
+ case "accept": {
11693
+ const record2 = {
11694
+ value: result.value,
11695
+ timestamp
11696
+ };
11697
+ this.map.merge(key, record2);
11698
+ return { applied: true, result, record: record2 };
11699
+ }
11700
+ case "merge": {
11701
+ const record2 = {
11702
+ value: result.value,
11703
+ timestamp
11704
+ };
11705
+ this.map.merge(key, record2);
11706
+ return { applied: true, result, record: record2 };
11707
+ }
11708
+ case "reject": {
11709
+ if (this.onRejection) {
11710
+ this.onRejection({
11711
+ mapName: this.mapName,
11712
+ key,
11713
+ attemptedValue: value,
11714
+ reason: result.reason,
11715
+ timestamp,
11716
+ nodeId: remoteNodeId
11717
+ });
11718
+ }
11719
+ return { applied: false, result };
11720
+ }
11721
+ case "local": {
11722
+ return { applied: false, result };
11723
+ }
11724
+ default:
11725
+ const record = {
11726
+ value: result.value ?? value,
11727
+ timestamp
11728
+ };
11729
+ this.map.merge(key, record);
11730
+ return { applied: true, result, record };
11731
+ }
11732
+ }
11733
+ /**
11734
+ * Remove a key.
11735
+ */
11736
+ remove(key) {
11737
+ return this.map.remove(key);
11738
+ }
11739
+ /**
11740
+ * Standard merge without resolver (for sync operations).
11741
+ */
11742
+ merge(key, record) {
11743
+ return this.map.merge(key, record);
11744
+ }
11745
+ /**
11746
+ * Merge with resolver support.
11747
+ * Equivalent to setWithResolver but takes a full record.
11748
+ */
11749
+ async mergeWithResolver(key, record, remoteNodeId, auth) {
11750
+ if (record.value === null) {
11751
+ const applied = this.map.merge(key, record);
11752
+ return {
11753
+ applied,
11754
+ result: applied ? { action: "accept", value: record.value } : { action: "local" },
11755
+ record: applied ? record : void 0
11756
+ };
11757
+ }
11758
+ return this.setWithResolver(
11759
+ key,
11760
+ record.value,
11761
+ record.timestamp,
11762
+ remoteNodeId,
11763
+ auth
11764
+ );
11765
+ }
11766
+ /**
11767
+ * Clear all data.
11768
+ */
11769
+ clear() {
11770
+ this.map.clear();
11771
+ }
11772
+ /**
11773
+ * Get map size.
11774
+ */
11775
+ get size() {
11776
+ return this.map.size;
11777
+ }
11778
+ /**
11779
+ * Iterate over entries.
11780
+ */
11781
+ entries() {
11782
+ return this.map.entries();
11783
+ }
11784
+ /**
11785
+ * Get all keys.
11786
+ */
11787
+ allKeys() {
11788
+ return this.map.allKeys();
11789
+ }
11790
+ /**
11791
+ * Subscribe to changes.
11792
+ */
11793
+ onChange(callback) {
11794
+ return this.map.onChange(callback);
11795
+ }
11796
+ /**
11797
+ * Get MerkleTree for sync.
11798
+ */
11799
+ getMerkleTree() {
11800
+ return this.map.getMerkleTree();
11801
+ }
11802
+ /**
11803
+ * Prune old tombstones.
11804
+ */
11805
+ prune(olderThan) {
11806
+ return this.map.prune(olderThan);
11807
+ }
11808
+ };
11809
+
11810
+ // src/config/IndexConfig.ts
11811
+ var DEFAULT_INDEX_CONFIG = {
11812
+ autoIndex: false,
11813
+ maxAutoIndexesPerMap: 10,
11814
+ maps: [],
11815
+ logStats: false,
11816
+ statsLogInterval: 6e4
11817
+ };
11818
+ function validateIndexConfig(config) {
11819
+ const errors = [];
11820
+ if (config.maxAutoIndexesPerMap !== void 0) {
11821
+ if (typeof config.maxAutoIndexesPerMap !== "number" || config.maxAutoIndexesPerMap < 1) {
11822
+ errors.push("maxAutoIndexesPerMap must be a positive number");
11823
+ }
11824
+ }
11825
+ if (config.statsLogInterval !== void 0) {
11826
+ if (typeof config.statsLogInterval !== "number" || config.statsLogInterval < 1e3) {
11827
+ errors.push("statsLogInterval must be at least 1000ms");
11828
+ }
11829
+ }
11830
+ if (config.maps) {
11831
+ const mapNames = /* @__PURE__ */ new Set();
11832
+ for (const mapConfig of config.maps) {
11833
+ if (!mapConfig.mapName || typeof mapConfig.mapName !== "string") {
11834
+ errors.push("Each map config must have a valid mapName");
11835
+ continue;
11836
+ }
11837
+ if (mapNames.has(mapConfig.mapName)) {
11838
+ errors.push(`Duplicate map config for: ${mapConfig.mapName}`);
11839
+ }
11840
+ mapNames.add(mapConfig.mapName);
11841
+ if (!Array.isArray(mapConfig.indexes)) {
11842
+ errors.push(`Map ${mapConfig.mapName}: indexes must be an array`);
11843
+ continue;
11844
+ }
11845
+ const attrNames = /* @__PURE__ */ new Set();
11846
+ for (const indexDef of mapConfig.indexes) {
11847
+ if (!indexDef.attribute || typeof indexDef.attribute !== "string") {
11848
+ errors.push(`Map ${mapConfig.mapName}: index must have valid attribute`);
11849
+ continue;
11850
+ }
11851
+ if (!["hash", "navigable"].includes(indexDef.type)) {
11852
+ errors.push(
11853
+ `Map ${mapConfig.mapName}: index type must be 'hash' or 'navigable'`
11854
+ );
11855
+ }
11856
+ if (indexDef.comparator && !["number", "string", "date"].includes(indexDef.comparator)) {
11857
+ errors.push(
11858
+ `Map ${mapConfig.mapName}: comparator must be 'number', 'string', or 'date'`
11859
+ );
11860
+ }
11861
+ const key = `${indexDef.attribute}:${indexDef.type}`;
11862
+ if (attrNames.has(key)) {
11863
+ errors.push(
11864
+ `Map ${mapConfig.mapName}: duplicate ${indexDef.type} index on ${indexDef.attribute}`
11865
+ );
11866
+ }
11867
+ attrNames.add(key);
11868
+ }
11869
+ }
11870
+ }
11871
+ return errors;
11872
+ }
11873
+ function mergeWithDefaults(userConfig) {
11874
+ return {
11875
+ ...DEFAULT_INDEX_CONFIG,
11876
+ ...userConfig,
11877
+ maps: userConfig.maps ?? DEFAULT_INDEX_CONFIG.maps
11878
+ };
11879
+ }
11880
+
11881
+ // src/config/MapFactory.ts
11882
+ import {
11883
+ LWWMap as LWWMap5,
11884
+ ORMap as ORMap3,
11885
+ IndexedLWWMap as IndexedLWWMap3,
11886
+ IndexedORMap as IndexedORMap3,
11887
+ simpleAttribute
11888
+ } from "@topgunbuild/core";
11889
+ var MapFactory = class {
11890
+ /**
11891
+ * Create a MapFactory.
11892
+ *
11893
+ * @param config - Server index configuration
11894
+ */
11895
+ constructor(config) {
11896
+ this.config = mergeWithDefaults(config ?? {});
11897
+ this.mapConfigs = /* @__PURE__ */ new Map();
11898
+ for (const mapConfig of this.config.maps ?? []) {
11899
+ this.mapConfigs.set(mapConfig.mapName, mapConfig);
11900
+ }
11901
+ }
11902
+ /**
11903
+ * Create an LWWMap or IndexedLWWMap based on configuration.
11904
+ *
11905
+ * @param mapName - Name of the map
11906
+ * @param hlc - Hybrid Logical Clock instance
11907
+ * @returns LWWMap or IndexedLWWMap depending on configuration
11908
+ */
11909
+ createLWWMap(mapName, hlc) {
11910
+ const mapConfig = this.mapConfigs.get(mapName);
11911
+ if (!mapConfig || mapConfig.indexes.length === 0) {
11912
+ return new LWWMap5(hlc);
11913
+ }
11914
+ const map = new IndexedLWWMap3(hlc);
11915
+ for (const indexDef of mapConfig.indexes) {
11916
+ this.addIndexToLWWMap(map, indexDef);
11917
+ }
11918
+ return map;
11919
+ }
11920
+ /**
11921
+ * Create an ORMap or IndexedORMap based on configuration.
11922
+ *
11923
+ * @param mapName - Name of the map
11924
+ * @param hlc - Hybrid Logical Clock instance
11925
+ * @returns ORMap or IndexedORMap depending on configuration
11926
+ */
11927
+ createORMap(mapName, hlc) {
11928
+ const mapConfig = this.mapConfigs.get(mapName);
11929
+ if (!mapConfig || mapConfig.indexes.length === 0) {
11930
+ return new ORMap3(hlc);
11931
+ }
11932
+ const map = new IndexedORMap3(hlc);
11933
+ for (const indexDef of mapConfig.indexes) {
11934
+ this.addIndexToORMap(map, indexDef);
11935
+ }
11936
+ return map;
11937
+ }
11938
+ /**
11939
+ * Add an index to an IndexedLWWMap based on definition.
11940
+ */
11941
+ addIndexToLWWMap(map, indexDef) {
11942
+ const attribute = this.createAttribute(indexDef.attribute);
11943
+ if (indexDef.type === "hash") {
11944
+ map.addHashIndex(attribute);
11945
+ } else if (indexDef.type === "navigable") {
11946
+ const navAttribute = attribute;
11947
+ const comparator = this.createComparator(indexDef.comparator);
11948
+ map.addNavigableIndex(navAttribute, comparator);
11949
+ }
11950
+ }
11951
+ /**
11952
+ * Add an index to an IndexedORMap based on definition.
11953
+ */
11954
+ addIndexToORMap(map, indexDef) {
11955
+ const attribute = this.createAttribute(indexDef.attribute);
11956
+ if (indexDef.type === "hash") {
11957
+ map.addHashIndex(attribute);
11958
+ } else if (indexDef.type === "navigable") {
11959
+ const navAttribute = attribute;
11960
+ const comparator = this.createComparator(indexDef.comparator);
11961
+ map.addNavigableIndex(navAttribute, comparator);
11962
+ }
11963
+ }
11964
+ /**
11965
+ * Create an Attribute for extracting values from records.
11966
+ * Supports dot notation for nested paths.
11967
+ */
11968
+ createAttribute(path) {
11969
+ return simpleAttribute(path, (record) => {
11970
+ return this.getNestedValue(record, path);
11971
+ });
11972
+ }
11973
+ /**
11974
+ * Get a nested value from an object using dot notation.
11975
+ *
11976
+ * @param obj - Object to extract value from
11977
+ * @param path - Dot-notation path (e.g., "user.email")
11978
+ * @returns Value at the path or undefined
11979
+ */
11980
+ getNestedValue(obj, path) {
11981
+ if (obj === null || obj === void 0) {
11982
+ return void 0;
11983
+ }
11984
+ const parts = path.split(".");
11985
+ let current = obj;
11986
+ for (const part of parts) {
11987
+ if (current === void 0 || current === null) {
11988
+ return void 0;
11989
+ }
11990
+ if (typeof current !== "object") {
11991
+ return void 0;
11992
+ }
11993
+ current = current[part];
11994
+ }
11995
+ return current;
11996
+ }
11997
+ /**
11998
+ * Create a comparator function for navigable indexes.
11999
+ */
12000
+ createComparator(type) {
12001
+ switch (type) {
12002
+ case "number":
12003
+ return (a, b) => {
12004
+ const numA = typeof a === "number" ? a : parseFloat(String(a));
12005
+ const numB = typeof b === "number" ? b : parseFloat(String(b));
12006
+ return numA - numB;
12007
+ };
12008
+ case "date":
12009
+ return (a, b) => {
12010
+ const dateA = new Date(a).getTime();
12011
+ const dateB = new Date(b).getTime();
12012
+ return dateA - dateB;
12013
+ };
12014
+ case "string":
12015
+ return (a, b) => {
12016
+ const strA = String(a);
12017
+ const strB = String(b);
12018
+ return strA.localeCompare(strB);
12019
+ };
12020
+ default:
12021
+ return void 0;
12022
+ }
12023
+ }
12024
+ /**
12025
+ * Check if a map should be indexed based on configuration.
12026
+ *
12027
+ * @param mapName - Name of the map
12028
+ * @returns true if map has index configuration
12029
+ */
12030
+ hasIndexConfig(mapName) {
12031
+ const config = this.mapConfigs.get(mapName);
12032
+ return config !== void 0 && config.indexes.length > 0;
12033
+ }
12034
+ /**
12035
+ * Get index configuration for a map.
12036
+ *
12037
+ * @param mapName - Name of the map
12038
+ * @returns Map index config or undefined
12039
+ */
12040
+ getMapConfig(mapName) {
12041
+ return this.mapConfigs.get(mapName);
12042
+ }
12043
+ /**
12044
+ * Get all configured map names.
12045
+ *
12046
+ * @returns Array of map names with index configuration
12047
+ */
12048
+ getConfiguredMaps() {
12049
+ return Array.from(this.mapConfigs.keys());
12050
+ }
12051
+ /**
12052
+ * Get the full server index configuration.
12053
+ */
12054
+ getConfig() {
12055
+ return this.config;
12056
+ }
12057
+ };
9575
12058
  export {
9576
12059
  BufferPool,
9577
12060
  ClusterCoordinator,
9578
12061
  ClusterManager,
12062
+ ConflictResolverHandler,
12063
+ ConflictResolverService,
9579
12064
  ConnectionRateLimiter,
9580
12065
  DEFAULT_CLUSTER_COORDINATOR_CONFIG,
12066
+ DEFAULT_CONFLICT_RESOLVER_CONFIG,
12067
+ DEFAULT_INDEX_CONFIG,
12068
+ DEFAULT_JOURNAL_SERVICE_CONFIG,
9581
12069
  DEFAULT_LAG_TRACKER_CONFIG,
12070
+ DEFAULT_SANDBOX_CONFIG,
12071
+ EntryProcessorHandler,
12072
+ EventJournalService,
9582
12073
  FilterTasklet,
9583
12074
  ForEachTasklet,
9584
12075
  IteratorTasklet,
9585
12076
  LagTracker,
9586
12077
  LockManager,
12078
+ MapFactory,
9587
12079
  MapTasklet,
12080
+ MapWithResolver,
9588
12081
  MemoryServerAdapter,
9589
12082
  MigrationManager,
9590
12083
  ObjectPool,
9591
12084
  PartitionService,
9592
12085
  PostgresAdapter,
12086
+ ProcessorSandbox,
9593
12087
  RateLimitInterceptor,
9594
12088
  ReduceTasklet,
9595
12089
  ReplicationPipeline,
@@ -9612,10 +12106,12 @@ export {
9612
12106
  getNativeStats,
9613
12107
  logNativeStatus,
9614
12108
  logger,
12109
+ mergeWithDefaults,
9615
12110
  setGlobalBufferPool,
9616
12111
  setGlobalEventPayloadPool,
9617
12112
  setGlobalMessagePool,
9618
12113
  setGlobalRecordPool,
9619
- setGlobalTimestampPool
12114
+ setGlobalTimestampPool,
12115
+ validateIndexConfig
9620
12116
  };
9621
12117
  //# sourceMappingURL=index.mjs.map