@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.js CHANGED
@@ -33,20 +33,31 @@ __export(index_exports, {
33
33
  BufferPool: () => BufferPool,
34
34
  ClusterCoordinator: () => ClusterCoordinator,
35
35
  ClusterManager: () => ClusterManager,
36
+ ConflictResolverHandler: () => ConflictResolverHandler,
37
+ ConflictResolverService: () => ConflictResolverService,
36
38
  ConnectionRateLimiter: () => ConnectionRateLimiter,
37
39
  DEFAULT_CLUSTER_COORDINATOR_CONFIG: () => DEFAULT_CLUSTER_COORDINATOR_CONFIG,
40
+ DEFAULT_CONFLICT_RESOLVER_CONFIG: () => DEFAULT_CONFLICT_RESOLVER_CONFIG,
41
+ DEFAULT_INDEX_CONFIG: () => DEFAULT_INDEX_CONFIG,
42
+ DEFAULT_JOURNAL_SERVICE_CONFIG: () => DEFAULT_JOURNAL_SERVICE_CONFIG,
38
43
  DEFAULT_LAG_TRACKER_CONFIG: () => DEFAULT_LAG_TRACKER_CONFIG,
44
+ DEFAULT_SANDBOX_CONFIG: () => DEFAULT_SANDBOX_CONFIG,
45
+ EntryProcessorHandler: () => EntryProcessorHandler,
46
+ EventJournalService: () => EventJournalService,
39
47
  FilterTasklet: () => FilterTasklet,
40
48
  ForEachTasklet: () => ForEachTasklet,
41
49
  IteratorTasklet: () => IteratorTasklet,
42
50
  LagTracker: () => LagTracker,
43
51
  LockManager: () => LockManager,
52
+ MapFactory: () => MapFactory,
44
53
  MapTasklet: () => MapTasklet,
54
+ MapWithResolver: () => MapWithResolver,
45
55
  MemoryServerAdapter: () => MemoryServerAdapter,
46
56
  MigrationManager: () => MigrationManager,
47
57
  ObjectPool: () => ObjectPool,
48
58
  PartitionService: () => PartitionService,
49
59
  PostgresAdapter: () => PostgresAdapter,
60
+ ProcessorSandbox: () => ProcessorSandbox,
50
61
  RateLimitInterceptor: () => RateLimitInterceptor,
51
62
  ReduceTasklet: () => ReduceTasklet,
52
63
  ReplicationPipeline: () => ReplicationPipeline,
@@ -69,11 +80,13 @@ __export(index_exports, {
69
80
  getNativeStats: () => getNativeStats,
70
81
  logNativeStatus: () => logNativeStatus,
71
82
  logger: () => logger,
83
+ mergeWithDefaults: () => mergeWithDefaults,
72
84
  setGlobalBufferPool: () => setGlobalBufferPool,
73
85
  setGlobalEventPayloadPool: () => setGlobalEventPayloadPool,
74
86
  setGlobalMessagePool: () => setGlobalMessagePool,
75
87
  setGlobalRecordPool: () => setGlobalRecordPool,
76
- setGlobalTimestampPool: () => setGlobalTimestampPool
88
+ setGlobalTimestampPool: () => setGlobalTimestampPool,
89
+ validateIndexConfig: () => validateIndexConfig
77
90
  });
78
91
  module.exports = __toCommonJS(index_exports);
79
92
 
@@ -82,7 +95,7 @@ var import_http = require("http");
82
95
  var import_https = require("https");
83
96
  var import_fs2 = require("fs");
84
97
  var import_ws3 = require("ws");
85
- var import_core10 = require("@topgunbuild/core");
98
+ var import_core15 = require("@topgunbuild/core");
86
99
  var jwt = __toESM(require("jsonwebtoken"));
87
100
  var crypto = __toESM(require("crypto"));
88
101
 
@@ -434,14 +447,70 @@ var QueryRegistry = class {
434
447
  /**
435
448
  * Processes a record change for all relevant subscriptions.
436
449
  * Calculates diffs and sends updates.
450
+ *
451
+ * For IndexedLWWMap: Uses StandingQueryRegistry for O(1) affected query detection.
452
+ * For regular maps: Falls back to ReverseQueryIndex.
437
453
  */
438
454
  processChange(mapName, map, changeKey, changeRecord, oldRecord) {
439
455
  const index = this.indexes.get(mapName);
440
456
  if (!index) return;
441
457
  const newVal = this.extractValue(changeRecord);
442
458
  const oldVal = this.extractValue(oldRecord);
459
+ if (map instanceof import_core2.IndexedLWWMap) {
460
+ this.processChangeWithStandingQuery(mapName, map, changeKey, newVal, oldVal);
461
+ return;
462
+ }
463
+ this.processChangeWithReverseIndex(mapName, map, changeKey, newVal, oldVal, index);
464
+ }
465
+ /**
466
+ * Process change using IndexedLWWMap's StandingQueryRegistry.
467
+ * O(1) detection of affected queries.
468
+ */
469
+ processChangeWithStandingQuery(mapName, map, changeKey, newVal, oldVal) {
470
+ const subs = this.subscriptions.get(mapName);
471
+ if (!subs || subs.size === 0) return;
472
+ const subsByQueryId = /* @__PURE__ */ new Map();
473
+ for (const sub of subs) {
474
+ subsByQueryId.set(sub.id, sub);
475
+ }
476
+ const standingRegistry = map.getStandingQueryRegistry();
477
+ let changes;
478
+ if (oldVal === null || oldVal === void 0) {
479
+ if (newVal !== null && newVal !== void 0) {
480
+ changes = standingRegistry.onRecordAdded(changeKey, newVal);
481
+ } else {
482
+ return;
483
+ }
484
+ } else if (newVal === null || newVal === void 0) {
485
+ changes = standingRegistry.onRecordRemoved(changeKey, oldVal);
486
+ } else {
487
+ changes = standingRegistry.onRecordUpdated(changeKey, oldVal, newVal);
488
+ }
489
+ for (const sub of subs) {
490
+ const coreQuery = this.convertToCoreQuery(sub.query);
491
+ if (!coreQuery) {
492
+ this.processSubscriptionFallback(sub, map, changeKey, newVal);
493
+ continue;
494
+ }
495
+ const queryHash = this.hashCoreQuery(coreQuery);
496
+ const change = changes.get(queryHash);
497
+ if (change === "added") {
498
+ sub.previousResultKeys.add(changeKey);
499
+ this.sendUpdate(sub, changeKey, newVal, "UPDATE");
500
+ } else if (change === "removed") {
501
+ sub.previousResultKeys.delete(changeKey);
502
+ this.sendUpdate(sub, changeKey, null, "REMOVE");
503
+ } else if (change === "updated") {
504
+ this.sendUpdate(sub, changeKey, newVal, "UPDATE");
505
+ }
506
+ }
507
+ }
508
+ /**
509
+ * Process change using legacy ReverseQueryIndex.
510
+ */
511
+ processChangeWithReverseIndex(mapName, map, changeKey, newVal, oldVal, index) {
443
512
  const changedFields = this.getChangedFields(oldVal, newVal);
444
- if (changedFields !== "ALL" && changedFields.size === 0 && oldRecord && changeRecord) {
513
+ if (changedFields !== "ALL" && changedFields.size === 0 && oldVal && newVal) {
445
514
  return;
446
515
  }
447
516
  const candidates = index.getCandidates(changedFields, oldVal, newVal);
@@ -483,6 +552,103 @@ var QueryRegistry = class {
483
552
  sub.previousResultKeys = newResultKeys;
484
553
  }
485
554
  }
555
+ /**
556
+ * Fallback processing for subscriptions that can't use StandingQueryRegistry.
557
+ */
558
+ processSubscriptionFallback(sub, map, changeKey, newVal) {
559
+ const dummyRecord = {
560
+ value: newVal,
561
+ timestamp: { millis: 0, counter: 0, nodeId: "" }
562
+ };
563
+ const isMatch = newVal !== null && matchesQuery(dummyRecord, sub.query);
564
+ const wasInResult = sub.previousResultKeys.has(changeKey);
565
+ if (isMatch && !wasInResult) {
566
+ sub.previousResultKeys.add(changeKey);
567
+ this.sendUpdate(sub, changeKey, newVal, "UPDATE");
568
+ } else if (!isMatch && wasInResult) {
569
+ sub.previousResultKeys.delete(changeKey);
570
+ this.sendUpdate(sub, changeKey, null, "REMOVE");
571
+ } else if (isMatch && wasInResult) {
572
+ this.sendUpdate(sub, changeKey, newVal, "UPDATE");
573
+ }
574
+ }
575
+ /**
576
+ * Convert server Query format to core Query format.
577
+ */
578
+ convertToCoreQuery(query) {
579
+ if (query.predicate) {
580
+ return this.predicateToCoreQuery(query.predicate);
581
+ }
582
+ if (query.where) {
583
+ const conditions = [];
584
+ for (const [attribute, condition] of Object.entries(query.where)) {
585
+ if (typeof condition !== "object" || condition === null) {
586
+ conditions.push({ type: "eq", attribute, value: condition });
587
+ } else {
588
+ for (const [op, value] of Object.entries(condition)) {
589
+ const coreOp = this.convertOperator(op);
590
+ if (coreOp) {
591
+ conditions.push({ type: coreOp, attribute, value });
592
+ }
593
+ }
594
+ }
595
+ }
596
+ if (conditions.length === 0) return null;
597
+ if (conditions.length === 1) return conditions[0];
598
+ return { type: "and", children: conditions };
599
+ }
600
+ return null;
601
+ }
602
+ predicateToCoreQuery(predicate) {
603
+ if (!predicate || !predicate.op) return null;
604
+ switch (predicate.op) {
605
+ case "eq":
606
+ case "neq":
607
+ case "gt":
608
+ case "gte":
609
+ case "lt":
610
+ case "lte":
611
+ return {
612
+ type: predicate.op,
613
+ attribute: predicate.attribute,
614
+ value: predicate.value
615
+ };
616
+ case "and":
617
+ case "or":
618
+ if (predicate.children && Array.isArray(predicate.children)) {
619
+ const children = predicate.children.map((c) => this.predicateToCoreQuery(c)).filter((c) => c !== null);
620
+ if (children.length === 0) return null;
621
+ if (children.length === 1) return children[0];
622
+ return { type: predicate.op, children };
623
+ }
624
+ return null;
625
+ case "not":
626
+ if (predicate.children && predicate.children[0]) {
627
+ const child = this.predicateToCoreQuery(predicate.children[0]);
628
+ if (child) {
629
+ return { type: "not", child };
630
+ }
631
+ }
632
+ return null;
633
+ default:
634
+ return null;
635
+ }
636
+ }
637
+ convertOperator(op) {
638
+ const mapping = {
639
+ "$eq": "eq",
640
+ "$ne": "neq",
641
+ "$neq": "neq",
642
+ "$gt": "gt",
643
+ "$gte": "gte",
644
+ "$lt": "lt",
645
+ "$lte": "lte"
646
+ };
647
+ return mapping[op] || null;
648
+ }
649
+ hashCoreQuery(query) {
650
+ return JSON.stringify(query);
651
+ }
486
652
  extractValue(record) {
487
653
  if (!record) return null;
488
654
  if (Array.isArray(record)) {
@@ -6468,118 +6634,1487 @@ var ReplicationPipeline = class extends import_events8.EventEmitter {
6468
6634
  }
6469
6635
  };
6470
6636
 
6471
- // src/ServerCoordinator.ts
6472
- var GC_INTERVAL_MS = 60 * 60 * 1e3;
6473
- var GC_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
6474
- var CLIENT_HEARTBEAT_TIMEOUT_MS = 2e4;
6475
- var CLIENT_HEARTBEAT_CHECK_INTERVAL_MS = 5e3;
6476
- var ServerCoordinator = class {
6477
- constructor(config) {
6478
- this.clients = /* @__PURE__ */ new Map();
6479
- // Interceptors
6480
- this.interceptors = [];
6481
- // In-memory storage (partitioned later)
6482
- this.maps = /* @__PURE__ */ new Map();
6483
- this.pendingClusterQueries = /* @__PURE__ */ new Map();
6484
- // GC Consensus State
6485
- this.gcReports = /* @__PURE__ */ new Map();
6486
- // Track map loading state to avoid returning empty results during async load
6487
- this.mapLoadingPromises = /* @__PURE__ */ new Map();
6488
- // Track pending batch operations for testing purposes
6489
- this.pendingBatchOperations = /* @__PURE__ */ new Set();
6490
- this._actualPort = 0;
6491
- this._actualClusterPort = 0;
6492
- this._readyPromise = new Promise((resolve) => {
6493
- this._readyResolve = resolve;
6494
- });
6495
- this.hlc = new import_core10.HLC(config.nodeId);
6496
- this.storage = config.storage;
6497
- const rawSecret = config.jwtSecret || process.env.JWT_SECRET || "topgun-secret-dev";
6498
- this.jwtSecret = rawSecret.replace(/\\n/g, "\n");
6499
- this.queryRegistry = new QueryRegistry();
6500
- this.securityManager = new SecurityManager(config.securityPolicies || []);
6501
- this.interceptors = config.interceptors || [];
6502
- this.metricsService = new MetricsService();
6503
- this.eventExecutor = new StripedEventExecutor({
6504
- stripeCount: config.eventStripeCount ?? 4,
6505
- queueCapacity: config.eventQueueCapacity ?? 1e4,
6506
- name: `${config.nodeId}-event-executor`,
6507
- onReject: (task) => {
6508
- logger.warn({ nodeId: config.nodeId, key: task.key }, "Event task rejected due to queue capacity");
6509
- this.metricsService.incEventQueueRejected();
6637
+ // src/handlers/CounterHandler.ts
6638
+ var import_core10 = require("@topgunbuild/core");
6639
+ var CounterHandler = class {
6640
+ // counterName -> Set<clientId>
6641
+ constructor(nodeId = "server") {
6642
+ this.nodeId = nodeId;
6643
+ this.counters = /* @__PURE__ */ new Map();
6644
+ this.subscriptions = /* @__PURE__ */ new Map();
6645
+ }
6646
+ /**
6647
+ * Get or create a counter by name.
6648
+ */
6649
+ getOrCreateCounter(name) {
6650
+ let counter = this.counters.get(name);
6651
+ if (!counter) {
6652
+ counter = new import_core10.PNCounterImpl({ nodeId: this.nodeId });
6653
+ this.counters.set(name, counter);
6654
+ logger.debug({ name }, "Created new counter");
6655
+ }
6656
+ return counter;
6657
+ }
6658
+ /**
6659
+ * Handle COUNTER_REQUEST - client wants initial state.
6660
+ * @returns Response message to send back to client
6661
+ */
6662
+ handleCounterRequest(clientId, name) {
6663
+ const counter = this.getOrCreateCounter(name);
6664
+ this.subscribe(clientId, name);
6665
+ const state = counter.getState();
6666
+ logger.debug({ clientId, name, value: counter.get() }, "Counter request handled");
6667
+ return {
6668
+ type: "COUNTER_RESPONSE",
6669
+ payload: {
6670
+ name,
6671
+ state: this.stateToObject(state)
6510
6672
  }
6511
- });
6512
- this.backpressure = new BackpressureRegulator({
6513
- syncFrequency: config.backpressureSyncFrequency ?? 100,
6514
- maxPendingOps: config.backpressureMaxPending ?? 1e3,
6515
- backoffTimeoutMs: config.backpressureBackoffMs ?? 5e3,
6516
- enabled: config.backpressureEnabled ?? true
6517
- });
6518
- this.writeCoalescingEnabled = config.writeCoalescingEnabled ?? true;
6519
- const preset = coalescingPresets[config.writeCoalescingPreset ?? "highThroughput"];
6520
- this.writeCoalescingOptions = {
6521
- maxBatchSize: config.writeCoalescingMaxBatch ?? preset.maxBatchSize,
6522
- maxDelayMs: config.writeCoalescingMaxDelayMs ?? preset.maxDelayMs,
6523
- maxBatchBytes: config.writeCoalescingMaxBytes ?? preset.maxBatchBytes
6524
6673
  };
6525
- this.eventPayloadPool = createEventPayloadPool({
6526
- maxSize: 4096,
6527
- initialSize: 128
6528
- });
6529
- this.taskletScheduler = new TaskletScheduler({
6530
- defaultTimeBudgetMs: 5,
6531
- maxConcurrent: 20
6532
- });
6533
- this.writeAckManager = new WriteAckManager({
6534
- defaultTimeout: config.writeAckTimeout ?? 5e3
6535
- });
6536
- this.rateLimitingEnabled = config.rateLimitingEnabled ?? true;
6537
- this.rateLimiter = new ConnectionRateLimiter({
6538
- maxConnectionsPerSecond: config.maxConnectionsPerSecond ?? 100,
6539
- maxPendingConnections: config.maxPendingConnections ?? 1e3,
6540
- cooldownMs: 1e3
6541
- });
6542
- if (config.workerPoolEnabled) {
6543
- this.workerPool = new WorkerPool({
6544
- minWorkers: config.workerPoolConfig?.minWorkers ?? 2,
6545
- maxWorkers: config.workerPoolConfig?.maxWorkers,
6546
- taskTimeout: config.workerPoolConfig?.taskTimeout ?? 5e3,
6547
- idleTimeout: config.workerPoolConfig?.idleTimeout ?? 3e4,
6548
- autoRestart: config.workerPoolConfig?.autoRestart ?? true
6549
- });
6550
- this.merkleWorker = new MerkleWorker(this.workerPool);
6551
- this.crdtMergeWorker = new CRDTMergeWorker(this.workerPool);
6552
- this.serializationWorker = new SerializationWorker(this.workerPool);
6553
- logger.info({
6554
- minWorkers: config.workerPoolConfig?.minWorkers ?? 2,
6555
- maxWorkers: config.workerPoolConfig?.maxWorkers ?? "auto"
6556
- }, "Worker pool initialized for CPU-bound operations");
6674
+ }
6675
+ /**
6676
+ * Handle COUNTER_SYNC - client sends their state to merge.
6677
+ * @returns Merged state and list of clients to broadcast to
6678
+ */
6679
+ handleCounterSync(clientId, name, stateObj) {
6680
+ const counter = this.getOrCreateCounter(name);
6681
+ const incomingState = this.objectToState(stateObj);
6682
+ counter.merge(incomingState);
6683
+ const mergedState = counter.getState();
6684
+ const mergedStateObj = this.stateToObject(mergedState);
6685
+ logger.debug(
6686
+ { clientId, name, value: counter.get() },
6687
+ "Counter sync handled"
6688
+ );
6689
+ this.subscribe(clientId, name);
6690
+ const subscribers = this.subscriptions.get(name) || /* @__PURE__ */ new Set();
6691
+ const broadcastTo = Array.from(subscribers).filter((id) => id !== clientId);
6692
+ return {
6693
+ // Response to the sending client
6694
+ response: {
6695
+ type: "COUNTER_UPDATE",
6696
+ payload: {
6697
+ name,
6698
+ state: mergedStateObj
6699
+ }
6700
+ },
6701
+ // Broadcast to other clients
6702
+ broadcastTo,
6703
+ broadcastMessage: {
6704
+ type: "COUNTER_UPDATE",
6705
+ payload: {
6706
+ name,
6707
+ state: mergedStateObj
6708
+ }
6709
+ }
6710
+ };
6711
+ }
6712
+ /**
6713
+ * Subscribe a client to counter updates.
6714
+ */
6715
+ subscribe(clientId, counterName) {
6716
+ if (!this.subscriptions.has(counterName)) {
6717
+ this.subscriptions.set(counterName, /* @__PURE__ */ new Set());
6557
6718
  }
6558
- if (config.tls?.enabled) {
6559
- const tlsOptions = this.buildTLSOptions(config.tls);
6560
- this.httpServer = (0, import_https.createServer)(tlsOptions, (_req, res) => {
6561
- res.writeHead(200);
6562
- res.end("TopGun Server Running (Secure)");
6563
- });
6564
- logger.info("TLS enabled for client connections");
6719
+ this.subscriptions.get(counterName).add(clientId);
6720
+ logger.debug({ clientId, counterName }, "Client subscribed to counter");
6721
+ }
6722
+ /**
6723
+ * Unsubscribe a client from counter updates.
6724
+ */
6725
+ unsubscribe(clientId, counterName) {
6726
+ const subs = this.subscriptions.get(counterName);
6727
+ if (subs) {
6728
+ subs.delete(clientId);
6729
+ if (subs.size === 0) {
6730
+ this.subscriptions.delete(counterName);
6731
+ }
6732
+ }
6733
+ }
6734
+ /**
6735
+ * Unsubscribe a client from all counters (e.g., on disconnect).
6736
+ */
6737
+ unsubscribeAll(clientId) {
6738
+ for (const [counterName, subs] of this.subscriptions) {
6739
+ subs.delete(clientId);
6740
+ if (subs.size === 0) {
6741
+ this.subscriptions.delete(counterName);
6742
+ }
6743
+ }
6744
+ logger.debug({ clientId }, "Client unsubscribed from all counters");
6745
+ }
6746
+ /**
6747
+ * Get current counter value (for monitoring/debugging).
6748
+ */
6749
+ getCounterValue(name) {
6750
+ const counter = this.counters.get(name);
6751
+ return counter ? counter.get() : 0;
6752
+ }
6753
+ /**
6754
+ * Get all counter names.
6755
+ */
6756
+ getCounterNames() {
6757
+ return Array.from(this.counters.keys());
6758
+ }
6759
+ /**
6760
+ * Get number of subscribers for a counter.
6761
+ */
6762
+ getSubscriberCount(name) {
6763
+ return this.subscriptions.get(name)?.size || 0;
6764
+ }
6765
+ /**
6766
+ * Convert Map-based state to plain object for serialization.
6767
+ */
6768
+ stateToObject(state) {
6769
+ return {
6770
+ p: Object.fromEntries(state.positive),
6771
+ n: Object.fromEntries(state.negative)
6772
+ };
6773
+ }
6774
+ /**
6775
+ * Convert plain object to Map-based state.
6776
+ */
6777
+ objectToState(obj) {
6778
+ return {
6779
+ positive: new Map(Object.entries(obj.p || {})),
6780
+ negative: new Map(Object.entries(obj.n || {}))
6781
+ };
6782
+ }
6783
+ };
6784
+
6785
+ // src/handlers/EntryProcessorHandler.ts
6786
+ var import_core12 = require("@topgunbuild/core");
6787
+
6788
+ // src/ProcessorSandbox.ts
6789
+ var import_core11 = require("@topgunbuild/core");
6790
+ var ivm = null;
6791
+ try {
6792
+ ivm = require("isolated-vm");
6793
+ } catch {
6794
+ const isProduction = process.env.NODE_ENV === "production";
6795
+ if (isProduction) {
6796
+ logger.error(
6797
+ "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"
6798
+ );
6799
+ } else {
6800
+ logger.warn("isolated-vm not available, falling back to less secure VM");
6801
+ }
6802
+ }
6803
+ var DEFAULT_SANDBOX_CONFIG = {
6804
+ memoryLimitMb: 8,
6805
+ timeoutMs: 100,
6806
+ maxCachedIsolates: 100,
6807
+ strictValidation: true
6808
+ };
6809
+ var ProcessorSandbox = class {
6810
+ constructor(config = {}) {
6811
+ this.isolateCache = /* @__PURE__ */ new Map();
6812
+ this.scriptCache = /* @__PURE__ */ new Map();
6813
+ this.fallbackScriptCache = /* @__PURE__ */ new Map();
6814
+ this.disposed = false;
6815
+ this.config = { ...DEFAULT_SANDBOX_CONFIG, ...config };
6816
+ }
6817
+ /**
6818
+ * Execute an entry processor in the sandbox.
6819
+ *
6820
+ * @param processor The processor definition (name, code, args)
6821
+ * @param value The current value for the key (or undefined)
6822
+ * @param key The key being processed
6823
+ * @returns Result containing success status, result, and new value
6824
+ */
6825
+ async execute(processor, value, key) {
6826
+ if (this.disposed) {
6827
+ return {
6828
+ success: false,
6829
+ error: "Sandbox has been disposed"
6830
+ };
6831
+ }
6832
+ if (this.config.strictValidation) {
6833
+ const validation = (0, import_core11.validateProcessorCode)(processor.code);
6834
+ if (!validation.valid) {
6835
+ return {
6836
+ success: false,
6837
+ error: validation.error
6838
+ };
6839
+ }
6840
+ }
6841
+ if (ivm) {
6842
+ return this.executeInIsolate(processor, value, key);
6565
6843
  } else {
6566
- this.httpServer = (0, import_http.createServer)((_req, res) => {
6567
- res.writeHead(200);
6568
- res.end("TopGun Server Running");
6844
+ return this.executeInFallback(processor, value, key);
6845
+ }
6846
+ }
6847
+ /**
6848
+ * Execute processor in isolated-vm (secure production mode).
6849
+ */
6850
+ async executeInIsolate(processor, value, key) {
6851
+ if (!ivm) {
6852
+ return { success: false, error: "isolated-vm not available" };
6853
+ }
6854
+ const isolate = this.getOrCreateIsolate(processor.name);
6855
+ try {
6856
+ const context = await isolate.createContext();
6857
+ const jail = context.global;
6858
+ await jail.set("global", jail.derefInto());
6859
+ await context.eval(`
6860
+ var value = ${JSON.stringify(value)};
6861
+ var key = ${JSON.stringify(key)};
6862
+ var args = ${JSON.stringify(processor.args)};
6863
+ `);
6864
+ const wrappedCode = `
6865
+ (function() {
6866
+ ${processor.code}
6867
+ })()
6868
+ `;
6869
+ const script = await this.getOrCompileScript(
6870
+ processor.name,
6871
+ wrappedCode,
6872
+ isolate
6873
+ );
6874
+ const result = await script.run(context, {
6875
+ timeout: this.config.timeoutMs
6569
6876
  });
6570
- if (process.env.NODE_ENV === "production") {
6571
- logger.warn("\u26A0\uFE0F TLS is disabled! Client connections are NOT encrypted.");
6877
+ const parsed = result;
6878
+ if (typeof parsed !== "object" || parsed === null) {
6879
+ return {
6880
+ success: false,
6881
+ error: "Processor must return { value, result? } object"
6882
+ };
6572
6883
  }
6884
+ return {
6885
+ success: true,
6886
+ result: parsed.result,
6887
+ newValue: parsed.value
6888
+ };
6889
+ } catch (error) {
6890
+ const message = error instanceof Error ? error.message : String(error);
6891
+ if (message.includes("Script execution timed out")) {
6892
+ return {
6893
+ success: false,
6894
+ error: "Processor execution timed out"
6895
+ };
6896
+ }
6897
+ return {
6898
+ success: false,
6899
+ error: message
6900
+ };
6573
6901
  }
6574
- const metricsPort = config.metricsPort !== void 0 ? config.metricsPort : 9090;
6575
- this.metricsServer = (0, import_http.createServer)(async (req, res) => {
6576
- if (req.url === "/metrics") {
6577
- try {
6578
- res.setHeader("Content-Type", this.metricsService.getContentType());
6579
- res.end(await this.metricsService.getMetrics());
6580
- } catch (err) {
6581
- res.statusCode = 500;
6582
- res.end("Internal Server Error");
6902
+ }
6903
+ /**
6904
+ * Execute processor in fallback VM (less secure, for development).
6905
+ */
6906
+ async executeInFallback(processor, value, key) {
6907
+ try {
6908
+ const isResolver = processor.name.startsWith("resolver:");
6909
+ let fn = isResolver ? void 0 : this.fallbackScriptCache.get(processor.name);
6910
+ if (!fn) {
6911
+ const wrappedCode = `
6912
+ return (function(value, key, args) {
6913
+ ${processor.code}
6914
+ })
6915
+ `;
6916
+ fn = new Function(wrappedCode)();
6917
+ if (!isResolver) {
6918
+ this.fallbackScriptCache.set(processor.name, fn);
6919
+ }
6920
+ }
6921
+ const timeoutPromise = new Promise((_, reject) => {
6922
+ setTimeout(() => reject(new Error("Processor execution timed out")), this.config.timeoutMs);
6923
+ });
6924
+ const executionPromise = Promise.resolve().then(() => fn(value, key, processor.args));
6925
+ const result = await Promise.race([executionPromise, timeoutPromise]);
6926
+ if (typeof result !== "object" || result === null) {
6927
+ return {
6928
+ success: false,
6929
+ error: "Processor must return { value, result? } object"
6930
+ };
6931
+ }
6932
+ return {
6933
+ success: true,
6934
+ result: result.result,
6935
+ newValue: result.value
6936
+ };
6937
+ } catch (error) {
6938
+ const message = error instanceof Error ? error.message : String(error);
6939
+ return {
6940
+ success: false,
6941
+ error: message
6942
+ };
6943
+ }
6944
+ }
6945
+ /**
6946
+ * Get or create an isolate for a processor.
6947
+ */
6948
+ getOrCreateIsolate(name) {
6949
+ if (!ivm) {
6950
+ throw new Error("isolated-vm not available");
6951
+ }
6952
+ let isolate = this.isolateCache.get(name);
6953
+ if (!isolate || isolate.isDisposed) {
6954
+ if (this.isolateCache.size >= this.config.maxCachedIsolates) {
6955
+ const oldest = this.isolateCache.keys().next().value;
6956
+ if (oldest) {
6957
+ const oldIsolate = this.isolateCache.get(oldest);
6958
+ if (oldIsolate && !oldIsolate.isDisposed) {
6959
+ oldIsolate.dispose();
6960
+ }
6961
+ this.isolateCache.delete(oldest);
6962
+ this.scriptCache.delete(oldest);
6963
+ }
6964
+ }
6965
+ isolate = new ivm.Isolate({
6966
+ memoryLimit: this.config.memoryLimitMb
6967
+ });
6968
+ this.isolateCache.set(name, isolate);
6969
+ }
6970
+ return isolate;
6971
+ }
6972
+ /**
6973
+ * Get or compile a script for a processor.
6974
+ */
6975
+ async getOrCompileScript(name, code, isolate) {
6976
+ let script = this.scriptCache.get(name);
6977
+ if (!script) {
6978
+ script = await isolate.compileScript(code);
6979
+ this.scriptCache.set(name, script);
6980
+ }
6981
+ return script;
6982
+ }
6983
+ /**
6984
+ * Clear script cache for a specific processor (e.g., when code changes).
6985
+ */
6986
+ clearCache(processorName) {
6987
+ if (processorName) {
6988
+ const isolate = this.isolateCache.get(processorName);
6989
+ if (isolate && !isolate.isDisposed) {
6990
+ isolate.dispose();
6991
+ }
6992
+ this.isolateCache.delete(processorName);
6993
+ this.scriptCache.delete(processorName);
6994
+ this.fallbackScriptCache.delete(processorName);
6995
+ } else {
6996
+ for (const isolate of this.isolateCache.values()) {
6997
+ if (!isolate.isDisposed) {
6998
+ isolate.dispose();
6999
+ }
7000
+ }
7001
+ this.isolateCache.clear();
7002
+ this.scriptCache.clear();
7003
+ this.fallbackScriptCache.clear();
7004
+ }
7005
+ }
7006
+ /**
7007
+ * Check if using secure isolated-vm mode.
7008
+ */
7009
+ isSecureMode() {
7010
+ return ivm !== null;
7011
+ }
7012
+ /**
7013
+ * Get current cache sizes.
7014
+ */
7015
+ getCacheStats() {
7016
+ return {
7017
+ isolates: this.isolateCache.size,
7018
+ scripts: this.scriptCache.size,
7019
+ fallbackScripts: this.fallbackScriptCache.size
7020
+ };
7021
+ }
7022
+ /**
7023
+ * Dispose of all isolates and clear caches.
7024
+ */
7025
+ dispose() {
7026
+ if (this.disposed) return;
7027
+ this.disposed = true;
7028
+ this.clearCache();
7029
+ logger.debug("ProcessorSandbox disposed");
7030
+ }
7031
+ };
7032
+
7033
+ // src/handlers/EntryProcessorHandler.ts
7034
+ var EntryProcessorHandler = class {
7035
+ constructor(config) {
7036
+ this.hlc = config.hlc;
7037
+ this.sandbox = new ProcessorSandbox(config.sandboxConfig);
7038
+ }
7039
+ /**
7040
+ * Execute a processor on a single key atomically.
7041
+ *
7042
+ * @param map The LWWMap to operate on
7043
+ * @param key The key to process
7044
+ * @param processorDef The processor definition (will be validated)
7045
+ * @returns Result with success status, processor result, and new value
7046
+ */
7047
+ async executeOnKey(map, key, processorDef) {
7048
+ const parseResult = import_core12.EntryProcessorDefSchema.safeParse(processorDef);
7049
+ if (!parseResult.success) {
7050
+ logger.warn(
7051
+ { key, error: parseResult.error.message },
7052
+ "Invalid processor definition"
7053
+ );
7054
+ return {
7055
+ result: {
7056
+ success: false,
7057
+ error: `Invalid processor: ${parseResult.error.message}`
7058
+ }
7059
+ };
7060
+ }
7061
+ const processor = parseResult.data;
7062
+ const currentValue = map.get(key);
7063
+ logger.debug(
7064
+ { key, processor: processor.name, hasValue: currentValue !== void 0 },
7065
+ "Executing entry processor"
7066
+ );
7067
+ const sandboxResult = await this.sandbox.execute(
7068
+ processor,
7069
+ currentValue,
7070
+ key
7071
+ );
7072
+ if (!sandboxResult.success) {
7073
+ logger.warn(
7074
+ { key, processor: processor.name, error: sandboxResult.error },
7075
+ "Processor execution failed"
7076
+ );
7077
+ return { result: sandboxResult };
7078
+ }
7079
+ let timestamp;
7080
+ if (sandboxResult.newValue !== void 0) {
7081
+ const record = map.set(key, sandboxResult.newValue);
7082
+ timestamp = record.timestamp;
7083
+ logger.debug(
7084
+ { key, processor: processor.name, timestamp },
7085
+ "Processor updated value"
7086
+ );
7087
+ } else if (currentValue !== void 0) {
7088
+ const tombstone = map.remove(key);
7089
+ timestamp = tombstone.timestamp;
7090
+ logger.debug(
7091
+ { key, processor: processor.name, timestamp },
7092
+ "Processor deleted value"
7093
+ );
7094
+ }
7095
+ return {
7096
+ result: sandboxResult,
7097
+ timestamp
7098
+ };
7099
+ }
7100
+ /**
7101
+ * Execute a processor on multiple keys.
7102
+ *
7103
+ * Each key is processed sequentially to ensure atomicity per-key.
7104
+ * For parallel execution across keys, use multiple calls.
7105
+ *
7106
+ * @param map The LWWMap to operate on
7107
+ * @param keys The keys to process
7108
+ * @param processorDef The processor definition
7109
+ * @returns Map of key -> result
7110
+ */
7111
+ async executeOnKeys(map, keys, processorDef) {
7112
+ const results = /* @__PURE__ */ new Map();
7113
+ const timestamps = /* @__PURE__ */ new Map();
7114
+ const parseResult = import_core12.EntryProcessorDefSchema.safeParse(processorDef);
7115
+ if (!parseResult.success) {
7116
+ const errorResult = {
7117
+ success: false,
7118
+ error: `Invalid processor: ${parseResult.error.message}`
7119
+ };
7120
+ for (const key of keys) {
7121
+ results.set(key, errorResult);
7122
+ }
7123
+ return { results, timestamps };
7124
+ }
7125
+ for (const key of keys) {
7126
+ const { result, timestamp } = await this.executeOnKey(
7127
+ map,
7128
+ key,
7129
+ processorDef
7130
+ );
7131
+ results.set(key, result);
7132
+ if (timestamp) {
7133
+ timestamps.set(key, timestamp);
7134
+ }
7135
+ }
7136
+ return { results, timestamps };
7137
+ }
7138
+ /**
7139
+ * Execute a processor on all entries matching a predicate.
7140
+ *
7141
+ * WARNING: This can be expensive for large maps.
7142
+ *
7143
+ * @param map The LWWMap to operate on
7144
+ * @param processorDef The processor definition
7145
+ * @param predicateCode Optional predicate code to filter entries
7146
+ * @returns Map of key -> result for processed entries
7147
+ */
7148
+ async executeOnEntries(map, processorDef, predicateCode) {
7149
+ const results = /* @__PURE__ */ new Map();
7150
+ const timestamps = /* @__PURE__ */ new Map();
7151
+ const parseResult = import_core12.EntryProcessorDefSchema.safeParse(processorDef);
7152
+ if (!parseResult.success) {
7153
+ return { results, timestamps };
7154
+ }
7155
+ const entries = map.entries();
7156
+ for (const [key, value] of entries) {
7157
+ if (predicateCode) {
7158
+ const predicateResult = await this.sandbox.execute(
7159
+ {
7160
+ name: "_predicate",
7161
+ code: `return { value, result: (function() { ${predicateCode} })() };`
7162
+ },
7163
+ value,
7164
+ key
7165
+ );
7166
+ if (!predicateResult.success || !predicateResult.result) {
7167
+ continue;
7168
+ }
7169
+ }
7170
+ const { result, timestamp } = await this.executeOnKey(
7171
+ map,
7172
+ key,
7173
+ processorDef
7174
+ );
7175
+ results.set(key, result);
7176
+ if (timestamp) {
7177
+ timestamps.set(key, timestamp);
7178
+ }
7179
+ }
7180
+ return { results, timestamps };
7181
+ }
7182
+ /**
7183
+ * Check if sandbox is in secure mode (using isolated-vm).
7184
+ */
7185
+ isSecureMode() {
7186
+ return this.sandbox.isSecureMode();
7187
+ }
7188
+ /**
7189
+ * Get sandbox cache statistics.
7190
+ */
7191
+ getCacheStats() {
7192
+ return this.sandbox.getCacheStats();
7193
+ }
7194
+ /**
7195
+ * Clear sandbox cache.
7196
+ */
7197
+ clearCache(processorName) {
7198
+ this.sandbox.clearCache(processorName);
7199
+ }
7200
+ /**
7201
+ * Dispose of the handler and its sandbox.
7202
+ */
7203
+ dispose() {
7204
+ this.sandbox.dispose();
7205
+ logger.debug("EntryProcessorHandler disposed");
7206
+ }
7207
+ };
7208
+
7209
+ // src/ConflictResolverService.ts
7210
+ var import_core13 = require("@topgunbuild/core");
7211
+ var DEFAULT_CONFLICT_RESOLVER_CONFIG = {
7212
+ maxResolversPerMap: 100,
7213
+ enableSandboxedResolvers: true,
7214
+ resolverTimeoutMs: 100
7215
+ };
7216
+ var ConflictResolverService = class {
7217
+ constructor(sandbox, config = {}) {
7218
+ this.resolvers = /* @__PURE__ */ new Map();
7219
+ this.disposed = false;
7220
+ this.sandbox = sandbox;
7221
+ this.config = { ...DEFAULT_CONFLICT_RESOLVER_CONFIG, ...config };
7222
+ }
7223
+ /**
7224
+ * Set callback for merge rejections.
7225
+ */
7226
+ onRejection(callback) {
7227
+ this.onRejectionCallback = callback;
7228
+ }
7229
+ /**
7230
+ * Register a resolver for a map.
7231
+ *
7232
+ * @param mapName The map this resolver applies to
7233
+ * @param resolver The resolver definition
7234
+ * @param registeredBy Optional client ID that registered this resolver
7235
+ */
7236
+ register(mapName, resolver, registeredBy) {
7237
+ if (this.disposed) {
7238
+ throw new Error("ConflictResolverService has been disposed");
7239
+ }
7240
+ if (resolver.code) {
7241
+ const parsed = import_core13.ConflictResolverDefSchema.safeParse({
7242
+ name: resolver.name,
7243
+ code: resolver.code,
7244
+ priority: resolver.priority,
7245
+ keyPattern: resolver.keyPattern
7246
+ });
7247
+ if (!parsed.success) {
7248
+ throw new Error(`Invalid resolver definition: ${parsed.error.message}`);
7249
+ }
7250
+ const validation = (0, import_core13.validateResolverCode)(resolver.code);
7251
+ if (!validation.valid) {
7252
+ throw new Error(`Invalid resolver code: ${validation.error}`);
7253
+ }
7254
+ }
7255
+ const entries = this.resolvers.get(mapName) ?? [];
7256
+ if (entries.length >= this.config.maxResolversPerMap) {
7257
+ throw new Error(
7258
+ `Maximum resolvers per map (${this.config.maxResolversPerMap}) exceeded`
7259
+ );
7260
+ }
7261
+ const filtered = entries.filter((e) => e.resolver.name !== resolver.name);
7262
+ const entry = {
7263
+ resolver,
7264
+ registeredBy
7265
+ };
7266
+ if (resolver.code && !resolver.fn && this.config.enableSandboxedResolvers) {
7267
+ entry.compiledFn = this.compileSandboxed(resolver.name, resolver.code);
7268
+ }
7269
+ filtered.push(entry);
7270
+ filtered.sort(
7271
+ (a, b) => (b.resolver.priority ?? 50) - (a.resolver.priority ?? 50)
7272
+ );
7273
+ this.resolvers.set(mapName, filtered);
7274
+ logger.debug(
7275
+ `Registered resolver '${resolver.name}' for map '${mapName}' with priority ${resolver.priority ?? 50}`
7276
+ );
7277
+ }
7278
+ /**
7279
+ * Unregister a resolver.
7280
+ *
7281
+ * @param mapName The map name
7282
+ * @param resolverName The resolver name to unregister
7283
+ * @param clientId Optional - only unregister if registered by this client
7284
+ */
7285
+ unregister(mapName, resolverName, clientId) {
7286
+ const entries = this.resolvers.get(mapName);
7287
+ if (!entries) return false;
7288
+ const entryIndex = entries.findIndex(
7289
+ (e) => e.resolver.name === resolverName && (!clientId || e.registeredBy === clientId)
7290
+ );
7291
+ if (entryIndex === -1) return false;
7292
+ entries.splice(entryIndex, 1);
7293
+ if (entries.length === 0) {
7294
+ this.resolvers.delete(mapName);
7295
+ }
7296
+ logger.debug(`Unregistered resolver '${resolverName}' from map '${mapName}'`);
7297
+ return true;
7298
+ }
7299
+ /**
7300
+ * Resolve a merge conflict using registered resolvers.
7301
+ *
7302
+ * @param context The merge context
7303
+ * @returns The merge result
7304
+ */
7305
+ async resolve(context) {
7306
+ if (this.disposed) {
7307
+ return { action: "accept", value: context.remoteValue };
7308
+ }
7309
+ const entries = this.resolvers.get(context.mapName) ?? [];
7310
+ const allEntries = [
7311
+ ...entries,
7312
+ { resolver: import_core13.BuiltInResolvers.LWW() }
7313
+ ];
7314
+ for (const entry of allEntries) {
7315
+ const { resolver } = entry;
7316
+ if (resolver.keyPattern && !this.matchKeyPattern(context.key, resolver.keyPattern)) {
7317
+ continue;
7318
+ }
7319
+ try {
7320
+ let result;
7321
+ if (resolver.fn) {
7322
+ const fn = resolver.fn;
7323
+ const maybePromise = fn(context);
7324
+ result = maybePromise instanceof Promise ? await maybePromise : maybePromise;
7325
+ } else if (entry.compiledFn) {
7326
+ const compiledFn = entry.compiledFn;
7327
+ result = await compiledFn(context);
7328
+ } else {
7329
+ continue;
7330
+ }
7331
+ if (result.action !== "local") {
7332
+ if (result.action === "reject") {
7333
+ logger.debug(
7334
+ `Resolver '${resolver.name}' rejected merge for key '${context.key}' in map '${context.mapName}': ${result.reason}`
7335
+ );
7336
+ if (this.onRejectionCallback) {
7337
+ this.onRejectionCallback({
7338
+ mapName: context.mapName,
7339
+ key: context.key,
7340
+ attemptedValue: context.remoteValue,
7341
+ reason: result.reason,
7342
+ timestamp: context.remoteTimestamp,
7343
+ nodeId: context.remoteNodeId
7344
+ });
7345
+ }
7346
+ }
7347
+ return result;
7348
+ }
7349
+ } catch (error) {
7350
+ const message = error instanceof Error ? error.message : String(error);
7351
+ logger.error(`Resolver '${resolver.name}' threw error: ${message}`);
7352
+ }
7353
+ }
7354
+ return { action: "accept", value: context.remoteValue };
7355
+ }
7356
+ /**
7357
+ * List registered resolvers.
7358
+ *
7359
+ * @param mapName Optional - filter by map name
7360
+ */
7361
+ list(mapName) {
7362
+ const result = [];
7363
+ if (mapName) {
7364
+ const entries = this.resolvers.get(mapName) ?? [];
7365
+ for (const entry of entries) {
7366
+ result.push({
7367
+ mapName,
7368
+ name: entry.resolver.name,
7369
+ priority: entry.resolver.priority,
7370
+ keyPattern: entry.resolver.keyPattern,
7371
+ registeredBy: entry.registeredBy
7372
+ });
7373
+ }
7374
+ } else {
7375
+ for (const [map, entries] of this.resolvers.entries()) {
7376
+ for (const entry of entries) {
7377
+ result.push({
7378
+ mapName: map,
7379
+ name: entry.resolver.name,
7380
+ priority: entry.resolver.priority,
7381
+ keyPattern: entry.resolver.keyPattern,
7382
+ registeredBy: entry.registeredBy
7383
+ });
7384
+ }
7385
+ }
7386
+ }
7387
+ return result;
7388
+ }
7389
+ /**
7390
+ * Check if a map has any registered resolvers.
7391
+ */
7392
+ hasResolvers(mapName) {
7393
+ const entries = this.resolvers.get(mapName);
7394
+ return entries !== void 0 && entries.length > 0;
7395
+ }
7396
+ /**
7397
+ * Get the number of registered resolvers.
7398
+ */
7399
+ get size() {
7400
+ let count = 0;
7401
+ for (const entries of this.resolvers.values()) {
7402
+ count += entries.length;
7403
+ }
7404
+ return count;
7405
+ }
7406
+ /**
7407
+ * Clear all registered resolvers.
7408
+ *
7409
+ * @param mapName Optional - only clear resolvers for specific map
7410
+ */
7411
+ clear(mapName) {
7412
+ if (mapName) {
7413
+ this.resolvers.delete(mapName);
7414
+ } else {
7415
+ this.resolvers.clear();
7416
+ }
7417
+ }
7418
+ /**
7419
+ * Clear resolvers registered by a specific client.
7420
+ */
7421
+ clearByClient(clientId) {
7422
+ let removed = 0;
7423
+ for (const [mapName, entries] of this.resolvers.entries()) {
7424
+ const before = entries.length;
7425
+ const filtered = entries.filter((e) => e.registeredBy !== clientId);
7426
+ removed += before - filtered.length;
7427
+ if (filtered.length === 0) {
7428
+ this.resolvers.delete(mapName);
7429
+ } else if (filtered.length !== before) {
7430
+ this.resolvers.set(mapName, filtered);
7431
+ }
7432
+ }
7433
+ return removed;
7434
+ }
7435
+ /**
7436
+ * Dispose the service.
7437
+ */
7438
+ dispose() {
7439
+ if (this.disposed) return;
7440
+ this.disposed = true;
7441
+ this.resolvers.clear();
7442
+ logger.debug("ConflictResolverService disposed");
7443
+ }
7444
+ /**
7445
+ * Match a key against a glob-like pattern.
7446
+ * Supports * (any chars) and ? (single char).
7447
+ */
7448
+ matchKeyPattern(key, pattern) {
7449
+ const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
7450
+ const regex = new RegExp(`^${regexPattern}$`);
7451
+ return regex.test(key);
7452
+ }
7453
+ /**
7454
+ * Compile sandboxed resolver code.
7455
+ */
7456
+ compileSandboxed(name, code) {
7457
+ return async (ctx) => {
7458
+ const wrappedCode = `
7459
+ const context = {
7460
+ mapName: ${JSON.stringify(ctx.mapName)},
7461
+ key: ${JSON.stringify(ctx.key)},
7462
+ localValue: ${JSON.stringify(ctx.localValue)},
7463
+ remoteValue: ${JSON.stringify(ctx.remoteValue)},
7464
+ localTimestamp: ${JSON.stringify(ctx.localTimestamp)},
7465
+ remoteTimestamp: ${JSON.stringify(ctx.remoteTimestamp)},
7466
+ remoteNodeId: ${JSON.stringify(ctx.remoteNodeId)},
7467
+ auth: ${JSON.stringify(ctx.auth)},
7468
+ };
7469
+
7470
+ function resolve(context) {
7471
+ ${code}
7472
+ }
7473
+
7474
+ const result = resolve(context);
7475
+ return { value: result, result };
7476
+ `;
7477
+ const result = await this.sandbox.execute(
7478
+ {
7479
+ name: `resolver:${name}`,
7480
+ code: wrappedCode
7481
+ },
7482
+ null,
7483
+ // value parameter unused for resolvers
7484
+ "resolver"
7485
+ );
7486
+ if (!result.success) {
7487
+ throw new Error(result.error || "Resolver execution failed");
7488
+ }
7489
+ const resolverResult = result.result;
7490
+ if (!resolverResult || typeof resolverResult !== "object") {
7491
+ throw new Error("Resolver must return a result object");
7492
+ }
7493
+ const action = resolverResult.action;
7494
+ if (!["accept", "reject", "merge", "local"].includes(action)) {
7495
+ throw new Error(`Invalid resolver action: ${action}`);
7496
+ }
7497
+ return resolverResult;
7498
+ };
7499
+ }
7500
+ };
7501
+
7502
+ // src/handlers/ConflictResolverHandler.ts
7503
+ var ConflictResolverHandler = class {
7504
+ constructor(config) {
7505
+ this.rejectionListeners = /* @__PURE__ */ new Set();
7506
+ this.nodeId = config.nodeId;
7507
+ this.sandbox = new ProcessorSandbox(config.sandboxConfig);
7508
+ this.resolverService = new ConflictResolverService(
7509
+ this.sandbox,
7510
+ config.resolverConfig
7511
+ );
7512
+ this.resolverService.onRejection((rejection) => {
7513
+ for (const listener of this.rejectionListeners) {
7514
+ try {
7515
+ listener(rejection);
7516
+ } catch (e) {
7517
+ logger.error({ error: e }, "Error in rejection listener");
7518
+ }
7519
+ }
7520
+ });
7521
+ }
7522
+ /**
7523
+ * Register a conflict resolver for a map.
7524
+ *
7525
+ * @param mapName The map name
7526
+ * @param resolver The resolver definition
7527
+ * @param clientId Optional client ID that registered this resolver
7528
+ */
7529
+ registerResolver(mapName, resolver, clientId) {
7530
+ this.resolverService.register(mapName, resolver, clientId);
7531
+ logger.info(
7532
+ {
7533
+ mapName,
7534
+ resolverName: resolver.name,
7535
+ priority: resolver.priority,
7536
+ clientId
7537
+ },
7538
+ "Resolver registered"
7539
+ );
7540
+ }
7541
+ /**
7542
+ * Unregister a conflict resolver.
7543
+ *
7544
+ * @param mapName The map name
7545
+ * @param resolverName The resolver name
7546
+ * @param clientId Optional - only unregister if registered by this client
7547
+ */
7548
+ unregisterResolver(mapName, resolverName, clientId) {
7549
+ const removed = this.resolverService.unregister(
7550
+ mapName,
7551
+ resolverName,
7552
+ clientId
7553
+ );
7554
+ if (removed) {
7555
+ logger.info({ mapName, resolverName, clientId }, "Resolver unregistered");
7556
+ }
7557
+ return removed;
7558
+ }
7559
+ /**
7560
+ * List registered resolvers.
7561
+ *
7562
+ * @param mapName Optional - filter by map name
7563
+ */
7564
+ listResolvers(mapName) {
7565
+ return this.resolverService.list(mapName);
7566
+ }
7567
+ /**
7568
+ * Apply a merge with conflict resolution.
7569
+ *
7570
+ * Deletions (tombstones) are also passed through resolvers to allow
7571
+ * protection via IMMUTABLE, OWNER_ONLY, or similar resolvers.
7572
+ * If no custom resolvers are registered, deletions use standard LWW.
7573
+ *
7574
+ * @param map The LWWMap to merge into
7575
+ * @param mapName The map name (for resolver lookup)
7576
+ * @param key The key being merged
7577
+ * @param record The incoming record
7578
+ * @param remoteNodeId The source node ID
7579
+ * @param auth Optional authentication context
7580
+ */
7581
+ async mergeWithResolver(map, mapName, key, record, remoteNodeId, auth) {
7582
+ const isDeletion = record.value === null;
7583
+ const localRecord = map.getRecord(key);
7584
+ const context = {
7585
+ mapName,
7586
+ key,
7587
+ localValue: localRecord?.value ?? void 0,
7588
+ // For deletions, remoteValue is null - resolvers can check this
7589
+ remoteValue: record.value,
7590
+ localTimestamp: localRecord?.timestamp,
7591
+ remoteTimestamp: record.timestamp,
7592
+ remoteNodeId,
7593
+ auth,
7594
+ readEntry: (k) => map.get(k)
7595
+ };
7596
+ const result = await this.resolverService.resolve(context);
7597
+ switch (result.action) {
7598
+ case "accept":
7599
+ case "merge": {
7600
+ const finalValue = isDeletion ? null : result.value;
7601
+ const finalRecord = {
7602
+ value: finalValue,
7603
+ timestamp: record.timestamp,
7604
+ ttlMs: record.ttlMs
7605
+ };
7606
+ map.merge(key, finalRecord);
7607
+ return { applied: true, result, record: finalRecord };
7608
+ }
7609
+ case "reject": {
7610
+ const rejection = {
7611
+ mapName,
7612
+ key,
7613
+ attemptedValue: record.value,
7614
+ reason: result.reason,
7615
+ timestamp: record.timestamp,
7616
+ nodeId: remoteNodeId
7617
+ };
7618
+ return { applied: false, result, rejection };
7619
+ }
7620
+ case "local":
7621
+ default:
7622
+ return { applied: false, result };
7623
+ }
7624
+ }
7625
+ /**
7626
+ * Check if a map has custom resolvers registered.
7627
+ */
7628
+ hasResolvers(mapName) {
7629
+ return this.resolverService.hasResolvers(mapName);
7630
+ }
7631
+ /**
7632
+ * Add a listener for merge rejections.
7633
+ */
7634
+ onRejection(listener) {
7635
+ this.rejectionListeners.add(listener);
7636
+ return () => this.rejectionListeners.delete(listener);
7637
+ }
7638
+ /**
7639
+ * Clear resolvers registered by a specific client.
7640
+ */
7641
+ clearByClient(clientId) {
7642
+ return this.resolverService.clearByClient(clientId);
7643
+ }
7644
+ /**
7645
+ * Get the number of registered resolvers.
7646
+ */
7647
+ get resolverCount() {
7648
+ return this.resolverService.size;
7649
+ }
7650
+ /**
7651
+ * Check if sandbox is in secure mode.
7652
+ */
7653
+ isSecureMode() {
7654
+ return this.sandbox.isSecureMode();
7655
+ }
7656
+ /**
7657
+ * Dispose of the handler.
7658
+ */
7659
+ dispose() {
7660
+ this.resolverService.dispose();
7661
+ this.sandbox.dispose();
7662
+ this.rejectionListeners.clear();
7663
+ logger.debug("ConflictResolverHandler disposed");
7664
+ }
7665
+ };
7666
+
7667
+ // src/EventJournalService.ts
7668
+ var import_core14 = require("@topgunbuild/core");
7669
+ var DEFAULT_JOURNAL_SERVICE_CONFIG = {
7670
+ ...import_core14.DEFAULT_EVENT_JOURNAL_CONFIG,
7671
+ tableName: "event_journal",
7672
+ persistBatchSize: 100,
7673
+ persistIntervalMs: 1e3
7674
+ };
7675
+ var TABLE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
7676
+ function validateTableName(name) {
7677
+ if (!TABLE_NAME_REGEX.test(name)) {
7678
+ throw new Error(
7679
+ `Invalid table name "${name}". Table name must start with a letter or underscore and contain only alphanumeric characters and underscores.`
7680
+ );
7681
+ }
7682
+ }
7683
+ var EventJournalService = class extends import_core14.EventJournalImpl {
7684
+ constructor(config) {
7685
+ super(config);
7686
+ this.pendingPersist = [];
7687
+ this.isPersisting = false;
7688
+ this.isInitialized = false;
7689
+ this.isLoadingFromStorage = false;
7690
+ this.pool = config.pool;
7691
+ this.tableName = config.tableName ?? DEFAULT_JOURNAL_SERVICE_CONFIG.tableName;
7692
+ this.persistBatchSize = config.persistBatchSize ?? DEFAULT_JOURNAL_SERVICE_CONFIG.persistBatchSize;
7693
+ this.persistIntervalMs = config.persistIntervalMs ?? DEFAULT_JOURNAL_SERVICE_CONFIG.persistIntervalMs;
7694
+ validateTableName(this.tableName);
7695
+ this.subscribe((event) => {
7696
+ if (this.isLoadingFromStorage) return;
7697
+ if (event.sequence >= 0n && this.getConfig().persistent) {
7698
+ this.pendingPersist.push(event);
7699
+ if (this.pendingPersist.length >= this.persistBatchSize) {
7700
+ this.persistToStorage().catch((err) => {
7701
+ logger.error({ err }, "Failed to persist journal events");
7702
+ });
7703
+ }
7704
+ }
7705
+ });
7706
+ this.startPersistTimer();
7707
+ }
7708
+ /**
7709
+ * Initialize the journal service, creating table if needed.
7710
+ */
7711
+ async initialize() {
7712
+ if (this.isInitialized) return;
7713
+ const client = await this.pool.connect();
7714
+ try {
7715
+ await client.query(`
7716
+ CREATE TABLE IF NOT EXISTS ${this.tableName} (
7717
+ sequence BIGINT PRIMARY KEY,
7718
+ type VARCHAR(10) NOT NULL CHECK (type IN ('PUT', 'UPDATE', 'DELETE')),
7719
+ map_name VARCHAR(255) NOT NULL,
7720
+ key VARCHAR(1024) NOT NULL,
7721
+ value JSONB,
7722
+ previous_value JSONB,
7723
+ timestamp JSONB NOT NULL,
7724
+ node_id VARCHAR(64) NOT NULL,
7725
+ metadata JSONB,
7726
+ created_at TIMESTAMPTZ DEFAULT NOW()
7727
+ );
7728
+ `);
7729
+ await client.query(`
7730
+ CREATE INDEX IF NOT EXISTS idx_${this.tableName}_map_name
7731
+ ON ${this.tableName}(map_name);
7732
+ `);
7733
+ await client.query(`
7734
+ CREATE INDEX IF NOT EXISTS idx_${this.tableName}_key
7735
+ ON ${this.tableName}(map_name, key);
7736
+ `);
7737
+ await client.query(`
7738
+ CREATE INDEX IF NOT EXISTS idx_${this.tableName}_created_at
7739
+ ON ${this.tableName}(created_at);
7740
+ `);
7741
+ await client.query(`
7742
+ CREATE INDEX IF NOT EXISTS idx_${this.tableName}_node_id
7743
+ ON ${this.tableName}(node_id);
7744
+ `);
7745
+ this.isInitialized = true;
7746
+ logger.info({ tableName: this.tableName }, "EventJournalService initialized");
7747
+ } finally {
7748
+ client.release();
7749
+ }
7750
+ }
7751
+ /**
7752
+ * Persist pending events to PostgreSQL.
7753
+ */
7754
+ async persistToStorage() {
7755
+ if (this.pendingPersist.length === 0 || this.isPersisting) return;
7756
+ this.isPersisting = true;
7757
+ const batch = this.pendingPersist.splice(0, this.persistBatchSize);
7758
+ try {
7759
+ if (batch.length === 0) return;
7760
+ const values = [];
7761
+ const placeholders = [];
7762
+ batch.forEach((e, i) => {
7763
+ const offset = i * 9;
7764
+ placeholders.push(
7765
+ `($${offset + 1}, $${offset + 2}, $${offset + 3}, $${offset + 4}, $${offset + 5}, $${offset + 6}, $${offset + 7}, $${offset + 8}, $${offset + 9})`
7766
+ );
7767
+ values.push(
7768
+ e.sequence.toString(),
7769
+ e.type,
7770
+ e.mapName,
7771
+ e.key,
7772
+ e.value !== void 0 ? JSON.stringify(e.value) : null,
7773
+ e.previousValue !== void 0 ? JSON.stringify(e.previousValue) : null,
7774
+ JSON.stringify(e.timestamp),
7775
+ e.nodeId,
7776
+ e.metadata ? JSON.stringify(e.metadata) : null
7777
+ );
7778
+ });
7779
+ await this.pool.query(
7780
+ `INSERT INTO ${this.tableName}
7781
+ (sequence, type, map_name, key, value, previous_value, timestamp, node_id, metadata)
7782
+ VALUES ${placeholders.join(", ")}
7783
+ ON CONFLICT (sequence) DO NOTHING`,
7784
+ values
7785
+ );
7786
+ logger.debug({ count: batch.length }, "Persisted journal events");
7787
+ } catch (error) {
7788
+ this.pendingPersist.unshift(...batch);
7789
+ throw error;
7790
+ } finally {
7791
+ this.isPersisting = false;
7792
+ }
7793
+ }
7794
+ /**
7795
+ * Load journal events from PostgreSQL on startup.
7796
+ */
7797
+ async loadFromStorage() {
7798
+ const config = this.getConfig();
7799
+ const result = await this.pool.query(
7800
+ `SELECT sequence, type, map_name, key, value, previous_value, timestamp, node_id, metadata
7801
+ FROM ${this.tableName}
7802
+ ORDER BY sequence DESC
7803
+ LIMIT $1`,
7804
+ [config.capacity]
7805
+ );
7806
+ const events = result.rows.reverse();
7807
+ this.isLoadingFromStorage = true;
7808
+ try {
7809
+ for (const row of events) {
7810
+ this.append({
7811
+ type: row.type,
7812
+ mapName: row.map_name,
7813
+ key: row.key,
7814
+ value: row.value,
7815
+ previousValue: row.previous_value,
7816
+ timestamp: typeof row.timestamp === "string" ? JSON.parse(row.timestamp) : row.timestamp,
7817
+ nodeId: row.node_id,
7818
+ metadata: row.metadata
7819
+ });
7820
+ }
7821
+ } finally {
7822
+ this.isLoadingFromStorage = false;
7823
+ }
7824
+ logger.info({ count: events.length }, "Loaded journal events from storage");
7825
+ }
7826
+ /**
7827
+ * Export events as NDJSON stream.
7828
+ */
7829
+ exportStream(options = {}) {
7830
+ const self = this;
7831
+ return new ReadableStream({
7832
+ start(controller) {
7833
+ const startSeq = options.fromSequence ?? self.getOldestSequence();
7834
+ const endSeq = options.toSequence ?? self.getLatestSequence();
7835
+ for (let seq = startSeq; seq <= endSeq; seq++) {
7836
+ const events = self.readFrom(seq, 1);
7837
+ if (events.length > 0) {
7838
+ const event = events[0];
7839
+ if (options.mapName && event.mapName !== options.mapName) continue;
7840
+ if (options.types && !options.types.includes(event.type)) continue;
7841
+ const serializable = {
7842
+ ...event,
7843
+ sequence: event.sequence.toString()
7844
+ };
7845
+ controller.enqueue(JSON.stringify(serializable) + "\n");
7846
+ }
7847
+ }
7848
+ controller.close();
7849
+ }
7850
+ });
7851
+ }
7852
+ /**
7853
+ * Get events for a specific map.
7854
+ */
7855
+ getMapEvents(mapName, fromSeq) {
7856
+ const events = this.readFrom(fromSeq ?? this.getOldestSequence(), this.getConfig().capacity);
7857
+ return events.filter((e) => e.mapName === mapName);
7858
+ }
7859
+ /**
7860
+ * Query events from PostgreSQL with filters.
7861
+ */
7862
+ async queryFromStorage(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.key) {
7871
+ conditions.push(`key = $${paramIndex++}`);
7872
+ params.push(options.key);
7873
+ }
7874
+ if (options.types && options.types.length > 0) {
7875
+ conditions.push(`type = ANY($${paramIndex++})`);
7876
+ params.push(options.types);
7877
+ }
7878
+ if (options.fromSequence !== void 0) {
7879
+ conditions.push(`sequence >= $${paramIndex++}`);
7880
+ params.push(options.fromSequence.toString());
7881
+ }
7882
+ if (options.toSequence !== void 0) {
7883
+ conditions.push(`sequence <= $${paramIndex++}`);
7884
+ params.push(options.toSequence.toString());
7885
+ }
7886
+ if (options.fromDate) {
7887
+ conditions.push(`created_at >= $${paramIndex++}`);
7888
+ params.push(options.fromDate);
7889
+ }
7890
+ if (options.toDate) {
7891
+ conditions.push(`created_at <= $${paramIndex++}`);
7892
+ params.push(options.toDate);
7893
+ }
7894
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
7895
+ const limit = options.limit ?? 100;
7896
+ const offset = options.offset ?? 0;
7897
+ const result = await this.pool.query(
7898
+ `SELECT sequence, type, map_name, key, value, previous_value, timestamp, node_id, metadata
7899
+ FROM ${this.tableName}
7900
+ ${whereClause}
7901
+ ORDER BY sequence ASC
7902
+ LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
7903
+ [...params, limit, offset]
7904
+ );
7905
+ return result.rows.map((row) => ({
7906
+ sequence: BigInt(row.sequence),
7907
+ type: row.type,
7908
+ mapName: row.map_name,
7909
+ key: row.key,
7910
+ value: row.value,
7911
+ previousValue: row.previous_value,
7912
+ timestamp: typeof row.timestamp === "string" ? JSON.parse(row.timestamp) : row.timestamp,
7913
+ nodeId: row.node_id,
7914
+ metadata: row.metadata
7915
+ }));
7916
+ }
7917
+ /**
7918
+ * Count events matching filters.
7919
+ */
7920
+ async countFromStorage(options = {}) {
7921
+ const conditions = [];
7922
+ const params = [];
7923
+ let paramIndex = 1;
7924
+ if (options.mapName) {
7925
+ conditions.push(`map_name = $${paramIndex++}`);
7926
+ params.push(options.mapName);
7927
+ }
7928
+ if (options.types && options.types.length > 0) {
7929
+ conditions.push(`type = ANY($${paramIndex++})`);
7930
+ params.push(options.types);
7931
+ }
7932
+ if (options.fromDate) {
7933
+ conditions.push(`created_at >= $${paramIndex++}`);
7934
+ params.push(options.fromDate);
7935
+ }
7936
+ if (options.toDate) {
7937
+ conditions.push(`created_at <= $${paramIndex++}`);
7938
+ params.push(options.toDate);
7939
+ }
7940
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
7941
+ const result = await this.pool.query(
7942
+ `SELECT COUNT(*) as count FROM ${this.tableName} ${whereClause}`,
7943
+ params
7944
+ );
7945
+ return parseInt(result.rows[0].count, 10);
7946
+ }
7947
+ /**
7948
+ * Cleanup old events based on retention policy.
7949
+ */
7950
+ async cleanupOldEvents(retentionDays) {
7951
+ const result = await this.pool.query(
7952
+ `DELETE FROM ${this.tableName}
7953
+ WHERE created_at < NOW() - ($1 || ' days')::INTERVAL
7954
+ RETURNING sequence`,
7955
+ [retentionDays]
7956
+ );
7957
+ const count = result.rowCount ?? 0;
7958
+ if (count > 0) {
7959
+ logger.info({ deletedCount: count, retentionDays }, "Cleaned up old journal events");
7960
+ }
7961
+ return count;
7962
+ }
7963
+ /**
7964
+ * Start the periodic persistence timer.
7965
+ */
7966
+ startPersistTimer() {
7967
+ this.persistTimer = setInterval(() => {
7968
+ if (this.pendingPersist.length > 0) {
7969
+ this.persistToStorage().catch((err) => {
7970
+ logger.error({ err }, "Periodic persist failed");
7971
+ });
7972
+ }
7973
+ }, this.persistIntervalMs);
7974
+ }
7975
+ /**
7976
+ * Stop the periodic persistence timer.
7977
+ */
7978
+ stopPersistTimer() {
7979
+ if (this.persistTimer) {
7980
+ clearInterval(this.persistTimer);
7981
+ this.persistTimer = void 0;
7982
+ }
7983
+ }
7984
+ /**
7985
+ * Dispose resources and persist remaining events.
7986
+ */
7987
+ dispose() {
7988
+ this.stopPersistTimer();
7989
+ if (this.pendingPersist.length > 0) {
7990
+ this.persistToStorage().catch((err) => {
7991
+ logger.error({ err }, "Final persist failed on dispose");
7992
+ });
7993
+ }
7994
+ super.dispose();
7995
+ }
7996
+ /**
7997
+ * Get pending persist count (for monitoring).
7998
+ */
7999
+ getPendingPersistCount() {
8000
+ return this.pendingPersist.length;
8001
+ }
8002
+ };
8003
+
8004
+ // src/ServerCoordinator.ts
8005
+ var GC_INTERVAL_MS = 60 * 60 * 1e3;
8006
+ var GC_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
8007
+ var CLIENT_HEARTBEAT_TIMEOUT_MS = 2e4;
8008
+ var CLIENT_HEARTBEAT_CHECK_INTERVAL_MS = 5e3;
8009
+ var ServerCoordinator = class {
8010
+ constructor(config) {
8011
+ this.clients = /* @__PURE__ */ new Map();
8012
+ // Interceptors
8013
+ this.interceptors = [];
8014
+ // In-memory storage (partitioned later)
8015
+ this.maps = /* @__PURE__ */ new Map();
8016
+ this.pendingClusterQueries = /* @__PURE__ */ new Map();
8017
+ // GC Consensus State
8018
+ this.gcReports = /* @__PURE__ */ new Map();
8019
+ // Track map loading state to avoid returning empty results during async load
8020
+ this.mapLoadingPromises = /* @__PURE__ */ new Map();
8021
+ // Track pending batch operations for testing purposes
8022
+ this.pendingBatchOperations = /* @__PURE__ */ new Set();
8023
+ this.journalSubscriptions = /* @__PURE__ */ new Map();
8024
+ this._actualPort = 0;
8025
+ this._actualClusterPort = 0;
8026
+ this._readyPromise = new Promise((resolve) => {
8027
+ this._readyResolve = resolve;
8028
+ });
8029
+ this._nodeId = config.nodeId;
8030
+ this.hlc = new import_core15.HLC(config.nodeId);
8031
+ this.storage = config.storage;
8032
+ const rawSecret = config.jwtSecret || process.env.JWT_SECRET || "topgun-secret-dev";
8033
+ this.jwtSecret = rawSecret.replace(/\\n/g, "\n");
8034
+ this.queryRegistry = new QueryRegistry();
8035
+ this.securityManager = new SecurityManager(config.securityPolicies || []);
8036
+ this.interceptors = config.interceptors || [];
8037
+ this.metricsService = new MetricsService();
8038
+ this.eventExecutor = new StripedEventExecutor({
8039
+ stripeCount: config.eventStripeCount ?? 4,
8040
+ queueCapacity: config.eventQueueCapacity ?? 1e4,
8041
+ name: `${config.nodeId}-event-executor`,
8042
+ onReject: (task) => {
8043
+ logger.warn({ nodeId: config.nodeId, key: task.key }, "Event task rejected due to queue capacity");
8044
+ this.metricsService.incEventQueueRejected();
8045
+ }
8046
+ });
8047
+ this.backpressure = new BackpressureRegulator({
8048
+ syncFrequency: config.backpressureSyncFrequency ?? 100,
8049
+ maxPendingOps: config.backpressureMaxPending ?? 1e3,
8050
+ backoffTimeoutMs: config.backpressureBackoffMs ?? 5e3,
8051
+ enabled: config.backpressureEnabled ?? true
8052
+ });
8053
+ this.writeCoalescingEnabled = config.writeCoalescingEnabled ?? true;
8054
+ const preset = coalescingPresets[config.writeCoalescingPreset ?? "highThroughput"];
8055
+ this.writeCoalescingOptions = {
8056
+ maxBatchSize: config.writeCoalescingMaxBatch ?? preset.maxBatchSize,
8057
+ maxDelayMs: config.writeCoalescingMaxDelayMs ?? preset.maxDelayMs,
8058
+ maxBatchBytes: config.writeCoalescingMaxBytes ?? preset.maxBatchBytes
8059
+ };
8060
+ this.eventPayloadPool = createEventPayloadPool({
8061
+ maxSize: 4096,
8062
+ initialSize: 128
8063
+ });
8064
+ this.taskletScheduler = new TaskletScheduler({
8065
+ defaultTimeBudgetMs: 5,
8066
+ maxConcurrent: 20
8067
+ });
8068
+ this.writeAckManager = new WriteAckManager({
8069
+ defaultTimeout: config.writeAckTimeout ?? 5e3
8070
+ });
8071
+ this.rateLimitingEnabled = config.rateLimitingEnabled ?? true;
8072
+ this.rateLimiter = new ConnectionRateLimiter({
8073
+ maxConnectionsPerSecond: config.maxConnectionsPerSecond ?? 100,
8074
+ maxPendingConnections: config.maxPendingConnections ?? 1e3,
8075
+ cooldownMs: 1e3
8076
+ });
8077
+ if (config.workerPoolEnabled) {
8078
+ this.workerPool = new WorkerPool({
8079
+ minWorkers: config.workerPoolConfig?.minWorkers ?? 2,
8080
+ maxWorkers: config.workerPoolConfig?.maxWorkers,
8081
+ taskTimeout: config.workerPoolConfig?.taskTimeout ?? 5e3,
8082
+ idleTimeout: config.workerPoolConfig?.idleTimeout ?? 3e4,
8083
+ autoRestart: config.workerPoolConfig?.autoRestart ?? true
8084
+ });
8085
+ this.merkleWorker = new MerkleWorker(this.workerPool);
8086
+ this.crdtMergeWorker = new CRDTMergeWorker(this.workerPool);
8087
+ this.serializationWorker = new SerializationWorker(this.workerPool);
8088
+ logger.info({
8089
+ minWorkers: config.workerPoolConfig?.minWorkers ?? 2,
8090
+ maxWorkers: config.workerPoolConfig?.maxWorkers ?? "auto"
8091
+ }, "Worker pool initialized for CPU-bound operations");
8092
+ }
8093
+ if (config.tls?.enabled) {
8094
+ const tlsOptions = this.buildTLSOptions(config.tls);
8095
+ this.httpServer = (0, import_https.createServer)(tlsOptions, (_req, res) => {
8096
+ res.writeHead(200);
8097
+ res.end("TopGun Server Running (Secure)");
8098
+ });
8099
+ logger.info("TLS enabled for client connections");
8100
+ } else {
8101
+ this.httpServer = (0, import_http.createServer)((_req, res) => {
8102
+ res.writeHead(200);
8103
+ res.end("TopGun Server Running");
8104
+ });
8105
+ if (process.env.NODE_ENV === "production") {
8106
+ logger.warn("\u26A0\uFE0F TLS is disabled! Client connections are NOT encrypted.");
8107
+ }
8108
+ }
8109
+ const metricsPort = config.metricsPort !== void 0 ? config.metricsPort : 9090;
8110
+ this.metricsServer = (0, import_http.createServer)(async (req, res) => {
8111
+ if (req.url === "/metrics") {
8112
+ try {
8113
+ res.setHeader("Content-Type", this.metricsService.getContentType());
8114
+ res.end(await this.metricsService.getMetrics());
8115
+ } catch (err) {
8116
+ res.statusCode = 500;
8117
+ res.end("Internal Server Error");
6583
8118
  }
6584
8119
  } else {
6585
8120
  res.statusCode = 404;
@@ -6634,8 +8169,8 @@ var ServerCoordinator = class {
6634
8169
  this.cluster,
6635
8170
  this.partitionService,
6636
8171
  {
6637
- ...import_core10.DEFAULT_REPLICATION_CONFIG,
6638
- defaultConsistency: config.defaultConsistency ?? import_core10.ConsistencyLevel.EVENTUAL,
8172
+ ...import_core15.DEFAULT_REPLICATION_CONFIG,
8173
+ defaultConsistency: config.defaultConsistency ?? import_core15.ConsistencyLevel.EVENTUAL,
6639
8174
  ...config.replicationConfig
6640
8175
  }
6641
8176
  );
@@ -6656,6 +8191,27 @@ var ServerCoordinator = class {
6656
8191
  }
6657
8192
  }
6658
8193
  });
8194
+ this.counterHandler = new CounterHandler(this._nodeId);
8195
+ this.entryProcessorHandler = new EntryProcessorHandler({ hlc: this.hlc });
8196
+ this.conflictResolverHandler = new ConflictResolverHandler({ nodeId: this._nodeId });
8197
+ this.conflictResolverHandler.onRejection((rejection) => {
8198
+ this.notifyMergeRejection(rejection);
8199
+ });
8200
+ if (config.eventJournalEnabled && this.storage && "pool" in this.storage) {
8201
+ const pool = this.storage.pool;
8202
+ this.eventJournalService = new EventJournalService({
8203
+ capacity: 1e4,
8204
+ ttlMs: 0,
8205
+ persistent: true,
8206
+ pool,
8207
+ ...config.eventJournalConfig
8208
+ });
8209
+ this.eventJournalService.initialize().then(() => {
8210
+ logger.info("EventJournalService initialized");
8211
+ }).catch((err) => {
8212
+ logger.error({ err }, "Failed to initialize EventJournalService");
8213
+ });
8214
+ }
6659
8215
  this.systemManager = new SystemManager(
6660
8216
  this.cluster,
6661
8217
  this.metricsService,
@@ -6761,7 +8317,7 @@ var ServerCoordinator = class {
6761
8317
  this.metricsService.destroy();
6762
8318
  this.wss.close();
6763
8319
  logger.info(`Closing ${this.clients.size} client connections...`);
6764
- const shutdownMsg = (0, import_core10.serialize)({ type: "SHUTDOWN_PENDING", retryAfter: 5e3 });
8320
+ const shutdownMsg = (0, import_core15.serialize)({ type: "SHUTDOWN_PENDING", retryAfter: 5e3 });
6765
8321
  for (const client of this.clients.values()) {
6766
8322
  try {
6767
8323
  if (client.socket.readyState === import_ws3.WebSocket.OPEN) {
@@ -6815,6 +8371,10 @@ var ServerCoordinator = class {
6815
8371
  this.eventPayloadPool.clear();
6816
8372
  this.taskletScheduler.shutdown();
6817
8373
  this.writeAckManager.shutdown();
8374
+ this.entryProcessorHandler.dispose();
8375
+ if (this.eventJournalService) {
8376
+ this.eventJournalService.dispose();
8377
+ }
6818
8378
  logger.info("Server Coordinator shutdown complete.");
6819
8379
  }
6820
8380
  async handleConnection(ws) {
@@ -6882,7 +8442,7 @@ var ServerCoordinator = class {
6882
8442
  buf = Buffer.from(message);
6883
8443
  }
6884
8444
  try {
6885
- data = (0, import_core10.deserialize)(buf);
8445
+ data = (0, import_core15.deserialize)(buf);
6886
8446
  } catch (e) {
6887
8447
  try {
6888
8448
  const text = Buffer.isBuffer(buf) ? buf.toString() : new TextDecoder().decode(buf);
@@ -6921,6 +8481,7 @@ var ServerCoordinator = class {
6921
8481
  }
6922
8482
  this.lockManager.handleClientDisconnect(clientId);
6923
8483
  this.topicManager.unsubscribeAll(clientId);
8484
+ this.counterHandler.unsubscribeAll(clientId);
6924
8485
  const members = this.cluster.getMembers();
6925
8486
  for (const memberId of members) {
6926
8487
  if (!this.cluster.isLocal(memberId)) {
@@ -6933,10 +8494,10 @@ var ServerCoordinator = class {
6933
8494
  this.clients.delete(clientId);
6934
8495
  this.metricsService.setConnectedClients(this.clients.size);
6935
8496
  });
6936
- ws.send((0, import_core10.serialize)({ type: "AUTH_REQUIRED" }));
8497
+ ws.send((0, import_core15.serialize)({ type: "AUTH_REQUIRED" }));
6937
8498
  }
6938
8499
  async handleMessage(client, rawMessage) {
6939
- const parseResult = import_core10.MessageSchema.safeParse(rawMessage);
8500
+ const parseResult = import_core15.MessageSchema.safeParse(rawMessage);
6940
8501
  if (!parseResult.success) {
6941
8502
  logger.error({ clientId: client.id, error: parseResult.error }, "Invalid message format from client");
6942
8503
  client.writer.write({
@@ -7176,7 +8737,7 @@ var ServerCoordinator = class {
7176
8737
  this.metricsService.incOp("GET", message.mapName);
7177
8738
  try {
7178
8739
  const mapForSync = await this.getMapAsync(message.mapName);
7179
- if (mapForSync instanceof import_core10.LWWMap) {
8740
+ if (mapForSync instanceof import_core15.LWWMap) {
7180
8741
  const tree = mapForSync.getMerkleTree();
7181
8742
  const rootHash = tree.getRootHash();
7182
8743
  client.writer.write({
@@ -7214,7 +8775,7 @@ var ServerCoordinator = class {
7214
8775
  const { mapName, path } = message.payload;
7215
8776
  try {
7216
8777
  const mapForBucket = await this.getMapAsync(mapName);
7217
- if (mapForBucket instanceof import_core10.LWWMap) {
8778
+ if (mapForBucket instanceof import_core15.LWWMap) {
7218
8779
  const treeForBucket = mapForBucket.getMerkleTree();
7219
8780
  const buckets = treeForBucket.getBuckets(path);
7220
8781
  const node = treeForBucket.getNode(path);
@@ -7343,6 +8904,219 @@ var ServerCoordinator = class {
7343
8904
  }
7344
8905
  break;
7345
8906
  }
8907
+ // ============ Phase 5.2: PN Counter Handlers ============
8908
+ case "COUNTER_REQUEST": {
8909
+ const { name } = message.payload;
8910
+ const response = this.counterHandler.handleCounterRequest(client.id, name);
8911
+ client.writer.write(response);
8912
+ logger.debug({ clientId: client.id, name }, "Counter request handled");
8913
+ break;
8914
+ }
8915
+ case "COUNTER_SYNC": {
8916
+ const { name, state } = message.payload;
8917
+ const result = this.counterHandler.handleCounterSync(client.id, name, state);
8918
+ client.writer.write(result.response);
8919
+ for (const targetClientId of result.broadcastTo) {
8920
+ const targetClient = this.clients.get(targetClientId);
8921
+ if (targetClient && targetClient.socket.readyState === import_ws3.WebSocket.OPEN) {
8922
+ targetClient.writer.write(result.broadcastMessage);
8923
+ }
8924
+ }
8925
+ logger.debug({ clientId: client.id, name, broadcastCount: result.broadcastTo.length }, "Counter sync handled");
8926
+ break;
8927
+ }
8928
+ // ============ Phase 5.03: Entry Processor Handlers ============
8929
+ case "ENTRY_PROCESS": {
8930
+ const { requestId, mapName, key, processor } = message;
8931
+ if (!this.securityManager.checkPermission(client.principal, mapName, "PUT")) {
8932
+ client.writer.write({
8933
+ type: "ENTRY_PROCESS_RESPONSE",
8934
+ requestId,
8935
+ success: false,
8936
+ error: `Access Denied for map ${mapName}`
8937
+ }, true);
8938
+ break;
8939
+ }
8940
+ const entryMap = this.getMap(mapName);
8941
+ const { result, timestamp } = await this.entryProcessorHandler.executeOnKey(
8942
+ entryMap,
8943
+ key,
8944
+ processor
8945
+ );
8946
+ client.writer.write({
8947
+ type: "ENTRY_PROCESS_RESPONSE",
8948
+ requestId,
8949
+ success: result.success,
8950
+ result: result.result,
8951
+ newValue: result.newValue,
8952
+ error: result.error
8953
+ });
8954
+ if (result.success && timestamp) {
8955
+ const record = entryMap.getRecord(key);
8956
+ if (record) {
8957
+ this.queryRegistry.processChange(mapName, entryMap, key, record, void 0);
8958
+ }
8959
+ }
8960
+ logger.debug({
8961
+ clientId: client.id,
8962
+ mapName,
8963
+ key,
8964
+ processor: processor.name,
8965
+ success: result.success
8966
+ }, "Entry processor executed");
8967
+ break;
8968
+ }
8969
+ case "ENTRY_PROCESS_BATCH": {
8970
+ const { requestId, mapName, keys, processor } = message;
8971
+ if (!this.securityManager.checkPermission(client.principal, mapName, "PUT")) {
8972
+ const errorResults = {};
8973
+ for (const key of keys) {
8974
+ errorResults[key] = {
8975
+ success: false,
8976
+ error: `Access Denied for map ${mapName}`
8977
+ };
8978
+ }
8979
+ client.writer.write({
8980
+ type: "ENTRY_PROCESS_BATCH_RESPONSE",
8981
+ requestId,
8982
+ results: errorResults
8983
+ }, true);
8984
+ break;
8985
+ }
8986
+ const batchMap = this.getMap(mapName);
8987
+ const { results, timestamps } = await this.entryProcessorHandler.executeOnKeys(
8988
+ batchMap,
8989
+ keys,
8990
+ processor
8991
+ );
8992
+ const resultsRecord = {};
8993
+ for (const [key, keyResult] of results) {
8994
+ resultsRecord[key] = {
8995
+ success: keyResult.success,
8996
+ result: keyResult.result,
8997
+ newValue: keyResult.newValue,
8998
+ error: keyResult.error
8999
+ };
9000
+ }
9001
+ client.writer.write({
9002
+ type: "ENTRY_PROCESS_BATCH_RESPONSE",
9003
+ requestId,
9004
+ results: resultsRecord
9005
+ });
9006
+ for (const [key] of timestamps) {
9007
+ const record = batchMap.getRecord(key);
9008
+ if (record) {
9009
+ this.queryRegistry.processChange(mapName, batchMap, key, record, void 0);
9010
+ }
9011
+ }
9012
+ logger.debug({
9013
+ clientId: client.id,
9014
+ mapName,
9015
+ keyCount: keys.length,
9016
+ processor: processor.name,
9017
+ successCount: Array.from(results.values()).filter((r) => r.success).length
9018
+ }, "Entry processor batch executed");
9019
+ break;
9020
+ }
9021
+ // ============ Phase 5.05: Conflict Resolver Handlers ============
9022
+ case "REGISTER_RESOLVER": {
9023
+ const { requestId, mapName, resolver } = message;
9024
+ if (!this.securityManager.checkPermission(client.principal, mapName, "PUT")) {
9025
+ client.writer.write({
9026
+ type: "REGISTER_RESOLVER_RESPONSE",
9027
+ requestId,
9028
+ success: false,
9029
+ error: `Access Denied for map ${mapName}`
9030
+ }, true);
9031
+ break;
9032
+ }
9033
+ try {
9034
+ this.conflictResolverHandler.registerResolver(
9035
+ mapName,
9036
+ {
9037
+ name: resolver.name,
9038
+ code: resolver.code,
9039
+ priority: resolver.priority,
9040
+ keyPattern: resolver.keyPattern
9041
+ },
9042
+ client.id
9043
+ );
9044
+ client.writer.write({
9045
+ type: "REGISTER_RESOLVER_RESPONSE",
9046
+ requestId,
9047
+ success: true
9048
+ });
9049
+ logger.info({
9050
+ clientId: client.id,
9051
+ mapName,
9052
+ resolverName: resolver.name,
9053
+ priority: resolver.priority
9054
+ }, "Conflict resolver registered");
9055
+ } catch (err) {
9056
+ const errorMessage = err instanceof Error ? err.message : String(err);
9057
+ client.writer.write({
9058
+ type: "REGISTER_RESOLVER_RESPONSE",
9059
+ requestId,
9060
+ success: false,
9061
+ error: errorMessage
9062
+ }, true);
9063
+ logger.warn({
9064
+ clientId: client.id,
9065
+ mapName,
9066
+ error: errorMessage
9067
+ }, "Failed to register conflict resolver");
9068
+ }
9069
+ break;
9070
+ }
9071
+ case "UNREGISTER_RESOLVER": {
9072
+ const { requestId, mapName, resolverName } = message;
9073
+ if (!this.securityManager.checkPermission(client.principal, mapName, "PUT")) {
9074
+ client.writer.write({
9075
+ type: "UNREGISTER_RESOLVER_RESPONSE",
9076
+ requestId,
9077
+ success: false,
9078
+ error: `Access Denied for map ${mapName}`
9079
+ }, true);
9080
+ break;
9081
+ }
9082
+ const removed = this.conflictResolverHandler.unregisterResolver(
9083
+ mapName,
9084
+ resolverName,
9085
+ client.id
9086
+ );
9087
+ client.writer.write({
9088
+ type: "UNREGISTER_RESOLVER_RESPONSE",
9089
+ requestId,
9090
+ success: removed,
9091
+ error: removed ? void 0 : "Resolver not found or not owned by this client"
9092
+ });
9093
+ if (removed) {
9094
+ logger.info({
9095
+ clientId: client.id,
9096
+ mapName,
9097
+ resolverName
9098
+ }, "Conflict resolver unregistered");
9099
+ }
9100
+ break;
9101
+ }
9102
+ case "LIST_RESOLVERS": {
9103
+ const { requestId, mapName } = message;
9104
+ if (mapName && !this.securityManager.checkPermission(client.principal, mapName, "READ")) {
9105
+ client.writer.write({
9106
+ type: "LIST_RESOLVERS_RESPONSE",
9107
+ requestId,
9108
+ resolvers: []
9109
+ });
9110
+ break;
9111
+ }
9112
+ const resolvers = this.conflictResolverHandler.listResolvers(mapName);
9113
+ client.writer.write({
9114
+ type: "LIST_RESOLVERS_RESPONSE",
9115
+ requestId,
9116
+ resolvers
9117
+ });
9118
+ break;
9119
+ }
7346
9120
  // ============ Phase 4: Partition Map Request Handler ============
7347
9121
  case "PARTITION_MAP_REQUEST": {
7348
9122
  const clientVersion = message.payload?.currentVersion ?? 0;
@@ -7383,7 +9157,7 @@ var ServerCoordinator = class {
7383
9157
  this.metricsService.incOp("GET", message.mapName);
7384
9158
  try {
7385
9159
  const mapForSync = await this.getMapAsync(message.mapName, "OR");
7386
- if (mapForSync instanceof import_core10.ORMap) {
9160
+ if (mapForSync instanceof import_core15.ORMap) {
7387
9161
  const tree = mapForSync.getMerkleTree();
7388
9162
  const rootHash = tree.getRootHash();
7389
9163
  client.writer.write({
@@ -7420,7 +9194,7 @@ var ServerCoordinator = class {
7420
9194
  const { mapName, path } = message.payload;
7421
9195
  try {
7422
9196
  const mapForBucket = await this.getMapAsync(mapName, "OR");
7423
- if (mapForBucket instanceof import_core10.ORMap) {
9197
+ if (mapForBucket instanceof import_core15.ORMap) {
7424
9198
  const tree = mapForBucket.getMerkleTree();
7425
9199
  const buckets = tree.getBuckets(path);
7426
9200
  const isLeaf = tree.isLeaf(path);
@@ -7464,7 +9238,7 @@ var ServerCoordinator = class {
7464
9238
  const { mapName: diffMapName, keys } = message.payload;
7465
9239
  try {
7466
9240
  const mapForDiff = await this.getMapAsync(diffMapName, "OR");
7467
- if (mapForDiff instanceof import_core10.ORMap) {
9241
+ if (mapForDiff instanceof import_core15.ORMap) {
7468
9242
  const entries = [];
7469
9243
  const allTombstones = mapForDiff.getTombstones();
7470
9244
  for (const key of keys) {
@@ -7496,7 +9270,7 @@ var ServerCoordinator = class {
7496
9270
  const { mapName: pushMapName, entries: pushEntries } = message.payload;
7497
9271
  try {
7498
9272
  const mapForPush = await this.getMapAsync(pushMapName, "OR");
7499
- if (mapForPush instanceof import_core10.ORMap) {
9273
+ if (mapForPush instanceof import_core15.ORMap) {
7500
9274
  let totalAdded = 0;
7501
9275
  let totalUpdated = 0;
7502
9276
  for (const entry of pushEntries) {
@@ -7538,6 +9312,92 @@ var ServerCoordinator = class {
7538
9312
  }
7539
9313
  break;
7540
9314
  }
9315
+ // === Event Journal Messages (Phase 5.04) ===
9316
+ case "JOURNAL_SUBSCRIBE": {
9317
+ if (!this.eventJournalService) {
9318
+ client.writer.write({
9319
+ type: "ERROR",
9320
+ payload: { code: 503, message: "Event journal not enabled" }
9321
+ }, true);
9322
+ break;
9323
+ }
9324
+ const { requestId, fromSequence, mapName, types } = message;
9325
+ const subscriptionId = requestId;
9326
+ this.journalSubscriptions.set(subscriptionId, {
9327
+ clientId: client.id,
9328
+ mapName,
9329
+ types
9330
+ });
9331
+ const unsubscribe = this.eventJournalService.subscribe(
9332
+ (event) => {
9333
+ if (mapName && event.mapName !== mapName) return;
9334
+ if (types && types.length > 0 && !types.includes(event.type)) return;
9335
+ const clientConn = this.clients.get(client.id);
9336
+ if (!clientConn) {
9337
+ unsubscribe();
9338
+ this.journalSubscriptions.delete(subscriptionId);
9339
+ return;
9340
+ }
9341
+ clientConn.writer.write({
9342
+ type: "JOURNAL_EVENT",
9343
+ event: {
9344
+ sequence: event.sequence.toString(),
9345
+ type: event.type,
9346
+ mapName: event.mapName,
9347
+ key: event.key,
9348
+ value: event.value,
9349
+ previousValue: event.previousValue,
9350
+ timestamp: event.timestamp,
9351
+ nodeId: event.nodeId,
9352
+ metadata: event.metadata
9353
+ }
9354
+ });
9355
+ },
9356
+ fromSequence ? BigInt(fromSequence) : void 0
9357
+ );
9358
+ logger.info({ clientId: client.id, subscriptionId, mapName }, "Journal subscription created");
9359
+ break;
9360
+ }
9361
+ case "JOURNAL_UNSUBSCRIBE": {
9362
+ const { subscriptionId } = message;
9363
+ this.journalSubscriptions.delete(subscriptionId);
9364
+ logger.info({ clientId: client.id, subscriptionId }, "Journal subscription removed");
9365
+ break;
9366
+ }
9367
+ case "JOURNAL_READ": {
9368
+ if (!this.eventJournalService) {
9369
+ client.writer.write({
9370
+ type: "ERROR",
9371
+ payload: { code: 503, message: "Event journal not enabled" }
9372
+ }, true);
9373
+ break;
9374
+ }
9375
+ const { requestId: readReqId, fromSequence: readFromSeq, limit, mapName: readMapName } = message;
9376
+ const startSeq = BigInt(readFromSeq);
9377
+ const eventLimit = limit ?? 100;
9378
+ let events = this.eventJournalService.readFrom(startSeq, eventLimit);
9379
+ if (readMapName) {
9380
+ events = events.filter((e) => e.mapName === readMapName);
9381
+ }
9382
+ const serializedEvents = events.map((e) => ({
9383
+ sequence: e.sequence.toString(),
9384
+ type: e.type,
9385
+ mapName: e.mapName,
9386
+ key: e.key,
9387
+ value: e.value,
9388
+ previousValue: e.previousValue,
9389
+ timestamp: e.timestamp,
9390
+ nodeId: e.nodeId,
9391
+ metadata: e.metadata
9392
+ }));
9393
+ client.writer.write({
9394
+ type: "JOURNAL_READ_RESPONSE",
9395
+ requestId: readReqId,
9396
+ events: serializedEvents,
9397
+ hasMore: events.length === eventLimit
9398
+ });
9399
+ break;
9400
+ }
7541
9401
  default:
7542
9402
  logger.warn({ type: message.type }, "Unknown message type");
7543
9403
  }
@@ -7551,7 +9411,7 @@ var ServerCoordinator = class {
7551
9411
  } else if (op.orRecord && op.orRecord.timestamp) {
7552
9412
  } else if (op.orTag) {
7553
9413
  try {
7554
- ts = import_core10.HLC.parse(op.orTag);
9414
+ ts = import_core15.HLC.parse(op.orTag);
7555
9415
  } catch (e) {
7556
9416
  }
7557
9417
  }
@@ -7585,6 +9445,39 @@ var ServerCoordinator = class {
7585
9445
  clientCount: broadcastCount
7586
9446
  }, "Broadcast partition map to clients");
7587
9447
  }
9448
+ /**
9449
+ * Notify a client about a merge rejection (Phase 5.05).
9450
+ * Finds the client by node ID and sends MERGE_REJECTED message.
9451
+ */
9452
+ notifyMergeRejection(rejection) {
9453
+ for (const [clientId, client] of this.clients) {
9454
+ if (clientId === rejection.nodeId || rejection.nodeId.includes(clientId)) {
9455
+ client.writer.write({
9456
+ type: "MERGE_REJECTED",
9457
+ mapName: rejection.mapName,
9458
+ key: rejection.key,
9459
+ attemptedValue: rejection.attemptedValue,
9460
+ reason: rejection.reason,
9461
+ timestamp: rejection.timestamp
9462
+ }, true);
9463
+ return;
9464
+ }
9465
+ }
9466
+ const subscribedClientIds = this.queryRegistry.getSubscribedClientIds(rejection.mapName);
9467
+ for (const clientId of subscribedClientIds) {
9468
+ const client = this.clients.get(clientId);
9469
+ if (client) {
9470
+ client.writer.write({
9471
+ type: "MERGE_REJECTED",
9472
+ mapName: rejection.mapName,
9473
+ key: rejection.key,
9474
+ attemptedValue: rejection.attemptedValue,
9475
+ reason: rejection.reason,
9476
+ timestamp: rejection.timestamp
9477
+ });
9478
+ }
9479
+ }
9480
+ }
7588
9481
  broadcast(message, excludeClientId) {
7589
9482
  const isServerEvent = message.type === "SERVER_EVENT";
7590
9483
  if (isServerEvent) {
@@ -7615,7 +9508,7 @@ var ServerCoordinator = class {
7615
9508
  client.writer.write({ ...message, payload: newPayload });
7616
9509
  }
7617
9510
  } else {
7618
- const msgData = (0, import_core10.serialize)(message);
9511
+ const msgData = (0, import_core15.serialize)(message);
7619
9512
  for (const [id, client] of this.clients) {
7620
9513
  if (id !== excludeClientId && client.socket.readyState === 1) {
7621
9514
  client.writer.writeRaw(msgData);
@@ -7693,7 +9586,7 @@ var ServerCoordinator = class {
7693
9586
  payload: { events: filteredEvents },
7694
9587
  timestamp: this.hlc.now()
7695
9588
  };
7696
- const serializedBatch = (0, import_core10.serialize)(batchMessage);
9589
+ const serializedBatch = (0, import_core15.serialize)(batchMessage);
7697
9590
  for (const client of clients) {
7698
9591
  try {
7699
9592
  client.writer.writeRaw(serializedBatch);
@@ -7778,7 +9671,7 @@ var ServerCoordinator = class {
7778
9671
  payload: { events: filteredEvents },
7779
9672
  timestamp: this.hlc.now()
7780
9673
  };
7781
- const serializedBatch = (0, import_core10.serialize)(batchMessage);
9674
+ const serializedBatch = (0, import_core15.serialize)(batchMessage);
7782
9675
  for (const client of clients) {
7783
9676
  sendPromises.push(new Promise((resolve, reject) => {
7784
9677
  try {
@@ -7921,15 +9814,35 @@ var ServerCoordinator = class {
7921
9814
  }
7922
9815
  async executeLocalQuery(mapName, query) {
7923
9816
  const map = await this.getMapAsync(mapName);
9817
+ const localQuery = { ...query };
9818
+ delete localQuery.offset;
9819
+ delete localQuery.limit;
9820
+ if (map instanceof import_core15.IndexedLWWMap) {
9821
+ const coreQuery = this.convertToCoreQuery(localQuery);
9822
+ if (coreQuery) {
9823
+ const entries = map.queryEntries(coreQuery);
9824
+ return entries.map(([key, value]) => {
9825
+ const record = map.getRecord(key);
9826
+ return { key, value, timestamp: record?.timestamp };
9827
+ });
9828
+ }
9829
+ }
9830
+ if (map instanceof import_core15.IndexedORMap) {
9831
+ const coreQuery = this.convertToCoreQuery(localQuery);
9832
+ if (coreQuery) {
9833
+ const results = map.query(coreQuery);
9834
+ return results.map(({ key, value }) => ({ key, value }));
9835
+ }
9836
+ }
7924
9837
  const records = /* @__PURE__ */ new Map();
7925
- if (map instanceof import_core10.LWWMap) {
9838
+ if (map instanceof import_core15.LWWMap) {
7926
9839
  for (const key of map.allKeys()) {
7927
9840
  const rec = map.getRecord(key);
7928
9841
  if (rec && rec.value !== null) {
7929
9842
  records.set(key, rec);
7930
9843
  }
7931
9844
  }
7932
- } else if (map instanceof import_core10.ORMap) {
9845
+ } else if (map instanceof import_core15.ORMap) {
7933
9846
  const items = map.items;
7934
9847
  for (const key of items.keys()) {
7935
9848
  const values = map.get(key);
@@ -7938,11 +9851,89 @@ var ServerCoordinator = class {
7938
9851
  }
7939
9852
  }
7940
9853
  }
7941
- const localQuery = { ...query };
7942
- delete localQuery.offset;
7943
- delete localQuery.limit;
7944
9854
  return executeQuery(records, localQuery);
7945
9855
  }
9856
+ /**
9857
+ * Convert server Query format to core Query format for indexed execution.
9858
+ * Returns null if conversion is not possible (complex queries).
9859
+ */
9860
+ convertToCoreQuery(query) {
9861
+ if (query.predicate) {
9862
+ return this.predicateToCoreQuery(query.predicate);
9863
+ }
9864
+ if (query.where) {
9865
+ const conditions = [];
9866
+ for (const [attribute, condition] of Object.entries(query.where)) {
9867
+ if (typeof condition !== "object" || condition === null) {
9868
+ conditions.push({ type: "eq", attribute, value: condition });
9869
+ } else {
9870
+ for (const [op, value] of Object.entries(condition)) {
9871
+ const coreOp = this.convertOperator(op);
9872
+ if (coreOp) {
9873
+ conditions.push({ type: coreOp, attribute, value });
9874
+ }
9875
+ }
9876
+ }
9877
+ }
9878
+ if (conditions.length === 0) return null;
9879
+ if (conditions.length === 1) return conditions[0];
9880
+ return { type: "and", children: conditions };
9881
+ }
9882
+ return null;
9883
+ }
9884
+ /**
9885
+ * Convert predicate node to core Query format.
9886
+ */
9887
+ predicateToCoreQuery(predicate) {
9888
+ if (!predicate || !predicate.op) return null;
9889
+ switch (predicate.op) {
9890
+ case "eq":
9891
+ case "neq":
9892
+ case "gt":
9893
+ case "gte":
9894
+ case "lt":
9895
+ case "lte":
9896
+ return {
9897
+ type: predicate.op,
9898
+ attribute: predicate.attribute,
9899
+ value: predicate.value
9900
+ };
9901
+ case "and":
9902
+ case "or":
9903
+ if (predicate.children && Array.isArray(predicate.children)) {
9904
+ const children = predicate.children.map((c) => this.predicateToCoreQuery(c)).filter((c) => c !== null);
9905
+ if (children.length === 0) return null;
9906
+ if (children.length === 1) return children[0];
9907
+ return { type: predicate.op, children };
9908
+ }
9909
+ return null;
9910
+ case "not":
9911
+ if (predicate.children && predicate.children[0]) {
9912
+ const child = this.predicateToCoreQuery(predicate.children[0]);
9913
+ if (child) {
9914
+ return { type: "not", child };
9915
+ }
9916
+ }
9917
+ return null;
9918
+ default:
9919
+ return null;
9920
+ }
9921
+ }
9922
+ /**
9923
+ * Convert server operator to core query type.
9924
+ */
9925
+ convertOperator(op) {
9926
+ const mapping = {
9927
+ "$eq": "eq",
9928
+ "$ne": "neq",
9929
+ "$neq": "neq",
9930
+ "$gt": "gt",
9931
+ "$gte": "gte",
9932
+ "$lt": "lt",
9933
+ "$lte": "lte"
9934
+ };
9935
+ return mapping[op] || null;
9936
+ }
7946
9937
  finalizeClusterQuery(requestId, timeout = false) {
7947
9938
  const pending = this.pendingClusterQueries.get(requestId);
7948
9939
  if (!pending) return;
@@ -7996,14 +9987,14 @@ var ServerCoordinator = class {
7996
9987
  *
7997
9988
  * @returns Event payload for broadcasting (or null if operation failed)
7998
9989
  */
7999
- applyOpToMap(op) {
9990
+ async applyOpToMap(op, remoteNodeId) {
8000
9991
  const typeHint = op.opType === "OR_ADD" || op.opType === "OR_REMOVE" ? "OR" : "LWW";
8001
9992
  const map = this.getMap(op.mapName, typeHint);
8002
- if (typeHint === "OR" && map instanceof import_core10.LWWMap) {
9993
+ if (typeHint === "OR" && map instanceof import_core15.LWWMap) {
8003
9994
  logger.error({ mapName: op.mapName }, "Map type mismatch: LWWMap but received OR op");
8004
9995
  throw new Error("Map type mismatch: LWWMap but received OR op");
8005
9996
  }
8006
- if (typeHint === "LWW" && map instanceof import_core10.ORMap) {
9997
+ if (typeHint === "LWW" && map instanceof import_core15.ORMap) {
8007
9998
  logger.error({ mapName: op.mapName }, "Map type mismatch: ORMap but received LWW op");
8008
9999
  throw new Error("Map type mismatch: ORMap but received LWW op");
8009
10000
  }
@@ -8014,13 +10005,35 @@ var ServerCoordinator = class {
8014
10005
  mapName: op.mapName,
8015
10006
  key: op.key
8016
10007
  };
8017
- if (map instanceof import_core10.LWWMap) {
10008
+ if (map instanceof import_core15.LWWMap) {
8018
10009
  oldRecord = map.getRecord(op.key);
8019
- map.merge(op.key, op.record);
8020
- recordToStore = op.record;
8021
- eventPayload.eventType = "UPDATED";
8022
- eventPayload.record = op.record;
8023
- } else if (map instanceof import_core10.ORMap) {
10010
+ if (this.conflictResolverHandler.hasResolvers(op.mapName)) {
10011
+ const mergeResult = await this.conflictResolverHandler.mergeWithResolver(
10012
+ map,
10013
+ op.mapName,
10014
+ op.key,
10015
+ op.record,
10016
+ remoteNodeId || this._nodeId
10017
+ );
10018
+ if (!mergeResult.applied) {
10019
+ if (mergeResult.rejection) {
10020
+ logger.debug(
10021
+ { mapName: op.mapName, key: op.key, reason: mergeResult.rejection.reason },
10022
+ "Merge rejected by resolver"
10023
+ );
10024
+ }
10025
+ return { eventPayload: null, oldRecord, rejected: true };
10026
+ }
10027
+ recordToStore = mergeResult.record;
10028
+ eventPayload.eventType = "UPDATED";
10029
+ eventPayload.record = mergeResult.record;
10030
+ } else {
10031
+ map.merge(op.key, op.record);
10032
+ recordToStore = op.record;
10033
+ eventPayload.eventType = "UPDATED";
10034
+ eventPayload.record = op.record;
10035
+ }
10036
+ } else if (map instanceof import_core15.ORMap) {
8024
10037
  oldRecord = map.getRecords(op.key);
8025
10038
  if (op.opType === "OR_ADD") {
8026
10039
  map.apply(op.key, op.orRecord);
@@ -8036,7 +10049,7 @@ var ServerCoordinator = class {
8036
10049
  }
8037
10050
  }
8038
10051
  this.queryRegistry.processChange(op.mapName, map, op.key, op.record || op.orRecord, oldRecord);
8039
- const mapSize = map instanceof import_core10.ORMap ? map.totalRecords : map.size;
10052
+ const mapSize = map instanceof import_core15.ORMap ? map.totalRecords : map.size;
8040
10053
  this.metricsService.setMapSize(op.mapName, mapSize);
8041
10054
  if (this.storage) {
8042
10055
  if (recordToStore) {
@@ -8050,6 +10063,21 @@ var ServerCoordinator = class {
8050
10063
  });
8051
10064
  }
8052
10065
  }
10066
+ if (this.eventJournalService) {
10067
+ const isDelete = op.opType === "REMOVE" || op.opType === "OR_REMOVE" || op.record && op.record.value === null;
10068
+ const isNew = !oldRecord || Array.isArray(oldRecord) && oldRecord.length === 0;
10069
+ const journalEventType = isDelete ? "DELETE" : isNew ? "PUT" : "UPDATE";
10070
+ const timestamp = op.record?.timestamp || op.orRecord?.timestamp || this.hlc.now();
10071
+ this.eventJournalService.append({
10072
+ type: journalEventType,
10073
+ mapName: op.mapName,
10074
+ key: op.key,
10075
+ value: op.record?.value ?? op.orRecord?.value,
10076
+ previousValue: oldRecord?.value ?? (Array.isArray(oldRecord) ? oldRecord[0]?.value : void 0),
10077
+ timestamp,
10078
+ nodeId: this._nodeId
10079
+ });
10080
+ }
8053
10081
  return { eventPayload, oldRecord };
8054
10082
  }
8055
10083
  /**
@@ -8071,7 +10099,10 @@ var ServerCoordinator = class {
8071
10099
  try {
8072
10100
  const op = operation;
8073
10101
  logger.debug({ sourceNode, opId, mapName: op.mapName, key: op.key }, "Applying replicated operation");
8074
- const { eventPayload } = this.applyOpToMap(op);
10102
+ const { eventPayload, rejected } = await this.applyOpToMap(op, sourceNode);
10103
+ if (rejected || !eventPayload) {
10104
+ return true;
10105
+ }
8075
10106
  this.broadcast({
8076
10107
  type: "SERVER_EVENT",
8077
10108
  payload: eventPayload,
@@ -8170,7 +10201,10 @@ var ServerCoordinator = class {
8170
10201
  logger.warn({ err, opId: op.id }, "Interceptor rejected op");
8171
10202
  throw err;
8172
10203
  }
8173
- const { eventPayload } = this.applyOpToMap(op);
10204
+ const { eventPayload, rejected } = await this.applyOpToMap(op, originalSenderId);
10205
+ if (rejected || !eventPayload) {
10206
+ return;
10207
+ }
8174
10208
  if (this.replicationPipeline && !fromCluster) {
8175
10209
  const opId = op.id || `${op.mapName}:${op.key}:${Date.now()}`;
8176
10210
  this.replicationPipeline.replicate(op, opId, op.key).catch((err) => {
@@ -8298,7 +10332,10 @@ var ServerCoordinator = class {
8298
10332
  logger.warn({ err, opId: op.id }, "Interceptor rejected op in batch");
8299
10333
  throw err;
8300
10334
  }
8301
- const { eventPayload } = this.applyOpToMap(op);
10335
+ const { eventPayload, rejected } = await this.applyOpToMap(op, clientId);
10336
+ if (rejected || !eventPayload) {
10337
+ return;
10338
+ }
8302
10339
  if (this.replicationPipeline) {
8303
10340
  const opId = op.id || `${op.mapName}:${op.key}:${Date.now()}`;
8304
10341
  this.replicationPipeline.replicate(op, opId, op.key).catch((err) => {
@@ -8312,11 +10349,11 @@ var ServerCoordinator = class {
8312
10349
  handleClusterEvent(payload) {
8313
10350
  const { mapName, key, eventType } = payload;
8314
10351
  const map = this.getMap(mapName, eventType === "OR_ADD" || eventType === "OR_REMOVE" ? "OR" : "LWW");
8315
- const oldRecord = map instanceof import_core10.LWWMap ? map.getRecord(key) : null;
10352
+ const oldRecord = map instanceof import_core15.LWWMap ? map.getRecord(key) : null;
8316
10353
  if (this.partitionService.isRelated(key)) {
8317
- if (map instanceof import_core10.LWWMap && payload.record) {
10354
+ if (map instanceof import_core15.LWWMap && payload.record) {
8318
10355
  map.merge(key, payload.record);
8319
- } else if (map instanceof import_core10.ORMap) {
10356
+ } else if (map instanceof import_core15.ORMap) {
8320
10357
  if (eventType === "OR_ADD" && payload.orRecord) {
8321
10358
  map.apply(key, payload.orRecord);
8322
10359
  } else if (eventType === "OR_REMOVE" && payload.orTag) {
@@ -8335,9 +10372,9 @@ var ServerCoordinator = class {
8335
10372
  if (!this.maps.has(name)) {
8336
10373
  let map;
8337
10374
  if (typeHint === "OR") {
8338
- map = new import_core10.ORMap(this.hlc);
10375
+ map = new import_core15.ORMap(this.hlc);
8339
10376
  } else {
8340
- map = new import_core10.LWWMap(this.hlc);
10377
+ map = new import_core15.LWWMap(this.hlc);
8341
10378
  }
8342
10379
  this.maps.set(name, map);
8343
10380
  if (this.storage) {
@@ -8360,7 +10397,7 @@ var ServerCoordinator = class {
8360
10397
  this.getMap(name, typeHint);
8361
10398
  const loadingPromise = this.mapLoadingPromises.get(name);
8362
10399
  const map = this.maps.get(name);
8363
- const mapSize = map instanceof import_core10.LWWMap ? Array.from(map.entries()).length : map instanceof import_core10.ORMap ? map.size : 0;
10400
+ const mapSize = map instanceof import_core15.LWWMap ? Array.from(map.entries()).length : map instanceof import_core15.ORMap ? map.size : 0;
8364
10401
  logger.info({
8365
10402
  mapName: name,
8366
10403
  mapExisted,
@@ -8370,7 +10407,7 @@ var ServerCoordinator = class {
8370
10407
  if (loadingPromise) {
8371
10408
  logger.info({ mapName: name }, "[getMapAsync] Waiting for loadMapFromStorage...");
8372
10409
  await loadingPromise;
8373
- const newMapSize = map instanceof import_core10.LWWMap ? Array.from(map.entries()).length : map instanceof import_core10.ORMap ? map.size : 0;
10410
+ const newMapSize = map instanceof import_core15.LWWMap ? Array.from(map.entries()).length : map instanceof import_core15.ORMap ? map.size : 0;
8374
10411
  logger.info({ mapName: name, mapSizeAfterLoad: newMapSize }, "[getMapAsync] Load completed");
8375
10412
  }
8376
10413
  return this.maps.get(name);
@@ -8396,16 +10433,16 @@ var ServerCoordinator = class {
8396
10433
  const currentMap = this.maps.get(name);
8397
10434
  if (!currentMap) return;
8398
10435
  let targetMap = currentMap;
8399
- if (isOR && currentMap instanceof import_core10.LWWMap) {
10436
+ if (isOR && currentMap instanceof import_core15.LWWMap) {
8400
10437
  logger.info({ mapName: name }, "Map auto-detected as ORMap. Switching type.");
8401
- targetMap = new import_core10.ORMap(this.hlc);
10438
+ targetMap = new import_core15.ORMap(this.hlc);
8402
10439
  this.maps.set(name, targetMap);
8403
- } else if (!isOR && currentMap instanceof import_core10.ORMap && typeHint !== "OR") {
10440
+ } else if (!isOR && currentMap instanceof import_core15.ORMap && typeHint !== "OR") {
8404
10441
  logger.info({ mapName: name }, "Map auto-detected as LWWMap. Switching type.");
8405
- targetMap = new import_core10.LWWMap(this.hlc);
10442
+ targetMap = new import_core15.LWWMap(this.hlc);
8406
10443
  this.maps.set(name, targetMap);
8407
10444
  }
8408
- if (targetMap instanceof import_core10.ORMap) {
10445
+ if (targetMap instanceof import_core15.ORMap) {
8409
10446
  for (const [key, record] of records) {
8410
10447
  if (key === "__tombstones__") {
8411
10448
  const t = record;
@@ -8418,7 +10455,7 @@ var ServerCoordinator = class {
8418
10455
  }
8419
10456
  }
8420
10457
  }
8421
- } else if (targetMap instanceof import_core10.LWWMap) {
10458
+ } else if (targetMap instanceof import_core15.LWWMap) {
8422
10459
  for (const [key, record] of records) {
8423
10460
  if (!record.type) {
8424
10461
  targetMap.merge(key, record);
@@ -8429,7 +10466,7 @@ var ServerCoordinator = class {
8429
10466
  if (count > 0) {
8430
10467
  logger.info({ mapName: name, count }, "Loaded records for map");
8431
10468
  this.queryRegistry.refreshSubscriptions(name, targetMap);
8432
- const mapSize = targetMap instanceof import_core10.ORMap ? targetMap.totalRecords : targetMap.size;
10469
+ const mapSize = targetMap instanceof import_core15.ORMap ? targetMap.totalRecords : targetMap.size;
8433
10470
  this.metricsService.setMapSize(name, mapSize);
8434
10471
  }
8435
10472
  } catch (err) {
@@ -8511,7 +10548,7 @@ var ServerCoordinator = class {
8511
10548
  reportLocalHlc() {
8512
10549
  let minHlc = this.hlc.now();
8513
10550
  for (const client of this.clients.values()) {
8514
- if (import_core10.HLC.compare(client.lastActiveHlc, minHlc) < 0) {
10551
+ if (import_core15.HLC.compare(client.lastActiveHlc, minHlc) < 0) {
8515
10552
  minHlc = client.lastActiveHlc;
8516
10553
  }
8517
10554
  }
@@ -8532,7 +10569,7 @@ var ServerCoordinator = class {
8532
10569
  let globalSafe = this.hlc.now();
8533
10570
  let initialized = false;
8534
10571
  for (const ts of this.gcReports.values()) {
8535
- if (!initialized || import_core10.HLC.compare(ts, globalSafe) < 0) {
10572
+ if (!initialized || import_core15.HLC.compare(ts, globalSafe) < 0) {
8536
10573
  globalSafe = ts;
8537
10574
  initialized = true;
8538
10575
  }
@@ -8567,7 +10604,7 @@ var ServerCoordinator = class {
8567
10604
  logger.info({ olderThanMillis: olderThan.millis }, "Performing Garbage Collection");
8568
10605
  const now = Date.now();
8569
10606
  for (const [name, map] of this.maps) {
8570
- if (map instanceof import_core10.LWWMap) {
10607
+ if (map instanceof import_core15.LWWMap) {
8571
10608
  for (const key of map.allKeys()) {
8572
10609
  const record = map.getRecord(key);
8573
10610
  if (record && record.value !== null && record.ttlMs) {
@@ -8619,7 +10656,7 @@ var ServerCoordinator = class {
8619
10656
  });
8620
10657
  }
8621
10658
  }
8622
- } else if (map instanceof import_core10.ORMap) {
10659
+ } else if (map instanceof import_core15.ORMap) {
8623
10660
  const items = map.items;
8624
10661
  const tombstonesSet = map.tombstones;
8625
10662
  const tagsToExpire = [];
@@ -8722,17 +10759,17 @@ var ServerCoordinator = class {
8722
10759
  stringToWriteConcern(value) {
8723
10760
  switch (value) {
8724
10761
  case "FIRE_AND_FORGET":
8725
- return import_core10.WriteConcern.FIRE_AND_FORGET;
10762
+ return import_core15.WriteConcern.FIRE_AND_FORGET;
8726
10763
  case "MEMORY":
8727
- return import_core10.WriteConcern.MEMORY;
10764
+ return import_core15.WriteConcern.MEMORY;
8728
10765
  case "APPLIED":
8729
- return import_core10.WriteConcern.APPLIED;
10766
+ return import_core15.WriteConcern.APPLIED;
8730
10767
  case "REPLICATED":
8731
- return import_core10.WriteConcern.REPLICATED;
10768
+ return import_core15.WriteConcern.REPLICATED;
8732
10769
  case "PERSISTED":
8733
- return import_core10.WriteConcern.PERSISTED;
10770
+ return import_core15.WriteConcern.PERSISTED;
8734
10771
  default:
8735
- return import_core10.WriteConcern.MEMORY;
10772
+ return import_core15.WriteConcern.MEMORY;
8736
10773
  }
8737
10774
  }
8738
10775
  /**
@@ -8789,7 +10826,7 @@ var ServerCoordinator = class {
8789
10826
  }
8790
10827
  });
8791
10828
  if (op.id) {
8792
- this.writeAckManager.notifyLevel(op.id, import_core10.WriteConcern.REPLICATED);
10829
+ this.writeAckManager.notifyLevel(op.id, import_core15.WriteConcern.REPLICATED);
8793
10830
  }
8794
10831
  }
8795
10832
  }
@@ -8797,7 +10834,7 @@ var ServerCoordinator = class {
8797
10834
  this.broadcastBatch(batchedEvents, clientId);
8798
10835
  for (const op of ops) {
8799
10836
  if (op.id && this.partitionService.isLocalOwner(op.key)) {
8800
- this.writeAckManager.notifyLevel(op.id, import_core10.WriteConcern.REPLICATED);
10837
+ this.writeAckManager.notifyLevel(op.id, import_core15.WriteConcern.REPLICATED);
8801
10838
  }
8802
10839
  }
8803
10840
  }
@@ -8825,7 +10862,7 @@ var ServerCoordinator = class {
8825
10862
  const owner = this.partitionService.getOwner(op.key);
8826
10863
  await this.forwardOpAndWait(op, owner);
8827
10864
  if (op.id) {
8828
- this.writeAckManager.notifyLevel(op.id, import_core10.WriteConcern.REPLICATED);
10865
+ this.writeAckManager.notifyLevel(op.id, import_core15.WriteConcern.REPLICATED);
8829
10866
  }
8830
10867
  }
8831
10868
  }
@@ -8833,7 +10870,7 @@ var ServerCoordinator = class {
8833
10870
  await this.broadcastBatchSync(batchedEvents, clientId);
8834
10871
  for (const op of ops) {
8835
10872
  if (op.id && this.partitionService.isLocalOwner(op.key)) {
8836
- this.writeAckManager.notifyLevel(op.id, import_core10.WriteConcern.REPLICATED);
10873
+ this.writeAckManager.notifyLevel(op.id, import_core15.WriteConcern.REPLICATED);
8837
10874
  }
8838
10875
  }
8839
10876
  }
@@ -8859,9 +10896,15 @@ var ServerCoordinator = class {
8859
10896
  }
8860
10897
  return;
8861
10898
  }
8862
- const { eventPayload } = this.applyOpToMap(op);
10899
+ const { eventPayload, rejected } = await this.applyOpToMap(op, clientId);
10900
+ if (rejected) {
10901
+ if (op.id) {
10902
+ this.writeAckManager.failPending(op.id, "Rejected by conflict resolver");
10903
+ }
10904
+ return;
10905
+ }
8863
10906
  if (op.id) {
8864
- this.writeAckManager.notifyLevel(op.id, import_core10.WriteConcern.APPLIED);
10907
+ this.writeAckManager.notifyLevel(op.id, import_core15.WriteConcern.APPLIED);
8865
10908
  }
8866
10909
  if (eventPayload) {
8867
10910
  batchedEvents.push({
@@ -8875,7 +10918,7 @@ var ServerCoordinator = class {
8875
10918
  try {
8876
10919
  await this.persistOpSync(op);
8877
10920
  if (op.id) {
8878
- this.writeAckManager.notifyLevel(op.id, import_core10.WriteConcern.PERSISTED);
10921
+ this.writeAckManager.notifyLevel(op.id, import_core15.WriteConcern.PERSISTED);
8879
10922
  }
8880
10923
  } catch (err) {
8881
10924
  logger.error({ opId: op.id, err }, "Persistence failed");
@@ -8941,9 +10984,9 @@ var ServerCoordinator = class {
8941
10984
  // src/storage/PostgresAdapter.ts
8942
10985
  var import_pg = require("pg");
8943
10986
  var DEFAULT_TABLE_NAME = "topgun_maps";
8944
- var TABLE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
8945
- function validateTableName(name) {
8946
- if (!TABLE_NAME_REGEX.test(name)) {
10987
+ var TABLE_NAME_REGEX2 = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
10988
+ function validateTableName2(name) {
10989
+ if (!TABLE_NAME_REGEX2.test(name)) {
8947
10990
  throw new Error(
8948
10991
  `Invalid table name "${name}". Table name must start with a letter or underscore and contain only alphanumeric characters and underscores.`
8949
10992
  );
@@ -8957,7 +11000,7 @@ var PostgresAdapter = class {
8957
11000
  this.pool = new import_pg.Pool(configOrPool);
8958
11001
  }
8959
11002
  const tableName = options?.tableName ?? DEFAULT_TABLE_NAME;
8960
- validateTableName(tableName);
11003
+ validateTableName2(tableName);
8961
11004
  this.tableName = tableName;
8962
11005
  }
8963
11006
  async initialize() {
@@ -9218,10 +11261,10 @@ var RateLimitInterceptor = class {
9218
11261
  };
9219
11262
 
9220
11263
  // src/utils/nativeStats.ts
9221
- var import_core11 = require("@topgunbuild/core");
11264
+ var import_core16 = require("@topgunbuild/core");
9222
11265
  function getNativeModuleStatus() {
9223
11266
  return {
9224
- nativeHash: (0, import_core11.isUsingNativeHash)(),
11267
+ nativeHash: (0, import_core16.isUsingNativeHash)(),
9225
11268
  sharedArrayBuffer: SharedMemoryManager.isAvailable()
9226
11269
  };
9227
11270
  }
@@ -9255,11 +11298,11 @@ function logNativeStatus() {
9255
11298
 
9256
11299
  // src/cluster/ClusterCoordinator.ts
9257
11300
  var import_events9 = require("events");
9258
- var import_core12 = require("@topgunbuild/core");
11301
+ var import_core17 = require("@topgunbuild/core");
9259
11302
  var DEFAULT_CLUSTER_COORDINATOR_CONFIG = {
9260
11303
  gradualRebalancing: true,
9261
- migration: import_core12.DEFAULT_MIGRATION_CONFIG,
9262
- replication: import_core12.DEFAULT_REPLICATION_CONFIG,
11304
+ migration: import_core17.DEFAULT_MIGRATION_CONFIG,
11305
+ replication: import_core17.DEFAULT_REPLICATION_CONFIG,
9263
11306
  replicationEnabled: true
9264
11307
  };
9265
11308
  var ClusterCoordinator = class extends import_events9.EventEmitter {
@@ -9625,25 +11668,469 @@ var ClusterCoordinator = class extends import_events9.EventEmitter {
9625
11668
  }
9626
11669
  }
9627
11670
  };
11671
+
11672
+ // src/MapWithResolver.ts
11673
+ var import_core18 = require("@topgunbuild/core");
11674
+ var MapWithResolver = class {
11675
+ constructor(config) {
11676
+ this.mapName = config.name;
11677
+ this.hlc = new import_core18.HLC(config.nodeId);
11678
+ this.map = new import_core18.LWWMap(this.hlc);
11679
+ this.resolverService = config.resolverService;
11680
+ this.onRejection = config.onRejection;
11681
+ }
11682
+ /**
11683
+ * Get the map name.
11684
+ */
11685
+ get name() {
11686
+ return this.mapName;
11687
+ }
11688
+ /**
11689
+ * Get the underlying LWWMap.
11690
+ */
11691
+ get rawMap() {
11692
+ return this.map;
11693
+ }
11694
+ /**
11695
+ * Get a value by key.
11696
+ */
11697
+ get(key) {
11698
+ return this.map.get(key);
11699
+ }
11700
+ /**
11701
+ * Get the full record for a key.
11702
+ */
11703
+ getRecord(key) {
11704
+ return this.map.getRecord(key);
11705
+ }
11706
+ /**
11707
+ * Get the timestamp for a key.
11708
+ */
11709
+ getTimestamp(key) {
11710
+ return this.map.getRecord(key)?.timestamp;
11711
+ }
11712
+ /**
11713
+ * Set a value locally (no resolver).
11714
+ * Use for server-initiated writes.
11715
+ */
11716
+ set(key, value, ttlMs) {
11717
+ return this.map.set(key, value, ttlMs);
11718
+ }
11719
+ /**
11720
+ * Set a value with conflict resolution.
11721
+ * Use for client-initiated writes.
11722
+ *
11723
+ * @param key The key to set
11724
+ * @param value The new value
11725
+ * @param timestamp The client's timestamp
11726
+ * @param remoteNodeId The client's node ID
11727
+ * @param auth Optional authentication context
11728
+ * @returns Result containing applied status and merge result
11729
+ */
11730
+ async setWithResolver(key, value, timestamp, remoteNodeId, auth) {
11731
+ const context = {
11732
+ mapName: this.mapName,
11733
+ key,
11734
+ localValue: this.map.get(key),
11735
+ remoteValue: value,
11736
+ localTimestamp: this.getTimestamp(key),
11737
+ remoteTimestamp: timestamp,
11738
+ remoteNodeId,
11739
+ auth,
11740
+ readEntry: (k) => this.map.get(k)
11741
+ };
11742
+ const result = await this.resolverService.resolve(context);
11743
+ switch (result.action) {
11744
+ case "accept": {
11745
+ const record2 = {
11746
+ value: result.value,
11747
+ timestamp
11748
+ };
11749
+ this.map.merge(key, record2);
11750
+ return { applied: true, result, record: record2 };
11751
+ }
11752
+ case "merge": {
11753
+ const record2 = {
11754
+ value: result.value,
11755
+ timestamp
11756
+ };
11757
+ this.map.merge(key, record2);
11758
+ return { applied: true, result, record: record2 };
11759
+ }
11760
+ case "reject": {
11761
+ if (this.onRejection) {
11762
+ this.onRejection({
11763
+ mapName: this.mapName,
11764
+ key,
11765
+ attemptedValue: value,
11766
+ reason: result.reason,
11767
+ timestamp,
11768
+ nodeId: remoteNodeId
11769
+ });
11770
+ }
11771
+ return { applied: false, result };
11772
+ }
11773
+ case "local": {
11774
+ return { applied: false, result };
11775
+ }
11776
+ default:
11777
+ const record = {
11778
+ value: result.value ?? value,
11779
+ timestamp
11780
+ };
11781
+ this.map.merge(key, record);
11782
+ return { applied: true, result, record };
11783
+ }
11784
+ }
11785
+ /**
11786
+ * Remove a key.
11787
+ */
11788
+ remove(key) {
11789
+ return this.map.remove(key);
11790
+ }
11791
+ /**
11792
+ * Standard merge without resolver (for sync operations).
11793
+ */
11794
+ merge(key, record) {
11795
+ return this.map.merge(key, record);
11796
+ }
11797
+ /**
11798
+ * Merge with resolver support.
11799
+ * Equivalent to setWithResolver but takes a full record.
11800
+ */
11801
+ async mergeWithResolver(key, record, remoteNodeId, auth) {
11802
+ if (record.value === null) {
11803
+ const applied = this.map.merge(key, record);
11804
+ return {
11805
+ applied,
11806
+ result: applied ? { action: "accept", value: record.value } : { action: "local" },
11807
+ record: applied ? record : void 0
11808
+ };
11809
+ }
11810
+ return this.setWithResolver(
11811
+ key,
11812
+ record.value,
11813
+ record.timestamp,
11814
+ remoteNodeId,
11815
+ auth
11816
+ );
11817
+ }
11818
+ /**
11819
+ * Clear all data.
11820
+ */
11821
+ clear() {
11822
+ this.map.clear();
11823
+ }
11824
+ /**
11825
+ * Get map size.
11826
+ */
11827
+ get size() {
11828
+ return this.map.size;
11829
+ }
11830
+ /**
11831
+ * Iterate over entries.
11832
+ */
11833
+ entries() {
11834
+ return this.map.entries();
11835
+ }
11836
+ /**
11837
+ * Get all keys.
11838
+ */
11839
+ allKeys() {
11840
+ return this.map.allKeys();
11841
+ }
11842
+ /**
11843
+ * Subscribe to changes.
11844
+ */
11845
+ onChange(callback) {
11846
+ return this.map.onChange(callback);
11847
+ }
11848
+ /**
11849
+ * Get MerkleTree for sync.
11850
+ */
11851
+ getMerkleTree() {
11852
+ return this.map.getMerkleTree();
11853
+ }
11854
+ /**
11855
+ * Prune old tombstones.
11856
+ */
11857
+ prune(olderThan) {
11858
+ return this.map.prune(olderThan);
11859
+ }
11860
+ };
11861
+
11862
+ // src/config/IndexConfig.ts
11863
+ var DEFAULT_INDEX_CONFIG = {
11864
+ autoIndex: false,
11865
+ maxAutoIndexesPerMap: 10,
11866
+ maps: [],
11867
+ logStats: false,
11868
+ statsLogInterval: 6e4
11869
+ };
11870
+ function validateIndexConfig(config) {
11871
+ const errors = [];
11872
+ if (config.maxAutoIndexesPerMap !== void 0) {
11873
+ if (typeof config.maxAutoIndexesPerMap !== "number" || config.maxAutoIndexesPerMap < 1) {
11874
+ errors.push("maxAutoIndexesPerMap must be a positive number");
11875
+ }
11876
+ }
11877
+ if (config.statsLogInterval !== void 0) {
11878
+ if (typeof config.statsLogInterval !== "number" || config.statsLogInterval < 1e3) {
11879
+ errors.push("statsLogInterval must be at least 1000ms");
11880
+ }
11881
+ }
11882
+ if (config.maps) {
11883
+ const mapNames = /* @__PURE__ */ new Set();
11884
+ for (const mapConfig of config.maps) {
11885
+ if (!mapConfig.mapName || typeof mapConfig.mapName !== "string") {
11886
+ errors.push("Each map config must have a valid mapName");
11887
+ continue;
11888
+ }
11889
+ if (mapNames.has(mapConfig.mapName)) {
11890
+ errors.push(`Duplicate map config for: ${mapConfig.mapName}`);
11891
+ }
11892
+ mapNames.add(mapConfig.mapName);
11893
+ if (!Array.isArray(mapConfig.indexes)) {
11894
+ errors.push(`Map ${mapConfig.mapName}: indexes must be an array`);
11895
+ continue;
11896
+ }
11897
+ const attrNames = /* @__PURE__ */ new Set();
11898
+ for (const indexDef of mapConfig.indexes) {
11899
+ if (!indexDef.attribute || typeof indexDef.attribute !== "string") {
11900
+ errors.push(`Map ${mapConfig.mapName}: index must have valid attribute`);
11901
+ continue;
11902
+ }
11903
+ if (!["hash", "navigable"].includes(indexDef.type)) {
11904
+ errors.push(
11905
+ `Map ${mapConfig.mapName}: index type must be 'hash' or 'navigable'`
11906
+ );
11907
+ }
11908
+ if (indexDef.comparator && !["number", "string", "date"].includes(indexDef.comparator)) {
11909
+ errors.push(
11910
+ `Map ${mapConfig.mapName}: comparator must be 'number', 'string', or 'date'`
11911
+ );
11912
+ }
11913
+ const key = `${indexDef.attribute}:${indexDef.type}`;
11914
+ if (attrNames.has(key)) {
11915
+ errors.push(
11916
+ `Map ${mapConfig.mapName}: duplicate ${indexDef.type} index on ${indexDef.attribute}`
11917
+ );
11918
+ }
11919
+ attrNames.add(key);
11920
+ }
11921
+ }
11922
+ }
11923
+ return errors;
11924
+ }
11925
+ function mergeWithDefaults(userConfig) {
11926
+ return {
11927
+ ...DEFAULT_INDEX_CONFIG,
11928
+ ...userConfig,
11929
+ maps: userConfig.maps ?? DEFAULT_INDEX_CONFIG.maps
11930
+ };
11931
+ }
11932
+
11933
+ // src/config/MapFactory.ts
11934
+ var import_core19 = require("@topgunbuild/core");
11935
+ var MapFactory = class {
11936
+ /**
11937
+ * Create a MapFactory.
11938
+ *
11939
+ * @param config - Server index configuration
11940
+ */
11941
+ constructor(config) {
11942
+ this.config = mergeWithDefaults(config ?? {});
11943
+ this.mapConfigs = /* @__PURE__ */ new Map();
11944
+ for (const mapConfig of this.config.maps ?? []) {
11945
+ this.mapConfigs.set(mapConfig.mapName, mapConfig);
11946
+ }
11947
+ }
11948
+ /**
11949
+ * Create an LWWMap or IndexedLWWMap based on configuration.
11950
+ *
11951
+ * @param mapName - Name of the map
11952
+ * @param hlc - Hybrid Logical Clock instance
11953
+ * @returns LWWMap or IndexedLWWMap depending on configuration
11954
+ */
11955
+ createLWWMap(mapName, hlc) {
11956
+ const mapConfig = this.mapConfigs.get(mapName);
11957
+ if (!mapConfig || mapConfig.indexes.length === 0) {
11958
+ return new import_core19.LWWMap(hlc);
11959
+ }
11960
+ const map = new import_core19.IndexedLWWMap(hlc);
11961
+ for (const indexDef of mapConfig.indexes) {
11962
+ this.addIndexToLWWMap(map, indexDef);
11963
+ }
11964
+ return map;
11965
+ }
11966
+ /**
11967
+ * Create an ORMap or IndexedORMap based on configuration.
11968
+ *
11969
+ * @param mapName - Name of the map
11970
+ * @param hlc - Hybrid Logical Clock instance
11971
+ * @returns ORMap or IndexedORMap depending on configuration
11972
+ */
11973
+ createORMap(mapName, hlc) {
11974
+ const mapConfig = this.mapConfigs.get(mapName);
11975
+ if (!mapConfig || mapConfig.indexes.length === 0) {
11976
+ return new import_core19.ORMap(hlc);
11977
+ }
11978
+ const map = new import_core19.IndexedORMap(hlc);
11979
+ for (const indexDef of mapConfig.indexes) {
11980
+ this.addIndexToORMap(map, indexDef);
11981
+ }
11982
+ return map;
11983
+ }
11984
+ /**
11985
+ * Add an index to an IndexedLWWMap based on definition.
11986
+ */
11987
+ addIndexToLWWMap(map, indexDef) {
11988
+ const attribute = this.createAttribute(indexDef.attribute);
11989
+ if (indexDef.type === "hash") {
11990
+ map.addHashIndex(attribute);
11991
+ } else if (indexDef.type === "navigable") {
11992
+ const navAttribute = attribute;
11993
+ const comparator = this.createComparator(indexDef.comparator);
11994
+ map.addNavigableIndex(navAttribute, comparator);
11995
+ }
11996
+ }
11997
+ /**
11998
+ * Add an index to an IndexedORMap based on definition.
11999
+ */
12000
+ addIndexToORMap(map, indexDef) {
12001
+ const attribute = this.createAttribute(indexDef.attribute);
12002
+ if (indexDef.type === "hash") {
12003
+ map.addHashIndex(attribute);
12004
+ } else if (indexDef.type === "navigable") {
12005
+ const navAttribute = attribute;
12006
+ const comparator = this.createComparator(indexDef.comparator);
12007
+ map.addNavigableIndex(navAttribute, comparator);
12008
+ }
12009
+ }
12010
+ /**
12011
+ * Create an Attribute for extracting values from records.
12012
+ * Supports dot notation for nested paths.
12013
+ */
12014
+ createAttribute(path) {
12015
+ return (0, import_core19.simpleAttribute)(path, (record) => {
12016
+ return this.getNestedValue(record, path);
12017
+ });
12018
+ }
12019
+ /**
12020
+ * Get a nested value from an object using dot notation.
12021
+ *
12022
+ * @param obj - Object to extract value from
12023
+ * @param path - Dot-notation path (e.g., "user.email")
12024
+ * @returns Value at the path or undefined
12025
+ */
12026
+ getNestedValue(obj, path) {
12027
+ if (obj === null || obj === void 0) {
12028
+ return void 0;
12029
+ }
12030
+ const parts = path.split(".");
12031
+ let current = obj;
12032
+ for (const part of parts) {
12033
+ if (current === void 0 || current === null) {
12034
+ return void 0;
12035
+ }
12036
+ if (typeof current !== "object") {
12037
+ return void 0;
12038
+ }
12039
+ current = current[part];
12040
+ }
12041
+ return current;
12042
+ }
12043
+ /**
12044
+ * Create a comparator function for navigable indexes.
12045
+ */
12046
+ createComparator(type) {
12047
+ switch (type) {
12048
+ case "number":
12049
+ return (a, b) => {
12050
+ const numA = typeof a === "number" ? a : parseFloat(String(a));
12051
+ const numB = typeof b === "number" ? b : parseFloat(String(b));
12052
+ return numA - numB;
12053
+ };
12054
+ case "date":
12055
+ return (a, b) => {
12056
+ const dateA = new Date(a).getTime();
12057
+ const dateB = new Date(b).getTime();
12058
+ return dateA - dateB;
12059
+ };
12060
+ case "string":
12061
+ return (a, b) => {
12062
+ const strA = String(a);
12063
+ const strB = String(b);
12064
+ return strA.localeCompare(strB);
12065
+ };
12066
+ default:
12067
+ return void 0;
12068
+ }
12069
+ }
12070
+ /**
12071
+ * Check if a map should be indexed based on configuration.
12072
+ *
12073
+ * @param mapName - Name of the map
12074
+ * @returns true if map has index configuration
12075
+ */
12076
+ hasIndexConfig(mapName) {
12077
+ const config = this.mapConfigs.get(mapName);
12078
+ return config !== void 0 && config.indexes.length > 0;
12079
+ }
12080
+ /**
12081
+ * Get index configuration for a map.
12082
+ *
12083
+ * @param mapName - Name of the map
12084
+ * @returns Map index config or undefined
12085
+ */
12086
+ getMapConfig(mapName) {
12087
+ return this.mapConfigs.get(mapName);
12088
+ }
12089
+ /**
12090
+ * Get all configured map names.
12091
+ *
12092
+ * @returns Array of map names with index configuration
12093
+ */
12094
+ getConfiguredMaps() {
12095
+ return Array.from(this.mapConfigs.keys());
12096
+ }
12097
+ /**
12098
+ * Get the full server index configuration.
12099
+ */
12100
+ getConfig() {
12101
+ return this.config;
12102
+ }
12103
+ };
9628
12104
  // Annotate the CommonJS export names for ESM import in node:
9629
12105
  0 && (module.exports = {
9630
12106
  BufferPool,
9631
12107
  ClusterCoordinator,
9632
12108
  ClusterManager,
12109
+ ConflictResolverHandler,
12110
+ ConflictResolverService,
9633
12111
  ConnectionRateLimiter,
9634
12112
  DEFAULT_CLUSTER_COORDINATOR_CONFIG,
12113
+ DEFAULT_CONFLICT_RESOLVER_CONFIG,
12114
+ DEFAULT_INDEX_CONFIG,
12115
+ DEFAULT_JOURNAL_SERVICE_CONFIG,
9635
12116
  DEFAULT_LAG_TRACKER_CONFIG,
12117
+ DEFAULT_SANDBOX_CONFIG,
12118
+ EntryProcessorHandler,
12119
+ EventJournalService,
9636
12120
  FilterTasklet,
9637
12121
  ForEachTasklet,
9638
12122
  IteratorTasklet,
9639
12123
  LagTracker,
9640
12124
  LockManager,
12125
+ MapFactory,
9641
12126
  MapTasklet,
12127
+ MapWithResolver,
9642
12128
  MemoryServerAdapter,
9643
12129
  MigrationManager,
9644
12130
  ObjectPool,
9645
12131
  PartitionService,
9646
12132
  PostgresAdapter,
12133
+ ProcessorSandbox,
9647
12134
  RateLimitInterceptor,
9648
12135
  ReduceTasklet,
9649
12136
  ReplicationPipeline,
@@ -9666,10 +12153,12 @@ var ClusterCoordinator = class extends import_events9.EventEmitter {
9666
12153
  getNativeStats,
9667
12154
  logNativeStatus,
9668
12155
  logger,
12156
+ mergeWithDefaults,
9669
12157
  setGlobalBufferPool,
9670
12158
  setGlobalEventPayloadPool,
9671
12159
  setGlobalMessagePool,
9672
12160
  setGlobalRecordPool,
9673
- setGlobalTimestampPool
12161
+ setGlobalTimestampPool,
12162
+ validateIndexConfig
9674
12163
  });
9675
12164
  //# sourceMappingURL=index.js.map