@topgunbuild/server 0.3.0 → 0.4.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,29 @@ __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_JOURNAL_SERVICE_CONFIG: () => DEFAULT_JOURNAL_SERVICE_CONFIG,
38
42
  DEFAULT_LAG_TRACKER_CONFIG: () => DEFAULT_LAG_TRACKER_CONFIG,
43
+ DEFAULT_SANDBOX_CONFIG: () => DEFAULT_SANDBOX_CONFIG,
44
+ EntryProcessorHandler: () => EntryProcessorHandler,
45
+ EventJournalService: () => EventJournalService,
39
46
  FilterTasklet: () => FilterTasklet,
40
47
  ForEachTasklet: () => ForEachTasklet,
41
48
  IteratorTasklet: () => IteratorTasklet,
42
49
  LagTracker: () => LagTracker,
43
50
  LockManager: () => LockManager,
44
51
  MapTasklet: () => MapTasklet,
52
+ MapWithResolver: () => MapWithResolver,
45
53
  MemoryServerAdapter: () => MemoryServerAdapter,
46
54
  MigrationManager: () => MigrationManager,
47
55
  ObjectPool: () => ObjectPool,
48
56
  PartitionService: () => PartitionService,
49
57
  PostgresAdapter: () => PostgresAdapter,
58
+ ProcessorSandbox: () => ProcessorSandbox,
50
59
  RateLimitInterceptor: () => RateLimitInterceptor,
51
60
  ReduceTasklet: () => ReduceTasklet,
52
61
  ReplicationPipeline: () => ReplicationPipeline,
@@ -82,7 +91,7 @@ var import_http = require("http");
82
91
  var import_https = require("https");
83
92
  var import_fs2 = require("fs");
84
93
  var import_ws3 = require("ws");
85
- var import_core10 = require("@topgunbuild/core");
94
+ var import_core15 = require("@topgunbuild/core");
86
95
  var jwt = __toESM(require("jsonwebtoken"));
87
96
  var crypto = __toESM(require("crypto"));
88
97
 
@@ -6468,231 +6477,1621 @@ var ReplicationPipeline = class extends import_events8.EventEmitter {
6468
6477
  }
6469
6478
  };
6470
6479
 
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();
6480
+ // src/handlers/CounterHandler.ts
6481
+ var import_core10 = require("@topgunbuild/core");
6482
+ var CounterHandler = class {
6483
+ // counterName -> Set<clientId>
6484
+ constructor(nodeId = "server") {
6485
+ this.nodeId = nodeId;
6486
+ this.counters = /* @__PURE__ */ new Map();
6487
+ this.subscriptions = /* @__PURE__ */ new Map();
6488
+ }
6489
+ /**
6490
+ * Get or create a counter by name.
6491
+ */
6492
+ getOrCreateCounter(name) {
6493
+ let counter = this.counters.get(name);
6494
+ if (!counter) {
6495
+ counter = new import_core10.PNCounterImpl({ nodeId: this.nodeId });
6496
+ this.counters.set(name, counter);
6497
+ logger.debug({ name }, "Created new counter");
6498
+ }
6499
+ return counter;
6500
+ }
6501
+ /**
6502
+ * Handle COUNTER_REQUEST - client wants initial state.
6503
+ * @returns Response message to send back to client
6504
+ */
6505
+ handleCounterRequest(clientId, name) {
6506
+ const counter = this.getOrCreateCounter(name);
6507
+ this.subscribe(clientId, name);
6508
+ const state = counter.getState();
6509
+ logger.debug({ clientId, name, value: counter.get() }, "Counter request handled");
6510
+ return {
6511
+ type: "COUNTER_RESPONSE",
6512
+ payload: {
6513
+ name,
6514
+ state: this.stateToObject(state)
6510
6515
  }
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
6516
  };
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");
6557
- }
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");
6565
- } else {
6566
- this.httpServer = (0, import_http.createServer)((_req, res) => {
6567
- res.writeHead(200);
6568
- res.end("TopGun Server Running");
6569
- });
6570
- if (process.env.NODE_ENV === "production") {
6571
- logger.warn("\u26A0\uFE0F TLS is disabled! Client connections are NOT encrypted.");
6517
+ }
6518
+ /**
6519
+ * Handle COUNTER_SYNC - client sends their state to merge.
6520
+ * @returns Merged state and list of clients to broadcast to
6521
+ */
6522
+ handleCounterSync(clientId, name, stateObj) {
6523
+ const counter = this.getOrCreateCounter(name);
6524
+ const incomingState = this.objectToState(stateObj);
6525
+ counter.merge(incomingState);
6526
+ const mergedState = counter.getState();
6527
+ const mergedStateObj = this.stateToObject(mergedState);
6528
+ logger.debug(
6529
+ { clientId, name, value: counter.get() },
6530
+ "Counter sync handled"
6531
+ );
6532
+ this.subscribe(clientId, name);
6533
+ const subscribers = this.subscriptions.get(name) || /* @__PURE__ */ new Set();
6534
+ const broadcastTo = Array.from(subscribers).filter((id) => id !== clientId);
6535
+ return {
6536
+ // Response to the sending client
6537
+ response: {
6538
+ type: "COUNTER_UPDATE",
6539
+ payload: {
6540
+ name,
6541
+ state: mergedStateObj
6542
+ }
6543
+ },
6544
+ // Broadcast to other clients
6545
+ broadcastTo,
6546
+ broadcastMessage: {
6547
+ type: "COUNTER_UPDATE",
6548
+ payload: {
6549
+ name,
6550
+ state: mergedStateObj
6551
+ }
6572
6552
  }
6553
+ };
6554
+ }
6555
+ /**
6556
+ * Subscribe a client to counter updates.
6557
+ */
6558
+ subscribe(clientId, counterName) {
6559
+ if (!this.subscriptions.has(counterName)) {
6560
+ this.subscriptions.set(counterName, /* @__PURE__ */ new Set());
6573
6561
  }
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");
6583
- }
6584
- } else {
6585
- res.statusCode = 404;
6586
- res.end();
6562
+ this.subscriptions.get(counterName).add(clientId);
6563
+ logger.debug({ clientId, counterName }, "Client subscribed to counter");
6564
+ }
6565
+ /**
6566
+ * Unsubscribe a client from counter updates.
6567
+ */
6568
+ unsubscribe(clientId, counterName) {
6569
+ const subs = this.subscriptions.get(counterName);
6570
+ if (subs) {
6571
+ subs.delete(clientId);
6572
+ if (subs.size === 0) {
6573
+ this.subscriptions.delete(counterName);
6587
6574
  }
6588
- });
6589
- this.metricsServer.listen(metricsPort, () => {
6590
- logger.info({ port: metricsPort }, "Metrics server listening");
6591
- });
6592
- this.metricsServer.on("error", (err) => {
6593
- logger.error({ err, port: metricsPort }, "Metrics server failed to start");
6594
- });
6595
- this.wss = new import_ws3.WebSocketServer({
6596
- server: this.httpServer,
6597
- // Increase backlog for pending connections (default Linux is 128)
6598
- backlog: config.wsBacklog ?? 511,
6599
- // Disable per-message deflate by default (CPU overhead)
6600
- perMessageDeflate: config.wsCompression ?? false,
6601
- // Max payload size (64MB default)
6602
- maxPayload: config.wsMaxPayload ?? 64 * 1024 * 1024,
6603
- // Skip UTF-8 validation for binary messages (performance)
6604
- skipUTF8Validation: true
6605
- });
6606
- this.wss.on("connection", (ws) => this.handleConnection(ws));
6607
- this.httpServer.maxConnections = config.maxConnections ?? 1e4;
6608
- this.httpServer.timeout = config.serverTimeout ?? 12e4;
6609
- this.httpServer.keepAliveTimeout = config.keepAliveTimeout ?? 5e3;
6610
- this.httpServer.headersTimeout = config.headersTimeout ?? 6e4;
6611
- this.httpServer.on("connection", (socket) => {
6612
- socket.setNoDelay(true);
6613
- socket.setKeepAlive(true, 6e4);
6614
- });
6615
- this.httpServer.listen(config.port, () => {
6616
- const addr = this.httpServer.address();
6617
- this._actualPort = typeof addr === "object" && addr ? addr.port : config.port;
6618
- logger.info({ port: this._actualPort }, "Server Coordinator listening");
6619
- const clusterPort = config.clusterPort ?? 0;
6620
- const peers = config.resolvePeers ? config.resolvePeers() : config.peers || [];
6621
- this.cluster = new ClusterManager({
6622
- nodeId: config.nodeId,
6623
- host: config.host || "localhost",
6624
- port: clusterPort,
6625
- peers,
6626
- discovery: config.discovery,
6627
- serviceName: config.serviceName,
6628
- discoveryInterval: config.discoveryInterval,
6629
- tls: config.clusterTls
6630
- });
6631
- this.partitionService = new PartitionService(this.cluster);
6632
- if (config.replicationEnabled !== false) {
6633
- this.replicationPipeline = new ReplicationPipeline(
6634
- this.cluster,
6635
- this.partitionService,
6636
- {
6637
- ...import_core10.DEFAULT_REPLICATION_CONFIG,
6638
- defaultConsistency: config.defaultConsistency ?? import_core10.ConsistencyLevel.EVENTUAL,
6639
- ...config.replicationConfig
6640
- }
6641
- );
6642
- this.replicationPipeline.setOperationApplier(this.applyReplicatedOperation.bind(this));
6643
- logger.info({ nodeId: config.nodeId }, "ReplicationPipeline initialized");
6575
+ }
6576
+ }
6577
+ /**
6578
+ * Unsubscribe a client from all counters (e.g., on disconnect).
6579
+ */
6580
+ unsubscribeAll(clientId) {
6581
+ for (const [counterName, subs] of this.subscriptions) {
6582
+ subs.delete(clientId);
6583
+ if (subs.size === 0) {
6584
+ this.subscriptions.delete(counterName);
6644
6585
  }
6645
- this.partitionService.on("rebalanced", (partitionMap, changes) => {
6646
- this.broadcastPartitionMap(partitionMap);
6647
- });
6648
- this.lockManager = new LockManager();
6649
- this.lockManager.on("lockGranted", (evt) => this.handleLockGranted(evt));
6650
- this.topicManager = new TopicManager({
6651
- cluster: this.cluster,
6652
- sendToClient: (clientId, message) => {
6653
- const client = this.clients.get(clientId);
6654
- if (client && client.socket.readyState === import_ws3.WebSocket.OPEN) {
6655
- client.writer.write(message);
6656
- }
6657
- }
6658
- });
6659
- this.systemManager = new SystemManager(
6660
- this.cluster,
6661
- this.metricsService,
6662
- (name) => this.getMap(name)
6663
- );
6664
- this.setupClusterListeners();
6665
- this.cluster.start().then((actualClusterPort) => {
6666
- this._actualClusterPort = actualClusterPort;
6667
- this.metricsService.setClusterMembers(this.cluster.getMembers().length);
6668
- logger.info({ clusterPort: this._actualClusterPort }, "Cluster started");
6669
- this.systemManager.start();
6670
- this._readyResolve();
6671
- }).catch((err) => {
6672
- this._actualClusterPort = clusterPort;
6673
- this.metricsService.setClusterMembers(this.cluster.getMembers().length);
6674
- logger.info({ clusterPort: this._actualClusterPort }, "Cluster started (sync)");
6675
- this.systemManager.start();
6676
- this._readyResolve();
6677
- });
6678
- });
6679
- if (this.storage) {
6680
- this.storage.initialize().then(() => {
6681
- logger.info("Storage adapter initialized");
6682
- }).catch((err) => {
6683
- logger.error({ err }, "Failed to initialize storage");
6684
- });
6685
6586
  }
6686
- this.startGarbageCollection();
6687
- this.startHeartbeatCheck();
6587
+ logger.debug({ clientId }, "Client unsubscribed from all counters");
6688
6588
  }
6689
- /** Wait for server to be fully ready (ports assigned) */
6690
- ready() {
6691
- return this._readyPromise;
6589
+ /**
6590
+ * Get current counter value (for monitoring/debugging).
6591
+ */
6592
+ getCounterValue(name) {
6593
+ const counter = this.counters.get(name);
6594
+ return counter ? counter.get() : 0;
6692
6595
  }
6693
6596
  /**
6694
- * Wait for all pending batch operations to complete.
6695
- * Useful for tests that need to verify state after OP_BATCH.
6597
+ * Get all counter names.
6598
+ */
6599
+ getCounterNames() {
6600
+ return Array.from(this.counters.keys());
6601
+ }
6602
+ /**
6603
+ * Get number of subscribers for a counter.
6604
+ */
6605
+ getSubscriberCount(name) {
6606
+ return this.subscriptions.get(name)?.size || 0;
6607
+ }
6608
+ /**
6609
+ * Convert Map-based state to plain object for serialization.
6610
+ */
6611
+ stateToObject(state) {
6612
+ return {
6613
+ p: Object.fromEntries(state.positive),
6614
+ n: Object.fromEntries(state.negative)
6615
+ };
6616
+ }
6617
+ /**
6618
+ * Convert plain object to Map-based state.
6619
+ */
6620
+ objectToState(obj) {
6621
+ return {
6622
+ positive: new Map(Object.entries(obj.p || {})),
6623
+ negative: new Map(Object.entries(obj.n || {}))
6624
+ };
6625
+ }
6626
+ };
6627
+
6628
+ // src/handlers/EntryProcessorHandler.ts
6629
+ var import_core12 = require("@topgunbuild/core");
6630
+
6631
+ // src/ProcessorSandbox.ts
6632
+ var import_core11 = require("@topgunbuild/core");
6633
+ var ivm = null;
6634
+ try {
6635
+ ivm = require("isolated-vm");
6636
+ } catch {
6637
+ const isProduction = process.env.NODE_ENV === "production";
6638
+ if (isProduction) {
6639
+ logger.error(
6640
+ "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"
6641
+ );
6642
+ } else {
6643
+ logger.warn("isolated-vm not available, falling back to less secure VM");
6644
+ }
6645
+ }
6646
+ var DEFAULT_SANDBOX_CONFIG = {
6647
+ memoryLimitMb: 8,
6648
+ timeoutMs: 100,
6649
+ maxCachedIsolates: 100,
6650
+ strictValidation: true
6651
+ };
6652
+ var ProcessorSandbox = class {
6653
+ constructor(config = {}) {
6654
+ this.isolateCache = /* @__PURE__ */ new Map();
6655
+ this.scriptCache = /* @__PURE__ */ new Map();
6656
+ this.fallbackScriptCache = /* @__PURE__ */ new Map();
6657
+ this.disposed = false;
6658
+ this.config = { ...DEFAULT_SANDBOX_CONFIG, ...config };
6659
+ }
6660
+ /**
6661
+ * Execute an entry processor in the sandbox.
6662
+ *
6663
+ * @param processor The processor definition (name, code, args)
6664
+ * @param value The current value for the key (or undefined)
6665
+ * @param key The key being processed
6666
+ * @returns Result containing success status, result, and new value
6667
+ */
6668
+ async execute(processor, value, key) {
6669
+ if (this.disposed) {
6670
+ return {
6671
+ success: false,
6672
+ error: "Sandbox has been disposed"
6673
+ };
6674
+ }
6675
+ if (this.config.strictValidation) {
6676
+ const validation = (0, import_core11.validateProcessorCode)(processor.code);
6677
+ if (!validation.valid) {
6678
+ return {
6679
+ success: false,
6680
+ error: validation.error
6681
+ };
6682
+ }
6683
+ }
6684
+ if (ivm) {
6685
+ return this.executeInIsolate(processor, value, key);
6686
+ } else {
6687
+ return this.executeInFallback(processor, value, key);
6688
+ }
6689
+ }
6690
+ /**
6691
+ * Execute processor in isolated-vm (secure production mode).
6692
+ */
6693
+ async executeInIsolate(processor, value, key) {
6694
+ if (!ivm) {
6695
+ return { success: false, error: "isolated-vm not available" };
6696
+ }
6697
+ const isolate = this.getOrCreateIsolate(processor.name);
6698
+ try {
6699
+ const context = await isolate.createContext();
6700
+ const jail = context.global;
6701
+ await jail.set("global", jail.derefInto());
6702
+ await context.eval(`
6703
+ var value = ${JSON.stringify(value)};
6704
+ var key = ${JSON.stringify(key)};
6705
+ var args = ${JSON.stringify(processor.args)};
6706
+ `);
6707
+ const wrappedCode = `
6708
+ (function() {
6709
+ ${processor.code}
6710
+ })()
6711
+ `;
6712
+ const script = await this.getOrCompileScript(
6713
+ processor.name,
6714
+ wrappedCode,
6715
+ isolate
6716
+ );
6717
+ const result = await script.run(context, {
6718
+ timeout: this.config.timeoutMs
6719
+ });
6720
+ const parsed = result;
6721
+ if (typeof parsed !== "object" || parsed === null) {
6722
+ return {
6723
+ success: false,
6724
+ error: "Processor must return { value, result? } object"
6725
+ };
6726
+ }
6727
+ return {
6728
+ success: true,
6729
+ result: parsed.result,
6730
+ newValue: parsed.value
6731
+ };
6732
+ } catch (error) {
6733
+ const message = error instanceof Error ? error.message : String(error);
6734
+ if (message.includes("Script execution timed out")) {
6735
+ return {
6736
+ success: false,
6737
+ error: "Processor execution timed out"
6738
+ };
6739
+ }
6740
+ return {
6741
+ success: false,
6742
+ error: message
6743
+ };
6744
+ }
6745
+ }
6746
+ /**
6747
+ * Execute processor in fallback VM (less secure, for development).
6748
+ */
6749
+ async executeInFallback(processor, value, key) {
6750
+ try {
6751
+ const isResolver = processor.name.startsWith("resolver:");
6752
+ let fn = isResolver ? void 0 : this.fallbackScriptCache.get(processor.name);
6753
+ if (!fn) {
6754
+ const wrappedCode = `
6755
+ return (function(value, key, args) {
6756
+ ${processor.code}
6757
+ })
6758
+ `;
6759
+ fn = new Function(wrappedCode)();
6760
+ if (!isResolver) {
6761
+ this.fallbackScriptCache.set(processor.name, fn);
6762
+ }
6763
+ }
6764
+ const timeoutPromise = new Promise((_, reject) => {
6765
+ setTimeout(() => reject(new Error("Processor execution timed out")), this.config.timeoutMs);
6766
+ });
6767
+ const executionPromise = Promise.resolve().then(() => fn(value, key, processor.args));
6768
+ const result = await Promise.race([executionPromise, timeoutPromise]);
6769
+ if (typeof result !== "object" || result === null) {
6770
+ return {
6771
+ success: false,
6772
+ error: "Processor must return { value, result? } object"
6773
+ };
6774
+ }
6775
+ return {
6776
+ success: true,
6777
+ result: result.result,
6778
+ newValue: result.value
6779
+ };
6780
+ } catch (error) {
6781
+ const message = error instanceof Error ? error.message : String(error);
6782
+ return {
6783
+ success: false,
6784
+ error: message
6785
+ };
6786
+ }
6787
+ }
6788
+ /**
6789
+ * Get or create an isolate for a processor.
6790
+ */
6791
+ getOrCreateIsolate(name) {
6792
+ if (!ivm) {
6793
+ throw new Error("isolated-vm not available");
6794
+ }
6795
+ let isolate = this.isolateCache.get(name);
6796
+ if (!isolate || isolate.isDisposed) {
6797
+ if (this.isolateCache.size >= this.config.maxCachedIsolates) {
6798
+ const oldest = this.isolateCache.keys().next().value;
6799
+ if (oldest) {
6800
+ const oldIsolate = this.isolateCache.get(oldest);
6801
+ if (oldIsolate && !oldIsolate.isDisposed) {
6802
+ oldIsolate.dispose();
6803
+ }
6804
+ this.isolateCache.delete(oldest);
6805
+ this.scriptCache.delete(oldest);
6806
+ }
6807
+ }
6808
+ isolate = new ivm.Isolate({
6809
+ memoryLimit: this.config.memoryLimitMb
6810
+ });
6811
+ this.isolateCache.set(name, isolate);
6812
+ }
6813
+ return isolate;
6814
+ }
6815
+ /**
6816
+ * Get or compile a script for a processor.
6817
+ */
6818
+ async getOrCompileScript(name, code, isolate) {
6819
+ let script = this.scriptCache.get(name);
6820
+ if (!script) {
6821
+ script = await isolate.compileScript(code);
6822
+ this.scriptCache.set(name, script);
6823
+ }
6824
+ return script;
6825
+ }
6826
+ /**
6827
+ * Clear script cache for a specific processor (e.g., when code changes).
6828
+ */
6829
+ clearCache(processorName) {
6830
+ if (processorName) {
6831
+ const isolate = this.isolateCache.get(processorName);
6832
+ if (isolate && !isolate.isDisposed) {
6833
+ isolate.dispose();
6834
+ }
6835
+ this.isolateCache.delete(processorName);
6836
+ this.scriptCache.delete(processorName);
6837
+ this.fallbackScriptCache.delete(processorName);
6838
+ } else {
6839
+ for (const isolate of this.isolateCache.values()) {
6840
+ if (!isolate.isDisposed) {
6841
+ isolate.dispose();
6842
+ }
6843
+ }
6844
+ this.isolateCache.clear();
6845
+ this.scriptCache.clear();
6846
+ this.fallbackScriptCache.clear();
6847
+ }
6848
+ }
6849
+ /**
6850
+ * Check if using secure isolated-vm mode.
6851
+ */
6852
+ isSecureMode() {
6853
+ return ivm !== null;
6854
+ }
6855
+ /**
6856
+ * Get current cache sizes.
6857
+ */
6858
+ getCacheStats() {
6859
+ return {
6860
+ isolates: this.isolateCache.size,
6861
+ scripts: this.scriptCache.size,
6862
+ fallbackScripts: this.fallbackScriptCache.size
6863
+ };
6864
+ }
6865
+ /**
6866
+ * Dispose of all isolates and clear caches.
6867
+ */
6868
+ dispose() {
6869
+ if (this.disposed) return;
6870
+ this.disposed = true;
6871
+ this.clearCache();
6872
+ logger.debug("ProcessorSandbox disposed");
6873
+ }
6874
+ };
6875
+
6876
+ // src/handlers/EntryProcessorHandler.ts
6877
+ var EntryProcessorHandler = class {
6878
+ constructor(config) {
6879
+ this.hlc = config.hlc;
6880
+ this.sandbox = new ProcessorSandbox(config.sandboxConfig);
6881
+ }
6882
+ /**
6883
+ * Execute a processor on a single key atomically.
6884
+ *
6885
+ * @param map The LWWMap to operate on
6886
+ * @param key The key to process
6887
+ * @param processorDef The processor definition (will be validated)
6888
+ * @returns Result with success status, processor result, and new value
6889
+ */
6890
+ async executeOnKey(map, key, processorDef) {
6891
+ const parseResult = import_core12.EntryProcessorDefSchema.safeParse(processorDef);
6892
+ if (!parseResult.success) {
6893
+ logger.warn(
6894
+ { key, error: parseResult.error.message },
6895
+ "Invalid processor definition"
6896
+ );
6897
+ return {
6898
+ result: {
6899
+ success: false,
6900
+ error: `Invalid processor: ${parseResult.error.message}`
6901
+ }
6902
+ };
6903
+ }
6904
+ const processor = parseResult.data;
6905
+ const currentValue = map.get(key);
6906
+ logger.debug(
6907
+ { key, processor: processor.name, hasValue: currentValue !== void 0 },
6908
+ "Executing entry processor"
6909
+ );
6910
+ const sandboxResult = await this.sandbox.execute(
6911
+ processor,
6912
+ currentValue,
6913
+ key
6914
+ );
6915
+ if (!sandboxResult.success) {
6916
+ logger.warn(
6917
+ { key, processor: processor.name, error: sandboxResult.error },
6918
+ "Processor execution failed"
6919
+ );
6920
+ return { result: sandboxResult };
6921
+ }
6922
+ let timestamp;
6923
+ if (sandboxResult.newValue !== void 0) {
6924
+ const record = map.set(key, sandboxResult.newValue);
6925
+ timestamp = record.timestamp;
6926
+ logger.debug(
6927
+ { key, processor: processor.name, timestamp },
6928
+ "Processor updated value"
6929
+ );
6930
+ } else if (currentValue !== void 0) {
6931
+ const tombstone = map.remove(key);
6932
+ timestamp = tombstone.timestamp;
6933
+ logger.debug(
6934
+ { key, processor: processor.name, timestamp },
6935
+ "Processor deleted value"
6936
+ );
6937
+ }
6938
+ return {
6939
+ result: sandboxResult,
6940
+ timestamp
6941
+ };
6942
+ }
6943
+ /**
6944
+ * Execute a processor on multiple keys.
6945
+ *
6946
+ * Each key is processed sequentially to ensure atomicity per-key.
6947
+ * For parallel execution across keys, use multiple calls.
6948
+ *
6949
+ * @param map The LWWMap to operate on
6950
+ * @param keys The keys to process
6951
+ * @param processorDef The processor definition
6952
+ * @returns Map of key -> result
6953
+ */
6954
+ async executeOnKeys(map, keys, processorDef) {
6955
+ const results = /* @__PURE__ */ new Map();
6956
+ const timestamps = /* @__PURE__ */ new Map();
6957
+ const parseResult = import_core12.EntryProcessorDefSchema.safeParse(processorDef);
6958
+ if (!parseResult.success) {
6959
+ const errorResult = {
6960
+ success: false,
6961
+ error: `Invalid processor: ${parseResult.error.message}`
6962
+ };
6963
+ for (const key of keys) {
6964
+ results.set(key, errorResult);
6965
+ }
6966
+ return { results, timestamps };
6967
+ }
6968
+ for (const key of keys) {
6969
+ const { result, timestamp } = await this.executeOnKey(
6970
+ map,
6971
+ key,
6972
+ processorDef
6973
+ );
6974
+ results.set(key, result);
6975
+ if (timestamp) {
6976
+ timestamps.set(key, timestamp);
6977
+ }
6978
+ }
6979
+ return { results, timestamps };
6980
+ }
6981
+ /**
6982
+ * Execute a processor on all entries matching a predicate.
6983
+ *
6984
+ * WARNING: This can be expensive for large maps.
6985
+ *
6986
+ * @param map The LWWMap to operate on
6987
+ * @param processorDef The processor definition
6988
+ * @param predicateCode Optional predicate code to filter entries
6989
+ * @returns Map of key -> result for processed entries
6990
+ */
6991
+ async executeOnEntries(map, processorDef, predicateCode) {
6992
+ const results = /* @__PURE__ */ new Map();
6993
+ const timestamps = /* @__PURE__ */ new Map();
6994
+ const parseResult = import_core12.EntryProcessorDefSchema.safeParse(processorDef);
6995
+ if (!parseResult.success) {
6996
+ return { results, timestamps };
6997
+ }
6998
+ const entries = map.entries();
6999
+ for (const [key, value] of entries) {
7000
+ if (predicateCode) {
7001
+ const predicateResult = await this.sandbox.execute(
7002
+ {
7003
+ name: "_predicate",
7004
+ code: `return { value, result: (function() { ${predicateCode} })() };`
7005
+ },
7006
+ value,
7007
+ key
7008
+ );
7009
+ if (!predicateResult.success || !predicateResult.result) {
7010
+ continue;
7011
+ }
7012
+ }
7013
+ const { result, timestamp } = await this.executeOnKey(
7014
+ map,
7015
+ key,
7016
+ processorDef
7017
+ );
7018
+ results.set(key, result);
7019
+ if (timestamp) {
7020
+ timestamps.set(key, timestamp);
7021
+ }
7022
+ }
7023
+ return { results, timestamps };
7024
+ }
7025
+ /**
7026
+ * Check if sandbox is in secure mode (using isolated-vm).
7027
+ */
7028
+ isSecureMode() {
7029
+ return this.sandbox.isSecureMode();
7030
+ }
7031
+ /**
7032
+ * Get sandbox cache statistics.
7033
+ */
7034
+ getCacheStats() {
7035
+ return this.sandbox.getCacheStats();
7036
+ }
7037
+ /**
7038
+ * Clear sandbox cache.
7039
+ */
7040
+ clearCache(processorName) {
7041
+ this.sandbox.clearCache(processorName);
7042
+ }
7043
+ /**
7044
+ * Dispose of the handler and its sandbox.
7045
+ */
7046
+ dispose() {
7047
+ this.sandbox.dispose();
7048
+ logger.debug("EntryProcessorHandler disposed");
7049
+ }
7050
+ };
7051
+
7052
+ // src/ConflictResolverService.ts
7053
+ var import_core13 = require("@topgunbuild/core");
7054
+ var DEFAULT_CONFLICT_RESOLVER_CONFIG = {
7055
+ maxResolversPerMap: 100,
7056
+ enableSandboxedResolvers: true,
7057
+ resolverTimeoutMs: 100
7058
+ };
7059
+ var ConflictResolverService = class {
7060
+ constructor(sandbox, config = {}) {
7061
+ this.resolvers = /* @__PURE__ */ new Map();
7062
+ this.disposed = false;
7063
+ this.sandbox = sandbox;
7064
+ this.config = { ...DEFAULT_CONFLICT_RESOLVER_CONFIG, ...config };
7065
+ }
7066
+ /**
7067
+ * Set callback for merge rejections.
7068
+ */
7069
+ onRejection(callback) {
7070
+ this.onRejectionCallback = callback;
7071
+ }
7072
+ /**
7073
+ * Register a resolver for a map.
7074
+ *
7075
+ * @param mapName The map this resolver applies to
7076
+ * @param resolver The resolver definition
7077
+ * @param registeredBy Optional client ID that registered this resolver
7078
+ */
7079
+ register(mapName, resolver, registeredBy) {
7080
+ if (this.disposed) {
7081
+ throw new Error("ConflictResolverService has been disposed");
7082
+ }
7083
+ if (resolver.code) {
7084
+ const parsed = import_core13.ConflictResolverDefSchema.safeParse({
7085
+ name: resolver.name,
7086
+ code: resolver.code,
7087
+ priority: resolver.priority,
7088
+ keyPattern: resolver.keyPattern
7089
+ });
7090
+ if (!parsed.success) {
7091
+ throw new Error(`Invalid resolver definition: ${parsed.error.message}`);
7092
+ }
7093
+ const validation = (0, import_core13.validateResolverCode)(resolver.code);
7094
+ if (!validation.valid) {
7095
+ throw new Error(`Invalid resolver code: ${validation.error}`);
7096
+ }
7097
+ }
7098
+ const entries = this.resolvers.get(mapName) ?? [];
7099
+ if (entries.length >= this.config.maxResolversPerMap) {
7100
+ throw new Error(
7101
+ `Maximum resolvers per map (${this.config.maxResolversPerMap}) exceeded`
7102
+ );
7103
+ }
7104
+ const filtered = entries.filter((e) => e.resolver.name !== resolver.name);
7105
+ const entry = {
7106
+ resolver,
7107
+ registeredBy
7108
+ };
7109
+ if (resolver.code && !resolver.fn && this.config.enableSandboxedResolvers) {
7110
+ entry.compiledFn = this.compileSandboxed(resolver.name, resolver.code);
7111
+ }
7112
+ filtered.push(entry);
7113
+ filtered.sort(
7114
+ (a, b) => (b.resolver.priority ?? 50) - (a.resolver.priority ?? 50)
7115
+ );
7116
+ this.resolvers.set(mapName, filtered);
7117
+ logger.debug(
7118
+ `Registered resolver '${resolver.name}' for map '${mapName}' with priority ${resolver.priority ?? 50}`
7119
+ );
7120
+ }
7121
+ /**
7122
+ * Unregister a resolver.
7123
+ *
7124
+ * @param mapName The map name
7125
+ * @param resolverName The resolver name to unregister
7126
+ * @param clientId Optional - only unregister if registered by this client
7127
+ */
7128
+ unregister(mapName, resolverName, clientId) {
7129
+ const entries = this.resolvers.get(mapName);
7130
+ if (!entries) return false;
7131
+ const entryIndex = entries.findIndex(
7132
+ (e) => e.resolver.name === resolverName && (!clientId || e.registeredBy === clientId)
7133
+ );
7134
+ if (entryIndex === -1) return false;
7135
+ entries.splice(entryIndex, 1);
7136
+ if (entries.length === 0) {
7137
+ this.resolvers.delete(mapName);
7138
+ }
7139
+ logger.debug(`Unregistered resolver '${resolverName}' from map '${mapName}'`);
7140
+ return true;
7141
+ }
7142
+ /**
7143
+ * Resolve a merge conflict using registered resolvers.
7144
+ *
7145
+ * @param context The merge context
7146
+ * @returns The merge result
7147
+ */
7148
+ async resolve(context) {
7149
+ if (this.disposed) {
7150
+ return { action: "accept", value: context.remoteValue };
7151
+ }
7152
+ const entries = this.resolvers.get(context.mapName) ?? [];
7153
+ const allEntries = [
7154
+ ...entries,
7155
+ { resolver: import_core13.BuiltInResolvers.LWW() }
7156
+ ];
7157
+ for (const entry of allEntries) {
7158
+ const { resolver } = entry;
7159
+ if (resolver.keyPattern && !this.matchKeyPattern(context.key, resolver.keyPattern)) {
7160
+ continue;
7161
+ }
7162
+ try {
7163
+ let result;
7164
+ if (resolver.fn) {
7165
+ const fn = resolver.fn;
7166
+ const maybePromise = fn(context);
7167
+ result = maybePromise instanceof Promise ? await maybePromise : maybePromise;
7168
+ } else if (entry.compiledFn) {
7169
+ const compiledFn = entry.compiledFn;
7170
+ result = await compiledFn(context);
7171
+ } else {
7172
+ continue;
7173
+ }
7174
+ if (result.action !== "local") {
7175
+ if (result.action === "reject") {
7176
+ logger.debug(
7177
+ `Resolver '${resolver.name}' rejected merge for key '${context.key}' in map '${context.mapName}': ${result.reason}`
7178
+ );
7179
+ if (this.onRejectionCallback) {
7180
+ this.onRejectionCallback({
7181
+ mapName: context.mapName,
7182
+ key: context.key,
7183
+ attemptedValue: context.remoteValue,
7184
+ reason: result.reason,
7185
+ timestamp: context.remoteTimestamp,
7186
+ nodeId: context.remoteNodeId
7187
+ });
7188
+ }
7189
+ }
7190
+ return result;
7191
+ }
7192
+ } catch (error) {
7193
+ const message = error instanceof Error ? error.message : String(error);
7194
+ logger.error(`Resolver '${resolver.name}' threw error: ${message}`);
7195
+ }
7196
+ }
7197
+ return { action: "accept", value: context.remoteValue };
7198
+ }
7199
+ /**
7200
+ * List registered resolvers.
7201
+ *
7202
+ * @param mapName Optional - filter by map name
7203
+ */
7204
+ list(mapName) {
7205
+ const result = [];
7206
+ if (mapName) {
7207
+ const entries = this.resolvers.get(mapName) ?? [];
7208
+ for (const entry of entries) {
7209
+ result.push({
7210
+ mapName,
7211
+ name: entry.resolver.name,
7212
+ priority: entry.resolver.priority,
7213
+ keyPattern: entry.resolver.keyPattern,
7214
+ registeredBy: entry.registeredBy
7215
+ });
7216
+ }
7217
+ } else {
7218
+ for (const [map, entries] of this.resolvers.entries()) {
7219
+ for (const entry of entries) {
7220
+ result.push({
7221
+ mapName: map,
7222
+ name: entry.resolver.name,
7223
+ priority: entry.resolver.priority,
7224
+ keyPattern: entry.resolver.keyPattern,
7225
+ registeredBy: entry.registeredBy
7226
+ });
7227
+ }
7228
+ }
7229
+ }
7230
+ return result;
7231
+ }
7232
+ /**
7233
+ * Check if a map has any registered resolvers.
7234
+ */
7235
+ hasResolvers(mapName) {
7236
+ const entries = this.resolvers.get(mapName);
7237
+ return entries !== void 0 && entries.length > 0;
7238
+ }
7239
+ /**
7240
+ * Get the number of registered resolvers.
7241
+ */
7242
+ get size() {
7243
+ let count = 0;
7244
+ for (const entries of this.resolvers.values()) {
7245
+ count += entries.length;
7246
+ }
7247
+ return count;
7248
+ }
7249
+ /**
7250
+ * Clear all registered resolvers.
7251
+ *
7252
+ * @param mapName Optional - only clear resolvers for specific map
7253
+ */
7254
+ clear(mapName) {
7255
+ if (mapName) {
7256
+ this.resolvers.delete(mapName);
7257
+ } else {
7258
+ this.resolvers.clear();
7259
+ }
7260
+ }
7261
+ /**
7262
+ * Clear resolvers registered by a specific client.
7263
+ */
7264
+ clearByClient(clientId) {
7265
+ let removed = 0;
7266
+ for (const [mapName, entries] of this.resolvers.entries()) {
7267
+ const before = entries.length;
7268
+ const filtered = entries.filter((e) => e.registeredBy !== clientId);
7269
+ removed += before - filtered.length;
7270
+ if (filtered.length === 0) {
7271
+ this.resolvers.delete(mapName);
7272
+ } else if (filtered.length !== before) {
7273
+ this.resolvers.set(mapName, filtered);
7274
+ }
7275
+ }
7276
+ return removed;
7277
+ }
7278
+ /**
7279
+ * Dispose the service.
7280
+ */
7281
+ dispose() {
7282
+ if (this.disposed) return;
7283
+ this.disposed = true;
7284
+ this.resolvers.clear();
7285
+ logger.debug("ConflictResolverService disposed");
7286
+ }
7287
+ /**
7288
+ * Match a key against a glob-like pattern.
7289
+ * Supports * (any chars) and ? (single char).
7290
+ */
7291
+ matchKeyPattern(key, pattern) {
7292
+ const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
7293
+ const regex = new RegExp(`^${regexPattern}$`);
7294
+ return regex.test(key);
7295
+ }
7296
+ /**
7297
+ * Compile sandboxed resolver code.
7298
+ */
7299
+ compileSandboxed(name, code) {
7300
+ return async (ctx) => {
7301
+ const wrappedCode = `
7302
+ const context = {
7303
+ mapName: ${JSON.stringify(ctx.mapName)},
7304
+ key: ${JSON.stringify(ctx.key)},
7305
+ localValue: ${JSON.stringify(ctx.localValue)},
7306
+ remoteValue: ${JSON.stringify(ctx.remoteValue)},
7307
+ localTimestamp: ${JSON.stringify(ctx.localTimestamp)},
7308
+ remoteTimestamp: ${JSON.stringify(ctx.remoteTimestamp)},
7309
+ remoteNodeId: ${JSON.stringify(ctx.remoteNodeId)},
7310
+ auth: ${JSON.stringify(ctx.auth)},
7311
+ };
7312
+
7313
+ function resolve(context) {
7314
+ ${code}
7315
+ }
7316
+
7317
+ const result = resolve(context);
7318
+ return { value: result, result };
7319
+ `;
7320
+ const result = await this.sandbox.execute(
7321
+ {
7322
+ name: `resolver:${name}`,
7323
+ code: wrappedCode
7324
+ },
7325
+ null,
7326
+ // value parameter unused for resolvers
7327
+ "resolver"
7328
+ );
7329
+ if (!result.success) {
7330
+ throw new Error(result.error || "Resolver execution failed");
7331
+ }
7332
+ const resolverResult = result.result;
7333
+ if (!resolverResult || typeof resolverResult !== "object") {
7334
+ throw new Error("Resolver must return a result object");
7335
+ }
7336
+ const action = resolverResult.action;
7337
+ if (!["accept", "reject", "merge", "local"].includes(action)) {
7338
+ throw new Error(`Invalid resolver action: ${action}`);
7339
+ }
7340
+ return resolverResult;
7341
+ };
7342
+ }
7343
+ };
7344
+
7345
+ // src/handlers/ConflictResolverHandler.ts
7346
+ var ConflictResolverHandler = class {
7347
+ constructor(config) {
7348
+ this.rejectionListeners = /* @__PURE__ */ new Set();
7349
+ this.nodeId = config.nodeId;
7350
+ this.sandbox = new ProcessorSandbox(config.sandboxConfig);
7351
+ this.resolverService = new ConflictResolverService(
7352
+ this.sandbox,
7353
+ config.resolverConfig
7354
+ );
7355
+ this.resolverService.onRejection((rejection) => {
7356
+ for (const listener of this.rejectionListeners) {
7357
+ try {
7358
+ listener(rejection);
7359
+ } catch (e) {
7360
+ logger.error({ error: e }, "Error in rejection listener");
7361
+ }
7362
+ }
7363
+ });
7364
+ }
7365
+ /**
7366
+ * Register a conflict resolver for a map.
7367
+ *
7368
+ * @param mapName The map name
7369
+ * @param resolver The resolver definition
7370
+ * @param clientId Optional client ID that registered this resolver
7371
+ */
7372
+ registerResolver(mapName, resolver, clientId) {
7373
+ this.resolverService.register(mapName, resolver, clientId);
7374
+ logger.info(
7375
+ {
7376
+ mapName,
7377
+ resolverName: resolver.name,
7378
+ priority: resolver.priority,
7379
+ clientId
7380
+ },
7381
+ "Resolver registered"
7382
+ );
7383
+ }
7384
+ /**
7385
+ * Unregister a conflict resolver.
7386
+ *
7387
+ * @param mapName The map name
7388
+ * @param resolverName The resolver name
7389
+ * @param clientId Optional - only unregister if registered by this client
7390
+ */
7391
+ unregisterResolver(mapName, resolverName, clientId) {
7392
+ const removed = this.resolverService.unregister(
7393
+ mapName,
7394
+ resolverName,
7395
+ clientId
7396
+ );
7397
+ if (removed) {
7398
+ logger.info({ mapName, resolverName, clientId }, "Resolver unregistered");
7399
+ }
7400
+ return removed;
7401
+ }
7402
+ /**
7403
+ * List registered resolvers.
7404
+ *
7405
+ * @param mapName Optional - filter by map name
7406
+ */
7407
+ listResolvers(mapName) {
7408
+ return this.resolverService.list(mapName);
7409
+ }
7410
+ /**
7411
+ * Apply a merge with conflict resolution.
7412
+ *
7413
+ * Deletions (tombstones) are also passed through resolvers to allow
7414
+ * protection via IMMUTABLE, OWNER_ONLY, or similar resolvers.
7415
+ * If no custom resolvers are registered, deletions use standard LWW.
7416
+ *
7417
+ * @param map The LWWMap to merge into
7418
+ * @param mapName The map name (for resolver lookup)
7419
+ * @param key The key being merged
7420
+ * @param record The incoming record
7421
+ * @param remoteNodeId The source node ID
7422
+ * @param auth Optional authentication context
7423
+ */
7424
+ async mergeWithResolver(map, mapName, key, record, remoteNodeId, auth) {
7425
+ const isDeletion = record.value === null;
7426
+ const localRecord = map.getRecord(key);
7427
+ const context = {
7428
+ mapName,
7429
+ key,
7430
+ localValue: localRecord?.value ?? void 0,
7431
+ // For deletions, remoteValue is null - resolvers can check this
7432
+ remoteValue: record.value,
7433
+ localTimestamp: localRecord?.timestamp,
7434
+ remoteTimestamp: record.timestamp,
7435
+ remoteNodeId,
7436
+ auth,
7437
+ readEntry: (k) => map.get(k)
7438
+ };
7439
+ const result = await this.resolverService.resolve(context);
7440
+ switch (result.action) {
7441
+ case "accept":
7442
+ case "merge": {
7443
+ const finalValue = isDeletion ? null : result.value;
7444
+ const finalRecord = {
7445
+ value: finalValue,
7446
+ timestamp: record.timestamp,
7447
+ ttlMs: record.ttlMs
7448
+ };
7449
+ map.merge(key, finalRecord);
7450
+ return { applied: true, result, record: finalRecord };
7451
+ }
7452
+ case "reject": {
7453
+ const rejection = {
7454
+ mapName,
7455
+ key,
7456
+ attemptedValue: record.value,
7457
+ reason: result.reason,
7458
+ timestamp: record.timestamp,
7459
+ nodeId: remoteNodeId
7460
+ };
7461
+ return { applied: false, result, rejection };
7462
+ }
7463
+ case "local":
7464
+ default:
7465
+ return { applied: false, result };
7466
+ }
7467
+ }
7468
+ /**
7469
+ * Check if a map has custom resolvers registered.
7470
+ */
7471
+ hasResolvers(mapName) {
7472
+ return this.resolverService.hasResolvers(mapName);
7473
+ }
7474
+ /**
7475
+ * Add a listener for merge rejections.
7476
+ */
7477
+ onRejection(listener) {
7478
+ this.rejectionListeners.add(listener);
7479
+ return () => this.rejectionListeners.delete(listener);
7480
+ }
7481
+ /**
7482
+ * Clear resolvers registered by a specific client.
7483
+ */
7484
+ clearByClient(clientId) {
7485
+ return this.resolverService.clearByClient(clientId);
7486
+ }
7487
+ /**
7488
+ * Get the number of registered resolvers.
7489
+ */
7490
+ get resolverCount() {
7491
+ return this.resolverService.size;
7492
+ }
7493
+ /**
7494
+ * Check if sandbox is in secure mode.
7495
+ */
7496
+ isSecureMode() {
7497
+ return this.sandbox.isSecureMode();
7498
+ }
7499
+ /**
7500
+ * Dispose of the handler.
7501
+ */
7502
+ dispose() {
7503
+ this.resolverService.dispose();
7504
+ this.sandbox.dispose();
7505
+ this.rejectionListeners.clear();
7506
+ logger.debug("ConflictResolverHandler disposed");
7507
+ }
7508
+ };
7509
+
7510
+ // src/EventJournalService.ts
7511
+ var import_core14 = require("@topgunbuild/core");
7512
+ var DEFAULT_JOURNAL_SERVICE_CONFIG = {
7513
+ ...import_core14.DEFAULT_EVENT_JOURNAL_CONFIG,
7514
+ tableName: "event_journal",
7515
+ persistBatchSize: 100,
7516
+ persistIntervalMs: 1e3
7517
+ };
7518
+ var TABLE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
7519
+ function validateTableName(name) {
7520
+ if (!TABLE_NAME_REGEX.test(name)) {
7521
+ throw new Error(
7522
+ `Invalid table name "${name}". Table name must start with a letter or underscore and contain only alphanumeric characters and underscores.`
7523
+ );
7524
+ }
7525
+ }
7526
+ var EventJournalService = class extends import_core14.EventJournalImpl {
7527
+ constructor(config) {
7528
+ super(config);
7529
+ this.pendingPersist = [];
7530
+ this.isPersisting = false;
7531
+ this.isInitialized = false;
7532
+ this.isLoadingFromStorage = false;
7533
+ this.pool = config.pool;
7534
+ this.tableName = config.tableName ?? DEFAULT_JOURNAL_SERVICE_CONFIG.tableName;
7535
+ this.persistBatchSize = config.persistBatchSize ?? DEFAULT_JOURNAL_SERVICE_CONFIG.persistBatchSize;
7536
+ this.persistIntervalMs = config.persistIntervalMs ?? DEFAULT_JOURNAL_SERVICE_CONFIG.persistIntervalMs;
7537
+ validateTableName(this.tableName);
7538
+ this.subscribe((event) => {
7539
+ if (this.isLoadingFromStorage) return;
7540
+ if (event.sequence >= 0n && this.getConfig().persistent) {
7541
+ this.pendingPersist.push(event);
7542
+ if (this.pendingPersist.length >= this.persistBatchSize) {
7543
+ this.persistToStorage().catch((err) => {
7544
+ logger.error({ err }, "Failed to persist journal events");
7545
+ });
7546
+ }
7547
+ }
7548
+ });
7549
+ this.startPersistTimer();
7550
+ }
7551
+ /**
7552
+ * Initialize the journal service, creating table if needed.
7553
+ */
7554
+ async initialize() {
7555
+ if (this.isInitialized) return;
7556
+ const client = await this.pool.connect();
7557
+ try {
7558
+ await client.query(`
7559
+ CREATE TABLE IF NOT EXISTS ${this.tableName} (
7560
+ sequence BIGINT PRIMARY KEY,
7561
+ type VARCHAR(10) NOT NULL CHECK (type IN ('PUT', 'UPDATE', 'DELETE')),
7562
+ map_name VARCHAR(255) NOT NULL,
7563
+ key VARCHAR(1024) NOT NULL,
7564
+ value JSONB,
7565
+ previous_value JSONB,
7566
+ timestamp JSONB NOT NULL,
7567
+ node_id VARCHAR(64) NOT NULL,
7568
+ metadata JSONB,
7569
+ created_at TIMESTAMPTZ DEFAULT NOW()
7570
+ );
7571
+ `);
7572
+ await client.query(`
7573
+ CREATE INDEX IF NOT EXISTS idx_${this.tableName}_map_name
7574
+ ON ${this.tableName}(map_name);
7575
+ `);
7576
+ await client.query(`
7577
+ CREATE INDEX IF NOT EXISTS idx_${this.tableName}_key
7578
+ ON ${this.tableName}(map_name, key);
7579
+ `);
7580
+ await client.query(`
7581
+ CREATE INDEX IF NOT EXISTS idx_${this.tableName}_created_at
7582
+ ON ${this.tableName}(created_at);
7583
+ `);
7584
+ await client.query(`
7585
+ CREATE INDEX IF NOT EXISTS idx_${this.tableName}_node_id
7586
+ ON ${this.tableName}(node_id);
7587
+ `);
7588
+ this.isInitialized = true;
7589
+ logger.info({ tableName: this.tableName }, "EventJournalService initialized");
7590
+ } finally {
7591
+ client.release();
7592
+ }
7593
+ }
7594
+ /**
7595
+ * Persist pending events to PostgreSQL.
7596
+ */
7597
+ async persistToStorage() {
7598
+ if (this.pendingPersist.length === 0 || this.isPersisting) return;
7599
+ this.isPersisting = true;
7600
+ const batch = this.pendingPersist.splice(0, this.persistBatchSize);
7601
+ try {
7602
+ if (batch.length === 0) return;
7603
+ const values = [];
7604
+ const placeholders = [];
7605
+ batch.forEach((e, i) => {
7606
+ const offset = i * 9;
7607
+ placeholders.push(
7608
+ `($${offset + 1}, $${offset + 2}, $${offset + 3}, $${offset + 4}, $${offset + 5}, $${offset + 6}, $${offset + 7}, $${offset + 8}, $${offset + 9})`
7609
+ );
7610
+ values.push(
7611
+ e.sequence.toString(),
7612
+ e.type,
7613
+ e.mapName,
7614
+ e.key,
7615
+ e.value !== void 0 ? JSON.stringify(e.value) : null,
7616
+ e.previousValue !== void 0 ? JSON.stringify(e.previousValue) : null,
7617
+ JSON.stringify(e.timestamp),
7618
+ e.nodeId,
7619
+ e.metadata ? JSON.stringify(e.metadata) : null
7620
+ );
7621
+ });
7622
+ await this.pool.query(
7623
+ `INSERT INTO ${this.tableName}
7624
+ (sequence, type, map_name, key, value, previous_value, timestamp, node_id, metadata)
7625
+ VALUES ${placeholders.join(", ")}
7626
+ ON CONFLICT (sequence) DO NOTHING`,
7627
+ values
7628
+ );
7629
+ logger.debug({ count: batch.length }, "Persisted journal events");
7630
+ } catch (error) {
7631
+ this.pendingPersist.unshift(...batch);
7632
+ throw error;
7633
+ } finally {
7634
+ this.isPersisting = false;
7635
+ }
7636
+ }
7637
+ /**
7638
+ * Load journal events from PostgreSQL on startup.
7639
+ */
7640
+ async loadFromStorage() {
7641
+ const config = this.getConfig();
7642
+ const result = await this.pool.query(
7643
+ `SELECT sequence, type, map_name, key, value, previous_value, timestamp, node_id, metadata
7644
+ FROM ${this.tableName}
7645
+ ORDER BY sequence DESC
7646
+ LIMIT $1`,
7647
+ [config.capacity]
7648
+ );
7649
+ const events = result.rows.reverse();
7650
+ this.isLoadingFromStorage = true;
7651
+ try {
7652
+ for (const row of events) {
7653
+ this.append({
7654
+ type: row.type,
7655
+ mapName: row.map_name,
7656
+ key: row.key,
7657
+ value: row.value,
7658
+ previousValue: row.previous_value,
7659
+ timestamp: typeof row.timestamp === "string" ? JSON.parse(row.timestamp) : row.timestamp,
7660
+ nodeId: row.node_id,
7661
+ metadata: row.metadata
7662
+ });
7663
+ }
7664
+ } finally {
7665
+ this.isLoadingFromStorage = false;
7666
+ }
7667
+ logger.info({ count: events.length }, "Loaded journal events from storage");
7668
+ }
7669
+ /**
7670
+ * Export events as NDJSON stream.
7671
+ */
7672
+ exportStream(options = {}) {
7673
+ const self = this;
7674
+ return new ReadableStream({
7675
+ start(controller) {
7676
+ const startSeq = options.fromSequence ?? self.getOldestSequence();
7677
+ const endSeq = options.toSequence ?? self.getLatestSequence();
7678
+ for (let seq = startSeq; seq <= endSeq; seq++) {
7679
+ const events = self.readFrom(seq, 1);
7680
+ if (events.length > 0) {
7681
+ const event = events[0];
7682
+ if (options.mapName && event.mapName !== options.mapName) continue;
7683
+ if (options.types && !options.types.includes(event.type)) continue;
7684
+ const serializable = {
7685
+ ...event,
7686
+ sequence: event.sequence.toString()
7687
+ };
7688
+ controller.enqueue(JSON.stringify(serializable) + "\n");
7689
+ }
7690
+ }
7691
+ controller.close();
7692
+ }
7693
+ });
7694
+ }
7695
+ /**
7696
+ * Get events for a specific map.
7697
+ */
7698
+ getMapEvents(mapName, fromSeq) {
7699
+ const events = this.readFrom(fromSeq ?? this.getOldestSequence(), this.getConfig().capacity);
7700
+ return events.filter((e) => e.mapName === mapName);
7701
+ }
7702
+ /**
7703
+ * Query events from PostgreSQL with filters.
7704
+ */
7705
+ async queryFromStorage(options = {}) {
7706
+ const conditions = [];
7707
+ const params = [];
7708
+ let paramIndex = 1;
7709
+ if (options.mapName) {
7710
+ conditions.push(`map_name = $${paramIndex++}`);
7711
+ params.push(options.mapName);
7712
+ }
7713
+ if (options.key) {
7714
+ conditions.push(`key = $${paramIndex++}`);
7715
+ params.push(options.key);
7716
+ }
7717
+ if (options.types && options.types.length > 0) {
7718
+ conditions.push(`type = ANY($${paramIndex++})`);
7719
+ params.push(options.types);
7720
+ }
7721
+ if (options.fromSequence !== void 0) {
7722
+ conditions.push(`sequence >= $${paramIndex++}`);
7723
+ params.push(options.fromSequence.toString());
7724
+ }
7725
+ if (options.toSequence !== void 0) {
7726
+ conditions.push(`sequence <= $${paramIndex++}`);
7727
+ params.push(options.toSequence.toString());
7728
+ }
7729
+ if (options.fromDate) {
7730
+ conditions.push(`created_at >= $${paramIndex++}`);
7731
+ params.push(options.fromDate);
7732
+ }
7733
+ if (options.toDate) {
7734
+ conditions.push(`created_at <= $${paramIndex++}`);
7735
+ params.push(options.toDate);
7736
+ }
7737
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
7738
+ const limit = options.limit ?? 100;
7739
+ const offset = options.offset ?? 0;
7740
+ const result = await this.pool.query(
7741
+ `SELECT sequence, type, map_name, key, value, previous_value, timestamp, node_id, metadata
7742
+ FROM ${this.tableName}
7743
+ ${whereClause}
7744
+ ORDER BY sequence ASC
7745
+ LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
7746
+ [...params, limit, offset]
7747
+ );
7748
+ return result.rows.map((row) => ({
7749
+ sequence: BigInt(row.sequence),
7750
+ type: row.type,
7751
+ mapName: row.map_name,
7752
+ key: row.key,
7753
+ value: row.value,
7754
+ previousValue: row.previous_value,
7755
+ timestamp: typeof row.timestamp === "string" ? JSON.parse(row.timestamp) : row.timestamp,
7756
+ nodeId: row.node_id,
7757
+ metadata: row.metadata
7758
+ }));
7759
+ }
7760
+ /**
7761
+ * Count events matching filters.
7762
+ */
7763
+ async countFromStorage(options = {}) {
7764
+ const conditions = [];
7765
+ const params = [];
7766
+ let paramIndex = 1;
7767
+ if (options.mapName) {
7768
+ conditions.push(`map_name = $${paramIndex++}`);
7769
+ params.push(options.mapName);
7770
+ }
7771
+ if (options.types && options.types.length > 0) {
7772
+ conditions.push(`type = ANY($${paramIndex++})`);
7773
+ params.push(options.types);
7774
+ }
7775
+ if (options.fromDate) {
7776
+ conditions.push(`created_at >= $${paramIndex++}`);
7777
+ params.push(options.fromDate);
7778
+ }
7779
+ if (options.toDate) {
7780
+ conditions.push(`created_at <= $${paramIndex++}`);
7781
+ params.push(options.toDate);
7782
+ }
7783
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
7784
+ const result = await this.pool.query(
7785
+ `SELECT COUNT(*) as count FROM ${this.tableName} ${whereClause}`,
7786
+ params
7787
+ );
7788
+ return parseInt(result.rows[0].count, 10);
7789
+ }
7790
+ /**
7791
+ * Cleanup old events based on retention policy.
7792
+ */
7793
+ async cleanupOldEvents(retentionDays) {
7794
+ const result = await this.pool.query(
7795
+ `DELETE FROM ${this.tableName}
7796
+ WHERE created_at < NOW() - ($1 || ' days')::INTERVAL
7797
+ RETURNING sequence`,
7798
+ [retentionDays]
7799
+ );
7800
+ const count = result.rowCount ?? 0;
7801
+ if (count > 0) {
7802
+ logger.info({ deletedCount: count, retentionDays }, "Cleaned up old journal events");
7803
+ }
7804
+ return count;
7805
+ }
7806
+ /**
7807
+ * Start the periodic persistence timer.
7808
+ */
7809
+ startPersistTimer() {
7810
+ this.persistTimer = setInterval(() => {
7811
+ if (this.pendingPersist.length > 0) {
7812
+ this.persistToStorage().catch((err) => {
7813
+ logger.error({ err }, "Periodic persist failed");
7814
+ });
7815
+ }
7816
+ }, this.persistIntervalMs);
7817
+ }
7818
+ /**
7819
+ * Stop the periodic persistence timer.
7820
+ */
7821
+ stopPersistTimer() {
7822
+ if (this.persistTimer) {
7823
+ clearInterval(this.persistTimer);
7824
+ this.persistTimer = void 0;
7825
+ }
7826
+ }
7827
+ /**
7828
+ * Dispose resources and persist remaining events.
7829
+ */
7830
+ dispose() {
7831
+ this.stopPersistTimer();
7832
+ if (this.pendingPersist.length > 0) {
7833
+ this.persistToStorage().catch((err) => {
7834
+ logger.error({ err }, "Final persist failed on dispose");
7835
+ });
7836
+ }
7837
+ super.dispose();
7838
+ }
7839
+ /**
7840
+ * Get pending persist count (for monitoring).
7841
+ */
7842
+ getPendingPersistCount() {
7843
+ return this.pendingPersist.length;
7844
+ }
7845
+ };
7846
+
7847
+ // src/ServerCoordinator.ts
7848
+ var GC_INTERVAL_MS = 60 * 60 * 1e3;
7849
+ var GC_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
7850
+ var CLIENT_HEARTBEAT_TIMEOUT_MS = 2e4;
7851
+ var CLIENT_HEARTBEAT_CHECK_INTERVAL_MS = 5e3;
7852
+ var ServerCoordinator = class {
7853
+ constructor(config) {
7854
+ this.clients = /* @__PURE__ */ new Map();
7855
+ // Interceptors
7856
+ this.interceptors = [];
7857
+ // In-memory storage (partitioned later)
7858
+ this.maps = /* @__PURE__ */ new Map();
7859
+ this.pendingClusterQueries = /* @__PURE__ */ new Map();
7860
+ // GC Consensus State
7861
+ this.gcReports = /* @__PURE__ */ new Map();
7862
+ // Track map loading state to avoid returning empty results during async load
7863
+ this.mapLoadingPromises = /* @__PURE__ */ new Map();
7864
+ // Track pending batch operations for testing purposes
7865
+ this.pendingBatchOperations = /* @__PURE__ */ new Set();
7866
+ this.journalSubscriptions = /* @__PURE__ */ new Map();
7867
+ this._actualPort = 0;
7868
+ this._actualClusterPort = 0;
7869
+ this._readyPromise = new Promise((resolve) => {
7870
+ this._readyResolve = resolve;
7871
+ });
7872
+ this._nodeId = config.nodeId;
7873
+ this.hlc = new import_core15.HLC(config.nodeId);
7874
+ this.storage = config.storage;
7875
+ const rawSecret = config.jwtSecret || process.env.JWT_SECRET || "topgun-secret-dev";
7876
+ this.jwtSecret = rawSecret.replace(/\\n/g, "\n");
7877
+ this.queryRegistry = new QueryRegistry();
7878
+ this.securityManager = new SecurityManager(config.securityPolicies || []);
7879
+ this.interceptors = config.interceptors || [];
7880
+ this.metricsService = new MetricsService();
7881
+ this.eventExecutor = new StripedEventExecutor({
7882
+ stripeCount: config.eventStripeCount ?? 4,
7883
+ queueCapacity: config.eventQueueCapacity ?? 1e4,
7884
+ name: `${config.nodeId}-event-executor`,
7885
+ onReject: (task) => {
7886
+ logger.warn({ nodeId: config.nodeId, key: task.key }, "Event task rejected due to queue capacity");
7887
+ this.metricsService.incEventQueueRejected();
7888
+ }
7889
+ });
7890
+ this.backpressure = new BackpressureRegulator({
7891
+ syncFrequency: config.backpressureSyncFrequency ?? 100,
7892
+ maxPendingOps: config.backpressureMaxPending ?? 1e3,
7893
+ backoffTimeoutMs: config.backpressureBackoffMs ?? 5e3,
7894
+ enabled: config.backpressureEnabled ?? true
7895
+ });
7896
+ this.writeCoalescingEnabled = config.writeCoalescingEnabled ?? true;
7897
+ const preset = coalescingPresets[config.writeCoalescingPreset ?? "highThroughput"];
7898
+ this.writeCoalescingOptions = {
7899
+ maxBatchSize: config.writeCoalescingMaxBatch ?? preset.maxBatchSize,
7900
+ maxDelayMs: config.writeCoalescingMaxDelayMs ?? preset.maxDelayMs,
7901
+ maxBatchBytes: config.writeCoalescingMaxBytes ?? preset.maxBatchBytes
7902
+ };
7903
+ this.eventPayloadPool = createEventPayloadPool({
7904
+ maxSize: 4096,
7905
+ initialSize: 128
7906
+ });
7907
+ this.taskletScheduler = new TaskletScheduler({
7908
+ defaultTimeBudgetMs: 5,
7909
+ maxConcurrent: 20
7910
+ });
7911
+ this.writeAckManager = new WriteAckManager({
7912
+ defaultTimeout: config.writeAckTimeout ?? 5e3
7913
+ });
7914
+ this.rateLimitingEnabled = config.rateLimitingEnabled ?? true;
7915
+ this.rateLimiter = new ConnectionRateLimiter({
7916
+ maxConnectionsPerSecond: config.maxConnectionsPerSecond ?? 100,
7917
+ maxPendingConnections: config.maxPendingConnections ?? 1e3,
7918
+ cooldownMs: 1e3
7919
+ });
7920
+ if (config.workerPoolEnabled) {
7921
+ this.workerPool = new WorkerPool({
7922
+ minWorkers: config.workerPoolConfig?.minWorkers ?? 2,
7923
+ maxWorkers: config.workerPoolConfig?.maxWorkers,
7924
+ taskTimeout: config.workerPoolConfig?.taskTimeout ?? 5e3,
7925
+ idleTimeout: config.workerPoolConfig?.idleTimeout ?? 3e4,
7926
+ autoRestart: config.workerPoolConfig?.autoRestart ?? true
7927
+ });
7928
+ this.merkleWorker = new MerkleWorker(this.workerPool);
7929
+ this.crdtMergeWorker = new CRDTMergeWorker(this.workerPool);
7930
+ this.serializationWorker = new SerializationWorker(this.workerPool);
7931
+ logger.info({
7932
+ minWorkers: config.workerPoolConfig?.minWorkers ?? 2,
7933
+ maxWorkers: config.workerPoolConfig?.maxWorkers ?? "auto"
7934
+ }, "Worker pool initialized for CPU-bound operations");
7935
+ }
7936
+ if (config.tls?.enabled) {
7937
+ const tlsOptions = this.buildTLSOptions(config.tls);
7938
+ this.httpServer = (0, import_https.createServer)(tlsOptions, (_req, res) => {
7939
+ res.writeHead(200);
7940
+ res.end("TopGun Server Running (Secure)");
7941
+ });
7942
+ logger.info("TLS enabled for client connections");
7943
+ } else {
7944
+ this.httpServer = (0, import_http.createServer)((_req, res) => {
7945
+ res.writeHead(200);
7946
+ res.end("TopGun Server Running");
7947
+ });
7948
+ if (process.env.NODE_ENV === "production") {
7949
+ logger.warn("\u26A0\uFE0F TLS is disabled! Client connections are NOT encrypted.");
7950
+ }
7951
+ }
7952
+ const metricsPort = config.metricsPort !== void 0 ? config.metricsPort : 9090;
7953
+ this.metricsServer = (0, import_http.createServer)(async (req, res) => {
7954
+ if (req.url === "/metrics") {
7955
+ try {
7956
+ res.setHeader("Content-Type", this.metricsService.getContentType());
7957
+ res.end(await this.metricsService.getMetrics());
7958
+ } catch (err) {
7959
+ res.statusCode = 500;
7960
+ res.end("Internal Server Error");
7961
+ }
7962
+ } else {
7963
+ res.statusCode = 404;
7964
+ res.end();
7965
+ }
7966
+ });
7967
+ this.metricsServer.listen(metricsPort, () => {
7968
+ logger.info({ port: metricsPort }, "Metrics server listening");
7969
+ });
7970
+ this.metricsServer.on("error", (err) => {
7971
+ logger.error({ err, port: metricsPort }, "Metrics server failed to start");
7972
+ });
7973
+ this.wss = new import_ws3.WebSocketServer({
7974
+ server: this.httpServer,
7975
+ // Increase backlog for pending connections (default Linux is 128)
7976
+ backlog: config.wsBacklog ?? 511,
7977
+ // Disable per-message deflate by default (CPU overhead)
7978
+ perMessageDeflate: config.wsCompression ?? false,
7979
+ // Max payload size (64MB default)
7980
+ maxPayload: config.wsMaxPayload ?? 64 * 1024 * 1024,
7981
+ // Skip UTF-8 validation for binary messages (performance)
7982
+ skipUTF8Validation: true
7983
+ });
7984
+ this.wss.on("connection", (ws) => this.handleConnection(ws));
7985
+ this.httpServer.maxConnections = config.maxConnections ?? 1e4;
7986
+ this.httpServer.timeout = config.serverTimeout ?? 12e4;
7987
+ this.httpServer.keepAliveTimeout = config.keepAliveTimeout ?? 5e3;
7988
+ this.httpServer.headersTimeout = config.headersTimeout ?? 6e4;
7989
+ this.httpServer.on("connection", (socket) => {
7990
+ socket.setNoDelay(true);
7991
+ socket.setKeepAlive(true, 6e4);
7992
+ });
7993
+ this.httpServer.listen(config.port, () => {
7994
+ const addr = this.httpServer.address();
7995
+ this._actualPort = typeof addr === "object" && addr ? addr.port : config.port;
7996
+ logger.info({ port: this._actualPort }, "Server Coordinator listening");
7997
+ const clusterPort = config.clusterPort ?? 0;
7998
+ const peers = config.resolvePeers ? config.resolvePeers() : config.peers || [];
7999
+ this.cluster = new ClusterManager({
8000
+ nodeId: config.nodeId,
8001
+ host: config.host || "localhost",
8002
+ port: clusterPort,
8003
+ peers,
8004
+ discovery: config.discovery,
8005
+ serviceName: config.serviceName,
8006
+ discoveryInterval: config.discoveryInterval,
8007
+ tls: config.clusterTls
8008
+ });
8009
+ this.partitionService = new PartitionService(this.cluster);
8010
+ if (config.replicationEnabled !== false) {
8011
+ this.replicationPipeline = new ReplicationPipeline(
8012
+ this.cluster,
8013
+ this.partitionService,
8014
+ {
8015
+ ...import_core15.DEFAULT_REPLICATION_CONFIG,
8016
+ defaultConsistency: config.defaultConsistency ?? import_core15.ConsistencyLevel.EVENTUAL,
8017
+ ...config.replicationConfig
8018
+ }
8019
+ );
8020
+ this.replicationPipeline.setOperationApplier(this.applyReplicatedOperation.bind(this));
8021
+ logger.info({ nodeId: config.nodeId }, "ReplicationPipeline initialized");
8022
+ }
8023
+ this.partitionService.on("rebalanced", (partitionMap, changes) => {
8024
+ this.broadcastPartitionMap(partitionMap);
8025
+ });
8026
+ this.lockManager = new LockManager();
8027
+ this.lockManager.on("lockGranted", (evt) => this.handleLockGranted(evt));
8028
+ this.topicManager = new TopicManager({
8029
+ cluster: this.cluster,
8030
+ sendToClient: (clientId, message) => {
8031
+ const client = this.clients.get(clientId);
8032
+ if (client && client.socket.readyState === import_ws3.WebSocket.OPEN) {
8033
+ client.writer.write(message);
8034
+ }
8035
+ }
8036
+ });
8037
+ this.counterHandler = new CounterHandler(this._nodeId);
8038
+ this.entryProcessorHandler = new EntryProcessorHandler({ hlc: this.hlc });
8039
+ this.conflictResolverHandler = new ConflictResolverHandler({ nodeId: this._nodeId });
8040
+ this.conflictResolverHandler.onRejection((rejection) => {
8041
+ this.notifyMergeRejection(rejection);
8042
+ });
8043
+ if (config.eventJournalEnabled && this.storage && "pool" in this.storage) {
8044
+ const pool = this.storage.pool;
8045
+ this.eventJournalService = new EventJournalService({
8046
+ capacity: 1e4,
8047
+ ttlMs: 0,
8048
+ persistent: true,
8049
+ pool,
8050
+ ...config.eventJournalConfig
8051
+ });
8052
+ this.eventJournalService.initialize().then(() => {
8053
+ logger.info("EventJournalService initialized");
8054
+ }).catch((err) => {
8055
+ logger.error({ err }, "Failed to initialize EventJournalService");
8056
+ });
8057
+ }
8058
+ this.systemManager = new SystemManager(
8059
+ this.cluster,
8060
+ this.metricsService,
8061
+ (name) => this.getMap(name)
8062
+ );
8063
+ this.setupClusterListeners();
8064
+ this.cluster.start().then((actualClusterPort) => {
8065
+ this._actualClusterPort = actualClusterPort;
8066
+ this.metricsService.setClusterMembers(this.cluster.getMembers().length);
8067
+ logger.info({ clusterPort: this._actualClusterPort }, "Cluster started");
8068
+ this.systemManager.start();
8069
+ this._readyResolve();
8070
+ }).catch((err) => {
8071
+ this._actualClusterPort = clusterPort;
8072
+ this.metricsService.setClusterMembers(this.cluster.getMembers().length);
8073
+ logger.info({ clusterPort: this._actualClusterPort }, "Cluster started (sync)");
8074
+ this.systemManager.start();
8075
+ this._readyResolve();
8076
+ });
8077
+ });
8078
+ if (this.storage) {
8079
+ this.storage.initialize().then(() => {
8080
+ logger.info("Storage adapter initialized");
8081
+ }).catch((err) => {
8082
+ logger.error({ err }, "Failed to initialize storage");
8083
+ });
8084
+ }
8085
+ this.startGarbageCollection();
8086
+ this.startHeartbeatCheck();
8087
+ }
8088
+ /** Wait for server to be fully ready (ports assigned) */
8089
+ ready() {
8090
+ return this._readyPromise;
8091
+ }
8092
+ /**
8093
+ * Wait for all pending batch operations to complete.
8094
+ * Useful for tests that need to verify state after OP_BATCH.
6696
8095
  */
6697
8096
  async waitForPendingBatches() {
6698
8097
  if (this.pendingBatchOperations.size === 0) return;
@@ -6761,7 +8160,7 @@ var ServerCoordinator = class {
6761
8160
  this.metricsService.destroy();
6762
8161
  this.wss.close();
6763
8162
  logger.info(`Closing ${this.clients.size} client connections...`);
6764
- const shutdownMsg = (0, import_core10.serialize)({ type: "SHUTDOWN_PENDING", retryAfter: 5e3 });
8163
+ const shutdownMsg = (0, import_core15.serialize)({ type: "SHUTDOWN_PENDING", retryAfter: 5e3 });
6765
8164
  for (const client of this.clients.values()) {
6766
8165
  try {
6767
8166
  if (client.socket.readyState === import_ws3.WebSocket.OPEN) {
@@ -6815,6 +8214,10 @@ var ServerCoordinator = class {
6815
8214
  this.eventPayloadPool.clear();
6816
8215
  this.taskletScheduler.shutdown();
6817
8216
  this.writeAckManager.shutdown();
8217
+ this.entryProcessorHandler.dispose();
8218
+ if (this.eventJournalService) {
8219
+ this.eventJournalService.dispose();
8220
+ }
6818
8221
  logger.info("Server Coordinator shutdown complete.");
6819
8222
  }
6820
8223
  async handleConnection(ws) {
@@ -6882,7 +8285,7 @@ var ServerCoordinator = class {
6882
8285
  buf = Buffer.from(message);
6883
8286
  }
6884
8287
  try {
6885
- data = (0, import_core10.deserialize)(buf);
8288
+ data = (0, import_core15.deserialize)(buf);
6886
8289
  } catch (e) {
6887
8290
  try {
6888
8291
  const text = Buffer.isBuffer(buf) ? buf.toString() : new TextDecoder().decode(buf);
@@ -6921,6 +8324,7 @@ var ServerCoordinator = class {
6921
8324
  }
6922
8325
  this.lockManager.handleClientDisconnect(clientId);
6923
8326
  this.topicManager.unsubscribeAll(clientId);
8327
+ this.counterHandler.unsubscribeAll(clientId);
6924
8328
  const members = this.cluster.getMembers();
6925
8329
  for (const memberId of members) {
6926
8330
  if (!this.cluster.isLocal(memberId)) {
@@ -6933,10 +8337,10 @@ var ServerCoordinator = class {
6933
8337
  this.clients.delete(clientId);
6934
8338
  this.metricsService.setConnectedClients(this.clients.size);
6935
8339
  });
6936
- ws.send((0, import_core10.serialize)({ type: "AUTH_REQUIRED" }));
8340
+ ws.send((0, import_core15.serialize)({ type: "AUTH_REQUIRED" }));
6937
8341
  }
6938
8342
  async handleMessage(client, rawMessage) {
6939
- const parseResult = import_core10.MessageSchema.safeParse(rawMessage);
8343
+ const parseResult = import_core15.MessageSchema.safeParse(rawMessage);
6940
8344
  if (!parseResult.success) {
6941
8345
  logger.error({ clientId: client.id, error: parseResult.error }, "Invalid message format from client");
6942
8346
  client.writer.write({
@@ -7176,7 +8580,7 @@ var ServerCoordinator = class {
7176
8580
  this.metricsService.incOp("GET", message.mapName);
7177
8581
  try {
7178
8582
  const mapForSync = await this.getMapAsync(message.mapName);
7179
- if (mapForSync instanceof import_core10.LWWMap) {
8583
+ if (mapForSync instanceof import_core15.LWWMap) {
7180
8584
  const tree = mapForSync.getMerkleTree();
7181
8585
  const rootHash = tree.getRootHash();
7182
8586
  client.writer.write({
@@ -7214,7 +8618,7 @@ var ServerCoordinator = class {
7214
8618
  const { mapName, path } = message.payload;
7215
8619
  try {
7216
8620
  const mapForBucket = await this.getMapAsync(mapName);
7217
- if (mapForBucket instanceof import_core10.LWWMap) {
8621
+ if (mapForBucket instanceof import_core15.LWWMap) {
7218
8622
  const treeForBucket = mapForBucket.getMerkleTree();
7219
8623
  const buckets = treeForBucket.getBuckets(path);
7220
8624
  const node = treeForBucket.getNode(path);
@@ -7343,6 +8747,219 @@ var ServerCoordinator = class {
7343
8747
  }
7344
8748
  break;
7345
8749
  }
8750
+ // ============ Phase 5.2: PN Counter Handlers ============
8751
+ case "COUNTER_REQUEST": {
8752
+ const { name } = message.payload;
8753
+ const response = this.counterHandler.handleCounterRequest(client.id, name);
8754
+ client.writer.write(response);
8755
+ logger.debug({ clientId: client.id, name }, "Counter request handled");
8756
+ break;
8757
+ }
8758
+ case "COUNTER_SYNC": {
8759
+ const { name, state } = message.payload;
8760
+ const result = this.counterHandler.handleCounterSync(client.id, name, state);
8761
+ client.writer.write(result.response);
8762
+ for (const targetClientId of result.broadcastTo) {
8763
+ const targetClient = this.clients.get(targetClientId);
8764
+ if (targetClient && targetClient.socket.readyState === import_ws3.WebSocket.OPEN) {
8765
+ targetClient.writer.write(result.broadcastMessage);
8766
+ }
8767
+ }
8768
+ logger.debug({ clientId: client.id, name, broadcastCount: result.broadcastTo.length }, "Counter sync handled");
8769
+ break;
8770
+ }
8771
+ // ============ Phase 5.03: Entry Processor Handlers ============
8772
+ case "ENTRY_PROCESS": {
8773
+ const { requestId, mapName, key, processor } = message;
8774
+ if (!this.securityManager.checkPermission(client.principal, mapName, "PUT")) {
8775
+ client.writer.write({
8776
+ type: "ENTRY_PROCESS_RESPONSE",
8777
+ requestId,
8778
+ success: false,
8779
+ error: `Access Denied for map ${mapName}`
8780
+ }, true);
8781
+ break;
8782
+ }
8783
+ const entryMap = this.getMap(mapName);
8784
+ const { result, timestamp } = await this.entryProcessorHandler.executeOnKey(
8785
+ entryMap,
8786
+ key,
8787
+ processor
8788
+ );
8789
+ client.writer.write({
8790
+ type: "ENTRY_PROCESS_RESPONSE",
8791
+ requestId,
8792
+ success: result.success,
8793
+ result: result.result,
8794
+ newValue: result.newValue,
8795
+ error: result.error
8796
+ });
8797
+ if (result.success && timestamp) {
8798
+ const record = entryMap.getRecord(key);
8799
+ if (record) {
8800
+ this.queryRegistry.processChange(mapName, entryMap, key, record, void 0);
8801
+ }
8802
+ }
8803
+ logger.debug({
8804
+ clientId: client.id,
8805
+ mapName,
8806
+ key,
8807
+ processor: processor.name,
8808
+ success: result.success
8809
+ }, "Entry processor executed");
8810
+ break;
8811
+ }
8812
+ case "ENTRY_PROCESS_BATCH": {
8813
+ const { requestId, mapName, keys, processor } = message;
8814
+ if (!this.securityManager.checkPermission(client.principal, mapName, "PUT")) {
8815
+ const errorResults = {};
8816
+ for (const key of keys) {
8817
+ errorResults[key] = {
8818
+ success: false,
8819
+ error: `Access Denied for map ${mapName}`
8820
+ };
8821
+ }
8822
+ client.writer.write({
8823
+ type: "ENTRY_PROCESS_BATCH_RESPONSE",
8824
+ requestId,
8825
+ results: errorResults
8826
+ }, true);
8827
+ break;
8828
+ }
8829
+ const batchMap = this.getMap(mapName);
8830
+ const { results, timestamps } = await this.entryProcessorHandler.executeOnKeys(
8831
+ batchMap,
8832
+ keys,
8833
+ processor
8834
+ );
8835
+ const resultsRecord = {};
8836
+ for (const [key, keyResult] of results) {
8837
+ resultsRecord[key] = {
8838
+ success: keyResult.success,
8839
+ result: keyResult.result,
8840
+ newValue: keyResult.newValue,
8841
+ error: keyResult.error
8842
+ };
8843
+ }
8844
+ client.writer.write({
8845
+ type: "ENTRY_PROCESS_BATCH_RESPONSE",
8846
+ requestId,
8847
+ results: resultsRecord
8848
+ });
8849
+ for (const [key] of timestamps) {
8850
+ const record = batchMap.getRecord(key);
8851
+ if (record) {
8852
+ this.queryRegistry.processChange(mapName, batchMap, key, record, void 0);
8853
+ }
8854
+ }
8855
+ logger.debug({
8856
+ clientId: client.id,
8857
+ mapName,
8858
+ keyCount: keys.length,
8859
+ processor: processor.name,
8860
+ successCount: Array.from(results.values()).filter((r) => r.success).length
8861
+ }, "Entry processor batch executed");
8862
+ break;
8863
+ }
8864
+ // ============ Phase 5.05: Conflict Resolver Handlers ============
8865
+ case "REGISTER_RESOLVER": {
8866
+ const { requestId, mapName, resolver } = message;
8867
+ if (!this.securityManager.checkPermission(client.principal, mapName, "PUT")) {
8868
+ client.writer.write({
8869
+ type: "REGISTER_RESOLVER_RESPONSE",
8870
+ requestId,
8871
+ success: false,
8872
+ error: `Access Denied for map ${mapName}`
8873
+ }, true);
8874
+ break;
8875
+ }
8876
+ try {
8877
+ this.conflictResolverHandler.registerResolver(
8878
+ mapName,
8879
+ {
8880
+ name: resolver.name,
8881
+ code: resolver.code,
8882
+ priority: resolver.priority,
8883
+ keyPattern: resolver.keyPattern
8884
+ },
8885
+ client.id
8886
+ );
8887
+ client.writer.write({
8888
+ type: "REGISTER_RESOLVER_RESPONSE",
8889
+ requestId,
8890
+ success: true
8891
+ });
8892
+ logger.info({
8893
+ clientId: client.id,
8894
+ mapName,
8895
+ resolverName: resolver.name,
8896
+ priority: resolver.priority
8897
+ }, "Conflict resolver registered");
8898
+ } catch (err) {
8899
+ const errorMessage = err instanceof Error ? err.message : String(err);
8900
+ client.writer.write({
8901
+ type: "REGISTER_RESOLVER_RESPONSE",
8902
+ requestId,
8903
+ success: false,
8904
+ error: errorMessage
8905
+ }, true);
8906
+ logger.warn({
8907
+ clientId: client.id,
8908
+ mapName,
8909
+ error: errorMessage
8910
+ }, "Failed to register conflict resolver");
8911
+ }
8912
+ break;
8913
+ }
8914
+ case "UNREGISTER_RESOLVER": {
8915
+ const { requestId, mapName, resolverName } = message;
8916
+ if (!this.securityManager.checkPermission(client.principal, mapName, "PUT")) {
8917
+ client.writer.write({
8918
+ type: "UNREGISTER_RESOLVER_RESPONSE",
8919
+ requestId,
8920
+ success: false,
8921
+ error: `Access Denied for map ${mapName}`
8922
+ }, true);
8923
+ break;
8924
+ }
8925
+ const removed = this.conflictResolverHandler.unregisterResolver(
8926
+ mapName,
8927
+ resolverName,
8928
+ client.id
8929
+ );
8930
+ client.writer.write({
8931
+ type: "UNREGISTER_RESOLVER_RESPONSE",
8932
+ requestId,
8933
+ success: removed,
8934
+ error: removed ? void 0 : "Resolver not found or not owned by this client"
8935
+ });
8936
+ if (removed) {
8937
+ logger.info({
8938
+ clientId: client.id,
8939
+ mapName,
8940
+ resolverName
8941
+ }, "Conflict resolver unregistered");
8942
+ }
8943
+ break;
8944
+ }
8945
+ case "LIST_RESOLVERS": {
8946
+ const { requestId, mapName } = message;
8947
+ if (mapName && !this.securityManager.checkPermission(client.principal, mapName, "READ")) {
8948
+ client.writer.write({
8949
+ type: "LIST_RESOLVERS_RESPONSE",
8950
+ requestId,
8951
+ resolvers: []
8952
+ });
8953
+ break;
8954
+ }
8955
+ const resolvers = this.conflictResolverHandler.listResolvers(mapName);
8956
+ client.writer.write({
8957
+ type: "LIST_RESOLVERS_RESPONSE",
8958
+ requestId,
8959
+ resolvers
8960
+ });
8961
+ break;
8962
+ }
7346
8963
  // ============ Phase 4: Partition Map Request Handler ============
7347
8964
  case "PARTITION_MAP_REQUEST": {
7348
8965
  const clientVersion = message.payload?.currentVersion ?? 0;
@@ -7383,7 +9000,7 @@ var ServerCoordinator = class {
7383
9000
  this.metricsService.incOp("GET", message.mapName);
7384
9001
  try {
7385
9002
  const mapForSync = await this.getMapAsync(message.mapName, "OR");
7386
- if (mapForSync instanceof import_core10.ORMap) {
9003
+ if (mapForSync instanceof import_core15.ORMap) {
7387
9004
  const tree = mapForSync.getMerkleTree();
7388
9005
  const rootHash = tree.getRootHash();
7389
9006
  client.writer.write({
@@ -7420,7 +9037,7 @@ var ServerCoordinator = class {
7420
9037
  const { mapName, path } = message.payload;
7421
9038
  try {
7422
9039
  const mapForBucket = await this.getMapAsync(mapName, "OR");
7423
- if (mapForBucket instanceof import_core10.ORMap) {
9040
+ if (mapForBucket instanceof import_core15.ORMap) {
7424
9041
  const tree = mapForBucket.getMerkleTree();
7425
9042
  const buckets = tree.getBuckets(path);
7426
9043
  const isLeaf = tree.isLeaf(path);
@@ -7464,7 +9081,7 @@ var ServerCoordinator = class {
7464
9081
  const { mapName: diffMapName, keys } = message.payload;
7465
9082
  try {
7466
9083
  const mapForDiff = await this.getMapAsync(diffMapName, "OR");
7467
- if (mapForDiff instanceof import_core10.ORMap) {
9084
+ if (mapForDiff instanceof import_core15.ORMap) {
7468
9085
  const entries = [];
7469
9086
  const allTombstones = mapForDiff.getTombstones();
7470
9087
  for (const key of keys) {
@@ -7496,7 +9113,7 @@ var ServerCoordinator = class {
7496
9113
  const { mapName: pushMapName, entries: pushEntries } = message.payload;
7497
9114
  try {
7498
9115
  const mapForPush = await this.getMapAsync(pushMapName, "OR");
7499
- if (mapForPush instanceof import_core10.ORMap) {
9116
+ if (mapForPush instanceof import_core15.ORMap) {
7500
9117
  let totalAdded = 0;
7501
9118
  let totalUpdated = 0;
7502
9119
  for (const entry of pushEntries) {
@@ -7538,6 +9155,92 @@ var ServerCoordinator = class {
7538
9155
  }
7539
9156
  break;
7540
9157
  }
9158
+ // === Event Journal Messages (Phase 5.04) ===
9159
+ case "JOURNAL_SUBSCRIBE": {
9160
+ if (!this.eventJournalService) {
9161
+ client.writer.write({
9162
+ type: "ERROR",
9163
+ payload: { code: 503, message: "Event journal not enabled" }
9164
+ }, true);
9165
+ break;
9166
+ }
9167
+ const { requestId, fromSequence, mapName, types } = message;
9168
+ const subscriptionId = requestId;
9169
+ this.journalSubscriptions.set(subscriptionId, {
9170
+ clientId: client.id,
9171
+ mapName,
9172
+ types
9173
+ });
9174
+ const unsubscribe = this.eventJournalService.subscribe(
9175
+ (event) => {
9176
+ if (mapName && event.mapName !== mapName) return;
9177
+ if (types && types.length > 0 && !types.includes(event.type)) return;
9178
+ const clientConn = this.clients.get(client.id);
9179
+ if (!clientConn) {
9180
+ unsubscribe();
9181
+ this.journalSubscriptions.delete(subscriptionId);
9182
+ return;
9183
+ }
9184
+ clientConn.writer.write({
9185
+ type: "JOURNAL_EVENT",
9186
+ event: {
9187
+ sequence: event.sequence.toString(),
9188
+ type: event.type,
9189
+ mapName: event.mapName,
9190
+ key: event.key,
9191
+ value: event.value,
9192
+ previousValue: event.previousValue,
9193
+ timestamp: event.timestamp,
9194
+ nodeId: event.nodeId,
9195
+ metadata: event.metadata
9196
+ }
9197
+ });
9198
+ },
9199
+ fromSequence ? BigInt(fromSequence) : void 0
9200
+ );
9201
+ logger.info({ clientId: client.id, subscriptionId, mapName }, "Journal subscription created");
9202
+ break;
9203
+ }
9204
+ case "JOURNAL_UNSUBSCRIBE": {
9205
+ const { subscriptionId } = message;
9206
+ this.journalSubscriptions.delete(subscriptionId);
9207
+ logger.info({ clientId: client.id, subscriptionId }, "Journal subscription removed");
9208
+ break;
9209
+ }
9210
+ case "JOURNAL_READ": {
9211
+ if (!this.eventJournalService) {
9212
+ client.writer.write({
9213
+ type: "ERROR",
9214
+ payload: { code: 503, message: "Event journal not enabled" }
9215
+ }, true);
9216
+ break;
9217
+ }
9218
+ const { requestId: readReqId, fromSequence: readFromSeq, limit, mapName: readMapName } = message;
9219
+ const startSeq = BigInt(readFromSeq);
9220
+ const eventLimit = limit ?? 100;
9221
+ let events = this.eventJournalService.readFrom(startSeq, eventLimit);
9222
+ if (readMapName) {
9223
+ events = events.filter((e) => e.mapName === readMapName);
9224
+ }
9225
+ const serializedEvents = events.map((e) => ({
9226
+ sequence: e.sequence.toString(),
9227
+ type: e.type,
9228
+ mapName: e.mapName,
9229
+ key: e.key,
9230
+ value: e.value,
9231
+ previousValue: e.previousValue,
9232
+ timestamp: e.timestamp,
9233
+ nodeId: e.nodeId,
9234
+ metadata: e.metadata
9235
+ }));
9236
+ client.writer.write({
9237
+ type: "JOURNAL_READ_RESPONSE",
9238
+ requestId: readReqId,
9239
+ events: serializedEvents,
9240
+ hasMore: events.length === eventLimit
9241
+ });
9242
+ break;
9243
+ }
7541
9244
  default:
7542
9245
  logger.warn({ type: message.type }, "Unknown message type");
7543
9246
  }
@@ -7551,7 +9254,7 @@ var ServerCoordinator = class {
7551
9254
  } else if (op.orRecord && op.orRecord.timestamp) {
7552
9255
  } else if (op.orTag) {
7553
9256
  try {
7554
- ts = import_core10.HLC.parse(op.orTag);
9257
+ ts = import_core15.HLC.parse(op.orTag);
7555
9258
  } catch (e) {
7556
9259
  }
7557
9260
  }
@@ -7585,6 +9288,39 @@ var ServerCoordinator = class {
7585
9288
  clientCount: broadcastCount
7586
9289
  }, "Broadcast partition map to clients");
7587
9290
  }
9291
+ /**
9292
+ * Notify a client about a merge rejection (Phase 5.05).
9293
+ * Finds the client by node ID and sends MERGE_REJECTED message.
9294
+ */
9295
+ notifyMergeRejection(rejection) {
9296
+ for (const [clientId, client] of this.clients) {
9297
+ if (clientId === rejection.nodeId || rejection.nodeId.includes(clientId)) {
9298
+ client.writer.write({
9299
+ type: "MERGE_REJECTED",
9300
+ mapName: rejection.mapName,
9301
+ key: rejection.key,
9302
+ attemptedValue: rejection.attemptedValue,
9303
+ reason: rejection.reason,
9304
+ timestamp: rejection.timestamp
9305
+ }, true);
9306
+ return;
9307
+ }
9308
+ }
9309
+ const subscribedClientIds = this.queryRegistry.getSubscribedClientIds(rejection.mapName);
9310
+ for (const clientId of subscribedClientIds) {
9311
+ const client = this.clients.get(clientId);
9312
+ if (client) {
9313
+ client.writer.write({
9314
+ type: "MERGE_REJECTED",
9315
+ mapName: rejection.mapName,
9316
+ key: rejection.key,
9317
+ attemptedValue: rejection.attemptedValue,
9318
+ reason: rejection.reason,
9319
+ timestamp: rejection.timestamp
9320
+ });
9321
+ }
9322
+ }
9323
+ }
7588
9324
  broadcast(message, excludeClientId) {
7589
9325
  const isServerEvent = message.type === "SERVER_EVENT";
7590
9326
  if (isServerEvent) {
@@ -7615,7 +9351,7 @@ var ServerCoordinator = class {
7615
9351
  client.writer.write({ ...message, payload: newPayload });
7616
9352
  }
7617
9353
  } else {
7618
- const msgData = (0, import_core10.serialize)(message);
9354
+ const msgData = (0, import_core15.serialize)(message);
7619
9355
  for (const [id, client] of this.clients) {
7620
9356
  if (id !== excludeClientId && client.socket.readyState === 1) {
7621
9357
  client.writer.writeRaw(msgData);
@@ -7693,7 +9429,7 @@ var ServerCoordinator = class {
7693
9429
  payload: { events: filteredEvents },
7694
9430
  timestamp: this.hlc.now()
7695
9431
  };
7696
- const serializedBatch = (0, import_core10.serialize)(batchMessage);
9432
+ const serializedBatch = (0, import_core15.serialize)(batchMessage);
7697
9433
  for (const client of clients) {
7698
9434
  try {
7699
9435
  client.writer.writeRaw(serializedBatch);
@@ -7778,7 +9514,7 @@ var ServerCoordinator = class {
7778
9514
  payload: { events: filteredEvents },
7779
9515
  timestamp: this.hlc.now()
7780
9516
  };
7781
- const serializedBatch = (0, import_core10.serialize)(batchMessage);
9517
+ const serializedBatch = (0, import_core15.serialize)(batchMessage);
7782
9518
  for (const client of clients) {
7783
9519
  sendPromises.push(new Promise((resolve, reject) => {
7784
9520
  try {
@@ -7922,14 +9658,14 @@ var ServerCoordinator = class {
7922
9658
  async executeLocalQuery(mapName, query) {
7923
9659
  const map = await this.getMapAsync(mapName);
7924
9660
  const records = /* @__PURE__ */ new Map();
7925
- if (map instanceof import_core10.LWWMap) {
9661
+ if (map instanceof import_core15.LWWMap) {
7926
9662
  for (const key of map.allKeys()) {
7927
9663
  const rec = map.getRecord(key);
7928
9664
  if (rec && rec.value !== null) {
7929
9665
  records.set(key, rec);
7930
9666
  }
7931
9667
  }
7932
- } else if (map instanceof import_core10.ORMap) {
9668
+ } else if (map instanceof import_core15.ORMap) {
7933
9669
  const items = map.items;
7934
9670
  for (const key of items.keys()) {
7935
9671
  const values = map.get(key);
@@ -7996,14 +9732,14 @@ var ServerCoordinator = class {
7996
9732
  *
7997
9733
  * @returns Event payload for broadcasting (or null if operation failed)
7998
9734
  */
7999
- applyOpToMap(op) {
9735
+ async applyOpToMap(op, remoteNodeId) {
8000
9736
  const typeHint = op.opType === "OR_ADD" || op.opType === "OR_REMOVE" ? "OR" : "LWW";
8001
9737
  const map = this.getMap(op.mapName, typeHint);
8002
- if (typeHint === "OR" && map instanceof import_core10.LWWMap) {
9738
+ if (typeHint === "OR" && map instanceof import_core15.LWWMap) {
8003
9739
  logger.error({ mapName: op.mapName }, "Map type mismatch: LWWMap but received OR op");
8004
9740
  throw new Error("Map type mismatch: LWWMap but received OR op");
8005
9741
  }
8006
- if (typeHint === "LWW" && map instanceof import_core10.ORMap) {
9742
+ if (typeHint === "LWW" && map instanceof import_core15.ORMap) {
8007
9743
  logger.error({ mapName: op.mapName }, "Map type mismatch: ORMap but received LWW op");
8008
9744
  throw new Error("Map type mismatch: ORMap but received LWW op");
8009
9745
  }
@@ -8014,13 +9750,35 @@ var ServerCoordinator = class {
8014
9750
  mapName: op.mapName,
8015
9751
  key: op.key
8016
9752
  };
8017
- if (map instanceof import_core10.LWWMap) {
9753
+ if (map instanceof import_core15.LWWMap) {
8018
9754
  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) {
9755
+ if (this.conflictResolverHandler.hasResolvers(op.mapName)) {
9756
+ const mergeResult = await this.conflictResolverHandler.mergeWithResolver(
9757
+ map,
9758
+ op.mapName,
9759
+ op.key,
9760
+ op.record,
9761
+ remoteNodeId || this._nodeId
9762
+ );
9763
+ if (!mergeResult.applied) {
9764
+ if (mergeResult.rejection) {
9765
+ logger.debug(
9766
+ { mapName: op.mapName, key: op.key, reason: mergeResult.rejection.reason },
9767
+ "Merge rejected by resolver"
9768
+ );
9769
+ }
9770
+ return { eventPayload: null, oldRecord, rejected: true };
9771
+ }
9772
+ recordToStore = mergeResult.record;
9773
+ eventPayload.eventType = "UPDATED";
9774
+ eventPayload.record = mergeResult.record;
9775
+ } else {
9776
+ map.merge(op.key, op.record);
9777
+ recordToStore = op.record;
9778
+ eventPayload.eventType = "UPDATED";
9779
+ eventPayload.record = op.record;
9780
+ }
9781
+ } else if (map instanceof import_core15.ORMap) {
8024
9782
  oldRecord = map.getRecords(op.key);
8025
9783
  if (op.opType === "OR_ADD") {
8026
9784
  map.apply(op.key, op.orRecord);
@@ -8036,7 +9794,7 @@ var ServerCoordinator = class {
8036
9794
  }
8037
9795
  }
8038
9796
  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;
9797
+ const mapSize = map instanceof import_core15.ORMap ? map.totalRecords : map.size;
8040
9798
  this.metricsService.setMapSize(op.mapName, mapSize);
8041
9799
  if (this.storage) {
8042
9800
  if (recordToStore) {
@@ -8050,6 +9808,21 @@ var ServerCoordinator = class {
8050
9808
  });
8051
9809
  }
8052
9810
  }
9811
+ if (this.eventJournalService) {
9812
+ const isDelete = op.opType === "REMOVE" || op.opType === "OR_REMOVE" || op.record && op.record.value === null;
9813
+ const isNew = !oldRecord || Array.isArray(oldRecord) && oldRecord.length === 0;
9814
+ const journalEventType = isDelete ? "DELETE" : isNew ? "PUT" : "UPDATE";
9815
+ const timestamp = op.record?.timestamp || op.orRecord?.timestamp || this.hlc.now();
9816
+ this.eventJournalService.append({
9817
+ type: journalEventType,
9818
+ mapName: op.mapName,
9819
+ key: op.key,
9820
+ value: op.record?.value ?? op.orRecord?.value,
9821
+ previousValue: oldRecord?.value ?? (Array.isArray(oldRecord) ? oldRecord[0]?.value : void 0),
9822
+ timestamp,
9823
+ nodeId: this._nodeId
9824
+ });
9825
+ }
8053
9826
  return { eventPayload, oldRecord };
8054
9827
  }
8055
9828
  /**
@@ -8071,7 +9844,10 @@ var ServerCoordinator = class {
8071
9844
  try {
8072
9845
  const op = operation;
8073
9846
  logger.debug({ sourceNode, opId, mapName: op.mapName, key: op.key }, "Applying replicated operation");
8074
- const { eventPayload } = this.applyOpToMap(op);
9847
+ const { eventPayload, rejected } = await this.applyOpToMap(op, sourceNode);
9848
+ if (rejected || !eventPayload) {
9849
+ return true;
9850
+ }
8075
9851
  this.broadcast({
8076
9852
  type: "SERVER_EVENT",
8077
9853
  payload: eventPayload,
@@ -8170,7 +9946,10 @@ var ServerCoordinator = class {
8170
9946
  logger.warn({ err, opId: op.id }, "Interceptor rejected op");
8171
9947
  throw err;
8172
9948
  }
8173
- const { eventPayload } = this.applyOpToMap(op);
9949
+ const { eventPayload, rejected } = await this.applyOpToMap(op, originalSenderId);
9950
+ if (rejected || !eventPayload) {
9951
+ return;
9952
+ }
8174
9953
  if (this.replicationPipeline && !fromCluster) {
8175
9954
  const opId = op.id || `${op.mapName}:${op.key}:${Date.now()}`;
8176
9955
  this.replicationPipeline.replicate(op, opId, op.key).catch((err) => {
@@ -8298,7 +10077,10 @@ var ServerCoordinator = class {
8298
10077
  logger.warn({ err, opId: op.id }, "Interceptor rejected op in batch");
8299
10078
  throw err;
8300
10079
  }
8301
- const { eventPayload } = this.applyOpToMap(op);
10080
+ const { eventPayload, rejected } = await this.applyOpToMap(op, clientId);
10081
+ if (rejected || !eventPayload) {
10082
+ return;
10083
+ }
8302
10084
  if (this.replicationPipeline) {
8303
10085
  const opId = op.id || `${op.mapName}:${op.key}:${Date.now()}`;
8304
10086
  this.replicationPipeline.replicate(op, opId, op.key).catch((err) => {
@@ -8312,11 +10094,11 @@ var ServerCoordinator = class {
8312
10094
  handleClusterEvent(payload) {
8313
10095
  const { mapName, key, eventType } = payload;
8314
10096
  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;
10097
+ const oldRecord = map instanceof import_core15.LWWMap ? map.getRecord(key) : null;
8316
10098
  if (this.partitionService.isRelated(key)) {
8317
- if (map instanceof import_core10.LWWMap && payload.record) {
10099
+ if (map instanceof import_core15.LWWMap && payload.record) {
8318
10100
  map.merge(key, payload.record);
8319
- } else if (map instanceof import_core10.ORMap) {
10101
+ } else if (map instanceof import_core15.ORMap) {
8320
10102
  if (eventType === "OR_ADD" && payload.orRecord) {
8321
10103
  map.apply(key, payload.orRecord);
8322
10104
  } else if (eventType === "OR_REMOVE" && payload.orTag) {
@@ -8335,9 +10117,9 @@ var ServerCoordinator = class {
8335
10117
  if (!this.maps.has(name)) {
8336
10118
  let map;
8337
10119
  if (typeHint === "OR") {
8338
- map = new import_core10.ORMap(this.hlc);
10120
+ map = new import_core15.ORMap(this.hlc);
8339
10121
  } else {
8340
- map = new import_core10.LWWMap(this.hlc);
10122
+ map = new import_core15.LWWMap(this.hlc);
8341
10123
  }
8342
10124
  this.maps.set(name, map);
8343
10125
  if (this.storage) {
@@ -8360,7 +10142,7 @@ var ServerCoordinator = class {
8360
10142
  this.getMap(name, typeHint);
8361
10143
  const loadingPromise = this.mapLoadingPromises.get(name);
8362
10144
  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;
10145
+ const mapSize = map instanceof import_core15.LWWMap ? Array.from(map.entries()).length : map instanceof import_core15.ORMap ? map.size : 0;
8364
10146
  logger.info({
8365
10147
  mapName: name,
8366
10148
  mapExisted,
@@ -8370,7 +10152,7 @@ var ServerCoordinator = class {
8370
10152
  if (loadingPromise) {
8371
10153
  logger.info({ mapName: name }, "[getMapAsync] Waiting for loadMapFromStorage...");
8372
10154
  await loadingPromise;
8373
- const newMapSize = map instanceof import_core10.LWWMap ? Array.from(map.entries()).length : map instanceof import_core10.ORMap ? map.size : 0;
10155
+ const newMapSize = map instanceof import_core15.LWWMap ? Array.from(map.entries()).length : map instanceof import_core15.ORMap ? map.size : 0;
8374
10156
  logger.info({ mapName: name, mapSizeAfterLoad: newMapSize }, "[getMapAsync] Load completed");
8375
10157
  }
8376
10158
  return this.maps.get(name);
@@ -8396,16 +10178,16 @@ var ServerCoordinator = class {
8396
10178
  const currentMap = this.maps.get(name);
8397
10179
  if (!currentMap) return;
8398
10180
  let targetMap = currentMap;
8399
- if (isOR && currentMap instanceof import_core10.LWWMap) {
10181
+ if (isOR && currentMap instanceof import_core15.LWWMap) {
8400
10182
  logger.info({ mapName: name }, "Map auto-detected as ORMap. Switching type.");
8401
- targetMap = new import_core10.ORMap(this.hlc);
10183
+ targetMap = new import_core15.ORMap(this.hlc);
8402
10184
  this.maps.set(name, targetMap);
8403
- } else if (!isOR && currentMap instanceof import_core10.ORMap && typeHint !== "OR") {
10185
+ } else if (!isOR && currentMap instanceof import_core15.ORMap && typeHint !== "OR") {
8404
10186
  logger.info({ mapName: name }, "Map auto-detected as LWWMap. Switching type.");
8405
- targetMap = new import_core10.LWWMap(this.hlc);
10187
+ targetMap = new import_core15.LWWMap(this.hlc);
8406
10188
  this.maps.set(name, targetMap);
8407
10189
  }
8408
- if (targetMap instanceof import_core10.ORMap) {
10190
+ if (targetMap instanceof import_core15.ORMap) {
8409
10191
  for (const [key, record] of records) {
8410
10192
  if (key === "__tombstones__") {
8411
10193
  const t = record;
@@ -8418,7 +10200,7 @@ var ServerCoordinator = class {
8418
10200
  }
8419
10201
  }
8420
10202
  }
8421
- } else if (targetMap instanceof import_core10.LWWMap) {
10203
+ } else if (targetMap instanceof import_core15.LWWMap) {
8422
10204
  for (const [key, record] of records) {
8423
10205
  if (!record.type) {
8424
10206
  targetMap.merge(key, record);
@@ -8429,7 +10211,7 @@ var ServerCoordinator = class {
8429
10211
  if (count > 0) {
8430
10212
  logger.info({ mapName: name, count }, "Loaded records for map");
8431
10213
  this.queryRegistry.refreshSubscriptions(name, targetMap);
8432
- const mapSize = targetMap instanceof import_core10.ORMap ? targetMap.totalRecords : targetMap.size;
10214
+ const mapSize = targetMap instanceof import_core15.ORMap ? targetMap.totalRecords : targetMap.size;
8433
10215
  this.metricsService.setMapSize(name, mapSize);
8434
10216
  }
8435
10217
  } catch (err) {
@@ -8511,7 +10293,7 @@ var ServerCoordinator = class {
8511
10293
  reportLocalHlc() {
8512
10294
  let minHlc = this.hlc.now();
8513
10295
  for (const client of this.clients.values()) {
8514
- if (import_core10.HLC.compare(client.lastActiveHlc, minHlc) < 0) {
10296
+ if (import_core15.HLC.compare(client.lastActiveHlc, minHlc) < 0) {
8515
10297
  minHlc = client.lastActiveHlc;
8516
10298
  }
8517
10299
  }
@@ -8532,7 +10314,7 @@ var ServerCoordinator = class {
8532
10314
  let globalSafe = this.hlc.now();
8533
10315
  let initialized = false;
8534
10316
  for (const ts of this.gcReports.values()) {
8535
- if (!initialized || import_core10.HLC.compare(ts, globalSafe) < 0) {
10317
+ if (!initialized || import_core15.HLC.compare(ts, globalSafe) < 0) {
8536
10318
  globalSafe = ts;
8537
10319
  initialized = true;
8538
10320
  }
@@ -8567,7 +10349,7 @@ var ServerCoordinator = class {
8567
10349
  logger.info({ olderThanMillis: olderThan.millis }, "Performing Garbage Collection");
8568
10350
  const now = Date.now();
8569
10351
  for (const [name, map] of this.maps) {
8570
- if (map instanceof import_core10.LWWMap) {
10352
+ if (map instanceof import_core15.LWWMap) {
8571
10353
  for (const key of map.allKeys()) {
8572
10354
  const record = map.getRecord(key);
8573
10355
  if (record && record.value !== null && record.ttlMs) {
@@ -8619,7 +10401,7 @@ var ServerCoordinator = class {
8619
10401
  });
8620
10402
  }
8621
10403
  }
8622
- } else if (map instanceof import_core10.ORMap) {
10404
+ } else if (map instanceof import_core15.ORMap) {
8623
10405
  const items = map.items;
8624
10406
  const tombstonesSet = map.tombstones;
8625
10407
  const tagsToExpire = [];
@@ -8722,17 +10504,17 @@ var ServerCoordinator = class {
8722
10504
  stringToWriteConcern(value) {
8723
10505
  switch (value) {
8724
10506
  case "FIRE_AND_FORGET":
8725
- return import_core10.WriteConcern.FIRE_AND_FORGET;
10507
+ return import_core15.WriteConcern.FIRE_AND_FORGET;
8726
10508
  case "MEMORY":
8727
- return import_core10.WriteConcern.MEMORY;
10509
+ return import_core15.WriteConcern.MEMORY;
8728
10510
  case "APPLIED":
8729
- return import_core10.WriteConcern.APPLIED;
10511
+ return import_core15.WriteConcern.APPLIED;
8730
10512
  case "REPLICATED":
8731
- return import_core10.WriteConcern.REPLICATED;
10513
+ return import_core15.WriteConcern.REPLICATED;
8732
10514
  case "PERSISTED":
8733
- return import_core10.WriteConcern.PERSISTED;
10515
+ return import_core15.WriteConcern.PERSISTED;
8734
10516
  default:
8735
- return import_core10.WriteConcern.MEMORY;
10517
+ return import_core15.WriteConcern.MEMORY;
8736
10518
  }
8737
10519
  }
8738
10520
  /**
@@ -8789,7 +10571,7 @@ var ServerCoordinator = class {
8789
10571
  }
8790
10572
  });
8791
10573
  if (op.id) {
8792
- this.writeAckManager.notifyLevel(op.id, import_core10.WriteConcern.REPLICATED);
10574
+ this.writeAckManager.notifyLevel(op.id, import_core15.WriteConcern.REPLICATED);
8793
10575
  }
8794
10576
  }
8795
10577
  }
@@ -8797,7 +10579,7 @@ var ServerCoordinator = class {
8797
10579
  this.broadcastBatch(batchedEvents, clientId);
8798
10580
  for (const op of ops) {
8799
10581
  if (op.id && this.partitionService.isLocalOwner(op.key)) {
8800
- this.writeAckManager.notifyLevel(op.id, import_core10.WriteConcern.REPLICATED);
10582
+ this.writeAckManager.notifyLevel(op.id, import_core15.WriteConcern.REPLICATED);
8801
10583
  }
8802
10584
  }
8803
10585
  }
@@ -8825,7 +10607,7 @@ var ServerCoordinator = class {
8825
10607
  const owner = this.partitionService.getOwner(op.key);
8826
10608
  await this.forwardOpAndWait(op, owner);
8827
10609
  if (op.id) {
8828
- this.writeAckManager.notifyLevel(op.id, import_core10.WriteConcern.REPLICATED);
10610
+ this.writeAckManager.notifyLevel(op.id, import_core15.WriteConcern.REPLICATED);
8829
10611
  }
8830
10612
  }
8831
10613
  }
@@ -8833,7 +10615,7 @@ var ServerCoordinator = class {
8833
10615
  await this.broadcastBatchSync(batchedEvents, clientId);
8834
10616
  for (const op of ops) {
8835
10617
  if (op.id && this.partitionService.isLocalOwner(op.key)) {
8836
- this.writeAckManager.notifyLevel(op.id, import_core10.WriteConcern.REPLICATED);
10618
+ this.writeAckManager.notifyLevel(op.id, import_core15.WriteConcern.REPLICATED);
8837
10619
  }
8838
10620
  }
8839
10621
  }
@@ -8859,9 +10641,15 @@ var ServerCoordinator = class {
8859
10641
  }
8860
10642
  return;
8861
10643
  }
8862
- const { eventPayload } = this.applyOpToMap(op);
10644
+ const { eventPayload, rejected } = await this.applyOpToMap(op, clientId);
10645
+ if (rejected) {
10646
+ if (op.id) {
10647
+ this.writeAckManager.failPending(op.id, "Rejected by conflict resolver");
10648
+ }
10649
+ return;
10650
+ }
8863
10651
  if (op.id) {
8864
- this.writeAckManager.notifyLevel(op.id, import_core10.WriteConcern.APPLIED);
10652
+ this.writeAckManager.notifyLevel(op.id, import_core15.WriteConcern.APPLIED);
8865
10653
  }
8866
10654
  if (eventPayload) {
8867
10655
  batchedEvents.push({
@@ -8875,7 +10663,7 @@ var ServerCoordinator = class {
8875
10663
  try {
8876
10664
  await this.persistOpSync(op);
8877
10665
  if (op.id) {
8878
- this.writeAckManager.notifyLevel(op.id, import_core10.WriteConcern.PERSISTED);
10666
+ this.writeAckManager.notifyLevel(op.id, import_core15.WriteConcern.PERSISTED);
8879
10667
  }
8880
10668
  } catch (err) {
8881
10669
  logger.error({ opId: op.id, err }, "Persistence failed");
@@ -8941,9 +10729,9 @@ var ServerCoordinator = class {
8941
10729
  // src/storage/PostgresAdapter.ts
8942
10730
  var import_pg = require("pg");
8943
10731
  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)) {
10732
+ var TABLE_NAME_REGEX2 = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
10733
+ function validateTableName2(name) {
10734
+ if (!TABLE_NAME_REGEX2.test(name)) {
8947
10735
  throw new Error(
8948
10736
  `Invalid table name "${name}". Table name must start with a letter or underscore and contain only alphanumeric characters and underscores.`
8949
10737
  );
@@ -8957,7 +10745,7 @@ var PostgresAdapter = class {
8957
10745
  this.pool = new import_pg.Pool(configOrPool);
8958
10746
  }
8959
10747
  const tableName = options?.tableName ?? DEFAULT_TABLE_NAME;
8960
- validateTableName(tableName);
10748
+ validateTableName2(tableName);
8961
10749
  this.tableName = tableName;
8962
10750
  }
8963
10751
  async initialize() {
@@ -9218,10 +11006,10 @@ var RateLimitInterceptor = class {
9218
11006
  };
9219
11007
 
9220
11008
  // src/utils/nativeStats.ts
9221
- var import_core11 = require("@topgunbuild/core");
11009
+ var import_core16 = require("@topgunbuild/core");
9222
11010
  function getNativeModuleStatus() {
9223
11011
  return {
9224
- nativeHash: (0, import_core11.isUsingNativeHash)(),
11012
+ nativeHash: (0, import_core16.isUsingNativeHash)(),
9225
11013
  sharedArrayBuffer: SharedMemoryManager.isAvailable()
9226
11014
  };
9227
11015
  }
@@ -9255,11 +11043,11 @@ function logNativeStatus() {
9255
11043
 
9256
11044
  // src/cluster/ClusterCoordinator.ts
9257
11045
  var import_events9 = require("events");
9258
- var import_core12 = require("@topgunbuild/core");
11046
+ var import_core17 = require("@topgunbuild/core");
9259
11047
  var DEFAULT_CLUSTER_COORDINATOR_CONFIG = {
9260
11048
  gradualRebalancing: true,
9261
- migration: import_core12.DEFAULT_MIGRATION_CONFIG,
9262
- replication: import_core12.DEFAULT_REPLICATION_CONFIG,
11049
+ migration: import_core17.DEFAULT_MIGRATION_CONFIG,
11050
+ replication: import_core17.DEFAULT_REPLICATION_CONFIG,
9263
11051
  replicationEnabled: true
9264
11052
  };
9265
11053
  var ClusterCoordinator = class extends import_events9.EventEmitter {
@@ -9625,25 +11413,224 @@ var ClusterCoordinator = class extends import_events9.EventEmitter {
9625
11413
  }
9626
11414
  }
9627
11415
  };
11416
+
11417
+ // src/MapWithResolver.ts
11418
+ var import_core18 = require("@topgunbuild/core");
11419
+ var MapWithResolver = class {
11420
+ constructor(config) {
11421
+ this.mapName = config.name;
11422
+ this.hlc = new import_core18.HLC(config.nodeId);
11423
+ this.map = new import_core18.LWWMap(this.hlc);
11424
+ this.resolverService = config.resolverService;
11425
+ this.onRejection = config.onRejection;
11426
+ }
11427
+ /**
11428
+ * Get the map name.
11429
+ */
11430
+ get name() {
11431
+ return this.mapName;
11432
+ }
11433
+ /**
11434
+ * Get the underlying LWWMap.
11435
+ */
11436
+ get rawMap() {
11437
+ return this.map;
11438
+ }
11439
+ /**
11440
+ * Get a value by key.
11441
+ */
11442
+ get(key) {
11443
+ return this.map.get(key);
11444
+ }
11445
+ /**
11446
+ * Get the full record for a key.
11447
+ */
11448
+ getRecord(key) {
11449
+ return this.map.getRecord(key);
11450
+ }
11451
+ /**
11452
+ * Get the timestamp for a key.
11453
+ */
11454
+ getTimestamp(key) {
11455
+ return this.map.getRecord(key)?.timestamp;
11456
+ }
11457
+ /**
11458
+ * Set a value locally (no resolver).
11459
+ * Use for server-initiated writes.
11460
+ */
11461
+ set(key, value, ttlMs) {
11462
+ return this.map.set(key, value, ttlMs);
11463
+ }
11464
+ /**
11465
+ * Set a value with conflict resolution.
11466
+ * Use for client-initiated writes.
11467
+ *
11468
+ * @param key The key to set
11469
+ * @param value The new value
11470
+ * @param timestamp The client's timestamp
11471
+ * @param remoteNodeId The client's node ID
11472
+ * @param auth Optional authentication context
11473
+ * @returns Result containing applied status and merge result
11474
+ */
11475
+ async setWithResolver(key, value, timestamp, remoteNodeId, auth) {
11476
+ const context = {
11477
+ mapName: this.mapName,
11478
+ key,
11479
+ localValue: this.map.get(key),
11480
+ remoteValue: value,
11481
+ localTimestamp: this.getTimestamp(key),
11482
+ remoteTimestamp: timestamp,
11483
+ remoteNodeId,
11484
+ auth,
11485
+ readEntry: (k) => this.map.get(k)
11486
+ };
11487
+ const result = await this.resolverService.resolve(context);
11488
+ switch (result.action) {
11489
+ case "accept": {
11490
+ const record2 = {
11491
+ value: result.value,
11492
+ timestamp
11493
+ };
11494
+ this.map.merge(key, record2);
11495
+ return { applied: true, result, record: record2 };
11496
+ }
11497
+ case "merge": {
11498
+ const record2 = {
11499
+ value: result.value,
11500
+ timestamp
11501
+ };
11502
+ this.map.merge(key, record2);
11503
+ return { applied: true, result, record: record2 };
11504
+ }
11505
+ case "reject": {
11506
+ if (this.onRejection) {
11507
+ this.onRejection({
11508
+ mapName: this.mapName,
11509
+ key,
11510
+ attemptedValue: value,
11511
+ reason: result.reason,
11512
+ timestamp,
11513
+ nodeId: remoteNodeId
11514
+ });
11515
+ }
11516
+ return { applied: false, result };
11517
+ }
11518
+ case "local": {
11519
+ return { applied: false, result };
11520
+ }
11521
+ default:
11522
+ const record = {
11523
+ value: result.value ?? value,
11524
+ timestamp
11525
+ };
11526
+ this.map.merge(key, record);
11527
+ return { applied: true, result, record };
11528
+ }
11529
+ }
11530
+ /**
11531
+ * Remove a key.
11532
+ */
11533
+ remove(key) {
11534
+ return this.map.remove(key);
11535
+ }
11536
+ /**
11537
+ * Standard merge without resolver (for sync operations).
11538
+ */
11539
+ merge(key, record) {
11540
+ return this.map.merge(key, record);
11541
+ }
11542
+ /**
11543
+ * Merge with resolver support.
11544
+ * Equivalent to setWithResolver but takes a full record.
11545
+ */
11546
+ async mergeWithResolver(key, record, remoteNodeId, auth) {
11547
+ if (record.value === null) {
11548
+ const applied = this.map.merge(key, record);
11549
+ return {
11550
+ applied,
11551
+ result: applied ? { action: "accept", value: record.value } : { action: "local" },
11552
+ record: applied ? record : void 0
11553
+ };
11554
+ }
11555
+ return this.setWithResolver(
11556
+ key,
11557
+ record.value,
11558
+ record.timestamp,
11559
+ remoteNodeId,
11560
+ auth
11561
+ );
11562
+ }
11563
+ /**
11564
+ * Clear all data.
11565
+ */
11566
+ clear() {
11567
+ this.map.clear();
11568
+ }
11569
+ /**
11570
+ * Get map size.
11571
+ */
11572
+ get size() {
11573
+ return this.map.size;
11574
+ }
11575
+ /**
11576
+ * Iterate over entries.
11577
+ */
11578
+ entries() {
11579
+ return this.map.entries();
11580
+ }
11581
+ /**
11582
+ * Get all keys.
11583
+ */
11584
+ allKeys() {
11585
+ return this.map.allKeys();
11586
+ }
11587
+ /**
11588
+ * Subscribe to changes.
11589
+ */
11590
+ onChange(callback) {
11591
+ return this.map.onChange(callback);
11592
+ }
11593
+ /**
11594
+ * Get MerkleTree for sync.
11595
+ */
11596
+ getMerkleTree() {
11597
+ return this.map.getMerkleTree();
11598
+ }
11599
+ /**
11600
+ * Prune old tombstones.
11601
+ */
11602
+ prune(olderThan) {
11603
+ return this.map.prune(olderThan);
11604
+ }
11605
+ };
9628
11606
  // Annotate the CommonJS export names for ESM import in node:
9629
11607
  0 && (module.exports = {
9630
11608
  BufferPool,
9631
11609
  ClusterCoordinator,
9632
11610
  ClusterManager,
11611
+ ConflictResolverHandler,
11612
+ ConflictResolverService,
9633
11613
  ConnectionRateLimiter,
9634
11614
  DEFAULT_CLUSTER_COORDINATOR_CONFIG,
11615
+ DEFAULT_CONFLICT_RESOLVER_CONFIG,
11616
+ DEFAULT_JOURNAL_SERVICE_CONFIG,
9635
11617
  DEFAULT_LAG_TRACKER_CONFIG,
11618
+ DEFAULT_SANDBOX_CONFIG,
11619
+ EntryProcessorHandler,
11620
+ EventJournalService,
9636
11621
  FilterTasklet,
9637
11622
  ForEachTasklet,
9638
11623
  IteratorTasklet,
9639
11624
  LagTracker,
9640
11625
  LockManager,
9641
11626
  MapTasklet,
11627
+ MapWithResolver,
9642
11628
  MemoryServerAdapter,
9643
11629
  MigrationManager,
9644
11630
  ObjectPool,
9645
11631
  PartitionService,
9646
11632
  PostgresAdapter,
11633
+ ProcessorSandbox,
9647
11634
  RateLimitInterceptor,
9648
11635
  ReduceTasklet,
9649
11636
  ReplicationPipeline,