@topgunbuild/client 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/SyncEngine.ts
2
- import { HLC, LWWMap, ORMap, serialize, deserialize, evaluatePredicate } from "@topgunbuild/core";
2
+ import { HLC, LWWMap as LWWMap2, ORMap as ORMap2, deserialize as deserialize2 } from "@topgunbuild/core";
3
3
 
4
4
  // src/utils/logger.ts
5
5
  import pino from "pino";
@@ -180,21 +180,6 @@ var SyncStateMachine = class {
180
180
  }
181
181
  };
182
182
 
183
- // src/errors/BackpressureError.ts
184
- var BackpressureError = class _BackpressureError extends Error {
185
- constructor(pendingCount, maxPending) {
186
- super(
187
- `Backpressure limit reached: ${pendingCount}/${maxPending} pending operations. Wait for acknowledgments or increase maxPendingOps.`
188
- );
189
- this.pendingCount = pendingCount;
190
- this.maxPending = maxPending;
191
- this.name = "BackpressureError";
192
- if (Error.captureStackTrace) {
193
- Error.captureStackTrace(this, _BackpressureError);
194
- }
195
- }
196
- };
197
-
198
183
  // src/BackpressureConfig.ts
199
184
  var DEFAULT_BACKPRESSURE_CONFIG = {
200
185
  maxPendingOps: 1e3,
@@ -203,233 +188,6 @@ var DEFAULT_BACKPRESSURE_CONFIG = {
203
188
  lowWaterMark: 0.5
204
189
  };
205
190
 
206
- // src/connection/SingleServerProvider.ts
207
- var DEFAULT_CONFIG = {
208
- maxReconnectAttempts: 10,
209
- reconnectDelayMs: 1e3,
210
- backoffMultiplier: 2,
211
- maxReconnectDelayMs: 3e4
212
- };
213
- var SingleServerProvider = class {
214
- constructor(config) {
215
- this.ws = null;
216
- this.reconnectAttempts = 0;
217
- this.reconnectTimer = null;
218
- this.isClosing = false;
219
- this.listeners = /* @__PURE__ */ new Map();
220
- this.url = config.url;
221
- this.config = {
222
- url: config.url,
223
- maxReconnectAttempts: config.maxReconnectAttempts ?? DEFAULT_CONFIG.maxReconnectAttempts,
224
- reconnectDelayMs: config.reconnectDelayMs ?? DEFAULT_CONFIG.reconnectDelayMs,
225
- backoffMultiplier: config.backoffMultiplier ?? DEFAULT_CONFIG.backoffMultiplier,
226
- maxReconnectDelayMs: config.maxReconnectDelayMs ?? DEFAULT_CONFIG.maxReconnectDelayMs
227
- };
228
- }
229
- /**
230
- * Connect to the WebSocket server.
231
- */
232
- async connect() {
233
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
234
- return;
235
- }
236
- this.isClosing = false;
237
- return new Promise((resolve, reject) => {
238
- try {
239
- this.ws = new WebSocket(this.url);
240
- this.ws.binaryType = "arraybuffer";
241
- this.ws.onopen = () => {
242
- this.reconnectAttempts = 0;
243
- logger.info({ url: this.url }, "SingleServerProvider connected");
244
- this.emit("connected", "default");
245
- resolve();
246
- };
247
- this.ws.onerror = (error) => {
248
- logger.error({ err: error, url: this.url }, "SingleServerProvider WebSocket error");
249
- this.emit("error", error);
250
- };
251
- this.ws.onclose = (event) => {
252
- logger.info({ url: this.url, code: event.code }, "SingleServerProvider disconnected");
253
- this.emit("disconnected", "default");
254
- if (!this.isClosing) {
255
- this.scheduleReconnect();
256
- }
257
- };
258
- this.ws.onmessage = (event) => {
259
- this.emit("message", "default", event.data);
260
- };
261
- const timeoutId = setTimeout(() => {
262
- if (this.ws && this.ws.readyState === WebSocket.CONNECTING) {
263
- this.ws.close();
264
- reject(new Error(`Connection timeout to ${this.url}`));
265
- }
266
- }, this.config.reconnectDelayMs * 5);
267
- const originalOnOpen = this.ws.onopen;
268
- const wsRef = this.ws;
269
- this.ws.onopen = (ev) => {
270
- clearTimeout(timeoutId);
271
- if (originalOnOpen) {
272
- originalOnOpen.call(wsRef, ev);
273
- }
274
- };
275
- } catch (error) {
276
- reject(error);
277
- }
278
- });
279
- }
280
- /**
281
- * Get connection for a specific key.
282
- * In single-server mode, key is ignored.
283
- */
284
- getConnection(_key) {
285
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
286
- throw new Error("Not connected");
287
- }
288
- return this.ws;
289
- }
290
- /**
291
- * Get any available connection.
292
- */
293
- getAnyConnection() {
294
- return this.getConnection("");
295
- }
296
- /**
297
- * Check if connected.
298
- */
299
- isConnected() {
300
- return this.ws?.readyState === WebSocket.OPEN;
301
- }
302
- /**
303
- * Get connected node IDs.
304
- * Single-server mode returns ['default'] when connected.
305
- */
306
- getConnectedNodes() {
307
- return this.isConnected() ? ["default"] : [];
308
- }
309
- /**
310
- * Subscribe to connection events.
311
- */
312
- on(event, handler2) {
313
- if (!this.listeners.has(event)) {
314
- this.listeners.set(event, /* @__PURE__ */ new Set());
315
- }
316
- this.listeners.get(event).add(handler2);
317
- }
318
- /**
319
- * Unsubscribe from connection events.
320
- */
321
- off(event, handler2) {
322
- this.listeners.get(event)?.delete(handler2);
323
- }
324
- /**
325
- * Send data via the WebSocket connection.
326
- * In single-server mode, key parameter is ignored.
327
- */
328
- send(data, _key) {
329
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
330
- throw new Error("Not connected");
331
- }
332
- this.ws.send(data);
333
- }
334
- /**
335
- * Close the WebSocket connection.
336
- */
337
- async close() {
338
- this.isClosing = true;
339
- if (this.reconnectTimer) {
340
- clearTimeout(this.reconnectTimer);
341
- this.reconnectTimer = null;
342
- }
343
- if (this.ws) {
344
- this.ws.onclose = null;
345
- this.ws.onerror = null;
346
- this.ws.onmessage = null;
347
- if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
348
- this.ws.close();
349
- }
350
- this.ws = null;
351
- }
352
- logger.info({ url: this.url }, "SingleServerProvider closed");
353
- }
354
- /**
355
- * Emit an event to all listeners.
356
- */
357
- emit(event, ...args) {
358
- const handlers = this.listeners.get(event);
359
- if (handlers) {
360
- for (const handler2 of handlers) {
361
- try {
362
- handler2(...args);
363
- } catch (err) {
364
- logger.error({ err, event }, "Error in SingleServerProvider event handler");
365
- }
366
- }
367
- }
368
- }
369
- /**
370
- * Schedule a reconnection attempt with exponential backoff.
371
- */
372
- scheduleReconnect() {
373
- if (this.reconnectTimer) {
374
- clearTimeout(this.reconnectTimer);
375
- this.reconnectTimer = null;
376
- }
377
- if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
378
- logger.error(
379
- { attempts: this.reconnectAttempts, url: this.url },
380
- "SingleServerProvider max reconnect attempts reached"
381
- );
382
- this.emit("error", new Error("Max reconnection attempts reached"));
383
- return;
384
- }
385
- const delay = this.calculateBackoffDelay();
386
- logger.info(
387
- { delay, attempt: this.reconnectAttempts, url: this.url },
388
- `SingleServerProvider scheduling reconnect in ${delay}ms`
389
- );
390
- this.reconnectTimer = setTimeout(async () => {
391
- this.reconnectTimer = null;
392
- this.reconnectAttempts++;
393
- try {
394
- await this.connect();
395
- this.emit("reconnected", "default");
396
- } catch (error) {
397
- logger.error({ err: error }, "SingleServerProvider reconnection failed");
398
- this.scheduleReconnect();
399
- }
400
- }, delay);
401
- }
402
- /**
403
- * Calculate backoff delay with exponential increase.
404
- */
405
- calculateBackoffDelay() {
406
- const { reconnectDelayMs, backoffMultiplier, maxReconnectDelayMs } = this.config;
407
- let delay = reconnectDelayMs * Math.pow(backoffMultiplier, this.reconnectAttempts);
408
- delay = Math.min(delay, maxReconnectDelayMs);
409
- delay = delay * (0.5 + Math.random());
410
- return Math.floor(delay);
411
- }
412
- /**
413
- * Get the WebSocket URL this provider connects to.
414
- */
415
- getUrl() {
416
- return this.url;
417
- }
418
- /**
419
- * Get current reconnection attempt count.
420
- */
421
- getReconnectAttempts() {
422
- return this.reconnectAttempts;
423
- }
424
- /**
425
- * Reset reconnection counter.
426
- * Called externally after successful authentication.
427
- */
428
- resetReconnectAttempts() {
429
- this.reconnectAttempts = 0;
430
- }
431
- };
432
-
433
191
  // src/ConflictResolverClient.ts
434
192
  var _ConflictResolverClient = class _ConflictResolverClient {
435
193
  // 10 seconds
@@ -661,189 +419,52 @@ var _ConflictResolverClient = class _ConflictResolverClient {
661
419
  _ConflictResolverClient.REQUEST_TIMEOUT = 1e4;
662
420
  var ConflictResolverClient = _ConflictResolverClient;
663
421
 
664
- // src/SyncEngine.ts
665
- var DEFAULT_BACKOFF_CONFIG = {
666
- initialDelayMs: 1e3,
667
- maxDelayMs: 3e4,
668
- multiplier: 2,
669
- jitter: true,
670
- maxRetries: 10
671
- };
672
- var _SyncEngine = class _SyncEngine {
422
+ // src/sync/WebSocketManager.ts
423
+ import { serialize, deserialize } from "@topgunbuild/core";
424
+ var WebSocketManager = class {
673
425
  constructor(config) {
674
- this.websocket = null;
675
- this.opLog = [];
676
- this.maps = /* @__PURE__ */ new Map();
677
- this.queries = /* @__PURE__ */ new Map();
678
- this.topics = /* @__PURE__ */ new Map();
679
- this.pendingLockRequests = /* @__PURE__ */ new Map();
680
- this.lastSyncTimestamp = 0;
426
+ // Reconnection state
681
427
  this.reconnectTimer = null;
682
- // NodeJS.Timeout
683
- this.authToken = null;
684
- this.tokenProvider = null;
685
428
  this.backoffAttempt = 0;
429
+ // Heartbeat state
686
430
  this.heartbeatInterval = null;
687
431
  this.lastPongReceived = Date.now();
688
432
  this.lastRoundTripTime = null;
689
- this.backpressurePaused = false;
690
- this.waitingForCapacity = [];
691
- this.highWaterMarkEmitted = false;
692
- this.backpressureListeners = /* @__PURE__ */ new Map();
693
- // Write Concern state (Phase 5.01)
694
- this.pendingWriteConcernPromises = /* @__PURE__ */ new Map();
695
- // ============================================
696
- // PN Counter Methods (Phase 5.2)
697
- // ============================================
698
- /** Counter update listeners by name */
699
- this.counterUpdateListeners = /* @__PURE__ */ new Map();
700
- // ============================================
701
- // Entry Processor Methods (Phase 5.03)
702
- // ============================================
703
- /** Pending entry processor requests by requestId */
704
- this.pendingProcessorRequests = /* @__PURE__ */ new Map();
705
- /** Pending batch entry processor requests by requestId */
706
- this.pendingBatchProcessorRequests = /* @__PURE__ */ new Map();
707
- // ============================================
708
- // Event Journal Methods (Phase 5.04)
709
- // ============================================
710
- /** Message listeners for journal and other generic messages */
711
- this.messageListeners = /* @__PURE__ */ new Set();
712
- // ============================================
713
- // Full-Text Search Methods (Phase 11.1a)
714
- // ============================================
715
- /** Pending search requests by requestId */
716
- this.pendingSearchRequests = /* @__PURE__ */ new Map();
717
- // ============================================
718
- // Hybrid Query Support (Phase 12)
719
- // ============================================
720
- /** Active hybrid query subscriptions */
721
- this.hybridQueries = /* @__PURE__ */ new Map();
722
- if (!config.serverUrl && !config.connectionProvider) {
723
- throw new Error("SyncEngine requires either serverUrl or connectionProvider");
724
- }
725
- this.nodeId = config.nodeId;
726
- this.serverUrl = config.serverUrl || "";
727
- this.storageAdapter = config.storageAdapter;
728
- this.hlc = new HLC(this.nodeId);
729
- this.stateMachine = new SyncStateMachine();
730
- this.heartbeatConfig = {
731
- intervalMs: config.heartbeat?.intervalMs ?? 5e3,
732
- timeoutMs: config.heartbeat?.timeoutMs ?? 15e3,
733
- enabled: config.heartbeat?.enabled ?? true
734
- };
735
- this.backoffConfig = {
736
- ...DEFAULT_BACKOFF_CONFIG,
737
- ...config.backoff
738
- };
739
- this.backpressureConfig = {
740
- ...DEFAULT_BACKPRESSURE_CONFIG,
741
- ...config.backpressure
742
- };
743
- if (config.connectionProvider) {
744
- this.connectionProvider = config.connectionProvider;
745
- this.useConnectionProvider = true;
746
- this.initConnectionProvider();
747
- } else {
748
- this.connectionProvider = new SingleServerProvider({ url: config.serverUrl });
749
- this.useConnectionProvider = false;
750
- this.initConnection();
751
- }
752
- this.conflictResolverClient = new ConflictResolverClient(this);
753
- this.loadOpLog();
754
- }
755
- // ============================================
756
- // State Machine Public API
757
- // ============================================
758
- /**
759
- * Get the current connection state
760
- */
761
- getConnectionState() {
762
- return this.stateMachine.getState();
763
- }
764
- /**
765
- * Subscribe to connection state changes
766
- * @returns Unsubscribe function
767
- */
768
- onConnectionStateChange(listener) {
769
- return this.stateMachine.onStateChange(listener);
770
- }
771
- /**
772
- * Get state machine history for debugging
773
- */
774
- getStateHistory(limit) {
775
- return this.stateMachine.getHistory(limit);
776
- }
777
- // ============================================
778
- // Internal State Helpers (replace boolean flags)
779
- // ============================================
780
- /**
781
- * Check if WebSocket is connected (but may not be authenticated yet)
782
- */
783
- isOnline() {
784
- const state = this.stateMachine.getState();
785
- return state === "CONNECTING" /* CONNECTING */ || state === "AUTHENTICATING" /* AUTHENTICATING */ || state === "SYNCING" /* SYNCING */ || state === "CONNECTED" /* CONNECTED */;
786
- }
787
- /**
788
- * Check if fully authenticated and ready for operations
789
- */
790
- isAuthenticated() {
791
- const state = this.stateMachine.getState();
792
- return state === "SYNCING" /* SYNCING */ || state === "CONNECTED" /* CONNECTED */;
433
+ this.config = config;
434
+ this.connectionProvider = config.connectionProvider;
793
435
  }
794
436
  /**
795
- * Check if fully connected and synced
437
+ * Initialize the connection.
438
+ * Sets up event handlers and starts the connection process.
796
439
  */
797
- isConnected() {
798
- return this.stateMachine.getState() === "CONNECTED" /* CONNECTED */;
440
+ connect() {
441
+ this.initConnectionProvider();
799
442
  }
800
- // ============================================
801
- // Connection Management
802
- // ============================================
803
443
  /**
804
- * Initialize connection using IConnectionProvider (Phase 4.5 cluster mode).
805
- * Sets up event handlers for the connection provider.
444
+ * Initialize connection using IConnectionProvider.
806
445
  */
807
446
  initConnectionProvider() {
808
- this.stateMachine.transition("CONNECTING" /* CONNECTING */);
447
+ this.config.stateMachine.transition("CONNECTING" /* CONNECTING */);
809
448
  this.connectionProvider.on("connected", (_nodeId) => {
810
- if (this.authToken || this.tokenProvider) {
811
- logger.info("ConnectionProvider connected. Sending auth...");
812
- this.stateMachine.transition("AUTHENTICATING" /* AUTHENTICATING */);
813
- this.sendAuth();
814
- } else {
815
- logger.info("ConnectionProvider connected. Waiting for auth token...");
816
- this.stateMachine.transition("AUTHENTICATING" /* AUTHENTICATING */);
817
- }
449
+ logger.info("ConnectionProvider connected.");
450
+ this.config.onConnected?.();
818
451
  });
819
452
  this.connectionProvider.on("disconnected", (_nodeId) => {
820
453
  logger.info("ConnectionProvider disconnected.");
821
454
  this.stopHeartbeat();
822
- this.stateMachine.transition("DISCONNECTED" /* DISCONNECTED */);
455
+ this.config.stateMachine.transition("DISCONNECTED" /* DISCONNECTED */);
456
+ this.config.onDisconnected?.();
823
457
  });
824
458
  this.connectionProvider.on("reconnected", (_nodeId) => {
825
459
  logger.info("ConnectionProvider reconnected.");
826
- this.stateMachine.transition("CONNECTING" /* CONNECTING */);
827
- if (this.authToken || this.tokenProvider) {
828
- this.stateMachine.transition("AUTHENTICATING" /* AUTHENTICATING */);
829
- this.sendAuth();
830
- }
460
+ this.config.stateMachine.transition("CONNECTING" /* CONNECTING */);
461
+ this.config.onReconnected?.();
831
462
  });
832
463
  this.connectionProvider.on("message", (_nodeId, data) => {
833
- let message;
834
- if (data instanceof ArrayBuffer) {
835
- message = deserialize(new Uint8Array(data));
836
- } else if (data instanceof Uint8Array) {
837
- message = deserialize(data);
838
- } else {
839
- try {
840
- message = typeof data === "string" ? JSON.parse(data) : data;
841
- } catch (e) {
842
- logger.error({ err: e }, "Failed to parse message from ConnectionProvider");
843
- return;
844
- }
464
+ const message = this.deserializeMessage(data);
465
+ if (message) {
466
+ this.handleMessage(message);
845
467
  }
846
- this.handleServerMessage(message);
847
468
  });
848
469
  this.connectionProvider.on("partitionMapUpdated", () => {
849
470
  logger.debug("Partition map updated");
@@ -853,109 +474,48 @@ var _SyncEngine = class _SyncEngine {
853
474
  });
854
475
  this.connectionProvider.connect().catch((err) => {
855
476
  logger.error({ err }, "Failed to connect via ConnectionProvider");
856
- this.stateMachine.transition("DISCONNECTED" /* DISCONNECTED */);
477
+ this.config.stateMachine.transition("DISCONNECTED" /* DISCONNECTED */);
857
478
  });
858
479
  }
859
480
  /**
860
- * Initialize connection using direct WebSocket (legacy single-server mode).
481
+ * Deserialize incoming message data.
861
482
  */
862
- initConnection() {
863
- this.stateMachine.transition("CONNECTING" /* CONNECTING */);
864
- this.websocket = new WebSocket(this.serverUrl);
865
- this.websocket.binaryType = "arraybuffer";
866
- this.websocket.onopen = () => {
867
- if (this.authToken || this.tokenProvider) {
868
- logger.info("WebSocket connected. Sending auth...");
869
- this.stateMachine.transition("AUTHENTICATING" /* AUTHENTICATING */);
870
- this.sendAuth();
871
- } else {
872
- logger.info("WebSocket connected. Waiting for auth token...");
873
- this.stateMachine.transition("AUTHENTICATING" /* AUTHENTICATING */);
874
- }
875
- };
876
- this.websocket.onmessage = (event) => {
877
- let message;
878
- if (event.data instanceof ArrayBuffer) {
879
- message = deserialize(new Uint8Array(event.data));
483
+ deserializeMessage(data) {
484
+ try {
485
+ if (data instanceof ArrayBuffer) {
486
+ return deserialize(new Uint8Array(data));
487
+ } else if (data instanceof Uint8Array) {
488
+ return deserialize(data);
489
+ } else if (typeof data === "string") {
490
+ return JSON.parse(data);
880
491
  } else {
881
- try {
882
- message = JSON.parse(event.data);
883
- } catch (e) {
884
- logger.error({ err: e }, "Failed to parse message");
885
- return;
886
- }
492
+ return data;
887
493
  }
888
- this.handleServerMessage(message);
889
- };
890
- this.websocket.onclose = () => {
891
- logger.info("WebSocket disconnected.");
892
- this.stopHeartbeat();
893
- this.stateMachine.transition("DISCONNECTED" /* DISCONNECTED */);
894
- this.scheduleReconnect();
895
- };
896
- this.websocket.onerror = (error) => {
897
- logger.error({ err: error }, "WebSocket error");
898
- };
899
- }
900
- scheduleReconnect() {
901
- if (this.reconnectTimer) {
902
- clearTimeout(this.reconnectTimer);
903
- this.reconnectTimer = null;
904
- }
905
- if (this.backoffAttempt >= this.backoffConfig.maxRetries) {
906
- logger.error(
907
- { attempts: this.backoffAttempt },
908
- "Max reconnection attempts reached. Entering ERROR state."
909
- );
910
- this.stateMachine.transition("ERROR" /* ERROR */);
911
- return;
912
- }
913
- this.stateMachine.transition("BACKOFF" /* BACKOFF */);
914
- const delay = this.calculateBackoffDelay();
915
- logger.info({ delay, attempt: this.backoffAttempt }, `Backing off for ${delay}ms`);
916
- this.reconnectTimer = setTimeout(() => {
917
- this.reconnectTimer = null;
918
- this.backoffAttempt++;
919
- this.initConnection();
920
- }, delay);
921
- }
922
- calculateBackoffDelay() {
923
- const { initialDelayMs, maxDelayMs, multiplier, jitter } = this.backoffConfig;
924
- let delay = initialDelayMs * Math.pow(multiplier, this.backoffAttempt);
925
- delay = Math.min(delay, maxDelayMs);
926
- if (jitter) {
927
- delay = delay * (0.5 + Math.random());
494
+ } catch (e) {
495
+ logger.error({ err: e }, "Failed to parse message");
496
+ return null;
928
497
  }
929
- return Math.floor(delay);
930
498
  }
931
499
  /**
932
- * Reset backoff counter (called on successful connection)
500
+ * Handle incoming message.
501
+ * Routes PONG to internal handler, all others to SyncEngine.
933
502
  */
934
- resetBackoff() {
935
- this.backoffAttempt = 0;
503
+ handleMessage(message) {
504
+ if (message.type === "PONG") {
505
+ this.handlePong(message);
506
+ }
507
+ this.config.onMessage(message);
936
508
  }
937
509
  /**
938
510
  * Send a message through the current connection.
939
- * Uses connectionProvider if in cluster mode, otherwise uses direct websocket.
940
- * @param message Message object to serialize and send
941
- * @param key Optional key for routing (cluster mode only)
942
- * @returns true if message was sent, false otherwise
943
511
  */
944
512
  sendMessage(message, key) {
945
513
  const data = serialize(message);
946
- if (this.useConnectionProvider) {
947
- try {
948
- this.connectionProvider.send(data, key);
949
- return true;
950
- } catch (err) {
951
- logger.warn({ err }, "Failed to send via ConnectionProvider");
952
- return false;
953
- }
954
- } else {
955
- if (this.websocket?.readyState === WebSocket.OPEN) {
956
- this.websocket.send(data);
957
- return true;
958
- }
514
+ try {
515
+ this.connectionProvider.send(data, key);
516
+ return true;
517
+ } catch (err) {
518
+ logger.warn({ err }, "Failed to send via ConnectionProvider");
959
519
  return false;
960
520
  }
961
521
  }
@@ -963,110 +523,2009 @@ var _SyncEngine = class _SyncEngine {
963
523
  * Check if we can send messages (connection is ready).
964
524
  */
965
525
  canSend() {
966
- if (this.useConnectionProvider) {
967
- return this.connectionProvider.isConnected();
968
- }
969
- return this.websocket?.readyState === WebSocket.OPEN;
526
+ return this.connectionProvider.isConnected();
970
527
  }
971
- async loadOpLog() {
972
- const storedTimestamp = await this.storageAdapter.getMeta("lastSyncTimestamp");
973
- if (storedTimestamp) {
974
- this.lastSyncTimestamp = storedTimestamp;
975
- }
976
- const pendingOps = await this.storageAdapter.getPendingOps();
977
- this.opLog = pendingOps.map((op) => ({
978
- ...op,
979
- id: String(op.id),
980
- synced: false
981
- }));
982
- if (this.opLog.length > 0) {
983
- logger.info({ count: this.opLog.length }, "Loaded pending operations from local storage");
528
+ /**
529
+ * Check if connected to the server.
530
+ */
531
+ isOnline() {
532
+ const state = this.config.stateMachine.getState();
533
+ return state === "CONNECTING" /* CONNECTING */ || state === "AUTHENTICATING" /* AUTHENTICATING */ || state === "SYNCING" /* SYNCING */ || state === "CONNECTED" /* CONNECTED */;
534
+ }
535
+ /**
536
+ * Get the connection provider.
537
+ */
538
+ getConnectionProvider() {
539
+ return this.connectionProvider;
540
+ }
541
+ /**
542
+ * Close the connection and clean up resources.
543
+ */
544
+ close() {
545
+ this.stopHeartbeat();
546
+ if (this.reconnectTimer) {
547
+ clearTimeout(this.reconnectTimer);
548
+ this.reconnectTimer = null;
984
549
  }
550
+ this.connectionProvider.close().catch((err) => {
551
+ logger.error({ err }, "Error closing ConnectionProvider");
552
+ });
985
553
  }
986
- async saveOpLog() {
987
- await this.storageAdapter.setMeta("lastSyncTimestamp", this.lastSyncTimestamp);
554
+ /**
555
+ * Reset connection state for a fresh reconnection.
556
+ */
557
+ reset() {
558
+ this.close();
559
+ this.resetBackoff();
988
560
  }
989
- registerMap(mapName, map) {
990
- this.maps.set(mapName, map);
561
+ /**
562
+ * Subscribe to connection events.
563
+ */
564
+ on(event, handler2) {
565
+ this.connectionProvider.on(event, handler2);
991
566
  }
992
- async recordOperation(mapName, opType, key, data) {
993
- await this.checkBackpressure();
994
- const opLogEntry = {
995
- mapName,
996
- opType,
997
- key,
998
- record: data.record,
999
- orRecord: data.orRecord,
1000
- orTag: data.orTag,
1001
- timestamp: data.timestamp,
1002
- synced: false
1003
- };
1004
- const id = await this.storageAdapter.appendOpLog(opLogEntry);
1005
- opLogEntry.id = String(id);
1006
- this.opLog.push(opLogEntry);
1007
- this.checkHighWaterMark();
1008
- if (this.isAuthenticated()) {
1009
- this.syncPendingOperations();
1010
- }
1011
- return opLogEntry.id;
567
+ /**
568
+ * Unsubscribe from connection events.
569
+ */
570
+ off(event, handler2) {
571
+ this.connectionProvider.off(event, handler2);
1012
572
  }
1013
- syncPendingOperations() {
1014
- const pending = this.opLog.filter((op) => !op.synced);
1015
- if (pending.length === 0) return;
1016
- logger.info({ count: pending.length }, "Syncing pending operations");
1017
- this.sendMessage({
1018
- type: "OP_BATCH",
1019
- payload: {
1020
- ops: pending
1021
- }
1022
- });
573
+ /**
574
+ * Reset backoff counter.
575
+ */
576
+ resetBackoff() {
577
+ this.backoffAttempt = 0;
1023
578
  }
1024
- startMerkleSync() {
1025
- for (const [mapName, map] of this.maps) {
1026
- if (map instanceof LWWMap) {
1027
- logger.info({ mapName }, "Starting Merkle sync for LWWMap");
1028
- this.sendMessage({
1029
- type: "SYNC_INIT",
1030
- mapName,
1031
- lastSyncTimestamp: this.lastSyncTimestamp
1032
- });
1033
- } else if (map instanceof ORMap) {
1034
- logger.info({ mapName }, "Starting Merkle sync for ORMap");
1035
- const tree = map.getMerkleTree();
1036
- const rootHash = tree.getRootHash();
1037
- const bucketHashes = tree.getBuckets("");
1038
- this.sendMessage({
1039
- type: "ORMAP_SYNC_INIT",
1040
- mapName,
1041
- rootHash,
1042
- bucketHashes,
1043
- lastSyncTimestamp: this.lastSyncTimestamp
1044
- });
1045
- }
1046
- }
579
+ /**
580
+ * Get current backoff attempt count.
581
+ */
582
+ getBackoffAttempt() {
583
+ return this.backoffAttempt;
1047
584
  }
1048
- setAuthToken(token) {
1049
- this.authToken = token;
1050
- this.tokenProvider = null;
1051
- const state = this.stateMachine.getState();
1052
- if (state === "AUTHENTICATING" /* AUTHENTICATING */ || state === "CONNECTING" /* CONNECTING */) {
1053
- this.sendAuth();
1054
- } else if (state === "BACKOFF" /* BACKOFF */ || state === "DISCONNECTED" /* DISCONNECTED */) {
1055
- logger.info("Auth token set during backoff/disconnect. Reconnecting immediately.");
1056
- if (this.reconnectTimer) {
1057
- clearTimeout(this.reconnectTimer);
1058
- this.reconnectTimer = null;
1059
- }
1060
- this.resetBackoff();
1061
- this.initConnection();
585
+ /**
586
+ * Clear reconnect timer (for external control, e.g., when new auth token provided).
587
+ */
588
+ clearReconnectTimer() {
589
+ if (this.reconnectTimer) {
590
+ clearTimeout(this.reconnectTimer);
591
+ this.reconnectTimer = null;
1062
592
  }
1063
593
  }
1064
- setTokenProvider(provider) {
1065
- this.tokenProvider = provider;
1066
- const state = this.stateMachine.getState();
1067
- if (state === "AUTHENTICATING" /* AUTHENTICATING */) {
1068
- this.sendAuth();
1069
- }
594
+ // ============================================
595
+ // Heartbeat Mechanism
596
+ // ============================================
597
+ /**
598
+ * Starts the heartbeat mechanism after successful connection.
599
+ */
600
+ startHeartbeat() {
601
+ if (!this.config.heartbeatConfig.enabled) {
602
+ return;
603
+ }
604
+ this.stopHeartbeat();
605
+ this.lastPongReceived = Date.now();
606
+ this.heartbeatInterval = setInterval(() => {
607
+ this.sendPing();
608
+ this.checkHeartbeatTimeout();
609
+ }, this.config.heartbeatConfig.intervalMs);
610
+ logger.info({ intervalMs: this.config.heartbeatConfig.intervalMs }, "Heartbeat started");
611
+ }
612
+ /**
613
+ * Stops the heartbeat mechanism.
614
+ */
615
+ stopHeartbeat() {
616
+ if (this.heartbeatInterval) {
617
+ clearInterval(this.heartbeatInterval);
618
+ this.heartbeatInterval = null;
619
+ logger.info("Heartbeat stopped");
620
+ }
621
+ }
622
+ /**
623
+ * Sends a PING message to the server.
624
+ */
625
+ sendPing() {
626
+ if (this.canSend()) {
627
+ const pingMessage = {
628
+ type: "PING",
629
+ timestamp: Date.now()
630
+ };
631
+ this.sendMessage(pingMessage);
632
+ }
633
+ }
634
+ /**
635
+ * Handles incoming PONG message from server.
636
+ */
637
+ handlePong(msg) {
638
+ const now = Date.now();
639
+ this.lastPongReceived = now;
640
+ this.lastRoundTripTime = now - msg.timestamp;
641
+ logger.debug({
642
+ rtt: this.lastRoundTripTime,
643
+ serverTime: msg.serverTime,
644
+ clockSkew: msg.serverTime - (msg.timestamp + this.lastRoundTripTime / 2)
645
+ }, "Received PONG");
646
+ }
647
+ /**
648
+ * Checks if heartbeat has timed out and triggers reconnection if needed.
649
+ */
650
+ checkHeartbeatTimeout() {
651
+ const now = Date.now();
652
+ const timeSinceLastPong = now - this.lastPongReceived;
653
+ if (timeSinceLastPong > this.config.heartbeatConfig.timeoutMs) {
654
+ logger.warn({
655
+ timeSinceLastPong,
656
+ timeoutMs: this.config.heartbeatConfig.timeoutMs
657
+ }, "Heartbeat timeout - triggering reconnection");
658
+ this.stopHeartbeat();
659
+ this.connectionProvider.close().catch((err) => {
660
+ logger.error({ err }, "Error closing ConnectionProvider on heartbeat timeout");
661
+ });
662
+ }
663
+ }
664
+ /**
665
+ * Returns the last measured round-trip time in milliseconds.
666
+ */
667
+ getLastRoundTripTime() {
668
+ return this.lastRoundTripTime;
669
+ }
670
+ /**
671
+ * Returns true if the connection is considered healthy based on heartbeat.
672
+ */
673
+ isConnectionHealthy() {
674
+ const state = this.config.stateMachine.getState();
675
+ const isOnline = state === "CONNECTING" /* CONNECTING */ || state === "AUTHENTICATING" /* AUTHENTICATING */ || state === "SYNCING" /* SYNCING */ || state === "CONNECTED" /* CONNECTED */;
676
+ const isAuthenticated = state === "SYNCING" /* SYNCING */ || state === "CONNECTED" /* CONNECTED */;
677
+ if (!isOnline || !isAuthenticated) {
678
+ return false;
679
+ }
680
+ if (!this.config.heartbeatConfig.enabled) {
681
+ return true;
682
+ }
683
+ const timeSinceLastPong = Date.now() - this.lastPongReceived;
684
+ return timeSinceLastPong < this.config.heartbeatConfig.timeoutMs;
685
+ }
686
+ };
687
+
688
+ // src/errors/BackpressureError.ts
689
+ var BackpressureError = class _BackpressureError extends Error {
690
+ constructor(pendingCount, maxPending) {
691
+ super(
692
+ `Backpressure limit reached: ${pendingCount}/${maxPending} pending operations. Wait for acknowledgments or increase maxPendingOps.`
693
+ );
694
+ this.pendingCount = pendingCount;
695
+ this.maxPending = maxPending;
696
+ this.name = "BackpressureError";
697
+ if (Error.captureStackTrace) {
698
+ Error.captureStackTrace(this, _BackpressureError);
699
+ }
700
+ }
701
+ };
702
+
703
+ // src/sync/BackpressureController.ts
704
+ var BackpressureController = class {
705
+ constructor(controllerConfig) {
706
+ // Internal state
707
+ this.backpressurePaused = false;
708
+ this.waitingForCapacity = [];
709
+ this.highWaterMarkEmitted = false;
710
+ this.backpressureListeners = /* @__PURE__ */ new Map();
711
+ this.config = controllerConfig.config;
712
+ this.opLog = controllerConfig.opLog;
713
+ }
714
+ // ============================================
715
+ // Status Methods
716
+ // ============================================
717
+ /**
718
+ * Get the current number of pending (unsynced) operations.
719
+ */
720
+ getPendingOpsCount() {
721
+ return this.opLog.filter((op) => !op.synced).length;
722
+ }
723
+ /**
724
+ * Get the current backpressure status.
725
+ */
726
+ getBackpressureStatus() {
727
+ const pending = this.getPendingOpsCount();
728
+ const max = this.config.maxPendingOps;
729
+ return {
730
+ pending,
731
+ max,
732
+ percentage: max > 0 ? pending / max : 0,
733
+ isPaused: this.backpressurePaused,
734
+ strategy: this.config.strategy
735
+ };
736
+ }
737
+ /**
738
+ * Returns true if writes are currently paused due to backpressure.
739
+ */
740
+ isBackpressurePaused() {
741
+ return this.backpressurePaused;
742
+ }
743
+ // ============================================
744
+ // Check Methods
745
+ // ============================================
746
+ /**
747
+ * Check backpressure before adding a new operation.
748
+ * May pause, throw, or drop depending on strategy.
749
+ */
750
+ async checkBackpressure() {
751
+ const pendingCount = this.getPendingOpsCount();
752
+ if (pendingCount < this.config.maxPendingOps) {
753
+ return;
754
+ }
755
+ switch (this.config.strategy) {
756
+ case "pause":
757
+ await this.waitForCapacity();
758
+ break;
759
+ case "throw":
760
+ throw new BackpressureError(
761
+ pendingCount,
762
+ this.config.maxPendingOps
763
+ );
764
+ case "drop-oldest":
765
+ this.dropOldestOp();
766
+ break;
767
+ }
768
+ }
769
+ /**
770
+ * Check high water mark and emit event if threshold reached.
771
+ */
772
+ checkHighWaterMark() {
773
+ const pendingCount = this.getPendingOpsCount();
774
+ const threshold = Math.floor(
775
+ this.config.maxPendingOps * this.config.highWaterMark
776
+ );
777
+ if (pendingCount >= threshold && !this.highWaterMarkEmitted) {
778
+ this.highWaterMarkEmitted = true;
779
+ logger.warn(
780
+ { pending: pendingCount, max: this.config.maxPendingOps },
781
+ "Backpressure high water mark reached"
782
+ );
783
+ this.emitBackpressureEvent("backpressure:high", {
784
+ pending: pendingCount,
785
+ max: this.config.maxPendingOps
786
+ });
787
+ }
788
+ }
789
+ /**
790
+ * Check low water mark and resume paused writes if threshold reached.
791
+ */
792
+ checkLowWaterMark() {
793
+ const pendingCount = this.getPendingOpsCount();
794
+ const lowThreshold = Math.floor(
795
+ this.config.maxPendingOps * this.config.lowWaterMark
796
+ );
797
+ const highThreshold = Math.floor(
798
+ this.config.maxPendingOps * this.config.highWaterMark
799
+ );
800
+ if (pendingCount < highThreshold && this.highWaterMarkEmitted) {
801
+ this.highWaterMarkEmitted = false;
802
+ }
803
+ if (pendingCount <= lowThreshold) {
804
+ if (this.backpressurePaused) {
805
+ this.backpressurePaused = false;
806
+ logger.info(
807
+ { pending: pendingCount, max: this.config.maxPendingOps },
808
+ "Backpressure low water mark reached, resuming writes"
809
+ );
810
+ this.emitBackpressureEvent("backpressure:low", {
811
+ pending: pendingCount,
812
+ max: this.config.maxPendingOps
813
+ });
814
+ this.emitBackpressureEvent("backpressure:resumed");
815
+ const waiting = this.waitingForCapacity;
816
+ this.waitingForCapacity = [];
817
+ for (const resolve of waiting) {
818
+ resolve();
819
+ }
820
+ }
821
+ }
822
+ }
823
+ // ============================================
824
+ // Event Methods
825
+ // ============================================
826
+ /**
827
+ * Subscribe to backpressure events.
828
+ * @param event Event name: 'backpressure:high', 'backpressure:low', 'backpressure:paused', 'backpressure:resumed', 'operation:dropped'
829
+ * @param listener Callback function
830
+ * @returns Unsubscribe function
831
+ */
832
+ onBackpressure(event, listener) {
833
+ if (!this.backpressureListeners.has(event)) {
834
+ this.backpressureListeners.set(event, /* @__PURE__ */ new Set());
835
+ }
836
+ this.backpressureListeners.get(event).add(listener);
837
+ return () => {
838
+ this.backpressureListeners.get(event)?.delete(listener);
839
+ };
840
+ }
841
+ // ============================================
842
+ // Private Methods
843
+ // ============================================
844
+ /**
845
+ * Emit a backpressure event to all listeners.
846
+ */
847
+ emitBackpressureEvent(event, data) {
848
+ const listeners = this.backpressureListeners.get(event);
849
+ if (listeners) {
850
+ for (const listener of listeners) {
851
+ try {
852
+ listener(data);
853
+ } catch (err) {
854
+ logger.error({ err, event }, "Error in backpressure event listener");
855
+ }
856
+ }
857
+ }
858
+ }
859
+ /**
860
+ * Wait for capacity to become available (used by 'pause' strategy).
861
+ */
862
+ async waitForCapacity() {
863
+ if (!this.backpressurePaused) {
864
+ this.backpressurePaused = true;
865
+ logger.warn("Backpressure paused - waiting for capacity");
866
+ this.emitBackpressureEvent("backpressure:paused");
867
+ }
868
+ return new Promise((resolve) => {
869
+ this.waitingForCapacity.push(resolve);
870
+ });
871
+ }
872
+ /**
873
+ * Drop the oldest pending operation (used by 'drop-oldest' strategy).
874
+ * Modifies opLog via shared reference.
875
+ */
876
+ dropOldestOp() {
877
+ const oldestIndex = this.opLog.findIndex((op) => !op.synced);
878
+ if (oldestIndex !== -1) {
879
+ const dropped = this.opLog[oldestIndex];
880
+ this.opLog.splice(oldestIndex, 1);
881
+ logger.warn(
882
+ { opId: dropped.id, mapName: dropped.mapName, key: dropped.key },
883
+ "Dropped oldest pending operation due to backpressure"
884
+ );
885
+ this.emitBackpressureEvent("operation:dropped", {
886
+ opId: dropped.id,
887
+ mapName: dropped.mapName,
888
+ opType: dropped.opType,
889
+ key: dropped.key
890
+ });
891
+ }
892
+ }
893
+ };
894
+
895
+ // src/sync/QueryManager.ts
896
+ import { evaluatePredicate } from "@topgunbuild/core";
897
+ var QueryManager = class {
898
+ constructor(config) {
899
+ /** Standard queries (single source of truth) */
900
+ this.queries = /* @__PURE__ */ new Map();
901
+ /** Hybrid queries with FTS support (single source of truth) */
902
+ this.hybridQueries = /* @__PURE__ */ new Map();
903
+ this.config = config;
904
+ }
905
+ // ============================================
906
+ // Query Access Methods
907
+ // ============================================
908
+ /**
909
+ * Get all queries (read-only access).
910
+ */
911
+ getQueries() {
912
+ return this.queries;
913
+ }
914
+ /**
915
+ * Get all hybrid queries.
916
+ */
917
+ getHybridQueries() {
918
+ return this.hybridQueries;
919
+ }
920
+ /**
921
+ * Get a hybrid query by ID.
922
+ */
923
+ getHybridQuery(queryId) {
924
+ return this.hybridQueries.get(queryId);
925
+ }
926
+ // ============================================
927
+ // Standard Query Methods
928
+ // ============================================
929
+ /**
930
+ * Subscribe to a standard query.
931
+ * Adds to queries Map and sends subscription to server if authenticated.
932
+ */
933
+ subscribeToQuery(query) {
934
+ this.queries.set(query.id, query);
935
+ if (this.config.isAuthenticated()) {
936
+ this.sendQuerySubscription(query);
937
+ }
938
+ }
939
+ /**
940
+ * Unsubscribe from a query.
941
+ * Removes from Map and sends unsubscription to server if authenticated.
942
+ */
943
+ unsubscribeFromQuery(queryId) {
944
+ this.queries.delete(queryId);
945
+ if (this.config.isAuthenticated()) {
946
+ this.config.sendMessage({
947
+ type: "QUERY_UNSUB",
948
+ payload: { queryId }
949
+ });
950
+ }
951
+ }
952
+ /**
953
+ * Send query subscription message to server.
954
+ */
955
+ sendQuerySubscription(query) {
956
+ this.config.sendMessage({
957
+ type: "QUERY_SUB",
958
+ payload: {
959
+ queryId: query.id,
960
+ mapName: query.getMapName(),
961
+ query: query.getFilter()
962
+ }
963
+ });
964
+ }
965
+ // ============================================
966
+ // Hybrid Query Methods
967
+ // ============================================
968
+ /**
969
+ * Subscribe to a hybrid query (FTS + filter combination).
970
+ */
971
+ subscribeToHybridQuery(query) {
972
+ this.hybridQueries.set(query.id, query);
973
+ const filter = query.getFilter();
974
+ const mapName = query.getMapName();
975
+ if (query.hasFTSPredicate() && this.config.isAuthenticated()) {
976
+ this.sendHybridQuerySubscription(query.id, mapName, filter);
977
+ }
978
+ this.runLocalHybridQuery(mapName, filter).then((results) => {
979
+ query.onResult(results, "local");
980
+ });
981
+ }
982
+ /**
983
+ * Unsubscribe from a hybrid query.
984
+ */
985
+ unsubscribeFromHybridQuery(queryId) {
986
+ const query = this.hybridQueries.get(queryId);
987
+ if (query) {
988
+ this.hybridQueries.delete(queryId);
989
+ if (this.config.isAuthenticated() && query.hasFTSPredicate()) {
990
+ this.config.sendMessage({
991
+ type: "HYBRID_QUERY_UNSUBSCRIBE",
992
+ payload: { subscriptionId: queryId }
993
+ });
994
+ }
995
+ }
996
+ }
997
+ /**
998
+ * Send hybrid query subscription message to server.
999
+ */
1000
+ sendHybridQuerySubscription(queryId, mapName, filter) {
1001
+ this.config.sendMessage({
1002
+ type: "HYBRID_QUERY_SUBSCRIBE",
1003
+ payload: {
1004
+ subscriptionId: queryId,
1005
+ mapName,
1006
+ predicate: filter.predicate,
1007
+ where: filter.where,
1008
+ sort: filter.sort,
1009
+ limit: filter.limit,
1010
+ cursor: filter.cursor
1011
+ }
1012
+ });
1013
+ }
1014
+ // ============================================
1015
+ // Local Query Execution
1016
+ // ============================================
1017
+ /**
1018
+ * Executes a query against local storage immediately.
1019
+ */
1020
+ async runLocalQuery(mapName, filter) {
1021
+ const keys = await this.config.storageAdapter.getAllKeys();
1022
+ const mapKeys = keys.filter((k) => k.startsWith(mapName + ":"));
1023
+ const results = [];
1024
+ for (const fullKey of mapKeys) {
1025
+ const record = await this.config.storageAdapter.get(fullKey);
1026
+ if (record && record.value) {
1027
+ const actualKey = fullKey.slice(mapName.length + 1);
1028
+ let matches = true;
1029
+ if (filter.where) {
1030
+ for (const [k, v] of Object.entries(filter.where)) {
1031
+ if (record.value[k] !== v) {
1032
+ matches = false;
1033
+ break;
1034
+ }
1035
+ }
1036
+ }
1037
+ if (matches && filter.predicate) {
1038
+ if (!evaluatePredicate(filter.predicate, record.value)) {
1039
+ matches = false;
1040
+ }
1041
+ }
1042
+ if (matches) {
1043
+ results.push({ key: actualKey, value: record.value });
1044
+ }
1045
+ }
1046
+ }
1047
+ return results;
1048
+ }
1049
+ /**
1050
+ * Run a local hybrid query (FTS + filter combination).
1051
+ * For FTS predicates, returns results with score = 0 (local-only mode).
1052
+ * Server provides actual FTS scoring.
1053
+ */
1054
+ async runLocalHybridQuery(mapName, filter) {
1055
+ const results = [];
1056
+ const allKeys = await this.config.storageAdapter.getAllKeys();
1057
+ const mapPrefix = `${mapName}:`;
1058
+ const entries = [];
1059
+ for (const fullKey of allKeys) {
1060
+ if (fullKey.startsWith(mapPrefix)) {
1061
+ const key = fullKey.substring(mapPrefix.length);
1062
+ const record = await this.config.storageAdapter.get(fullKey);
1063
+ if (record) {
1064
+ entries.push([key, record]);
1065
+ }
1066
+ }
1067
+ }
1068
+ for (const [key, record] of entries) {
1069
+ if (record === null || record.value === null) continue;
1070
+ const value = record.value;
1071
+ if (filter.predicate) {
1072
+ const matches = evaluatePredicate(filter.predicate, value);
1073
+ if (!matches) continue;
1074
+ }
1075
+ if (filter.where) {
1076
+ let whereMatches = true;
1077
+ for (const [field, expected] of Object.entries(filter.where)) {
1078
+ if (value[field] !== expected) {
1079
+ whereMatches = false;
1080
+ break;
1081
+ }
1082
+ }
1083
+ if (!whereMatches) continue;
1084
+ }
1085
+ results.push({
1086
+ key,
1087
+ value,
1088
+ score: 0,
1089
+ // Local doesn't have FTS scoring
1090
+ matchedTerms: []
1091
+ });
1092
+ }
1093
+ if (filter.sort) {
1094
+ results.sort((a, b) => {
1095
+ for (const [field, direction] of Object.entries(filter.sort)) {
1096
+ let valA;
1097
+ let valB;
1098
+ if (field === "_score") {
1099
+ valA = a.score ?? 0;
1100
+ valB = b.score ?? 0;
1101
+ } else if (field === "_key") {
1102
+ valA = a.key;
1103
+ valB = b.key;
1104
+ } else {
1105
+ valA = a.value[field];
1106
+ valB = b.value[field];
1107
+ }
1108
+ if (valA < valB) return direction === "asc" ? -1 : 1;
1109
+ if (valA > valB) return direction === "asc" ? 1 : -1;
1110
+ }
1111
+ return 0;
1112
+ });
1113
+ }
1114
+ let sliced = results;
1115
+ if (filter.limit) {
1116
+ sliced = sliced.slice(0, filter.limit);
1117
+ }
1118
+ return sliced;
1119
+ }
1120
+ // ============================================
1121
+ // Re-subscription (after auth)
1122
+ // ============================================
1123
+ /**
1124
+ * Re-subscribe all queries after authentication.
1125
+ * Called by SyncEngine after AUTH_ACK.
1126
+ */
1127
+ resubscribeAll() {
1128
+ logger.debug({ queryCount: this.queries.size, hybridCount: this.hybridQueries.size }, "QueryManager: resubscribing all queries");
1129
+ for (const query of this.queries.values()) {
1130
+ this.sendQuerySubscription(query);
1131
+ }
1132
+ for (const query of this.hybridQueries.values()) {
1133
+ if (query.hasFTSPredicate()) {
1134
+ this.sendHybridQuerySubscription(query.id, query.getMapName(), query.getFilter());
1135
+ }
1136
+ }
1137
+ }
1138
+ };
1139
+
1140
+ // src/sync/TopicManager.ts
1141
+ var TopicManager = class {
1142
+ constructor(config) {
1143
+ // Topic subscriptions (single source of truth)
1144
+ this.topics = /* @__PURE__ */ new Map();
1145
+ // Offline message queue
1146
+ this.topicQueue = [];
1147
+ this.config = config;
1148
+ }
1149
+ // ============================================
1150
+ // Public API
1151
+ // ============================================
1152
+ /**
1153
+ * Subscribe to a topic.
1154
+ * Adds to topics Map and sends subscription to server if authenticated.
1155
+ */
1156
+ subscribeToTopic(topic, handle) {
1157
+ this.topics.set(topic, handle);
1158
+ if (this.config.isAuthenticated()) {
1159
+ this.sendTopicSubscription(topic);
1160
+ }
1161
+ }
1162
+ /**
1163
+ * Unsubscribe from a topic.
1164
+ * Removes from Map and sends unsubscription to server if authenticated.
1165
+ */
1166
+ unsubscribeFromTopic(topic) {
1167
+ this.topics.delete(topic);
1168
+ if (this.config.isAuthenticated()) {
1169
+ this.config.sendMessage({
1170
+ type: "TOPIC_UNSUB",
1171
+ payload: { topic }
1172
+ });
1173
+ }
1174
+ }
1175
+ /**
1176
+ * Publish a message to a topic.
1177
+ * Sends immediately if authenticated, otherwise queues for later.
1178
+ */
1179
+ publishTopic(topic, data) {
1180
+ if (this.config.isAuthenticated()) {
1181
+ this.config.sendMessage({
1182
+ type: "TOPIC_PUB",
1183
+ payload: { topic, data }
1184
+ });
1185
+ } else {
1186
+ this.queueTopicMessage(topic, data);
1187
+ }
1188
+ }
1189
+ /**
1190
+ * Flush all queued topic messages.
1191
+ * Called by SyncEngine after authentication.
1192
+ */
1193
+ flushTopicQueue() {
1194
+ if (this.topicQueue.length === 0) return;
1195
+ logger.info({ count: this.topicQueue.length }, "Flushing queued topic messages");
1196
+ for (const msg of this.topicQueue) {
1197
+ this.config.sendMessage({
1198
+ type: "TOPIC_PUB",
1199
+ payload: { topic: msg.topic, data: msg.data }
1200
+ });
1201
+ }
1202
+ this.topicQueue = [];
1203
+ }
1204
+ /**
1205
+ * Get topic queue status.
1206
+ */
1207
+ getTopicQueueStatus() {
1208
+ return {
1209
+ size: this.topicQueue.length,
1210
+ maxSize: this.config.topicQueueConfig.maxSize
1211
+ };
1212
+ }
1213
+ /**
1214
+ * Get all subscribed topics.
1215
+ * Used for resubscription after authentication.
1216
+ */
1217
+ getTopics() {
1218
+ return this.topics.keys();
1219
+ }
1220
+ /**
1221
+ * Re-subscribe all topics after authentication.
1222
+ * Called by SyncEngine after AUTH_ACK.
1223
+ */
1224
+ resubscribeAll() {
1225
+ for (const topic of this.topics.keys()) {
1226
+ this.sendTopicSubscription(topic);
1227
+ }
1228
+ }
1229
+ /**
1230
+ * Handle incoming topic message from server.
1231
+ */
1232
+ handleTopicMessage(topic, data, publisherId, timestamp) {
1233
+ const handle = this.topics.get(topic);
1234
+ if (handle) {
1235
+ handle.onMessage(data, { publisherId, timestamp });
1236
+ }
1237
+ }
1238
+ // ============================================
1239
+ // Private Methods
1240
+ // ============================================
1241
+ /**
1242
+ * Queue a topic message for offline publishing.
1243
+ */
1244
+ queueTopicMessage(topic, data) {
1245
+ const message = {
1246
+ topic,
1247
+ data,
1248
+ timestamp: Date.now()
1249
+ };
1250
+ if (this.topicQueue.length >= this.config.topicQueueConfig.maxSize) {
1251
+ if (this.config.topicQueueConfig.strategy === "drop-oldest") {
1252
+ const dropped = this.topicQueue.shift();
1253
+ logger.warn({ topic: dropped?.topic }, "Dropped oldest queued topic message (queue full)");
1254
+ } else {
1255
+ logger.warn({ topic }, "Dropped newest topic message (queue full)");
1256
+ return;
1257
+ }
1258
+ }
1259
+ this.topicQueue.push(message);
1260
+ logger.debug({ topic, queueSize: this.topicQueue.length }, "Queued topic message for offline");
1261
+ }
1262
+ /**
1263
+ * Send topic subscription message to server.
1264
+ */
1265
+ sendTopicSubscription(topic) {
1266
+ this.config.sendMessage({
1267
+ type: "TOPIC_SUB",
1268
+ payload: { topic }
1269
+ });
1270
+ }
1271
+ };
1272
+
1273
+ // src/sync/LockManager.ts
1274
+ var LockManager = class {
1275
+ constructor(config) {
1276
+ // Pending lock requests (single source of truth)
1277
+ this.pendingLockRequests = /* @__PURE__ */ new Map();
1278
+ this.config = config;
1279
+ }
1280
+ // ============================================
1281
+ // Public API
1282
+ // ============================================
1283
+ /**
1284
+ * Request a distributed lock.
1285
+ */
1286
+ requestLock(name, requestId, ttl) {
1287
+ if (!this.config.isAuthenticated()) {
1288
+ return Promise.reject(new Error("Not connected or authenticated"));
1289
+ }
1290
+ return new Promise((resolve, reject) => {
1291
+ const timer = setTimeout(() => {
1292
+ if (this.pendingLockRequests.has(requestId)) {
1293
+ this.pendingLockRequests.delete(requestId);
1294
+ reject(new Error("Lock request timed out waiting for server response"));
1295
+ }
1296
+ }, 3e4);
1297
+ this.pendingLockRequests.set(requestId, { resolve, reject, timer });
1298
+ try {
1299
+ const sent = this.config.sendMessage({
1300
+ type: "LOCK_REQUEST",
1301
+ payload: { requestId, name, ttl }
1302
+ });
1303
+ if (!sent) {
1304
+ clearTimeout(timer);
1305
+ this.pendingLockRequests.delete(requestId);
1306
+ reject(new Error("Failed to send lock request"));
1307
+ }
1308
+ } catch (e) {
1309
+ clearTimeout(timer);
1310
+ this.pendingLockRequests.delete(requestId);
1311
+ reject(e);
1312
+ }
1313
+ });
1314
+ }
1315
+ /**
1316
+ * Release a distributed lock.
1317
+ */
1318
+ releaseLock(name, requestId, fencingToken) {
1319
+ if (!this.config.isOnline()) return Promise.resolve(false);
1320
+ return new Promise((resolve, reject) => {
1321
+ const timer = setTimeout(() => {
1322
+ if (this.pendingLockRequests.has(requestId)) {
1323
+ this.pendingLockRequests.delete(requestId);
1324
+ resolve(false);
1325
+ }
1326
+ }, 5e3);
1327
+ this.pendingLockRequests.set(requestId, { resolve, reject, timer });
1328
+ try {
1329
+ const sent = this.config.sendMessage({
1330
+ type: "LOCK_RELEASE",
1331
+ payload: { requestId, name, fencingToken }
1332
+ });
1333
+ if (!sent) {
1334
+ clearTimeout(timer);
1335
+ this.pendingLockRequests.delete(requestId);
1336
+ resolve(false);
1337
+ }
1338
+ } catch (e) {
1339
+ clearTimeout(timer);
1340
+ this.pendingLockRequests.delete(requestId);
1341
+ resolve(false);
1342
+ }
1343
+ });
1344
+ }
1345
+ /**
1346
+ * Handle lock granted message from server.
1347
+ */
1348
+ handleLockGranted(requestId, fencingToken) {
1349
+ const req = this.pendingLockRequests.get(requestId);
1350
+ if (req) {
1351
+ clearTimeout(req.timer);
1352
+ this.pendingLockRequests.delete(requestId);
1353
+ req.resolve({ fencingToken });
1354
+ }
1355
+ }
1356
+ /**
1357
+ * Handle lock released message from server.
1358
+ */
1359
+ handleLockReleased(requestId, success) {
1360
+ const req = this.pendingLockRequests.get(requestId);
1361
+ if (req) {
1362
+ clearTimeout(req.timer);
1363
+ this.pendingLockRequests.delete(requestId);
1364
+ req.resolve(success);
1365
+ }
1366
+ }
1367
+ };
1368
+
1369
+ // src/sync/WriteConcernManager.ts
1370
+ var WriteConcernManager = class {
1371
+ constructor(config) {
1372
+ // Pending write concern promises (single source of truth)
1373
+ this.pendingWriteConcernPromises = /* @__PURE__ */ new Map();
1374
+ this.config = config;
1375
+ }
1376
+ // ============================================
1377
+ // Public API
1378
+ // ============================================
1379
+ /**
1380
+ * Register a pending Write Concern promise for an operation.
1381
+ * The promise will be resolved when the server sends an ACK with the operation result.
1382
+ *
1383
+ * @param opId - Operation ID
1384
+ * @param timeout - Timeout in ms (default: 5000)
1385
+ * @returns Promise that resolves with the Write Concern result
1386
+ */
1387
+ registerWriteConcernPromise(opId, timeout = 5e3) {
1388
+ const actualTimeout = timeout ?? this.config.defaultTimeout ?? 5e3;
1389
+ return new Promise((resolve, reject) => {
1390
+ const timeoutHandle = setTimeout(() => {
1391
+ this.pendingWriteConcernPromises.delete(opId);
1392
+ reject(new Error(`Write Concern timeout for operation ${opId}`));
1393
+ }, actualTimeout);
1394
+ this.pendingWriteConcernPromises.set(opId, {
1395
+ resolve,
1396
+ reject,
1397
+ timeoutHandle
1398
+ });
1399
+ });
1400
+ }
1401
+ /**
1402
+ * Resolve a pending Write Concern promise with the server result.
1403
+ *
1404
+ * @param opId - Operation ID
1405
+ * @param result - Result from server ACK
1406
+ */
1407
+ resolveWriteConcernPromise(opId, result) {
1408
+ const pending = this.pendingWriteConcernPromises.get(opId);
1409
+ if (pending) {
1410
+ if (pending.timeoutHandle) {
1411
+ clearTimeout(pending.timeoutHandle);
1412
+ }
1413
+ pending.resolve(result);
1414
+ this.pendingWriteConcernPromises.delete(opId);
1415
+ }
1416
+ }
1417
+ /**
1418
+ * Cancel all pending Write Concern promises (e.g., on disconnect).
1419
+ */
1420
+ cancelAllWriteConcernPromises(error) {
1421
+ for (const [opId, pending] of this.pendingWriteConcernPromises.entries()) {
1422
+ if (pending.timeoutHandle) {
1423
+ clearTimeout(pending.timeoutHandle);
1424
+ }
1425
+ pending.reject(error);
1426
+ }
1427
+ this.pendingWriteConcernPromises.clear();
1428
+ }
1429
+ };
1430
+
1431
+ // src/sync/CounterManager.ts
1432
+ var CounterManager = class {
1433
+ constructor(config) {
1434
+ // Counter update listeners by name
1435
+ this.counterUpdateListeners = /* @__PURE__ */ new Map();
1436
+ this.config = config;
1437
+ }
1438
+ // ============================================
1439
+ // Public API
1440
+ // ============================================
1441
+ /**
1442
+ * Subscribe to counter updates from server.
1443
+ * @param name Counter name
1444
+ * @param listener Callback when counter state is updated
1445
+ * @returns Unsubscribe function
1446
+ */
1447
+ onCounterUpdate(name, listener) {
1448
+ if (!this.counterUpdateListeners.has(name)) {
1449
+ this.counterUpdateListeners.set(name, /* @__PURE__ */ new Set());
1450
+ }
1451
+ this.counterUpdateListeners.get(name).add(listener);
1452
+ return () => {
1453
+ this.counterUpdateListeners.get(name)?.delete(listener);
1454
+ if (this.counterUpdateListeners.get(name)?.size === 0) {
1455
+ this.counterUpdateListeners.delete(name);
1456
+ }
1457
+ };
1458
+ }
1459
+ /**
1460
+ * Request initial counter state from server.
1461
+ * @param name Counter name
1462
+ */
1463
+ requestCounter(name) {
1464
+ if (this.config.isAuthenticated()) {
1465
+ this.config.sendMessage({
1466
+ type: "COUNTER_REQUEST",
1467
+ payload: { name }
1468
+ });
1469
+ }
1470
+ }
1471
+ /**
1472
+ * Sync local counter state to server.
1473
+ * @param name Counter name
1474
+ * @param state Counter state to sync
1475
+ */
1476
+ syncCounter(name, state) {
1477
+ if (this.config.isAuthenticated()) {
1478
+ const stateObj = {
1479
+ positive: Object.fromEntries(state.positive),
1480
+ negative: Object.fromEntries(state.negative)
1481
+ };
1482
+ this.config.sendMessage({
1483
+ type: "COUNTER_SYNC",
1484
+ payload: {
1485
+ name,
1486
+ state: stateObj
1487
+ }
1488
+ });
1489
+ }
1490
+ }
1491
+ /**
1492
+ * Handle incoming counter update from server.
1493
+ * Called by SyncEngine for COUNTER_UPDATE and COUNTER_RESPONSE messages.
1494
+ */
1495
+ handleCounterUpdate(name, stateObj) {
1496
+ const state = {
1497
+ positive: new Map(Object.entries(stateObj.positive)),
1498
+ negative: new Map(Object.entries(stateObj.negative))
1499
+ };
1500
+ const listeners = this.counterUpdateListeners.get(name);
1501
+ if (listeners) {
1502
+ for (const listener of listeners) {
1503
+ try {
1504
+ listener(state);
1505
+ } catch (e) {
1506
+ logger.error({ err: e, counterName: name }, "Counter update listener error");
1507
+ }
1508
+ }
1509
+ }
1510
+ }
1511
+ /**
1512
+ * Clean up resources.
1513
+ * Clears all counter update listeners.
1514
+ */
1515
+ close() {
1516
+ this.counterUpdateListeners.clear();
1517
+ }
1518
+ };
1519
+
1520
+ // src/sync/EntryProcessorClient.ts
1521
+ var DEFAULT_PROCESSOR_TIMEOUT = 3e4;
1522
+ var EntryProcessorClient = class {
1523
+ constructor(config) {
1524
+ // Pending entry processor requests by requestId
1525
+ this.pendingProcessorRequests = /* @__PURE__ */ new Map();
1526
+ // Pending batch entry processor requests by requestId
1527
+ this.pendingBatchProcessorRequests = /* @__PURE__ */ new Map();
1528
+ this.config = config;
1529
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_PROCESSOR_TIMEOUT;
1530
+ }
1531
+ // ============================================
1532
+ // Public API
1533
+ // ============================================
1534
+ /**
1535
+ * Execute an entry processor on a single key atomically.
1536
+ *
1537
+ * @param mapName Name of the map
1538
+ * @param key Key to process
1539
+ * @param processor Processor definition
1540
+ * @returns Promise resolving to the processor result
1541
+ */
1542
+ async executeOnKey(mapName, key, processor) {
1543
+ if (!this.config.isAuthenticated()) {
1544
+ return {
1545
+ success: false,
1546
+ error: "Not connected to server"
1547
+ };
1548
+ }
1549
+ const requestId = crypto.randomUUID();
1550
+ return new Promise((resolve, reject) => {
1551
+ const timeout = setTimeout(() => {
1552
+ this.pendingProcessorRequests.delete(requestId);
1553
+ reject(new Error("Entry processor request timed out"));
1554
+ }, this.timeoutMs);
1555
+ this.pendingProcessorRequests.set(requestId, {
1556
+ resolve: (result) => {
1557
+ clearTimeout(timeout);
1558
+ resolve(result);
1559
+ },
1560
+ reject,
1561
+ timeout
1562
+ });
1563
+ const sent = this.config.sendMessage({
1564
+ type: "ENTRY_PROCESS",
1565
+ requestId,
1566
+ mapName,
1567
+ key,
1568
+ processor: {
1569
+ name: processor.name,
1570
+ code: processor.code,
1571
+ args: processor.args
1572
+ }
1573
+ }, key);
1574
+ if (!sent) {
1575
+ this.pendingProcessorRequests.delete(requestId);
1576
+ clearTimeout(timeout);
1577
+ reject(new Error("Failed to send entry processor request"));
1578
+ }
1579
+ });
1580
+ }
1581
+ /**
1582
+ * Execute an entry processor on multiple keys.
1583
+ *
1584
+ * @param mapName Name of the map
1585
+ * @param keys Keys to process
1586
+ * @param processor Processor definition
1587
+ * @returns Promise resolving to a map of key -> result
1588
+ */
1589
+ async executeOnKeys(mapName, keys, processor) {
1590
+ if (!this.config.isAuthenticated()) {
1591
+ const results = /* @__PURE__ */ new Map();
1592
+ const error = {
1593
+ success: false,
1594
+ error: "Not connected to server"
1595
+ };
1596
+ for (const key of keys) {
1597
+ results.set(key, error);
1598
+ }
1599
+ return results;
1600
+ }
1601
+ const requestId = crypto.randomUUID();
1602
+ return new Promise((resolve, reject) => {
1603
+ const timeout = setTimeout(() => {
1604
+ this.pendingBatchProcessorRequests.delete(requestId);
1605
+ reject(new Error("Entry processor batch request timed out"));
1606
+ }, this.timeoutMs);
1607
+ this.pendingBatchProcessorRequests.set(requestId, {
1608
+ resolve: (results) => {
1609
+ clearTimeout(timeout);
1610
+ resolve(results);
1611
+ },
1612
+ reject,
1613
+ timeout
1614
+ });
1615
+ const sent = this.config.sendMessage({
1616
+ type: "ENTRY_PROCESS_BATCH",
1617
+ requestId,
1618
+ mapName,
1619
+ keys,
1620
+ processor: {
1621
+ name: processor.name,
1622
+ code: processor.code,
1623
+ args: processor.args
1624
+ }
1625
+ });
1626
+ if (!sent) {
1627
+ this.pendingBatchProcessorRequests.delete(requestId);
1628
+ clearTimeout(timeout);
1629
+ reject(new Error("Failed to send entry processor batch request"));
1630
+ }
1631
+ });
1632
+ }
1633
+ /**
1634
+ * Handle entry processor response from server.
1635
+ * Called by SyncEngine for ENTRY_PROCESS_RESPONSE messages.
1636
+ */
1637
+ handleEntryProcessResponse(message) {
1638
+ const pending = this.pendingProcessorRequests.get(message.requestId);
1639
+ if (pending) {
1640
+ this.pendingProcessorRequests.delete(message.requestId);
1641
+ pending.resolve({
1642
+ success: message.success,
1643
+ result: message.result,
1644
+ newValue: message.newValue,
1645
+ error: message.error
1646
+ });
1647
+ }
1648
+ }
1649
+ /**
1650
+ * Handle entry processor batch response from server.
1651
+ * Called by SyncEngine for ENTRY_PROCESS_BATCH_RESPONSE messages.
1652
+ */
1653
+ handleEntryProcessBatchResponse(message) {
1654
+ const pending = this.pendingBatchProcessorRequests.get(message.requestId);
1655
+ if (pending) {
1656
+ this.pendingBatchProcessorRequests.delete(message.requestId);
1657
+ const resultsMap = /* @__PURE__ */ new Map();
1658
+ for (const [key, result] of Object.entries(message.results)) {
1659
+ resultsMap.set(key, {
1660
+ success: result.success,
1661
+ result: result.result,
1662
+ newValue: result.newValue,
1663
+ error: result.error
1664
+ });
1665
+ }
1666
+ pending.resolve(resultsMap);
1667
+ }
1668
+ }
1669
+ /**
1670
+ * Clean up resources.
1671
+ * Clears pending timeouts without rejecting promises to match original SyncEngine behavior.
1672
+ * Note: This may leave promises hanging, but maintains backward compatibility with tests.
1673
+ */
1674
+ close(error) {
1675
+ for (const [requestId, pending] of this.pendingProcessorRequests.entries()) {
1676
+ clearTimeout(pending.timeout);
1677
+ }
1678
+ this.pendingProcessorRequests.clear();
1679
+ for (const [requestId, pending] of this.pendingBatchProcessorRequests.entries()) {
1680
+ clearTimeout(pending.timeout);
1681
+ }
1682
+ this.pendingBatchProcessorRequests.clear();
1683
+ }
1684
+ };
1685
+
1686
+ // src/sync/SearchClient.ts
1687
+ var DEFAULT_SEARCH_TIMEOUT = 3e4;
1688
+ var SearchClient = class {
1689
+ constructor(config) {
1690
+ // Pending search requests by requestId
1691
+ this.pendingSearchRequests = /* @__PURE__ */ new Map();
1692
+ this.config = config;
1693
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT;
1694
+ }
1695
+ // ============================================
1696
+ // Public API
1697
+ // ============================================
1698
+ /**
1699
+ * Perform a one-shot BM25 search on the server.
1700
+ *
1701
+ * @param mapName Name of the map to search
1702
+ * @param query Search query text
1703
+ * @param options Search options (limit, minScore, boost)
1704
+ * @returns Promise resolving to search results
1705
+ */
1706
+ async search(mapName, query, options) {
1707
+ if (!this.config.isAuthenticated()) {
1708
+ throw new Error("Not connected to server");
1709
+ }
1710
+ const requestId = crypto.randomUUID();
1711
+ return new Promise((resolve, reject) => {
1712
+ const timeout = setTimeout(() => {
1713
+ this.pendingSearchRequests.delete(requestId);
1714
+ reject(new Error("Search request timed out"));
1715
+ }, this.timeoutMs);
1716
+ this.pendingSearchRequests.set(requestId, {
1717
+ resolve: (results) => {
1718
+ clearTimeout(timeout);
1719
+ resolve(results);
1720
+ },
1721
+ reject: (error) => {
1722
+ clearTimeout(timeout);
1723
+ reject(error);
1724
+ },
1725
+ timeout
1726
+ });
1727
+ const sent = this.config.sendMessage({
1728
+ type: "SEARCH",
1729
+ payload: {
1730
+ requestId,
1731
+ mapName,
1732
+ query,
1733
+ options
1734
+ }
1735
+ });
1736
+ if (!sent) {
1737
+ this.pendingSearchRequests.delete(requestId);
1738
+ clearTimeout(timeout);
1739
+ reject(new Error("Failed to send search request"));
1740
+ }
1741
+ });
1742
+ }
1743
+ /**
1744
+ * Handle search response from server.
1745
+ * Called by SyncEngine for SEARCH_RESP messages.
1746
+ */
1747
+ handleSearchResponse(payload) {
1748
+ const pending = this.pendingSearchRequests.get(payload.requestId);
1749
+ if (pending) {
1750
+ this.pendingSearchRequests.delete(payload.requestId);
1751
+ if (payload.error) {
1752
+ pending.reject(new Error(payload.error));
1753
+ } else {
1754
+ pending.resolve(payload.results);
1755
+ }
1756
+ }
1757
+ }
1758
+ /**
1759
+ * Clean up resources.
1760
+ * Clears pending timeouts without rejecting promises to match original SyncEngine behavior.
1761
+ * Note: This may leave promises hanging, but maintains backward compatibility with tests.
1762
+ */
1763
+ close(error) {
1764
+ for (const [requestId, pending] of this.pendingSearchRequests.entries()) {
1765
+ clearTimeout(pending.timeout);
1766
+ }
1767
+ this.pendingSearchRequests.clear();
1768
+ }
1769
+ };
1770
+
1771
+ // src/sync/MerkleSyncHandler.ts
1772
+ import { LWWMap } from "@topgunbuild/core";
1773
+ var MerkleSyncHandler = class {
1774
+ constructor(config) {
1775
+ this.lastSyncTimestamp = 0;
1776
+ this.config = config;
1777
+ }
1778
+ /**
1779
+ * Handle SYNC_RESET_REQUIRED message from server.
1780
+ * Resets the map and triggers a fresh sync.
1781
+ */
1782
+ async handleSyncResetRequired(payload) {
1783
+ const { mapName } = payload;
1784
+ logger.warn({ mapName }, "Sync Reset Required due to GC Age");
1785
+ await this.config.resetMap(mapName);
1786
+ this.config.sendMessage({
1787
+ type: "SYNC_INIT",
1788
+ mapName,
1789
+ lastSyncTimestamp: 0
1790
+ });
1791
+ }
1792
+ /**
1793
+ * Handle SYNC_RESP_ROOT message from server.
1794
+ * Compares root hashes and requests buckets if mismatch detected.
1795
+ */
1796
+ async handleSyncRespRoot(payload) {
1797
+ const { mapName, rootHash, timestamp } = payload;
1798
+ const map = this.config.getMap(mapName);
1799
+ if (map instanceof LWWMap) {
1800
+ const localRootHash = map.getMerkleTree().getRootHash();
1801
+ if (localRootHash !== rootHash) {
1802
+ logger.info({ mapName, localRootHash, remoteRootHash: rootHash }, "Root hash mismatch, requesting buckets");
1803
+ this.config.sendMessage({
1804
+ type: "MERKLE_REQ_BUCKET",
1805
+ payload: { mapName, path: "" }
1806
+ });
1807
+ } else {
1808
+ logger.info({ mapName }, "Map is in sync");
1809
+ }
1810
+ }
1811
+ if (timestamp) {
1812
+ await this.config.onTimestampUpdate(timestamp);
1813
+ }
1814
+ }
1815
+ /**
1816
+ * Handle SYNC_RESP_BUCKETS message from server.
1817
+ * Compares bucket hashes and requests mismatched buckets.
1818
+ */
1819
+ handleSyncRespBuckets(payload) {
1820
+ const { mapName, path, buckets } = payload;
1821
+ const map = this.config.getMap(mapName);
1822
+ if (map instanceof LWWMap) {
1823
+ const tree = map.getMerkleTree();
1824
+ const localBuckets = tree.getBuckets(path);
1825
+ for (const [bucketKey, remoteHash] of Object.entries(buckets)) {
1826
+ const localHash = localBuckets[bucketKey] || 0;
1827
+ if (localHash !== remoteHash) {
1828
+ const newPath = path + bucketKey;
1829
+ this.config.sendMessage({
1830
+ type: "MERKLE_REQ_BUCKET",
1831
+ payload: { mapName, path: newPath }
1832
+ });
1833
+ }
1834
+ }
1835
+ }
1836
+ }
1837
+ /**
1838
+ * Handle SYNC_RESP_LEAF message from server.
1839
+ * Merges leaf records into local map and persists to storage.
1840
+ */
1841
+ async handleSyncRespLeaf(payload) {
1842
+ const { mapName, records } = payload;
1843
+ const map = this.config.getMap(mapName);
1844
+ if (map instanceof LWWMap) {
1845
+ let updateCount = 0;
1846
+ for (const { key, record } of records) {
1847
+ const updated = map.merge(key, record);
1848
+ if (updated) {
1849
+ updateCount++;
1850
+ await this.config.storageAdapter.put(`${mapName}:${key}`, record);
1851
+ }
1852
+ }
1853
+ if (updateCount > 0) {
1854
+ logger.info({ mapName, count: updateCount }, "Synced records from server");
1855
+ }
1856
+ }
1857
+ }
1858
+ /**
1859
+ * Send SYNC_INIT message to server to start sync.
1860
+ * Encapsulates sync init message construction.
1861
+ */
1862
+ sendSyncInit(mapName, lastSyncTimestamp) {
1863
+ this.lastSyncTimestamp = lastSyncTimestamp;
1864
+ logger.info({ mapName }, "Starting Merkle sync for LWWMap");
1865
+ this.config.sendMessage({
1866
+ type: "SYNC_INIT",
1867
+ mapName,
1868
+ lastSyncTimestamp
1869
+ });
1870
+ }
1871
+ /**
1872
+ * Get the last sync timestamp for debugging/testing.
1873
+ */
1874
+ getLastSyncTimestamp() {
1875
+ return this.lastSyncTimestamp;
1876
+ }
1877
+ };
1878
+
1879
+ // src/sync/ORMapSyncHandler.ts
1880
+ import { ORMap } from "@topgunbuild/core";
1881
+ var ORMapSyncHandler = class {
1882
+ constructor(config) {
1883
+ this.lastSyncTimestamp = 0;
1884
+ this.config = config;
1885
+ }
1886
+ /**
1887
+ * Handle ORMAP_SYNC_RESP_ROOT message from server.
1888
+ * Compares root hashes and requests buckets if mismatch detected.
1889
+ */
1890
+ async handleORMapSyncRespRoot(payload) {
1891
+ const { mapName, rootHash, timestamp } = payload;
1892
+ const map = this.config.getMap(mapName);
1893
+ if (map instanceof ORMap) {
1894
+ const localTree = map.getMerkleTree();
1895
+ const localRootHash = localTree.getRootHash();
1896
+ if (localRootHash !== rootHash) {
1897
+ logger.info({ mapName, localRootHash, remoteRootHash: rootHash }, "ORMap root hash mismatch, requesting buckets");
1898
+ this.config.sendMessage({
1899
+ type: "ORMAP_MERKLE_REQ_BUCKET",
1900
+ payload: { mapName, path: "" }
1901
+ });
1902
+ } else {
1903
+ logger.info({ mapName }, "ORMap is in sync");
1904
+ }
1905
+ }
1906
+ if (timestamp) {
1907
+ await this.config.onTimestampUpdate(timestamp);
1908
+ }
1909
+ }
1910
+ /**
1911
+ * Handle ORMAP_SYNC_RESP_BUCKETS message from server.
1912
+ * Compares bucket hashes and requests mismatched buckets.
1913
+ * Also pushes local data that server doesn't have.
1914
+ */
1915
+ async handleORMapSyncRespBuckets(payload) {
1916
+ const { mapName, path, buckets } = payload;
1917
+ const map = this.config.getMap(mapName);
1918
+ if (map instanceof ORMap) {
1919
+ const tree = map.getMerkleTree();
1920
+ const localBuckets = tree.getBuckets(path);
1921
+ for (const [bucketKey, remoteHash] of Object.entries(buckets)) {
1922
+ const localHash = localBuckets[bucketKey] || 0;
1923
+ if (localHash !== remoteHash) {
1924
+ const newPath = path + bucketKey;
1925
+ this.config.sendMessage({
1926
+ type: "ORMAP_MERKLE_REQ_BUCKET",
1927
+ payload: { mapName, path: newPath }
1928
+ });
1929
+ }
1930
+ }
1931
+ for (const [bucketKey, localHash] of Object.entries(localBuckets)) {
1932
+ if (!(bucketKey in buckets) && localHash !== 0) {
1933
+ const newPath = path + bucketKey;
1934
+ const keys = tree.getKeysInBucket(newPath);
1935
+ if (keys.length > 0) {
1936
+ await this.pushORMapDiff(mapName, keys, map);
1937
+ }
1938
+ }
1939
+ }
1940
+ }
1941
+ }
1942
+ /**
1943
+ * Handle ORMAP_SYNC_RESP_LEAF message from server.
1944
+ * Merges leaf entries into local map and pushes local diff back.
1945
+ */
1946
+ async handleORMapSyncRespLeaf(payload) {
1947
+ const { mapName, entries } = payload;
1948
+ const map = this.config.getMap(mapName);
1949
+ if (map instanceof ORMap) {
1950
+ let totalAdded = 0;
1951
+ let totalUpdated = 0;
1952
+ for (const entry of entries) {
1953
+ const { key, records, tombstones } = entry;
1954
+ const result = map.mergeKey(key, records, tombstones);
1955
+ totalAdded += result.added;
1956
+ totalUpdated += result.updated;
1957
+ }
1958
+ if (totalAdded > 0 || totalUpdated > 0) {
1959
+ logger.info({ mapName, added: totalAdded, updated: totalUpdated }, "Synced ORMap records from server");
1960
+ }
1961
+ const keysToCheck = entries.map((e) => e.key);
1962
+ await this.pushORMapDiff(mapName, keysToCheck, map);
1963
+ }
1964
+ }
1965
+ /**
1966
+ * Handle ORMAP_DIFF_RESPONSE message from server.
1967
+ * Merges diff entries into local map.
1968
+ */
1969
+ async handleORMapDiffResponse(payload) {
1970
+ const { mapName, entries } = payload;
1971
+ const map = this.config.getMap(mapName);
1972
+ if (map instanceof ORMap) {
1973
+ let totalAdded = 0;
1974
+ let totalUpdated = 0;
1975
+ for (const entry of entries) {
1976
+ const { key, records, tombstones } = entry;
1977
+ const result = map.mergeKey(key, records, tombstones);
1978
+ totalAdded += result.added;
1979
+ totalUpdated += result.updated;
1980
+ }
1981
+ if (totalAdded > 0 || totalUpdated > 0) {
1982
+ logger.info({ mapName, added: totalAdded, updated: totalUpdated }, "Merged ORMap diff from server");
1983
+ }
1984
+ }
1985
+ }
1986
+ /**
1987
+ * Push local ORMap diff to server for the given keys.
1988
+ * Sends local records and tombstones that the server might not have.
1989
+ */
1990
+ async pushORMapDiff(mapName, keys, map) {
1991
+ const entries = [];
1992
+ const snapshot = map.getSnapshot();
1993
+ for (const key of keys) {
1994
+ const recordsMap = map.getRecordsMap(key);
1995
+ if (recordsMap && recordsMap.size > 0) {
1996
+ const records = Array.from(recordsMap.values());
1997
+ const tombstones = [];
1998
+ for (const tag of snapshot.tombstones) {
1999
+ tombstones.push(tag);
2000
+ }
2001
+ entries.push({
2002
+ key,
2003
+ records,
2004
+ tombstones
2005
+ });
2006
+ }
2007
+ }
2008
+ if (entries.length > 0) {
2009
+ this.config.sendMessage({
2010
+ type: "ORMAP_PUSH_DIFF",
2011
+ payload: {
2012
+ mapName,
2013
+ entries
2014
+ }
2015
+ });
2016
+ logger.debug({ mapName, keyCount: entries.length }, "Pushed ORMap diff to server");
2017
+ }
2018
+ }
2019
+ /**
2020
+ * Send ORMAP_SYNC_INIT message to server to start sync.
2021
+ * Encapsulates sync init message construction.
2022
+ */
2023
+ sendSyncInit(mapName, lastSyncTimestamp) {
2024
+ this.lastSyncTimestamp = lastSyncTimestamp;
2025
+ const map = this.config.getMap(mapName);
2026
+ if (map instanceof ORMap) {
2027
+ logger.info({ mapName }, "Starting Merkle sync for ORMap");
2028
+ const tree = map.getMerkleTree();
2029
+ const rootHash = tree.getRootHash();
2030
+ const bucketHashes = tree.getBuckets("");
2031
+ this.config.sendMessage({
2032
+ type: "ORMAP_SYNC_INIT",
2033
+ mapName,
2034
+ rootHash,
2035
+ bucketHashes,
2036
+ lastSyncTimestamp
2037
+ });
2038
+ }
2039
+ }
2040
+ /**
2041
+ * Get the last sync timestamp for debugging/testing.
2042
+ */
2043
+ getLastSyncTimestamp() {
2044
+ return this.lastSyncTimestamp;
2045
+ }
2046
+ };
2047
+
2048
+ // src/sync/MessageRouter.ts
2049
+ var MessageRouter = class {
2050
+ constructor(config = {}) {
2051
+ this.handlers = config.handlers ? new Map(config.handlers) : /* @__PURE__ */ new Map();
2052
+ this.onUnhandled = config.onUnhandled;
2053
+ }
2054
+ /**
2055
+ * Register a handler for a message type.
2056
+ * @param type - Message type to handle
2057
+ * @param handler - Handler function
2058
+ */
2059
+ registerHandler(type, handler2) {
2060
+ if (this.handlers.has(type)) {
2061
+ logger.warn({ type }, "Overwriting existing handler for message type");
2062
+ }
2063
+ this.handlers.set(type, handler2);
2064
+ }
2065
+ /**
2066
+ * Register multiple handlers at once.
2067
+ * @param handlers - Record of type -> handler
2068
+ */
2069
+ registerHandlers(handlers) {
2070
+ for (const [type, handler2] of Object.entries(handlers)) {
2071
+ this.registerHandler(type, handler2);
2072
+ }
2073
+ }
2074
+ /**
2075
+ * Route a message to its registered handler.
2076
+ * Returns true if handled, false if no handler found.
2077
+ * @param message - Message to route
2078
+ * @returns Promise resolving to true if handled
2079
+ */
2080
+ async route(message) {
2081
+ const type = message?.type;
2082
+ if (!type) {
2083
+ logger.warn({ message }, "Cannot route message without type");
2084
+ return false;
2085
+ }
2086
+ const handler2 = this.handlers.get(type);
2087
+ if (!handler2) {
2088
+ if (this.onUnhandled) {
2089
+ this.onUnhandled(message);
2090
+ }
2091
+ return false;
2092
+ }
2093
+ try {
2094
+ await handler2(message);
2095
+ return true;
2096
+ } catch (error) {
2097
+ logger.error({ type, error }, "Error in message handler");
2098
+ return true;
2099
+ }
2100
+ }
2101
+ /**
2102
+ * Check if a handler is registered for a message type.
2103
+ * @param type - Message type to check
2104
+ * @returns true if handler exists
2105
+ */
2106
+ hasHandler(type) {
2107
+ return this.handlers.has(type);
2108
+ }
2109
+ /**
2110
+ * Get the count of registered handlers.
2111
+ * Useful for debugging/testing.
2112
+ */
2113
+ get handlerCount() {
2114
+ return this.handlers.size;
2115
+ }
2116
+ /**
2117
+ * Get all registered message types.
2118
+ * Useful for debugging/testing.
2119
+ */
2120
+ getRegisteredTypes() {
2121
+ return Array.from(this.handlers.keys());
2122
+ }
2123
+ };
2124
+
2125
+ // src/sync/ClientMessageHandlers.ts
2126
+ function registerClientMessageHandlers(router, delegates, managers) {
2127
+ router.registerHandlers({
2128
+ // AUTH handlers
2129
+ "AUTH_REQUIRED": () => delegates.sendAuth(),
2130
+ "AUTH_ACK": () => delegates.handleAuthAck(),
2131
+ "AUTH_FAIL": (msg) => delegates.handleAuthFail(msg),
2132
+ // HEARTBEAT - handled by WebSocketManager, no-op here
2133
+ "PONG": () => {
2134
+ },
2135
+ // SYNC handlers
2136
+ "OP_ACK": (msg) => delegates.handleOpAck(msg),
2137
+ "SYNC_RESP_ROOT": (msg) => managers.merkleSyncHandler.handleSyncRespRoot(msg.payload),
2138
+ "SYNC_RESP_BUCKETS": (msg) => managers.merkleSyncHandler.handleSyncRespBuckets(msg.payload),
2139
+ "SYNC_RESP_LEAF": (msg) => managers.merkleSyncHandler.handleSyncRespLeaf(msg.payload),
2140
+ "SYNC_RESET_REQUIRED": (msg) => managers.merkleSyncHandler.handleSyncResetRequired(msg.payload),
2141
+ // ORMAP SYNC handlers
2142
+ "ORMAP_SYNC_RESP_ROOT": (msg) => managers.orMapSyncHandler.handleORMapSyncRespRoot(msg.payload),
2143
+ "ORMAP_SYNC_RESP_BUCKETS": (msg) => managers.orMapSyncHandler.handleORMapSyncRespBuckets(msg.payload),
2144
+ "ORMAP_SYNC_RESP_LEAF": (msg) => managers.orMapSyncHandler.handleORMapSyncRespLeaf(msg.payload),
2145
+ "ORMAP_DIFF_RESPONSE": (msg) => managers.orMapSyncHandler.handleORMapDiffResponse(msg.payload),
2146
+ // QUERY handlers
2147
+ "QUERY_RESP": (msg) => delegates.handleQueryResp(msg),
2148
+ "QUERY_UPDATE": (msg) => delegates.handleQueryUpdate(msg),
2149
+ // EVENT handlers
2150
+ "SERVER_EVENT": (msg) => delegates.handleServerEvent(msg),
2151
+ "SERVER_BATCH_EVENT": (msg) => delegates.handleServerBatchEvent(msg),
2152
+ // TOPIC handlers
2153
+ "TOPIC_MESSAGE": (msg) => {
2154
+ const { topic, data, publisherId, timestamp } = msg.payload;
2155
+ managers.topicManager.handleTopicMessage(topic, data, publisherId, timestamp);
2156
+ },
2157
+ // LOCK handlers
2158
+ "LOCK_GRANTED": (msg) => {
2159
+ const { requestId, fencingToken } = msg.payload;
2160
+ managers.lockManager.handleLockGranted(requestId, fencingToken);
2161
+ },
2162
+ "LOCK_RELEASED": (msg) => {
2163
+ const { requestId, success } = msg.payload;
2164
+ managers.lockManager.handleLockReleased(requestId, success);
2165
+ },
2166
+ // GC handler
2167
+ "GC_PRUNE": (msg) => delegates.handleGcPrune(msg),
2168
+ // COUNTER handlers
2169
+ "COUNTER_UPDATE": (msg) => {
2170
+ const { name, state } = msg.payload;
2171
+ managers.counterManager.handleCounterUpdate(name, state);
2172
+ },
2173
+ "COUNTER_RESPONSE": (msg) => {
2174
+ const { name, state } = msg.payload;
2175
+ managers.counterManager.handleCounterUpdate(name, state);
2176
+ },
2177
+ // PROCESSOR handlers
2178
+ "ENTRY_PROCESS_RESPONSE": (msg) => {
2179
+ managers.entryProcessorClient.handleEntryProcessResponse(msg);
2180
+ },
2181
+ "ENTRY_PROCESS_BATCH_RESPONSE": (msg) => {
2182
+ managers.entryProcessorClient.handleEntryProcessBatchResponse(msg);
2183
+ },
2184
+ // RESOLVER handlers
2185
+ "REGISTER_RESOLVER_RESPONSE": (msg) => {
2186
+ managers.conflictResolverClient.handleRegisterResponse(msg);
2187
+ },
2188
+ "UNREGISTER_RESOLVER_RESPONSE": (msg) => {
2189
+ managers.conflictResolverClient.handleUnregisterResponse(msg);
2190
+ },
2191
+ "LIST_RESOLVERS_RESPONSE": (msg) => {
2192
+ managers.conflictResolverClient.handleListResponse(msg);
2193
+ },
2194
+ "MERGE_REJECTED": (msg) => {
2195
+ managers.conflictResolverClient.handleMergeRejected(msg);
2196
+ },
2197
+ // SEARCH handlers
2198
+ "SEARCH_RESP": (msg) => {
2199
+ managers.searchClient.handleSearchResponse(msg.payload);
2200
+ },
2201
+ "SEARCH_UPDATE": () => {
2202
+ },
2203
+ // HYBRID handlers
2204
+ "HYBRID_QUERY_RESP": (msg) => delegates.handleHybridQueryResponse(msg.payload),
2205
+ "HYBRID_QUERY_DELTA": (msg) => delegates.handleHybridQueryDelta(msg.payload)
2206
+ });
2207
+ }
2208
+
2209
+ // src/SyncEngine.ts
2210
+ var DEFAULT_TOPIC_QUEUE_CONFIG = {
2211
+ maxSize: 100,
2212
+ strategy: "drop-oldest"
2213
+ };
2214
+ var DEFAULT_BACKOFF_CONFIG = {
2215
+ initialDelayMs: 1e3,
2216
+ maxDelayMs: 3e4,
2217
+ multiplier: 2,
2218
+ jitter: true,
2219
+ maxRetries: 10
2220
+ };
2221
+ var SyncEngine = class {
2222
+ constructor(config) {
2223
+ this.opLog = [];
2224
+ this.maps = /* @__PURE__ */ new Map();
2225
+ this.lastSyncTimestamp = 0;
2226
+ this.authToken = null;
2227
+ this.tokenProvider = null;
2228
+ // ============================================
2229
+ // Event Journal Methods
2230
+ // ============================================
2231
+ /** Message listeners for journal and other generic messages */
2232
+ this.messageListeners = /* @__PURE__ */ new Set();
2233
+ if (!config.connectionProvider) {
2234
+ throw new Error("SyncEngine requires connectionProvider");
2235
+ }
2236
+ this.nodeId = config.nodeId;
2237
+ this.storageAdapter = config.storageAdapter;
2238
+ this.hlc = new HLC(this.nodeId);
2239
+ this.stateMachine = new SyncStateMachine();
2240
+ this.heartbeatConfig = {
2241
+ intervalMs: config.heartbeat?.intervalMs ?? 5e3,
2242
+ timeoutMs: config.heartbeat?.timeoutMs ?? 15e3,
2243
+ enabled: config.heartbeat?.enabled ?? true
2244
+ };
2245
+ this.backoffConfig = {
2246
+ ...DEFAULT_BACKOFF_CONFIG,
2247
+ ...config.backoff
2248
+ };
2249
+ this.backpressureConfig = {
2250
+ ...DEFAULT_BACKPRESSURE_CONFIG,
2251
+ ...config.backpressure
2252
+ };
2253
+ this.backpressureController = new BackpressureController({
2254
+ config: this.backpressureConfig,
2255
+ opLog: this.opLog
2256
+ // Pass reference, not copy
2257
+ });
2258
+ const topicQueueConfig = {
2259
+ ...DEFAULT_TOPIC_QUEUE_CONFIG,
2260
+ ...config.topicQueue
2261
+ };
2262
+ this.webSocketManager = new WebSocketManager({
2263
+ connectionProvider: config.connectionProvider,
2264
+ stateMachine: this.stateMachine,
2265
+ backoffConfig: this.backoffConfig,
2266
+ heartbeatConfig: this.heartbeatConfig,
2267
+ onMessage: (msg) => this.handleServerMessage(msg),
2268
+ onConnected: () => this.handleConnectionEstablished(),
2269
+ onDisconnected: () => this.handleConnectionLost(),
2270
+ onReconnected: () => this.handleReconnection()
2271
+ });
2272
+ this.queryManager = new QueryManager({
2273
+ storageAdapter: this.storageAdapter,
2274
+ sendMessage: (msg, key) => this.webSocketManager.sendMessage(msg, key),
2275
+ isAuthenticated: () => this.isAuthenticated()
2276
+ });
2277
+ this.topicManager = new TopicManager({
2278
+ topicQueueConfig,
2279
+ sendMessage: (msg, key) => this.webSocketManager.sendMessage(msg, key),
2280
+ isAuthenticated: () => this.isAuthenticated()
2281
+ });
2282
+ this.lockManager = new LockManager({
2283
+ sendMessage: (msg, key) => this.webSocketManager.sendMessage(msg, key),
2284
+ isAuthenticated: () => this.isAuthenticated(),
2285
+ isOnline: () => this.isOnline()
2286
+ });
2287
+ this.writeConcernManager = new WriteConcernManager({
2288
+ defaultTimeout: 5e3
2289
+ });
2290
+ this.counterManager = new CounterManager({
2291
+ sendMessage: (msg) => this.sendMessage(msg),
2292
+ isAuthenticated: () => this.isAuthenticated()
2293
+ });
2294
+ this.entryProcessorClient = new EntryProcessorClient({
2295
+ sendMessage: (msg, key) => key !== void 0 ? this.sendMessage(msg, key) : this.sendMessage(msg),
2296
+ isAuthenticated: () => this.isAuthenticated()
2297
+ });
2298
+ this.searchClient = new SearchClient({
2299
+ sendMessage: (msg) => this.sendMessage(msg),
2300
+ isAuthenticated: () => this.isAuthenticated()
2301
+ });
2302
+ this.merkleSyncHandler = new MerkleSyncHandler({
2303
+ getMap: (name) => this.maps.get(name),
2304
+ sendMessage: (msg, key) => this.webSocketManager.sendMessage(msg, key),
2305
+ storageAdapter: this.storageAdapter,
2306
+ hlc: this.hlc,
2307
+ onTimestampUpdate: async (ts) => {
2308
+ this.hlc.update(ts);
2309
+ this.lastSyncTimestamp = ts.millis;
2310
+ await this.saveOpLog();
2311
+ },
2312
+ resetMap: (name) => this.resetMap(name)
2313
+ });
2314
+ this.orMapSyncHandler = new ORMapSyncHandler({
2315
+ getMap: (name) => this.maps.get(name),
2316
+ sendMessage: (msg, key) => this.webSocketManager.sendMessage(msg, key),
2317
+ hlc: this.hlc,
2318
+ onTimestampUpdate: async (ts) => {
2319
+ this.hlc.update(ts);
2320
+ this.lastSyncTimestamp = ts.millis;
2321
+ await this.saveOpLog();
2322
+ }
2323
+ });
2324
+ this.conflictResolverClient = new ConflictResolverClient(this);
2325
+ this.messageRouter = new MessageRouter({
2326
+ onUnhandled: (msg) => logger.warn({ type: msg?.type }, "Unhandled message type")
2327
+ });
2328
+ registerClientMessageHandlers(
2329
+ this.messageRouter,
2330
+ {
2331
+ sendAuth: () => this.sendAuth(),
2332
+ handleAuthAck: () => this.handleAuthAck(),
2333
+ handleAuthFail: (msg) => this.handleAuthFail(msg),
2334
+ handleOpAck: (msg) => this.handleOpAck(msg),
2335
+ handleQueryResp: (msg) => this.handleQueryResp(msg),
2336
+ handleQueryUpdate: (msg) => this.handleQueryUpdate(msg),
2337
+ handleServerEvent: (msg) => this.handleServerEvent(msg),
2338
+ handleServerBatchEvent: (msg) => this.handleServerBatchEvent(msg),
2339
+ handleGcPrune: (msg) => this.handleGcPrune(msg),
2340
+ handleHybridQueryResponse: (payload) => this.handleHybridQueryResponse(payload),
2341
+ handleHybridQueryDelta: (payload) => this.handleHybridQueryDelta(payload)
2342
+ },
2343
+ {
2344
+ topicManager: this.topicManager,
2345
+ lockManager: this.lockManager,
2346
+ counterManager: this.counterManager,
2347
+ entryProcessorClient: this.entryProcessorClient,
2348
+ conflictResolverClient: this.conflictResolverClient,
2349
+ searchClient: this.searchClient,
2350
+ merkleSyncHandler: this.merkleSyncHandler,
2351
+ orMapSyncHandler: this.orMapSyncHandler
2352
+ }
2353
+ );
2354
+ this.webSocketManager.connect();
2355
+ this.loadOpLog();
2356
+ }
2357
+ // ============================================
2358
+ // Connection Callbacks (from WebSocketManager)
2359
+ // ============================================
2360
+ /**
2361
+ * Called when connection is established (initial or reconnect).
2362
+ */
2363
+ handleConnectionEstablished() {
2364
+ if (this.authToken || this.tokenProvider) {
2365
+ logger.info("Connection established. Sending auth...");
2366
+ this.stateMachine.transition("AUTHENTICATING" /* AUTHENTICATING */);
2367
+ this.sendAuth();
2368
+ } else {
2369
+ logger.info("Connection established. Waiting for auth token...");
2370
+ this.stateMachine.transition("AUTHENTICATING" /* AUTHENTICATING */);
2371
+ }
2372
+ }
2373
+ /**
2374
+ * Called when connection is lost.
2375
+ */
2376
+ handleConnectionLost() {
2377
+ }
2378
+ /**
2379
+ * Called when reconnection succeeds.
2380
+ */
2381
+ handleReconnection() {
2382
+ if (this.authToken || this.tokenProvider) {
2383
+ this.stateMachine.transition("AUTHENTICATING" /* AUTHENTICATING */);
2384
+ this.sendAuth();
2385
+ }
2386
+ }
2387
+ // ============================================
2388
+ // State Machine Public API
2389
+ // ============================================
2390
+ /**
2391
+ * Get the current connection state
2392
+ */
2393
+ getConnectionState() {
2394
+ return this.stateMachine.getState();
2395
+ }
2396
+ /**
2397
+ * Subscribe to connection state changes
2398
+ * @returns Unsubscribe function
2399
+ */
2400
+ onConnectionStateChange(listener) {
2401
+ return this.stateMachine.onStateChange(listener);
2402
+ }
2403
+ /**
2404
+ * Get state machine history for debugging
2405
+ */
2406
+ getStateHistory(limit) {
2407
+ return this.stateMachine.getHistory(limit);
2408
+ }
2409
+ // ============================================
2410
+ // Internal State Helpers (replace boolean flags)
2411
+ // ============================================
2412
+ /**
2413
+ * Check if WebSocket is connected (but may not be authenticated yet)
2414
+ */
2415
+ isOnline() {
2416
+ const state = this.stateMachine.getState();
2417
+ return state === "CONNECTING" /* CONNECTING */ || state === "AUTHENTICATING" /* AUTHENTICATING */ || state === "SYNCING" /* SYNCING */ || state === "CONNECTED" /* CONNECTED */;
2418
+ }
2419
+ /**
2420
+ * Check if fully authenticated and ready for operations
2421
+ */
2422
+ isAuthenticated() {
2423
+ const state = this.stateMachine.getState();
2424
+ return state === "SYNCING" /* SYNCING */ || state === "CONNECTED" /* CONNECTED */;
2425
+ }
2426
+ /**
2427
+ * Check if fully connected and synced
2428
+ */
2429
+ isConnected() {
2430
+ return this.stateMachine.getState() === "CONNECTED" /* CONNECTED */;
2431
+ }
2432
+ // ============================================
2433
+ // Message Sending (delegates to WebSocketManager)
2434
+ // ============================================
2435
+ /**
2436
+ * Send a message through the current connection.
2437
+ * Delegates to WebSocketManager.
2438
+ */
2439
+ sendMessage(message, key) {
2440
+ return this.webSocketManager.sendMessage(message, key);
2441
+ }
2442
+ // ============================================
2443
+ // Op Log Management
2444
+ // ============================================
2445
+ async loadOpLog() {
2446
+ const storedTimestamp = await this.storageAdapter.getMeta("lastSyncTimestamp");
2447
+ if (storedTimestamp) {
2448
+ this.lastSyncTimestamp = storedTimestamp;
2449
+ }
2450
+ const pendingOps = await this.storageAdapter.getPendingOps();
2451
+ this.opLog.length = 0;
2452
+ for (const op of pendingOps) {
2453
+ this.opLog.push({
2454
+ ...op,
2455
+ id: String(op.id),
2456
+ synced: false
2457
+ });
2458
+ }
2459
+ if (this.opLog.length > 0) {
2460
+ logger.info({ count: this.opLog.length }, "Loaded pending operations from local storage");
2461
+ }
2462
+ }
2463
+ async saveOpLog() {
2464
+ await this.storageAdapter.setMeta("lastSyncTimestamp", this.lastSyncTimestamp);
2465
+ }
2466
+ registerMap(mapName, map) {
2467
+ this.maps.set(mapName, map);
2468
+ }
2469
+ async recordOperation(mapName, opType, key, data) {
2470
+ await this.backpressureController.checkBackpressure();
2471
+ const opLogEntry = {
2472
+ mapName,
2473
+ opType,
2474
+ key,
2475
+ record: data.record,
2476
+ orRecord: data.orRecord,
2477
+ orTag: data.orTag,
2478
+ timestamp: data.timestamp,
2479
+ synced: false
2480
+ };
2481
+ const id = await this.storageAdapter.appendOpLog(opLogEntry);
2482
+ opLogEntry.id = String(id);
2483
+ this.opLog.push(opLogEntry);
2484
+ this.backpressureController.checkHighWaterMark();
2485
+ if (this.isAuthenticated()) {
2486
+ this.syncPendingOperations();
2487
+ }
2488
+ return opLogEntry.id;
2489
+ }
2490
+ syncPendingOperations() {
2491
+ const pending = this.opLog.filter((op) => !op.synced);
2492
+ if (pending.length === 0) return;
2493
+ logger.info({ count: pending.length }, "Syncing pending operations");
2494
+ this.sendMessage({
2495
+ type: "OP_BATCH",
2496
+ payload: {
2497
+ ops: pending
2498
+ }
2499
+ });
2500
+ }
2501
+ startMerkleSync() {
2502
+ for (const [mapName, map] of this.maps) {
2503
+ if (map instanceof LWWMap2) {
2504
+ this.merkleSyncHandler.sendSyncInit(mapName, this.lastSyncTimestamp);
2505
+ } else if (map instanceof ORMap2) {
2506
+ this.orMapSyncHandler.sendSyncInit(mapName, this.lastSyncTimestamp);
2507
+ }
2508
+ }
2509
+ }
2510
+ setAuthToken(token) {
2511
+ this.authToken = token;
2512
+ this.tokenProvider = null;
2513
+ const state = this.stateMachine.getState();
2514
+ if (state === "AUTHENTICATING" /* AUTHENTICATING */ || state === "CONNECTING" /* CONNECTING */) {
2515
+ this.sendAuth();
2516
+ } else if (state === "BACKOFF" /* BACKOFF */ || state === "DISCONNECTED" /* DISCONNECTED */) {
2517
+ logger.info("Auth token set during backoff/disconnect. Reconnecting immediately.");
2518
+ this.webSocketManager.clearReconnectTimer();
2519
+ this.webSocketManager.resetBackoff();
2520
+ this.webSocketManager.connect();
2521
+ }
2522
+ }
2523
+ setTokenProvider(provider) {
2524
+ this.tokenProvider = provider;
2525
+ const state = this.stateMachine.getState();
2526
+ if (state === "AUTHENTICATING" /* AUTHENTICATING */) {
2527
+ this.sendAuth();
2528
+ }
1070
2529
  }
1071
2530
  async sendAuth() {
1072
2531
  if (this.tokenProvider) {
@@ -1087,545 +2546,204 @@ var _SyncEngine = class _SyncEngine {
1087
2546
  token
1088
2547
  });
1089
2548
  }
2549
+ /**
2550
+ * Subscribe to a standard query.
2551
+ * Delegates to QueryManager.
2552
+ */
1090
2553
  subscribeToQuery(query) {
1091
- this.queries.set(query.id, query);
1092
- if (this.isAuthenticated()) {
1093
- this.sendQuerySubscription(query);
1094
- }
2554
+ this.queryManager.subscribeToQuery(query);
1095
2555
  }
2556
+ /**
2557
+ * Subscribe to a topic.
2558
+ * Delegates to TopicManager.
2559
+ */
1096
2560
  subscribeToTopic(topic, handle) {
1097
- this.topics.set(topic, handle);
1098
- if (this.isAuthenticated()) {
1099
- this.sendTopicSubscription(topic);
1100
- }
1101
- }
1102
- unsubscribeFromTopic(topic) {
1103
- this.topics.delete(topic);
1104
- if (this.isAuthenticated()) {
1105
- this.sendMessage({
1106
- type: "TOPIC_UNSUB",
1107
- payload: { topic }
1108
- });
1109
- }
1110
- }
1111
- publishTopic(topic, data) {
1112
- if (this.isAuthenticated()) {
1113
- this.sendMessage({
1114
- type: "TOPIC_PUB",
1115
- payload: { topic, data }
1116
- });
1117
- } else {
1118
- logger.warn({ topic }, "Dropped topic publish (offline)");
1119
- }
1120
- }
1121
- sendTopicSubscription(topic) {
1122
- this.sendMessage({
1123
- type: "TOPIC_SUB",
1124
- payload: { topic }
1125
- });
2561
+ this.topicManager.subscribeToTopic(topic, handle);
1126
2562
  }
1127
2563
  /**
1128
- * Executes a query against local storage immediately
2564
+ * Unsubscribe from a topic.
2565
+ * Delegates to TopicManager.
1129
2566
  */
1130
- async runLocalQuery(mapName, filter) {
1131
- const keys = await this.storageAdapter.getAllKeys();
1132
- const mapKeys = keys.filter((k) => k.startsWith(mapName + ":"));
1133
- const results = [];
1134
- for (const fullKey of mapKeys) {
1135
- const record = await this.storageAdapter.get(fullKey);
1136
- if (record && record.value) {
1137
- const actualKey = fullKey.slice(mapName.length + 1);
1138
- let matches = true;
1139
- if (filter.where) {
1140
- for (const [k, v] of Object.entries(filter.where)) {
1141
- if (record.value[k] !== v) {
1142
- matches = false;
1143
- break;
1144
- }
1145
- }
1146
- }
1147
- if (matches && filter.predicate) {
1148
- if (!evaluatePredicate(filter.predicate, record.value)) {
1149
- matches = false;
1150
- }
1151
- }
1152
- if (matches) {
1153
- results.push({ key: actualKey, value: record.value });
1154
- }
1155
- }
1156
- }
1157
- return results;
1158
- }
1159
- unsubscribeFromQuery(queryId) {
1160
- this.queries.delete(queryId);
1161
- if (this.isAuthenticated()) {
1162
- this.sendMessage({
1163
- type: "QUERY_UNSUB",
1164
- payload: { queryId }
1165
- });
1166
- }
1167
- }
1168
- sendQuerySubscription(query) {
1169
- this.sendMessage({
1170
- type: "QUERY_SUB",
1171
- payload: {
1172
- queryId: query.id,
1173
- mapName: query.getMapName(),
1174
- query: query.getFilter()
1175
- }
1176
- });
1177
- }
1178
- requestLock(name, requestId, ttl) {
1179
- if (!this.isAuthenticated()) {
1180
- return Promise.reject(new Error("Not connected or authenticated"));
1181
- }
1182
- return new Promise((resolve, reject) => {
1183
- const timer = setTimeout(() => {
1184
- if (this.pendingLockRequests.has(requestId)) {
1185
- this.pendingLockRequests.delete(requestId);
1186
- reject(new Error("Lock request timed out waiting for server response"));
1187
- }
1188
- }, 3e4);
1189
- this.pendingLockRequests.set(requestId, { resolve, reject, timer });
1190
- try {
1191
- const sent = this.sendMessage({
1192
- type: "LOCK_REQUEST",
1193
- payload: { requestId, name, ttl }
1194
- });
1195
- if (!sent) {
1196
- clearTimeout(timer);
1197
- this.pendingLockRequests.delete(requestId);
1198
- reject(new Error("Failed to send lock request"));
1199
- }
1200
- } catch (e) {
1201
- clearTimeout(timer);
1202
- this.pendingLockRequests.delete(requestId);
1203
- reject(e);
1204
- }
1205
- });
1206
- }
1207
- releaseLock(name, requestId, fencingToken) {
1208
- if (!this.isOnline()) return Promise.resolve(false);
1209
- return new Promise((resolve, reject) => {
1210
- const timer = setTimeout(() => {
1211
- if (this.pendingLockRequests.has(requestId)) {
1212
- this.pendingLockRequests.delete(requestId);
1213
- resolve(false);
1214
- }
1215
- }, 5e3);
1216
- this.pendingLockRequests.set(requestId, { resolve, reject, timer });
1217
- try {
1218
- const sent = this.sendMessage({
1219
- type: "LOCK_RELEASE",
1220
- payload: { requestId, name, fencingToken }
1221
- });
1222
- if (!sent) {
1223
- clearTimeout(timer);
1224
- this.pendingLockRequests.delete(requestId);
1225
- resolve(false);
1226
- }
1227
- } catch (e) {
1228
- clearTimeout(timer);
1229
- this.pendingLockRequests.delete(requestId);
1230
- resolve(false);
1231
- }
1232
- });
2567
+ unsubscribeFromTopic(topic) {
2568
+ this.topicManager.unsubscribeFromTopic(topic);
1233
2569
  }
1234
- async handleServerMessage(message) {
1235
- this.emitMessage(message);
1236
- switch (message.type) {
1237
- case "BATCH": {
1238
- const batchData = message.data;
1239
- const view = new DataView(batchData.buffer, batchData.byteOffset, batchData.byteLength);
1240
- let offset = 0;
1241
- const count = view.getUint32(offset, true);
1242
- offset += 4;
1243
- for (let i = 0; i < count; i++) {
1244
- const msgLen = view.getUint32(offset, true);
1245
- offset += 4;
1246
- const msgData = batchData.slice(offset, offset + msgLen);
1247
- offset += msgLen;
1248
- const innerMsg = deserialize(msgData);
1249
- await this.handleServerMessage(innerMsg);
1250
- }
1251
- break;
1252
- }
1253
- case "AUTH_REQUIRED":
1254
- this.sendAuth();
1255
- break;
1256
- case "AUTH_ACK": {
1257
- logger.info("Authenticated successfully");
1258
- const wasAuthenticated = this.isAuthenticated();
1259
- this.stateMachine.transition("SYNCING" /* SYNCING */);
1260
- this.resetBackoff();
1261
- this.syncPendingOperations();
1262
- if (!wasAuthenticated) {
1263
- this.startHeartbeat();
1264
- this.startMerkleSync();
1265
- for (const query of this.queries.values()) {
1266
- this.sendQuerySubscription(query);
1267
- }
1268
- for (const topic of this.topics.keys()) {
1269
- this.sendTopicSubscription(topic);
1270
- }
1271
- }
1272
- this.stateMachine.transition("CONNECTED" /* CONNECTED */);
1273
- break;
1274
- }
1275
- case "PONG": {
1276
- this.handlePong(message);
1277
- break;
1278
- }
1279
- case "AUTH_FAIL":
1280
- logger.error({ error: message.error }, "Authentication failed");
1281
- this.authToken = null;
1282
- break;
1283
- case "OP_ACK": {
1284
- const { lastId, achievedLevel, results } = message.payload;
1285
- logger.info({ lastId, achievedLevel, hasResults: !!results }, "Received ACK for ops");
1286
- if (results && Array.isArray(results)) {
1287
- for (const result of results) {
1288
- const op = this.opLog.find((o) => o.id === result.opId);
1289
- if (op && !op.synced) {
1290
- op.synced = true;
1291
- logger.debug({ opId: result.opId, achievedLevel: result.achievedLevel, success: result.success }, "Op ACK with Write Concern");
1292
- }
1293
- this.resolveWriteConcernPromise(result.opId, result);
1294
- }
1295
- }
1296
- let maxSyncedId = -1;
1297
- let ackedCount = 0;
1298
- this.opLog.forEach((op) => {
1299
- if (op.id && op.id <= lastId) {
1300
- if (!op.synced) {
1301
- ackedCount++;
1302
- }
1303
- op.synced = true;
1304
- const idNum = parseInt(op.id, 10);
1305
- if (!isNaN(idNum) && idNum > maxSyncedId) {
1306
- maxSyncedId = idNum;
1307
- }
1308
- }
1309
- });
1310
- if (maxSyncedId !== -1) {
1311
- this.storageAdapter.markOpsSynced(maxSyncedId).catch((err) => logger.error({ err }, "Failed to mark ops synced"));
1312
- }
1313
- if (ackedCount > 0) {
1314
- this.checkLowWaterMark();
1315
- }
1316
- break;
1317
- }
1318
- case "LOCK_GRANTED": {
1319
- const { requestId, fencingToken } = message.payload;
1320
- const req = this.pendingLockRequests.get(requestId);
1321
- if (req) {
1322
- clearTimeout(req.timer);
1323
- this.pendingLockRequests.delete(requestId);
1324
- req.resolve({ fencingToken });
1325
- }
1326
- break;
1327
- }
1328
- case "LOCK_RELEASED": {
1329
- const { requestId, success } = message.payload;
1330
- const req = this.pendingLockRequests.get(requestId);
1331
- if (req) {
1332
- clearTimeout(req.timer);
1333
- this.pendingLockRequests.delete(requestId);
1334
- req.resolve(success);
1335
- }
1336
- break;
1337
- }
1338
- case "QUERY_RESP": {
1339
- const { queryId, results, nextCursor, hasMore, cursorStatus } = message.payload;
1340
- const query = this.queries.get(queryId);
1341
- if (query) {
1342
- query.onResult(results, "server");
1343
- query.updatePaginationInfo({ nextCursor, hasMore, cursorStatus });
1344
- }
1345
- break;
1346
- }
1347
- case "QUERY_UPDATE": {
1348
- const { queryId, key, value, type } = message.payload;
1349
- const query = this.queries.get(queryId);
1350
- if (query) {
1351
- query.onUpdate(key, type === "REMOVE" ? null : value);
1352
- }
1353
- break;
1354
- }
1355
- case "SERVER_EVENT": {
1356
- const { mapName, eventType, key, record, orRecord, orTag } = message.payload;
1357
- await this.applyServerEvent(mapName, eventType, key, record, orRecord, orTag);
1358
- break;
1359
- }
1360
- case "SERVER_BATCH_EVENT": {
1361
- const { events } = message.payload;
1362
- for (const event of events) {
1363
- await this.applyServerEvent(
1364
- event.mapName,
1365
- event.eventType,
1366
- event.key,
1367
- event.record,
1368
- event.orRecord,
1369
- event.orTag
1370
- );
1371
- }
1372
- break;
1373
- }
1374
- case "TOPIC_MESSAGE": {
1375
- const { topic, data, publisherId, timestamp } = message.payload;
1376
- const handle = this.topics.get(topic);
1377
- if (handle) {
1378
- handle.onMessage(data, { publisherId, timestamp });
1379
- }
1380
- break;
1381
- }
1382
- case "GC_PRUNE": {
1383
- const { olderThan } = message.payload;
1384
- logger.info({ olderThan: olderThan.millis }, "Received GC_PRUNE request");
1385
- for (const [name, map] of this.maps) {
1386
- if (map instanceof LWWMap) {
1387
- const removedKeys = map.prune(olderThan);
1388
- for (const key of removedKeys) {
1389
- await this.storageAdapter.remove(`${name}:${key}`);
1390
- }
1391
- if (removedKeys.length > 0) {
1392
- logger.info({ mapName: name, count: removedKeys.length }, "Pruned tombstones from LWWMap");
1393
- }
1394
- } else if (map instanceof ORMap) {
1395
- const removedTags = map.prune(olderThan);
1396
- if (removedTags.length > 0) {
1397
- logger.info({ mapName: name, count: removedTags.length }, "Pruned tombstones from ORMap");
1398
- }
1399
- }
1400
- }
1401
- break;
1402
- }
1403
- case "SYNC_RESET_REQUIRED": {
1404
- const { mapName } = message.payload;
1405
- logger.warn({ mapName }, "Sync Reset Required due to GC Age");
1406
- await this.resetMap(mapName);
1407
- this.sendMessage({
1408
- type: "SYNC_INIT",
1409
- mapName,
1410
- lastSyncTimestamp: 0
1411
- });
1412
- break;
1413
- }
1414
- case "SYNC_RESP_ROOT": {
1415
- const { mapName, rootHash, timestamp } = message.payload;
1416
- const map = this.maps.get(mapName);
1417
- if (map instanceof LWWMap) {
1418
- const localRootHash = map.getMerkleTree().getRootHash();
1419
- if (localRootHash !== rootHash) {
1420
- logger.info({ mapName, localRootHash, remoteRootHash: rootHash }, "Root hash mismatch, requesting buckets");
1421
- this.sendMessage({
1422
- type: "MERKLE_REQ_BUCKET",
1423
- payload: { mapName, path: "" }
1424
- });
1425
- } else {
1426
- logger.info({ mapName }, "Map is in sync");
1427
- }
1428
- }
1429
- if (timestamp) {
1430
- this.hlc.update(timestamp);
1431
- this.lastSyncTimestamp = timestamp.millis;
1432
- await this.saveOpLog();
1433
- }
1434
- break;
1435
- }
1436
- case "SYNC_RESP_BUCKETS": {
1437
- const { mapName, path, buckets } = message.payload;
1438
- const map = this.maps.get(mapName);
1439
- if (map instanceof LWWMap) {
1440
- const tree = map.getMerkleTree();
1441
- const localBuckets = tree.getBuckets(path);
1442
- for (const [bucketKey, remoteHash] of Object.entries(buckets)) {
1443
- const localHash = localBuckets[bucketKey] || 0;
1444
- if (localHash !== remoteHash) {
1445
- const newPath = path + bucketKey;
1446
- this.sendMessage({
1447
- type: "MERKLE_REQ_BUCKET",
1448
- payload: { mapName, path: newPath }
1449
- });
1450
- }
1451
- }
1452
- }
1453
- break;
1454
- }
1455
- case "SYNC_RESP_LEAF": {
1456
- const { mapName, records } = message.payload;
1457
- const map = this.maps.get(mapName);
1458
- if (map instanceof LWWMap) {
1459
- let updateCount = 0;
1460
- for (const { key, record } of records) {
1461
- const updated = map.merge(key, record);
1462
- if (updated) {
1463
- updateCount++;
1464
- await this.storageAdapter.put(`${mapName}:${key}`, record);
1465
- }
1466
- }
1467
- if (updateCount > 0) {
1468
- logger.info({ mapName, count: updateCount }, "Synced records from server");
1469
- }
2570
+ /**
2571
+ * Publish a message to a topic.
2572
+ * Delegates to TopicManager.
2573
+ */
2574
+ publishTopic(topic, data) {
2575
+ this.topicManager.publishTopic(topic, data);
2576
+ }
2577
+ /**
2578
+ * Get topic queue status.
2579
+ * Delegates to TopicManager.
2580
+ */
2581
+ getTopicQueueStatus() {
2582
+ return this.topicManager.getTopicQueueStatus();
2583
+ }
2584
+ /**
2585
+ * Executes a query against local storage immediately.
2586
+ * Delegates to QueryManager.
2587
+ */
2588
+ async runLocalQuery(mapName, filter) {
2589
+ return this.queryManager.runLocalQuery(mapName, filter);
2590
+ }
2591
+ /**
2592
+ * Unsubscribe from a query.
2593
+ * Delegates to QueryManager.
2594
+ */
2595
+ unsubscribeFromQuery(queryId) {
2596
+ this.queryManager.unsubscribeFromQuery(queryId);
2597
+ }
2598
+ /**
2599
+ * Request a distributed lock.
2600
+ * Delegates to LockManager.
2601
+ */
2602
+ requestLock(name, requestId, ttl) {
2603
+ return this.lockManager.requestLock(name, requestId, ttl);
2604
+ }
2605
+ /**
2606
+ * Release a distributed lock.
2607
+ * Delegates to LockManager.
2608
+ */
2609
+ releaseLock(name, requestId, fencingToken) {
2610
+ return this.lockManager.releaseLock(name, requestId, fencingToken);
2611
+ }
2612
+ async handleServerMessage(message) {
2613
+ this.emitMessage(message);
2614
+ if (message.type === "BATCH") {
2615
+ await this.handleBatch(message);
2616
+ return;
2617
+ }
2618
+ await this.messageRouter.route(message);
2619
+ if (message.timestamp) {
2620
+ this.hlc.update(message.timestamp);
2621
+ this.lastSyncTimestamp = message.timestamp.millis;
2622
+ await this.saveOpLog();
2623
+ }
2624
+ }
2625
+ // ============================================
2626
+ // Message Handler Helpers (extracted from switch)
2627
+ // ============================================
2628
+ async handleBatch(message) {
2629
+ const batchData = message.data;
2630
+ const view = new DataView(batchData.buffer, batchData.byteOffset, batchData.byteLength);
2631
+ let offset = 0;
2632
+ const count = view.getUint32(offset, true);
2633
+ offset += 4;
2634
+ for (let i = 0; i < count; i++) {
2635
+ const msgLen = view.getUint32(offset, true);
2636
+ offset += 4;
2637
+ const msgData = batchData.slice(offset, offset + msgLen);
2638
+ offset += msgLen;
2639
+ const innerMsg = deserialize2(msgData);
2640
+ await this.handleServerMessage(innerMsg);
2641
+ }
2642
+ }
2643
+ handleAuthAck() {
2644
+ logger.info("Authenticated successfully");
2645
+ const wasAuthenticated = this.isAuthenticated();
2646
+ this.stateMachine.transition("SYNCING" /* SYNCING */);
2647
+ this.webSocketManager.resetBackoff();
2648
+ this.syncPendingOperations();
2649
+ this.topicManager.flushTopicQueue();
2650
+ if (!wasAuthenticated) {
2651
+ this.webSocketManager.startHeartbeat();
2652
+ this.startMerkleSync();
2653
+ this.queryManager.resubscribeAll();
2654
+ this.topicManager.resubscribeAll();
2655
+ }
2656
+ this.stateMachine.transition("CONNECTED" /* CONNECTED */);
2657
+ }
2658
+ handleAuthFail(message) {
2659
+ logger.error({ error: message.error }, "Authentication failed");
2660
+ this.authToken = null;
2661
+ }
2662
+ handleOpAck(message) {
2663
+ const { lastId, achievedLevel, results } = message.payload;
2664
+ logger.info({ lastId, achievedLevel, hasResults: !!results }, "Received ACK for ops");
2665
+ if (results && Array.isArray(results)) {
2666
+ for (const result of results) {
2667
+ const op = this.opLog.find((o) => o.id === result.opId);
2668
+ if (op && !op.synced) {
2669
+ op.synced = true;
2670
+ logger.debug({ opId: result.opId, achievedLevel: result.achievedLevel, success: result.success }, "Op ACK with Write Concern");
1470
2671
  }
1471
- break;
2672
+ this.writeConcernManager.resolveWriteConcernPromise(result.opId, result);
1472
2673
  }
1473
- // ============ ORMap Sync Message Handlers ============
1474
- case "ORMAP_SYNC_RESP_ROOT": {
1475
- const { mapName, rootHash, timestamp } = message.payload;
1476
- const map = this.maps.get(mapName);
1477
- if (map instanceof ORMap) {
1478
- const localTree = map.getMerkleTree();
1479
- const localRootHash = localTree.getRootHash();
1480
- if (localRootHash !== rootHash) {
1481
- logger.info({ mapName, localRootHash, remoteRootHash: rootHash }, "ORMap root hash mismatch, requesting buckets");
1482
- this.sendMessage({
1483
- type: "ORMAP_MERKLE_REQ_BUCKET",
1484
- payload: { mapName, path: "" }
1485
- });
1486
- } else {
1487
- logger.info({ mapName }, "ORMap is in sync");
1488
- }
2674
+ }
2675
+ let maxSyncedId = -1;
2676
+ let ackedCount = 0;
2677
+ this.opLog.forEach((op) => {
2678
+ if (op.id && op.id <= lastId) {
2679
+ if (!op.synced) {
2680
+ ackedCount++;
1489
2681
  }
1490
- if (timestamp) {
1491
- this.hlc.update(timestamp);
1492
- this.lastSyncTimestamp = timestamp.millis;
1493
- await this.saveOpLog();
2682
+ op.synced = true;
2683
+ const idNum = parseInt(op.id, 10);
2684
+ if (!isNaN(idNum) && idNum > maxSyncedId) {
2685
+ maxSyncedId = idNum;
1494
2686
  }
1495
- break;
1496
2687
  }
1497
- case "ORMAP_SYNC_RESP_BUCKETS": {
1498
- const { mapName, path, buckets } = message.payload;
1499
- const map = this.maps.get(mapName);
1500
- if (map instanceof ORMap) {
1501
- const tree = map.getMerkleTree();
1502
- const localBuckets = tree.getBuckets(path);
1503
- for (const [bucketKey, remoteHash] of Object.entries(buckets)) {
1504
- const localHash = localBuckets[bucketKey] || 0;
1505
- if (localHash !== remoteHash) {
1506
- const newPath = path + bucketKey;
1507
- this.sendMessage({
1508
- type: "ORMAP_MERKLE_REQ_BUCKET",
1509
- payload: { mapName, path: newPath }
1510
- });
1511
- }
1512
- }
1513
- for (const [bucketKey, localHash] of Object.entries(localBuckets)) {
1514
- if (!(bucketKey in buckets) && localHash !== 0) {
1515
- const newPath = path + bucketKey;
1516
- const keys = tree.getKeysInBucket(newPath);
1517
- if (keys.length > 0) {
1518
- this.pushORMapDiff(mapName, keys, map);
1519
- }
1520
- }
1521
- }
2688
+ });
2689
+ if (maxSyncedId !== -1) {
2690
+ this.storageAdapter.markOpsSynced(maxSyncedId).catch((err) => logger.error({ err }, "Failed to mark ops synced"));
2691
+ }
2692
+ if (ackedCount > 0) {
2693
+ this.backpressureController.checkLowWaterMark();
2694
+ }
2695
+ }
2696
+ handleQueryResp(message) {
2697
+ const { queryId, results, nextCursor, hasMore, cursorStatus } = message.payload;
2698
+ const query = this.queryManager.getQueries().get(queryId);
2699
+ if (query) {
2700
+ query.onResult(results, "server");
2701
+ query.updatePaginationInfo({ nextCursor, hasMore, cursorStatus });
2702
+ }
2703
+ }
2704
+ handleQueryUpdate(message) {
2705
+ const { queryId, key, value, type } = message.payload;
2706
+ const query = this.queryManager.getQueries().get(queryId);
2707
+ if (query) {
2708
+ query.onUpdate(key, type === "REMOVE" ? null : value);
2709
+ }
2710
+ }
2711
+ async handleServerEvent(message) {
2712
+ const { mapName, eventType, key, record, orRecord, orTag } = message.payload;
2713
+ await this.applyServerEvent(mapName, eventType, key, record, orRecord, orTag);
2714
+ }
2715
+ async handleServerBatchEvent(message) {
2716
+ const { events } = message.payload;
2717
+ for (const event of events) {
2718
+ await this.applyServerEvent(
2719
+ event.mapName,
2720
+ event.eventType,
2721
+ event.key,
2722
+ event.record,
2723
+ event.orRecord,
2724
+ event.orTag
2725
+ );
2726
+ }
2727
+ }
2728
+ async handleGcPrune(message) {
2729
+ const { olderThan } = message.payload;
2730
+ logger.info({ olderThan: olderThan.millis }, "Received GC_PRUNE request");
2731
+ for (const [name, map] of this.maps) {
2732
+ if (map instanceof LWWMap2) {
2733
+ const removedKeys = map.prune(olderThan);
2734
+ for (const key of removedKeys) {
2735
+ await this.storageAdapter.remove(`${name}:${key}`);
1522
2736
  }
1523
- break;
1524
- }
1525
- case "ORMAP_SYNC_RESP_LEAF": {
1526
- const { mapName, entries } = message.payload;
1527
- const map = this.maps.get(mapName);
1528
- if (map instanceof ORMap) {
1529
- let totalAdded = 0;
1530
- let totalUpdated = 0;
1531
- for (const entry of entries) {
1532
- const { key, records, tombstones } = entry;
1533
- const result = map.mergeKey(key, records, tombstones);
1534
- totalAdded += result.added;
1535
- totalUpdated += result.updated;
1536
- }
1537
- if (totalAdded > 0 || totalUpdated > 0) {
1538
- logger.info({ mapName, added: totalAdded, updated: totalUpdated }, "Synced ORMap records from server");
1539
- }
1540
- const keysToCheck = entries.map((e) => e.key);
1541
- await this.pushORMapDiff(mapName, keysToCheck, map);
2737
+ if (removedKeys.length > 0) {
2738
+ logger.info({ mapName: name, count: removedKeys.length }, "Pruned tombstones from LWWMap");
1542
2739
  }
1543
- break;
1544
- }
1545
- case "ORMAP_DIFF_RESPONSE": {
1546
- const { mapName, entries } = message.payload;
1547
- const map = this.maps.get(mapName);
1548
- if (map instanceof ORMap) {
1549
- let totalAdded = 0;
1550
- let totalUpdated = 0;
1551
- for (const entry of entries) {
1552
- const { key, records, tombstones } = entry;
1553
- const result = map.mergeKey(key, records, tombstones);
1554
- totalAdded += result.added;
1555
- totalUpdated += result.updated;
1556
- }
1557
- if (totalAdded > 0 || totalUpdated > 0) {
1558
- logger.info({ mapName, added: totalAdded, updated: totalUpdated }, "Merged ORMap diff from server");
1559
- }
2740
+ } else if (map instanceof ORMap2) {
2741
+ const removedTags = map.prune(olderThan);
2742
+ if (removedTags.length > 0) {
2743
+ logger.info({ mapName: name, count: removedTags.length }, "Pruned tombstones from ORMap");
1560
2744
  }
1561
- break;
1562
- }
1563
- // ============ PN Counter Message Handlers (Phase 5.2) ============
1564
- case "COUNTER_UPDATE": {
1565
- const { name, state } = message.payload;
1566
- logger.debug({ name }, "Received COUNTER_UPDATE");
1567
- this.handleCounterUpdate(name, state);
1568
- break;
1569
- }
1570
- case "COUNTER_RESPONSE": {
1571
- const { name, state } = message.payload;
1572
- logger.debug({ name }, "Received COUNTER_RESPONSE");
1573
- this.handleCounterUpdate(name, state);
1574
- break;
1575
- }
1576
- // ============ Entry Processor Message Handlers (Phase 5.03) ============
1577
- case "ENTRY_PROCESS_RESPONSE": {
1578
- logger.debug({ requestId: message.requestId, success: message.success }, "Received ENTRY_PROCESS_RESPONSE");
1579
- this.handleEntryProcessResponse(message);
1580
- break;
1581
- }
1582
- case "ENTRY_PROCESS_BATCH_RESPONSE": {
1583
- logger.debug({ requestId: message.requestId }, "Received ENTRY_PROCESS_BATCH_RESPONSE");
1584
- this.handleEntryProcessBatchResponse(message);
1585
- break;
1586
- }
1587
- // ============ Conflict Resolver Message Handlers (Phase 5.05) ============
1588
- case "REGISTER_RESOLVER_RESPONSE": {
1589
- logger.debug({ requestId: message.requestId, success: message.success }, "Received REGISTER_RESOLVER_RESPONSE");
1590
- this.conflictResolverClient.handleRegisterResponse(message);
1591
- break;
1592
- }
1593
- case "UNREGISTER_RESOLVER_RESPONSE": {
1594
- logger.debug({ requestId: message.requestId, success: message.success }, "Received UNREGISTER_RESOLVER_RESPONSE");
1595
- this.conflictResolverClient.handleUnregisterResponse(message);
1596
- break;
1597
- }
1598
- case "LIST_RESOLVERS_RESPONSE": {
1599
- logger.debug({ requestId: message.requestId }, "Received LIST_RESOLVERS_RESPONSE");
1600
- this.conflictResolverClient.handleListResponse(message);
1601
- break;
1602
- }
1603
- case "MERGE_REJECTED": {
1604
- logger.debug({ mapName: message.mapName, key: message.key, reason: message.reason }, "Received MERGE_REJECTED");
1605
- this.conflictResolverClient.handleMergeRejected(message);
1606
- break;
1607
- }
1608
- // ============ Full-Text Search Message Handlers (Phase 11.1a) ============
1609
- case "SEARCH_RESP": {
1610
- logger.debug({ requestId: message.payload?.requestId, resultCount: message.payload?.results?.length }, "Received SEARCH_RESP");
1611
- this.handleSearchResponse(message.payload);
1612
- break;
1613
- }
1614
- // ============ Live Search Message Handlers (Phase 11.1b) ============
1615
- case "SEARCH_UPDATE": {
1616
- logger.debug({
1617
- subscriptionId: message.payload?.subscriptionId,
1618
- key: message.payload?.key,
1619
- type: message.payload?.type
1620
- }, "Received SEARCH_UPDATE");
1621
- break;
1622
2745
  }
1623
2746
  }
1624
- if (message.timestamp) {
1625
- this.hlc.update(message.timestamp);
1626
- this.lastSyncTimestamp = message.timestamp.millis;
1627
- await this.saveOpLog();
1628
- }
1629
2747
  }
1630
2748
  getHLC() {
1631
2749
  return this.hlc;
@@ -1637,10 +2755,10 @@ var _SyncEngine = class _SyncEngine {
1637
2755
  async applyServerEvent(mapName, eventType, key, record, orRecord, orTag) {
1638
2756
  const localMap = this.maps.get(mapName);
1639
2757
  if (localMap) {
1640
- if (localMap instanceof LWWMap && record) {
2758
+ if (localMap instanceof LWWMap2 && record) {
1641
2759
  localMap.merge(key, record);
1642
2760
  await this.storageAdapter.put(`${mapName}:${key}`, record);
1643
- } else if (localMap instanceof ORMap) {
2761
+ } else if (localMap instanceof ORMap2) {
1644
2762
  if (eventType === "OR_ADD" && orRecord) {
1645
2763
  localMap.apply(key, orRecord);
1646
2764
  } else if (eventType === "OR_REMOVE" && orTag) {
@@ -1653,21 +2771,11 @@ var _SyncEngine = class _SyncEngine {
1653
2771
  * Closes the WebSocket connection and cleans up resources.
1654
2772
  */
1655
2773
  close() {
1656
- this.stopHeartbeat();
1657
- if (this.reconnectTimer) {
1658
- clearTimeout(this.reconnectTimer);
1659
- this.reconnectTimer = null;
1660
- }
1661
- if (this.useConnectionProvider) {
1662
- this.connectionProvider.close().catch((err) => {
1663
- logger.error({ err }, "Error closing ConnectionProvider");
1664
- });
1665
- } else if (this.websocket) {
1666
- this.websocket.onclose = null;
1667
- this.websocket.close();
1668
- this.websocket = null;
1669
- }
1670
- this.cancelAllWriteConcernPromises(new Error("SyncEngine closed"));
2774
+ this.webSocketManager.close();
2775
+ this.writeConcernManager.cancelAllWriteConcernPromises(new Error("SyncEngine closed"));
2776
+ this.counterManager.close();
2777
+ this.entryProcessorClient.close(new Error("SyncEngine closed"));
2778
+ this.searchClient.close(new Error("SyncEngine closed"));
1671
2779
  this.stateMachine.transition("DISCONNECTED" /* DISCONNECTED */);
1672
2780
  logger.info("SyncEngine closed");
1673
2781
  }
@@ -1678,15 +2786,11 @@ var _SyncEngine = class _SyncEngine {
1678
2786
  resetConnection() {
1679
2787
  this.close();
1680
2788
  this.stateMachine.reset();
1681
- this.resetBackoff();
1682
- if (this.useConnectionProvider) {
1683
- this.initConnectionProvider();
1684
- } else {
1685
- this.initConnection();
1686
- }
2789
+ this.webSocketManager.reset();
2790
+ this.webSocketManager.connect();
1687
2791
  }
1688
2792
  // ============================================
1689
- // Failover Support Methods (Phase 4.5 Task 05)
2793
+ // Failover Support Methods
1690
2794
  // ============================================
1691
2795
  /**
1692
2796
  * Wait for a partition map update from the connection provider.
@@ -1699,12 +2803,13 @@ var _SyncEngine = class _SyncEngine {
1699
2803
  waitForPartitionMapUpdate(timeoutMs = 5e3) {
1700
2804
  return new Promise((resolve) => {
1701
2805
  const timeout = setTimeout(resolve, timeoutMs);
2806
+ const connectionProvider = this.webSocketManager.getConnectionProvider();
1702
2807
  const handler2 = () => {
1703
2808
  clearTimeout(timeout);
1704
- this.connectionProvider.off("partitionMapUpdated", handler2);
2809
+ connectionProvider.off("partitionMapUpdated", handler2);
1705
2810
  resolve();
1706
2811
  };
1707
- this.connectionProvider.on("partitionMapUpdated", handler2);
2812
+ connectionProvider.on("partitionMapUpdated", handler2);
1708
2813
  });
1709
2814
  }
1710
2815
  /**
@@ -1717,20 +2822,21 @@ var _SyncEngine = class _SyncEngine {
1717
2822
  */
1718
2823
  waitForConnection(timeoutMs = 1e4) {
1719
2824
  return new Promise((resolve, reject) => {
1720
- if (this.connectionProvider.isConnected()) {
2825
+ const connectionProvider = this.webSocketManager.getConnectionProvider();
2826
+ if (connectionProvider.isConnected()) {
1721
2827
  resolve();
1722
2828
  return;
1723
2829
  }
1724
2830
  const timeout = setTimeout(() => {
1725
- this.connectionProvider.off("connected", handler2);
2831
+ connectionProvider.off("connected", handler2);
1726
2832
  reject(new Error("Connection timeout waiting for reconnection"));
1727
2833
  }, timeoutMs);
1728
2834
  const handler2 = () => {
1729
2835
  clearTimeout(timeout);
1730
- this.connectionProvider.off("connected", handler2);
2836
+ connectionProvider.off("connected", handler2);
1731
2837
  resolve();
1732
2838
  };
1733
- this.connectionProvider.on("connected", handler2);
2839
+ connectionProvider.on("connected", handler2);
1734
2840
  });
1735
2841
  }
1736
2842
  /**
@@ -1765,21 +2871,21 @@ var _SyncEngine = class _SyncEngine {
1765
2871
  * Convenience method for failover logic.
1766
2872
  */
1767
2873
  isProviderConnected() {
1768
- return this.connectionProvider.isConnected();
2874
+ return this.webSocketManager.getConnectionProvider().isConnected();
1769
2875
  }
1770
2876
  /**
1771
2877
  * Get the connection provider for direct access.
1772
2878
  * Use with caution - prefer using SyncEngine methods.
1773
2879
  */
1774
2880
  getConnectionProvider() {
1775
- return this.connectionProvider;
2881
+ return this.webSocketManager.getConnectionProvider();
1776
2882
  }
1777
2883
  async resetMap(mapName) {
1778
2884
  const map = this.maps.get(mapName);
1779
2885
  if (map) {
1780
- if (map instanceof LWWMap) {
2886
+ if (map instanceof LWWMap2) {
1781
2887
  map.clear();
1782
- } else if (map instanceof ORMap) {
2888
+ } else if (map instanceof ORMap2) {
1783
2889
  map.clear();
1784
2890
  }
1785
2891
  }
@@ -1790,468 +2896,116 @@ var _SyncEngine = class _SyncEngine {
1790
2896
  }
1791
2897
  logger.info({ mapName, removedStorageCount: mapKeys.length }, "Reset map: Cleared memory and storage");
1792
2898
  }
1793
- // ============ Heartbeat Methods ============
1794
- /**
1795
- * Starts the heartbeat mechanism after successful connection.
1796
- */
1797
- startHeartbeat() {
1798
- if (!this.heartbeatConfig.enabled) {
1799
- return;
1800
- }
1801
- this.stopHeartbeat();
1802
- this.lastPongReceived = Date.now();
1803
- this.heartbeatInterval = setInterval(() => {
1804
- this.sendPing();
1805
- this.checkHeartbeatTimeout();
1806
- }, this.heartbeatConfig.intervalMs);
1807
- logger.info({ intervalMs: this.heartbeatConfig.intervalMs }, "Heartbeat started");
1808
- }
1809
- /**
1810
- * Stops the heartbeat mechanism.
1811
- */
1812
- stopHeartbeat() {
1813
- if (this.heartbeatInterval) {
1814
- clearInterval(this.heartbeatInterval);
1815
- this.heartbeatInterval = null;
1816
- logger.info("Heartbeat stopped");
1817
- }
1818
- }
1819
- /**
1820
- * Sends a PING message to the server.
1821
- */
1822
- sendPing() {
1823
- if (this.canSend()) {
1824
- const pingMessage = {
1825
- type: "PING",
1826
- timestamp: Date.now()
1827
- };
1828
- this.sendMessage(pingMessage);
1829
- }
1830
- }
1831
- /**
1832
- * Handles incoming PONG message from server.
1833
- */
1834
- handlePong(msg) {
1835
- const now = Date.now();
1836
- this.lastPongReceived = now;
1837
- this.lastRoundTripTime = now - msg.timestamp;
1838
- logger.debug({
1839
- rtt: this.lastRoundTripTime,
1840
- serverTime: msg.serverTime,
1841
- clockSkew: msg.serverTime - (msg.timestamp + this.lastRoundTripTime / 2)
1842
- }, "Received PONG");
1843
- }
1844
- /**
1845
- * Checks if heartbeat has timed out and triggers reconnection if needed.
1846
- */
1847
- checkHeartbeatTimeout() {
1848
- const now = Date.now();
1849
- const timeSinceLastPong = now - this.lastPongReceived;
1850
- if (timeSinceLastPong > this.heartbeatConfig.timeoutMs) {
1851
- logger.warn({
1852
- timeSinceLastPong,
1853
- timeoutMs: this.heartbeatConfig.timeoutMs
1854
- }, "Heartbeat timeout - triggering reconnection");
1855
- this.stopHeartbeat();
1856
- if (this.websocket) {
1857
- this.websocket.close();
1858
- }
1859
- }
1860
- }
1861
- /**
1862
- * Returns the last measured round-trip time in milliseconds.
1863
- * Returns null if no PONG has been received yet.
1864
- */
1865
- getLastRoundTripTime() {
1866
- return this.lastRoundTripTime;
1867
- }
1868
- /**
1869
- * Returns true if the connection is considered healthy based on heartbeat.
1870
- * A connection is healthy if it's online, authenticated, and has received
1871
- * a PONG within the timeout window.
1872
- */
1873
- isConnectionHealthy() {
1874
- if (!this.isOnline() || !this.isAuthenticated()) {
1875
- return false;
1876
- }
1877
- if (!this.heartbeatConfig.enabled) {
1878
- return true;
1879
- }
1880
- const timeSinceLastPong = Date.now() - this.lastPongReceived;
1881
- return timeSinceLastPong < this.heartbeatConfig.timeoutMs;
1882
- }
1883
- // ============ ORMap Sync Methods ============
1884
- /**
1885
- * Push local ORMap diff to server for the given keys.
1886
- * Sends local records and tombstones that the server might not have.
1887
- */
1888
- async pushORMapDiff(mapName, keys, map) {
1889
- const entries = [];
1890
- const snapshot = map.getSnapshot();
1891
- for (const key of keys) {
1892
- const recordsMap = map.getRecordsMap(key);
1893
- if (recordsMap && recordsMap.size > 0) {
1894
- const records = Array.from(recordsMap.values());
1895
- const tombstones = [];
1896
- for (const tag of snapshot.tombstones) {
1897
- tombstones.push(tag);
1898
- }
1899
- entries.push({
1900
- key,
1901
- records,
1902
- tombstones
1903
- });
1904
- }
1905
- }
1906
- if (entries.length > 0) {
1907
- this.sendMessage({
1908
- type: "ORMAP_PUSH_DIFF",
1909
- payload: {
1910
- mapName,
1911
- entries
1912
- }
1913
- });
1914
- logger.debug({ mapName, keyCount: entries.length }, "Pushed ORMap diff to server");
1915
- }
1916
- }
1917
- // ============ Backpressure Methods ============
1918
- /**
1919
- * Get the current number of pending (unsynced) operations.
1920
- */
1921
- getPendingOpsCount() {
1922
- return this.opLog.filter((op) => !op.synced).length;
1923
- }
1924
- /**
1925
- * Get the current backpressure status.
1926
- */
1927
- getBackpressureStatus() {
1928
- const pending = this.getPendingOpsCount();
1929
- const max = this.backpressureConfig.maxPendingOps;
1930
- return {
1931
- pending,
1932
- max,
1933
- percentage: max > 0 ? pending / max : 0,
1934
- isPaused: this.backpressurePaused,
1935
- strategy: this.backpressureConfig.strategy
1936
- };
1937
- }
1938
- /**
1939
- * Returns true if writes are currently paused due to backpressure.
1940
- */
1941
- isBackpressurePaused() {
1942
- return this.backpressurePaused;
1943
- }
1944
- /**
1945
- * Subscribe to backpressure events.
1946
- * @param event Event name: 'backpressure:high', 'backpressure:low', 'backpressure:paused', 'backpressure:resumed', 'operation:dropped'
1947
- * @param listener Callback function
1948
- * @returns Unsubscribe function
1949
- */
1950
- onBackpressure(event, listener) {
1951
- if (!this.backpressureListeners.has(event)) {
1952
- this.backpressureListeners.set(event, /* @__PURE__ */ new Set());
1953
- }
1954
- this.backpressureListeners.get(event).add(listener);
1955
- return () => {
1956
- this.backpressureListeners.get(event)?.delete(listener);
1957
- };
1958
- }
2899
+ // ============ Heartbeat Methods (delegate to WebSocketManager) ============
1959
2900
  /**
1960
- * Emit a backpressure event to all listeners.
2901
+ * Returns the last measured round-trip time in milliseconds.
2902
+ * Returns null if no PONG has been received yet.
1961
2903
  */
1962
- emitBackpressureEvent(event, data) {
1963
- const listeners = this.backpressureListeners.get(event);
1964
- if (listeners) {
1965
- for (const listener of listeners) {
1966
- try {
1967
- listener(data);
1968
- } catch (err) {
1969
- logger.error({ err, event }, "Error in backpressure event listener");
1970
- }
1971
- }
1972
- }
2904
+ getLastRoundTripTime() {
2905
+ return this.webSocketManager.getLastRoundTripTime();
1973
2906
  }
1974
2907
  /**
1975
- * Check backpressure before adding a new operation.
1976
- * May pause, throw, or drop depending on strategy.
2908
+ * Returns true if the connection is considered healthy based on heartbeat.
2909
+ * A connection is healthy if it's online, authenticated, and has received
2910
+ * a PONG within the timeout window.
1977
2911
  */
1978
- async checkBackpressure() {
1979
- const pendingCount = this.getPendingOpsCount();
1980
- if (pendingCount < this.backpressureConfig.maxPendingOps) {
1981
- return;
1982
- }
1983
- switch (this.backpressureConfig.strategy) {
1984
- case "pause":
1985
- await this.waitForCapacity();
1986
- break;
1987
- case "throw":
1988
- throw new BackpressureError(
1989
- pendingCount,
1990
- this.backpressureConfig.maxPendingOps
1991
- );
1992
- case "drop-oldest":
1993
- this.dropOldestOp();
1994
- break;
1995
- }
2912
+ isConnectionHealthy() {
2913
+ return this.webSocketManager.isConnectionHealthy();
1996
2914
  }
2915
+ // ============ Backpressure Methods (delegated to BackpressureController) ============
1997
2916
  /**
1998
- * Check high water mark and emit event if threshold reached.
2917
+ * Get the current number of pending (unsynced) operations.
2918
+ * Delegates to BackpressureController.
1999
2919
  */
2000
- checkHighWaterMark() {
2001
- const pendingCount = this.getPendingOpsCount();
2002
- const threshold = Math.floor(
2003
- this.backpressureConfig.maxPendingOps * this.backpressureConfig.highWaterMark
2004
- );
2005
- if (pendingCount >= threshold && !this.highWaterMarkEmitted) {
2006
- this.highWaterMarkEmitted = true;
2007
- logger.warn(
2008
- { pending: pendingCount, max: this.backpressureConfig.maxPendingOps },
2009
- "Backpressure high water mark reached"
2010
- );
2011
- this.emitBackpressureEvent("backpressure:high", {
2012
- pending: pendingCount,
2013
- max: this.backpressureConfig.maxPendingOps
2014
- });
2015
- }
2920
+ getPendingOpsCount() {
2921
+ return this.backpressureController.getPendingOpsCount();
2016
2922
  }
2017
2923
  /**
2018
- * Check low water mark and resume paused writes if threshold reached.
2924
+ * Get the current backpressure status.
2925
+ * Delegates to BackpressureController.
2019
2926
  */
2020
- checkLowWaterMark() {
2021
- const pendingCount = this.getPendingOpsCount();
2022
- const lowThreshold = Math.floor(
2023
- this.backpressureConfig.maxPendingOps * this.backpressureConfig.lowWaterMark
2024
- );
2025
- const highThreshold = Math.floor(
2026
- this.backpressureConfig.maxPendingOps * this.backpressureConfig.highWaterMark
2027
- );
2028
- if (pendingCount < highThreshold && this.highWaterMarkEmitted) {
2029
- this.highWaterMarkEmitted = false;
2030
- }
2031
- if (pendingCount <= lowThreshold) {
2032
- if (this.backpressurePaused) {
2033
- this.backpressurePaused = false;
2034
- logger.info(
2035
- { pending: pendingCount, max: this.backpressureConfig.maxPendingOps },
2036
- "Backpressure low water mark reached, resuming writes"
2037
- );
2038
- this.emitBackpressureEvent("backpressure:low", {
2039
- pending: pendingCount,
2040
- max: this.backpressureConfig.maxPendingOps
2041
- });
2042
- this.emitBackpressureEvent("backpressure:resumed");
2043
- const waiting = this.waitingForCapacity;
2044
- this.waitingForCapacity = [];
2045
- for (const resolve of waiting) {
2046
- resolve();
2047
- }
2048
- }
2049
- }
2927
+ getBackpressureStatus() {
2928
+ return this.backpressureController.getBackpressureStatus();
2050
2929
  }
2051
2930
  /**
2052
- * Wait for capacity to become available (used by 'pause' strategy).
2931
+ * Returns true if writes are currently paused due to backpressure.
2932
+ * Delegates to BackpressureController.
2053
2933
  */
2054
- async waitForCapacity() {
2055
- if (!this.backpressurePaused) {
2056
- this.backpressurePaused = true;
2057
- logger.warn("Backpressure paused - waiting for capacity");
2058
- this.emitBackpressureEvent("backpressure:paused");
2059
- }
2060
- return new Promise((resolve) => {
2061
- this.waitingForCapacity.push(resolve);
2062
- });
2934
+ isBackpressurePaused() {
2935
+ return this.backpressureController.isBackpressurePaused();
2063
2936
  }
2064
2937
  /**
2065
- * Drop the oldest pending operation (used by 'drop-oldest' strategy).
2938
+ * Subscribe to backpressure events.
2939
+ * Delegates to BackpressureController.
2940
+ * @param event Event name: 'backpressure:high', 'backpressure:low', 'backpressure:paused', 'backpressure:resumed', 'operation:dropped'
2941
+ * @param listener Callback function
2942
+ * @returns Unsubscribe function
2066
2943
  */
2067
- dropOldestOp() {
2068
- const oldestIndex = this.opLog.findIndex((op) => !op.synced);
2069
- if (oldestIndex !== -1) {
2070
- const dropped = this.opLog[oldestIndex];
2071
- this.opLog.splice(oldestIndex, 1);
2072
- logger.warn(
2073
- { opId: dropped.id, mapName: dropped.mapName, key: dropped.key },
2074
- "Dropped oldest pending operation due to backpressure"
2075
- );
2076
- this.emitBackpressureEvent("operation:dropped", {
2077
- opId: dropped.id,
2078
- mapName: dropped.mapName,
2079
- opType: dropped.opType,
2080
- key: dropped.key
2081
- });
2082
- }
2944
+ onBackpressure(event, listener) {
2945
+ return this.backpressureController.onBackpressure(event, listener);
2083
2946
  }
2084
2947
  // ============================================
2085
- // Write Concern Methods (Phase 5.01)
2948
+ // Write Concern Methods
2086
2949
  // ============================================
2087
2950
  /**
2088
2951
  * Register a pending Write Concern promise for an operation.
2089
- * The promise will be resolved when the server sends an ACK with the operation result.
2952
+ * Delegates to WriteConcernManager.
2090
2953
  *
2091
2954
  * @param opId - Operation ID
2092
2955
  * @param timeout - Timeout in ms (default: 5000)
2093
2956
  * @returns Promise that resolves with the Write Concern result
2094
2957
  */
2095
2958
  registerWriteConcernPromise(opId, timeout = 5e3) {
2096
- return new Promise((resolve, reject) => {
2097
- const timeoutHandle = setTimeout(() => {
2098
- this.pendingWriteConcernPromises.delete(opId);
2099
- reject(new Error(`Write Concern timeout for operation ${opId}`));
2100
- }, timeout);
2101
- this.pendingWriteConcernPromises.set(opId, {
2102
- resolve,
2103
- reject,
2104
- timeoutHandle
2105
- });
2106
- });
2107
- }
2108
- /**
2109
- * Resolve a pending Write Concern promise with the server result.
2110
- *
2111
- * @param opId - Operation ID
2112
- * @param result - Result from server ACK
2113
- */
2114
- resolveWriteConcernPromise(opId, result) {
2115
- const pending = this.pendingWriteConcernPromises.get(opId);
2116
- if (pending) {
2117
- if (pending.timeoutHandle) {
2118
- clearTimeout(pending.timeoutHandle);
2119
- }
2120
- pending.resolve(result);
2121
- this.pendingWriteConcernPromises.delete(opId);
2122
- }
2123
- }
2124
- /**
2125
- * Cancel all pending Write Concern promises (e.g., on disconnect).
2126
- */
2127
- cancelAllWriteConcernPromises(error) {
2128
- for (const [opId, pending] of this.pendingWriteConcernPromises.entries()) {
2129
- if (pending.timeoutHandle) {
2130
- clearTimeout(pending.timeoutHandle);
2131
- }
2132
- pending.reject(error);
2133
- }
2134
- this.pendingWriteConcernPromises.clear();
2959
+ return this.writeConcernManager.registerWriteConcernPromise(opId, timeout);
2135
2960
  }
2961
+ // ============================================
2962
+ // PN Counter Methods - Delegates to CounterManager
2963
+ // ============================================
2136
2964
  /**
2137
2965
  * Subscribe to counter updates from server.
2966
+ * Delegates to CounterManager.
2138
2967
  * @param name Counter name
2139
2968
  * @param listener Callback when counter state is updated
2140
2969
  * @returns Unsubscribe function
2141
2970
  */
2142
2971
  onCounterUpdate(name, listener) {
2143
- if (!this.counterUpdateListeners.has(name)) {
2144
- this.counterUpdateListeners.set(name, /* @__PURE__ */ new Set());
2145
- }
2146
- this.counterUpdateListeners.get(name).add(listener);
2147
- return () => {
2148
- this.counterUpdateListeners.get(name)?.delete(listener);
2149
- if (this.counterUpdateListeners.get(name)?.size === 0) {
2150
- this.counterUpdateListeners.delete(name);
2151
- }
2152
- };
2972
+ return this.counterManager.onCounterUpdate(name, listener);
2153
2973
  }
2154
2974
  /**
2155
2975
  * Request initial counter state from server.
2156
- * @param name Counter name
2157
- */
2158
- requestCounter(name) {
2159
- if (this.isAuthenticated()) {
2160
- this.sendMessage({
2161
- type: "COUNTER_REQUEST",
2162
- payload: { name }
2163
- });
2164
- }
2165
- }
2166
- /**
2167
- * Sync local counter state to server.
2168
- * @param name Counter name
2169
- * @param state Counter state to sync
2170
- */
2171
- syncCounter(name, state) {
2172
- if (this.isAuthenticated()) {
2173
- const stateObj = {
2174
- positive: Object.fromEntries(state.positive),
2175
- negative: Object.fromEntries(state.negative)
2176
- };
2177
- this.sendMessage({
2178
- type: "COUNTER_SYNC",
2179
- payload: {
2180
- name,
2181
- state: stateObj
2182
- }
2183
- });
2184
- }
2185
- }
2186
- /**
2187
- * Handle incoming counter update from server.
2188
- * Called by handleServerMessage for COUNTER_UPDATE messages.
2189
- */
2190
- handleCounterUpdate(name, stateObj) {
2191
- const state = {
2192
- positive: new Map(Object.entries(stateObj.positive)),
2193
- negative: new Map(Object.entries(stateObj.negative))
2194
- };
2195
- const listeners = this.counterUpdateListeners.get(name);
2196
- if (listeners) {
2197
- for (const listener of listeners) {
2198
- try {
2199
- listener(state);
2200
- } catch (e) {
2201
- logger.error({ err: e, counterName: name }, "Counter update listener error");
2202
- }
2203
- }
2204
- }
2205
- }
2206
- /**
2207
- * Execute an entry processor on a single key atomically.
2208
- *
2209
- * @param mapName Name of the map
2210
- * @param key Key to process
2211
- * @param processor Processor definition
2212
- * @returns Promise resolving to the processor result
2213
- */
2214
- async executeOnKey(mapName, key, processor) {
2215
- if (!this.isAuthenticated()) {
2216
- return {
2217
- success: false,
2218
- error: "Not connected to server"
2219
- };
2220
- }
2221
- const requestId = crypto.randomUUID();
2222
- return new Promise((resolve, reject) => {
2223
- const timeout = setTimeout(() => {
2224
- this.pendingProcessorRequests.delete(requestId);
2225
- reject(new Error("Entry processor request timed out"));
2226
- }, _SyncEngine.PROCESSOR_TIMEOUT);
2227
- this.pendingProcessorRequests.set(requestId, {
2228
- resolve: (result) => {
2229
- clearTimeout(timeout);
2230
- resolve(result);
2231
- },
2232
- reject,
2233
- timeout
2234
- });
2235
- const sent = this.sendMessage({
2236
- type: "ENTRY_PROCESS",
2237
- requestId,
2238
- mapName,
2239
- key,
2240
- processor: {
2241
- name: processor.name,
2242
- code: processor.code,
2243
- args: processor.args
2244
- }
2245
- }, key);
2246
- if (!sent) {
2247
- this.pendingProcessorRequests.delete(requestId);
2248
- clearTimeout(timeout);
2249
- reject(new Error("Failed to send entry processor request"));
2250
- }
2251
- });
2976
+ * Delegates to CounterManager.
2977
+ * @param name Counter name
2978
+ */
2979
+ requestCounter(name) {
2980
+ this.counterManager.requestCounter(name);
2981
+ }
2982
+ /**
2983
+ * Sync local counter state to server.
2984
+ * Delegates to CounterManager.
2985
+ * @param name Counter name
2986
+ * @param state Counter state to sync
2987
+ */
2988
+ syncCounter(name, state) {
2989
+ this.counterManager.syncCounter(name, state);
2990
+ }
2991
+ // ============================================
2992
+ // Entry Processor Methods - Delegates to EntryProcessorClient
2993
+ // ============================================
2994
+ /**
2995
+ * Execute an entry processor on a single key atomically.
2996
+ * Delegates to EntryProcessorClient.
2997
+ *
2998
+ * @param mapName Name of the map
2999
+ * @param key Key to process
3000
+ * @param processor Processor definition
3001
+ * @returns Promise resolving to the processor result
3002
+ */
3003
+ async executeOnKey(mapName, key, processor) {
3004
+ return this.entryProcessorClient.executeOnKey(mapName, key, processor);
2252
3005
  }
2253
3006
  /**
2254
3007
  * Execute an entry processor on multiple keys.
3008
+ * Delegates to EntryProcessorClient.
2255
3009
  *
2256
3010
  * @param mapName Name of the map
2257
3011
  * @param keys Keys to process
@@ -2259,84 +3013,7 @@ var _SyncEngine = class _SyncEngine {
2259
3013
  * @returns Promise resolving to a map of key -> result
2260
3014
  */
2261
3015
  async executeOnKeys(mapName, keys, processor) {
2262
- if (!this.isAuthenticated()) {
2263
- const results = /* @__PURE__ */ new Map();
2264
- const error = {
2265
- success: false,
2266
- error: "Not connected to server"
2267
- };
2268
- for (const key of keys) {
2269
- results.set(key, error);
2270
- }
2271
- return results;
2272
- }
2273
- const requestId = crypto.randomUUID();
2274
- return new Promise((resolve, reject) => {
2275
- const timeout = setTimeout(() => {
2276
- this.pendingBatchProcessorRequests.delete(requestId);
2277
- reject(new Error("Entry processor batch request timed out"));
2278
- }, _SyncEngine.PROCESSOR_TIMEOUT);
2279
- this.pendingBatchProcessorRequests.set(requestId, {
2280
- resolve: (results) => {
2281
- clearTimeout(timeout);
2282
- resolve(results);
2283
- },
2284
- reject,
2285
- timeout
2286
- });
2287
- const sent = this.sendMessage({
2288
- type: "ENTRY_PROCESS_BATCH",
2289
- requestId,
2290
- mapName,
2291
- keys,
2292
- processor: {
2293
- name: processor.name,
2294
- code: processor.code,
2295
- args: processor.args
2296
- }
2297
- });
2298
- if (!sent) {
2299
- this.pendingBatchProcessorRequests.delete(requestId);
2300
- clearTimeout(timeout);
2301
- reject(new Error("Failed to send entry processor batch request"));
2302
- }
2303
- });
2304
- }
2305
- /**
2306
- * Handle entry processor response from server.
2307
- * Called by handleServerMessage for ENTRY_PROCESS_RESPONSE messages.
2308
- */
2309
- handleEntryProcessResponse(message) {
2310
- const pending = this.pendingProcessorRequests.get(message.requestId);
2311
- if (pending) {
2312
- this.pendingProcessorRequests.delete(message.requestId);
2313
- pending.resolve({
2314
- success: message.success,
2315
- result: message.result,
2316
- newValue: message.newValue,
2317
- error: message.error
2318
- });
2319
- }
2320
- }
2321
- /**
2322
- * Handle entry processor batch response from server.
2323
- * Called by handleServerMessage for ENTRY_PROCESS_BATCH_RESPONSE messages.
2324
- */
2325
- handleEntryProcessBatchResponse(message) {
2326
- const pending = this.pendingBatchProcessorRequests.get(message.requestId);
2327
- if (pending) {
2328
- this.pendingBatchProcessorRequests.delete(message.requestId);
2329
- const resultsMap = /* @__PURE__ */ new Map();
2330
- for (const [key, result] of Object.entries(message.results)) {
2331
- resultsMap.set(key, {
2332
- success: result.success,
2333
- result: result.result,
2334
- newValue: result.newValue,
2335
- error: result.error
2336
- });
2337
- }
2338
- pending.resolve(resultsMap);
2339
- }
3016
+ return this.entryProcessorClient.executeOnKeys(mapName, keys, processor);
2340
3017
  }
2341
3018
  /**
2342
3019
  * Subscribe to all incoming messages.
@@ -2383,8 +3060,12 @@ var _SyncEngine = class _SyncEngine {
2383
3060
  }
2384
3061
  }
2385
3062
  }
3063
+ // ============================================
3064
+ // Full-Text Search Methods - Delegates to SearchClient
3065
+ // ============================================
2386
3066
  /**
2387
3067
  * Perform a one-shot BM25 search on the server.
3068
+ * Delegates to SearchClient.
2388
3069
  *
2389
3070
  * @param mapName Name of the map to search
2390
3071
  * @param query Search query text
@@ -2392,58 +3073,10 @@ var _SyncEngine = class _SyncEngine {
2392
3073
  * @returns Promise resolving to search results
2393
3074
  */
2394
3075
  async search(mapName, query, options) {
2395
- if (!this.isAuthenticated()) {
2396
- throw new Error("Not connected to server");
2397
- }
2398
- const requestId = crypto.randomUUID();
2399
- return new Promise((resolve, reject) => {
2400
- const timeout = setTimeout(() => {
2401
- this.pendingSearchRequests.delete(requestId);
2402
- reject(new Error("Search request timed out"));
2403
- }, _SyncEngine.SEARCH_TIMEOUT);
2404
- this.pendingSearchRequests.set(requestId, {
2405
- resolve: (results) => {
2406
- clearTimeout(timeout);
2407
- resolve(results);
2408
- },
2409
- reject: (error) => {
2410
- clearTimeout(timeout);
2411
- reject(error);
2412
- },
2413
- timeout
2414
- });
2415
- const sent = this.sendMessage({
2416
- type: "SEARCH",
2417
- payload: {
2418
- requestId,
2419
- mapName,
2420
- query,
2421
- options
2422
- }
2423
- });
2424
- if (!sent) {
2425
- this.pendingSearchRequests.delete(requestId);
2426
- clearTimeout(timeout);
2427
- reject(new Error("Failed to send search request"));
2428
- }
2429
- });
2430
- }
2431
- /**
2432
- * Handle search response from server.
2433
- */
2434
- handleSearchResponse(payload) {
2435
- const pending = this.pendingSearchRequests.get(payload.requestId);
2436
- if (pending) {
2437
- this.pendingSearchRequests.delete(payload.requestId);
2438
- if (payload.error) {
2439
- pending.reject(new Error(payload.error));
2440
- } else {
2441
- pending.resolve(payload.results);
2442
- }
2443
- }
3076
+ return this.searchClient.search(mapName, query, options);
2444
3077
  }
2445
3078
  // ============================================
2446
- // Conflict Resolver Client (Phase 5.05)
3079
+ // Conflict Resolver Client
2447
3080
  // ============================================
2448
3081
  /**
2449
3082
  * Get the conflict resolver client for registering custom resolvers
@@ -2452,132 +3085,35 @@ var _SyncEngine = class _SyncEngine {
2452
3085
  getConflictResolverClient() {
2453
3086
  return this.conflictResolverClient;
2454
3087
  }
2455
- /**
2456
- * Subscribe to a hybrid query (FTS + filter combination).
2457
- */
2458
- subscribeToHybridQuery(query) {
2459
- this.hybridQueries.set(query.id, query);
2460
- const filter = query.getFilter();
2461
- const mapName = query.getMapName();
2462
- if (query.hasFTSPredicate() && this.stateMachine.getState() === "CONNECTED" /* CONNECTED */) {
2463
- this.sendHybridQuerySubscription(query.id, mapName, filter);
2464
- }
2465
- this.runLocalHybridQuery(mapName, filter).then((results) => {
2466
- query.onResult(results, "local");
2467
- });
2468
- }
2469
- /**
2470
- * Unsubscribe from a hybrid query.
2471
- */
2472
- unsubscribeFromHybridQuery(queryId) {
2473
- const query = this.hybridQueries.get(queryId);
2474
- if (query) {
2475
- this.hybridQueries.delete(queryId);
2476
- if (this.stateMachine.getState() === "CONNECTED" /* CONNECTED */ && query.hasFTSPredicate()) {
2477
- this.sendMessage({
2478
- type: "HYBRID_QUERY_UNSUBSCRIBE",
2479
- payload: { subscriptionId: queryId }
2480
- });
2481
- }
2482
- }
2483
- }
2484
- /**
2485
- * Send hybrid query subscription to server.
2486
- */
2487
- sendHybridQuerySubscription(queryId, mapName, filter) {
2488
- this.sendMessage({
2489
- type: "HYBRID_QUERY_SUBSCRIBE",
2490
- payload: {
2491
- subscriptionId: queryId,
2492
- mapName,
2493
- predicate: filter.predicate,
2494
- where: filter.where,
2495
- sort: filter.sort,
2496
- limit: filter.limit,
2497
- cursor: filter.cursor
2498
- // Phase 14.1: replaces offset
2499
- }
2500
- });
2501
- }
2502
- /**
2503
- * Run a local hybrid query (FTS + filter combination).
2504
- * For FTS predicates, returns results with score = 0 (local-only mode).
2505
- * Server provides actual FTS scoring.
2506
- */
2507
- async runLocalHybridQuery(mapName, filter) {
2508
- if (!this.storageAdapter) {
2509
- return [];
2510
- }
2511
- const results = [];
2512
- const allKeys = await this.storageAdapter.getAllKeys();
2513
- const mapPrefix = `${mapName}:`;
2514
- const entries = [];
2515
- for (const fullKey of allKeys) {
2516
- if (fullKey.startsWith(mapPrefix)) {
2517
- const key = fullKey.substring(mapPrefix.length);
2518
- const record = await this.storageAdapter.get(fullKey);
2519
- if (record) {
2520
- entries.push([key, record]);
2521
- }
2522
- }
2523
- }
2524
- for (const [key, record] of entries) {
2525
- if (record === null || record.value === null) continue;
2526
- const value = record.value;
2527
- if (filter.predicate) {
2528
- const matches = evaluatePredicate(filter.predicate, value);
2529
- if (!matches) continue;
2530
- }
2531
- if (filter.where) {
2532
- let whereMatches = true;
2533
- for (const [field, expected] of Object.entries(filter.where)) {
2534
- if (value[field] !== expected) {
2535
- whereMatches = false;
2536
- break;
2537
- }
2538
- }
2539
- if (!whereMatches) continue;
2540
- }
2541
- results.push({
2542
- key,
2543
- value,
2544
- score: 0,
2545
- // Local doesn't have FTS scoring
2546
- matchedTerms: []
2547
- });
2548
- }
2549
- if (filter.sort) {
2550
- results.sort((a, b) => {
2551
- for (const [field, direction] of Object.entries(filter.sort)) {
2552
- let valA;
2553
- let valB;
2554
- if (field === "_score") {
2555
- valA = a.score ?? 0;
2556
- valB = b.score ?? 0;
2557
- } else if (field === "_key") {
2558
- valA = a.key;
2559
- valB = b.key;
2560
- } else {
2561
- valA = a.value[field];
2562
- valB = b.value[field];
2563
- }
2564
- if (valA < valB) return direction === "asc" ? -1 : 1;
2565
- if (valA > valB) return direction === "asc" ? 1 : -1;
2566
- }
2567
- return 0;
2568
- });
2569
- }
2570
- let sliced = results;
2571
- if (filter.limit) {
2572
- sliced = sliced.slice(0, filter.limit);
2573
- }
2574
- return sliced;
3088
+ // ============================================
3089
+ // Hybrid Query Support - Delegates to QueryManager
3090
+ // ============================================
3091
+ /**
3092
+ * Subscribe to a hybrid query (FTS + filter combination).
3093
+ * Delegates to QueryManager.
3094
+ */
3095
+ subscribeToHybridQuery(query) {
3096
+ this.queryManager.subscribeToHybridQuery(query);
3097
+ }
3098
+ /**
3099
+ * Unsubscribe from a hybrid query.
3100
+ * Delegates to QueryManager.
3101
+ */
3102
+ unsubscribeFromHybridQuery(queryId) {
3103
+ this.queryManager.unsubscribeFromHybridQuery(queryId);
3104
+ }
3105
+ /**
3106
+ * Run a local hybrid query (FTS + filter combination).
3107
+ * Delegates to QueryManager.
3108
+ */
3109
+ async runLocalHybridQuery(mapName, filter) {
3110
+ return this.queryManager.runLocalHybridQuery(mapName, filter);
2575
3111
  }
2576
3112
  /**
2577
3113
  * Handle hybrid query response from server.
2578
3114
  */
2579
3115
  handleHybridQueryResponse(payload) {
2580
- const query = this.hybridQueries.get(payload.subscriptionId);
3116
+ const query = this.queryManager.getHybridQuery(payload.subscriptionId);
2581
3117
  if (query) {
2582
3118
  query.onResult(payload.results, "server");
2583
3119
  query.updatePaginationInfo({
@@ -2591,7 +3127,7 @@ var _SyncEngine = class _SyncEngine {
2591
3127
  * Handle hybrid query delta update from server.
2592
3128
  */
2593
3129
  handleHybridQueryDelta(payload) {
2594
- const query = this.hybridQueries.get(payload.subscriptionId);
3130
+ const query = this.queryManager.getHybridQuery(payload.subscriptionId);
2595
3131
  if (query) {
2596
3132
  if (payload.type === "LEAVE") {
2597
3133
  query.onUpdate(payload.key, null);
@@ -2601,14 +3137,9 @@ var _SyncEngine = class _SyncEngine {
2601
3137
  }
2602
3138
  }
2603
3139
  };
2604
- /** Default timeout for entry processor requests (ms) */
2605
- _SyncEngine.PROCESSOR_TIMEOUT = 3e4;
2606
- /** Default timeout for search requests (ms) */
2607
- _SyncEngine.SEARCH_TIMEOUT = 3e4;
2608
- var SyncEngine = _SyncEngine;
2609
3140
 
2610
3141
  // src/TopGunClient.ts
2611
- import { LWWMap as LWWMap2, ORMap as ORMap2 } from "@topgunbuild/core";
3142
+ import { LWWMap as LWWMap3, ORMap as ORMap3 } from "@topgunbuild/core";
2612
3143
 
2613
3144
  // src/utils/deepEqual.ts
2614
3145
  function deepEqual(a, b) {
@@ -2703,11 +3234,11 @@ var QueryHandle = class {
2703
3234
  constructor(syncEngine, mapName, filter = {}) {
2704
3235
  this.listeners = /* @__PURE__ */ new Set();
2705
3236
  this.currentResults = /* @__PURE__ */ new Map();
2706
- // Change tracking (Phase 5.1)
3237
+ // Change tracking for delta notifications
2707
3238
  this.changeTracker = new ChangeTracker();
2708
3239
  this.pendingChanges = [];
2709
3240
  this.changeListeners = /* @__PURE__ */ new Set();
2710
- // Pagination info (Phase 14.1)
3241
+ // Pagination info
2711
3242
  this._paginationInfo = { hasMore: false, cursorStatus: "none" };
2712
3243
  this.paginationListeners = /* @__PURE__ */ new Set();
2713
3244
  // Track if we've received authoritative server response
@@ -2804,7 +3335,7 @@ var QueryHandle = class {
2804
3335
  this.notify();
2805
3336
  }
2806
3337
  /**
2807
- * Subscribe to change events (Phase 5.1).
3338
+ * Subscribe to change events.
2808
3339
  * Returns an unsubscribe function.
2809
3340
  *
2810
3341
  * @example
@@ -2823,7 +3354,7 @@ var QueryHandle = class {
2823
3354
  return () => this.changeListeners.delete(listener);
2824
3355
  }
2825
3356
  /**
2826
- * Get and clear pending changes (Phase 5.1).
3357
+ * Get and clear pending changes.
2827
3358
  * Call this to retrieve all changes since the last consume.
2828
3359
  */
2829
3360
  consumeChanges() {
@@ -2832,26 +3363,26 @@ var QueryHandle = class {
2832
3363
  return changes;
2833
3364
  }
2834
3365
  /**
2835
- * Get last change without consuming (Phase 5.1).
3366
+ * Get last change without consuming.
2836
3367
  * Returns null if no pending changes.
2837
3368
  */
2838
3369
  getLastChange() {
2839
3370
  return this.pendingChanges.length > 0 ? this.pendingChanges[this.pendingChanges.length - 1] : null;
2840
3371
  }
2841
3372
  /**
2842
- * Get all pending changes without consuming (Phase 5.1).
3373
+ * Get all pending changes without consuming.
2843
3374
  */
2844
3375
  getPendingChanges() {
2845
3376
  return [...this.pendingChanges];
2846
3377
  }
2847
3378
  /**
2848
- * Clear all pending changes (Phase 5.1).
3379
+ * Clear all pending changes.
2849
3380
  */
2850
3381
  clearChanges() {
2851
3382
  this.pendingChanges = [];
2852
3383
  }
2853
3384
  /**
2854
- * Reset change tracker (Phase 5.1).
3385
+ * Reset change tracker.
2855
3386
  * Use when query filter changes or on reconnect.
2856
3387
  */
2857
3388
  resetChangeTracker() {
@@ -2903,7 +3434,7 @@ var QueryHandle = class {
2903
3434
  getMapName() {
2904
3435
  return this.mapName;
2905
3436
  }
2906
- // ============== Pagination Methods (Phase 14.1) ==============
3437
+ // ============== Pagination Methods ==============
2907
3438
  /**
2908
3439
  * Get current pagination info.
2909
3440
  * Returns nextCursor, hasMore, and cursorStatus.
@@ -3022,7 +3553,7 @@ var TopicHandle = class {
3022
3553
  try {
3023
3554
  cb(data, context);
3024
3555
  } catch (e) {
3025
- console.error("Error in topic listener", e);
3556
+ logger.error({ err: e, topic: this.topic, context: "listener" }, "Error in topic listener");
3026
3557
  }
3027
3558
  });
3028
3559
  }
@@ -3520,7 +4051,7 @@ var SearchHandle = class {
3520
4051
  try {
3521
4052
  listener(results);
3522
4053
  } catch (err) {
3523
- console.error("SearchHandle listener error:", err);
4054
+ logger.error({ err, mapName: this.mapName, context: "listener" }, "SearchHandle listener error");
3524
4055
  }
3525
4056
  }
3526
4057
  }
@@ -3537,7 +4068,7 @@ var HybridQueryHandle = class {
3537
4068
  this.changeListeners = /* @__PURE__ */ new Set();
3538
4069
  // Track server data reception
3539
4070
  this.hasReceivedServerData = false;
3540
- // Pagination info (Phase 14.1)
4071
+ // Pagination info
3541
4072
  this._paginationInfo = { hasMore: false, cursorStatus: "none" };
3542
4073
  this.paginationListeners = /* @__PURE__ */ new Set();
3543
4074
  this.id = crypto.randomUUID();
@@ -3754,7 +4285,7 @@ var HybridQueryHandle = class {
3754
4285
  }
3755
4286
  return false;
3756
4287
  }
3757
- // ============== Pagination Methods (Phase 14.1) ==============
4288
+ // ============== Pagination Methods ==============
3758
4289
  /**
3759
4290
  * Get current pagination info.
3760
4291
  * Returns nextCursor, hasMore, and cursorStatus.
@@ -3811,7 +4342,7 @@ import {
3811
4342
  import {
3812
4343
  DEFAULT_CONNECTION_POOL_CONFIG
3813
4344
  } from "@topgunbuild/core";
3814
- import { serialize as serialize2, deserialize as deserialize2 } from "@topgunbuild/core";
4345
+ import { serialize as serialize2, deserialize as deserialize3 } from "@topgunbuild/core";
3815
4346
  var ConnectionPool = class {
3816
4347
  constructor(config = {}) {
3817
4348
  this.listeners = /* @__PURE__ */ new Map();
@@ -4112,7 +4643,7 @@ var ConnectionPool = class {
4112
4643
  let message;
4113
4644
  try {
4114
4645
  if (event.data instanceof ArrayBuffer) {
4115
- message = deserialize2(new Uint8Array(event.data));
4646
+ message = deserialize3(new Uint8Array(event.data));
4116
4647
  } else {
4117
4648
  message = JSON.parse(event.data);
4118
4649
  }
@@ -4904,16 +5435,6 @@ var ClusterClient = class {
4904
5435
  setAuthToken(token) {
4905
5436
  this.connectionPool.setAuthToken(token);
4906
5437
  }
4907
- /**
4908
- * Send operation with automatic routing (legacy API for cluster operations).
4909
- * @deprecated Use send(data, key) for IConnectionProvider interface
4910
- */
4911
- sendMessage(key, message) {
4912
- if (this.config.routingMode === "direct" && this.routingActive) {
4913
- return this.sendDirect(key, message);
4914
- }
4915
- return this.sendForward(message);
4916
- }
4917
5438
  /**
4918
5439
  * Send directly to partition owner
4919
5440
  */
@@ -5225,6 +5746,233 @@ var ClusterClient = class {
5225
5746
  }
5226
5747
  };
5227
5748
 
5749
+ // src/connection/SingleServerProvider.ts
5750
+ var DEFAULT_CONFIG = {
5751
+ maxReconnectAttempts: 10,
5752
+ reconnectDelayMs: 1e3,
5753
+ backoffMultiplier: 2,
5754
+ maxReconnectDelayMs: 3e4
5755
+ };
5756
+ var SingleServerProvider = class {
5757
+ constructor(config) {
5758
+ this.ws = null;
5759
+ this.reconnectAttempts = 0;
5760
+ this.reconnectTimer = null;
5761
+ this.isClosing = false;
5762
+ this.listeners = /* @__PURE__ */ new Map();
5763
+ this.url = config.url;
5764
+ this.config = {
5765
+ url: config.url,
5766
+ maxReconnectAttempts: config.maxReconnectAttempts ?? DEFAULT_CONFIG.maxReconnectAttempts,
5767
+ reconnectDelayMs: config.reconnectDelayMs ?? DEFAULT_CONFIG.reconnectDelayMs,
5768
+ backoffMultiplier: config.backoffMultiplier ?? DEFAULT_CONFIG.backoffMultiplier,
5769
+ maxReconnectDelayMs: config.maxReconnectDelayMs ?? DEFAULT_CONFIG.maxReconnectDelayMs
5770
+ };
5771
+ }
5772
+ /**
5773
+ * Connect to the WebSocket server.
5774
+ */
5775
+ async connect() {
5776
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
5777
+ return;
5778
+ }
5779
+ this.isClosing = false;
5780
+ return new Promise((resolve, reject) => {
5781
+ try {
5782
+ this.ws = new WebSocket(this.url);
5783
+ this.ws.binaryType = "arraybuffer";
5784
+ this.ws.onopen = () => {
5785
+ this.reconnectAttempts = 0;
5786
+ logger.info({ url: this.url }, "SingleServerProvider connected");
5787
+ this.emit("connected", "default");
5788
+ resolve();
5789
+ };
5790
+ this.ws.onerror = (error) => {
5791
+ logger.error({ err: error, url: this.url }, "SingleServerProvider WebSocket error");
5792
+ this.emit("error", error);
5793
+ };
5794
+ this.ws.onclose = (event) => {
5795
+ logger.info({ url: this.url, code: event.code }, "SingleServerProvider disconnected");
5796
+ this.emit("disconnected", "default");
5797
+ if (!this.isClosing) {
5798
+ this.scheduleReconnect();
5799
+ }
5800
+ };
5801
+ this.ws.onmessage = (event) => {
5802
+ this.emit("message", "default", event.data);
5803
+ };
5804
+ const timeoutId = setTimeout(() => {
5805
+ if (this.ws && this.ws.readyState === WebSocket.CONNECTING) {
5806
+ this.ws.close();
5807
+ reject(new Error(`Connection timeout to ${this.url}`));
5808
+ }
5809
+ }, this.config.reconnectDelayMs * 5);
5810
+ const originalOnOpen = this.ws.onopen;
5811
+ const wsRef = this.ws;
5812
+ this.ws.onopen = (ev) => {
5813
+ clearTimeout(timeoutId);
5814
+ if (originalOnOpen) {
5815
+ originalOnOpen.call(wsRef, ev);
5816
+ }
5817
+ };
5818
+ } catch (error) {
5819
+ reject(error);
5820
+ }
5821
+ });
5822
+ }
5823
+ /**
5824
+ * Get connection for a specific key.
5825
+ * In single-server mode, key is ignored.
5826
+ */
5827
+ getConnection(_key) {
5828
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
5829
+ throw new Error("Not connected");
5830
+ }
5831
+ return this.ws;
5832
+ }
5833
+ /**
5834
+ * Get any available connection.
5835
+ */
5836
+ getAnyConnection() {
5837
+ return this.getConnection("");
5838
+ }
5839
+ /**
5840
+ * Check if connected.
5841
+ */
5842
+ isConnected() {
5843
+ return this.ws?.readyState === WebSocket.OPEN;
5844
+ }
5845
+ /**
5846
+ * Get connected node IDs.
5847
+ * Single-server mode returns ['default'] when connected.
5848
+ */
5849
+ getConnectedNodes() {
5850
+ return this.isConnected() ? ["default"] : [];
5851
+ }
5852
+ /**
5853
+ * Subscribe to connection events.
5854
+ */
5855
+ on(event, handler2) {
5856
+ if (!this.listeners.has(event)) {
5857
+ this.listeners.set(event, /* @__PURE__ */ new Set());
5858
+ }
5859
+ this.listeners.get(event).add(handler2);
5860
+ }
5861
+ /**
5862
+ * Unsubscribe from connection events.
5863
+ */
5864
+ off(event, handler2) {
5865
+ this.listeners.get(event)?.delete(handler2);
5866
+ }
5867
+ /**
5868
+ * Send data via the WebSocket connection.
5869
+ * In single-server mode, key parameter is ignored.
5870
+ */
5871
+ send(data, _key) {
5872
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
5873
+ throw new Error("Not connected");
5874
+ }
5875
+ this.ws.send(data);
5876
+ }
5877
+ /**
5878
+ * Close the WebSocket connection.
5879
+ */
5880
+ async close() {
5881
+ this.isClosing = true;
5882
+ if (this.reconnectTimer) {
5883
+ clearTimeout(this.reconnectTimer);
5884
+ this.reconnectTimer = null;
5885
+ }
5886
+ if (this.ws) {
5887
+ this.ws.onclose = null;
5888
+ this.ws.onerror = null;
5889
+ this.ws.onmessage = null;
5890
+ if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
5891
+ this.ws.close();
5892
+ }
5893
+ this.ws = null;
5894
+ }
5895
+ logger.info({ url: this.url }, "SingleServerProvider closed");
5896
+ }
5897
+ /**
5898
+ * Emit an event to all listeners.
5899
+ */
5900
+ emit(event, ...args) {
5901
+ const handlers = this.listeners.get(event);
5902
+ if (handlers) {
5903
+ for (const handler2 of handlers) {
5904
+ try {
5905
+ handler2(...args);
5906
+ } catch (err) {
5907
+ logger.error({ err, event }, "Error in SingleServerProvider event handler");
5908
+ }
5909
+ }
5910
+ }
5911
+ }
5912
+ /**
5913
+ * Schedule a reconnection attempt with exponential backoff.
5914
+ */
5915
+ scheduleReconnect() {
5916
+ if (this.reconnectTimer) {
5917
+ clearTimeout(this.reconnectTimer);
5918
+ this.reconnectTimer = null;
5919
+ }
5920
+ if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
5921
+ logger.error(
5922
+ { attempts: this.reconnectAttempts, url: this.url },
5923
+ "SingleServerProvider max reconnect attempts reached"
5924
+ );
5925
+ this.emit("error", new Error("Max reconnection attempts reached"));
5926
+ return;
5927
+ }
5928
+ const delay = this.calculateBackoffDelay();
5929
+ logger.info(
5930
+ { delay, attempt: this.reconnectAttempts, url: this.url },
5931
+ `SingleServerProvider scheduling reconnect in ${delay}ms`
5932
+ );
5933
+ this.reconnectTimer = setTimeout(async () => {
5934
+ this.reconnectTimer = null;
5935
+ this.reconnectAttempts++;
5936
+ try {
5937
+ await this.connect();
5938
+ this.emit("reconnected", "default");
5939
+ } catch (error) {
5940
+ logger.error({ err: error }, "SingleServerProvider reconnection failed");
5941
+ this.scheduleReconnect();
5942
+ }
5943
+ }, delay);
5944
+ }
5945
+ /**
5946
+ * Calculate backoff delay with exponential increase.
5947
+ */
5948
+ calculateBackoffDelay() {
5949
+ const { reconnectDelayMs, backoffMultiplier, maxReconnectDelayMs } = this.config;
5950
+ let delay = reconnectDelayMs * Math.pow(backoffMultiplier, this.reconnectAttempts);
5951
+ delay = Math.min(delay, maxReconnectDelayMs);
5952
+ delay = delay * (0.5 + Math.random());
5953
+ return Math.floor(delay);
5954
+ }
5955
+ /**
5956
+ * Get the WebSocket URL this provider connects to.
5957
+ */
5958
+ getUrl() {
5959
+ return this.url;
5960
+ }
5961
+ /**
5962
+ * Get current reconnection attempt count.
5963
+ */
5964
+ getReconnectAttempts() {
5965
+ return this.reconnectAttempts;
5966
+ }
5967
+ /**
5968
+ * Reset reconnection counter.
5969
+ * Called externally after successful authentication.
5970
+ */
5971
+ resetReconnectAttempts() {
5972
+ this.reconnectAttempts = 0;
5973
+ }
5974
+ };
5975
+
5228
5976
  // src/TopGunClient.ts
5229
5977
  var DEFAULT_CLUSTER_CONFIG = {
5230
5978
  connectionsPerNode: 1,
@@ -5280,9 +6028,10 @@ var TopGunClient = class {
5280
6028
  });
5281
6029
  logger.info({ seeds: this.clusterConfig.seeds }, "TopGunClient initialized in cluster mode");
5282
6030
  } else {
6031
+ const singleServerProvider = new SingleServerProvider({ url: config.serverUrl });
5283
6032
  this.syncEngine = new SyncEngine({
5284
6033
  nodeId: this.nodeId,
5285
- serverUrl: config.serverUrl,
6034
+ connectionProvider: singleServerProvider,
5286
6035
  storageAdapter: this.storageAdapter,
5287
6036
  backoff: config.backoff,
5288
6037
  backpressure: config.backpressure
@@ -5358,12 +6107,12 @@ var TopGunClient = class {
5358
6107
  getMap(name) {
5359
6108
  if (this.maps.has(name)) {
5360
6109
  const map = this.maps.get(name);
5361
- if (map instanceof LWWMap2) {
6110
+ if (map instanceof LWWMap3) {
5362
6111
  return map;
5363
6112
  }
5364
6113
  throw new Error(`Map ${name} exists but is not an LWWMap`);
5365
6114
  }
5366
- const lwwMap = new LWWMap2(this.syncEngine.getHLC());
6115
+ const lwwMap = new LWWMap3(this.syncEngine.getHLC());
5367
6116
  this.maps.set(name, lwwMap);
5368
6117
  this.syncEngine.registerMap(name, lwwMap);
5369
6118
  this.storageAdapter.getAllKeys().then(async (keys) => {
@@ -5402,12 +6151,12 @@ var TopGunClient = class {
5402
6151
  getORMap(name) {
5403
6152
  if (this.maps.has(name)) {
5404
6153
  const map = this.maps.get(name);
5405
- if (map instanceof ORMap2) {
6154
+ if (map instanceof ORMap3) {
5406
6155
  return map;
5407
6156
  }
5408
6157
  throw new Error(`Map ${name} exists but is not an ORMap`);
5409
6158
  }
5410
- const orMap = new ORMap2(this.syncEngine.getHLC());
6159
+ const orMap = new ORMap3(this.syncEngine.getHLC());
5411
6160
  this.maps.set(name, orMap);
5412
6161
  this.syncEngine.registerMap(name, orMap);
5413
6162
  this.restoreORMap(name, orMap);
@@ -5623,7 +6372,7 @@ var TopGunClient = class {
5623
6372
  return this.syncEngine.onBackpressure(event, listener);
5624
6373
  }
5625
6374
  // ============================================
5626
- // Full-Text Search API (Phase 11.1a)
6375
+ // Full-Text Search API
5627
6376
  // ============================================
5628
6377
  /**
5629
6378
  * Perform a one-shot BM25 search on the server.
@@ -5653,7 +6402,7 @@ var TopGunClient = class {
5653
6402
  return this.syncEngine.search(mapName, query, options);
5654
6403
  }
5655
6404
  // ============================================
5656
- // Live Search API (Phase 11.1b)
6405
+ // Live Search API
5657
6406
  // ============================================
5658
6407
  /**
5659
6408
  * Subscribe to live search results with real-time updates.
@@ -5693,7 +6442,7 @@ var TopGunClient = class {
5693
6442
  return new SearchHandle(this.syncEngine, mapName, query, options);
5694
6443
  }
5695
6444
  // ============================================
5696
- // Hybrid Query API (Phase 12)
6445
+ // Hybrid Query API
5697
6446
  // ============================================
5698
6447
  /**
5699
6448
  * Create a hybrid query combining FTS with traditional filters.
@@ -5730,7 +6479,7 @@ var TopGunClient = class {
5730
6479
  return new HybridQueryHandle(this.syncEngine, mapName, filter);
5731
6480
  }
5732
6481
  // ============================================
5733
- // Entry Processor API (Phase 5.03)
6482
+ // Entry Processor API
5734
6483
  // ============================================
5735
6484
  /**
5736
6485
  * Execute an entry processor on a single key atomically.
@@ -5767,7 +6516,7 @@ var TopGunClient = class {
5767
6516
  const result = await this.syncEngine.executeOnKey(mapName, key, processor);
5768
6517
  if (result.success && result.newValue !== void 0) {
5769
6518
  const map = this.maps.get(mapName);
5770
- if (map instanceof LWWMap2) {
6519
+ if (map instanceof LWWMap3) {
5771
6520
  map.set(key, result.newValue);
5772
6521
  }
5773
6522
  }
@@ -5804,7 +6553,7 @@ var TopGunClient = class {
5804
6553
  async executeOnKeys(mapName, keys, processor) {
5805
6554
  const results = await this.syncEngine.executeOnKeys(mapName, keys, processor);
5806
6555
  const map = this.maps.get(mapName);
5807
- if (map instanceof LWWMap2) {
6556
+ if (map instanceof LWWMap3) {
5808
6557
  for (const [key, result] of results) {
5809
6558
  if (result.success && result.newValue !== void 0) {
5810
6559
  map.set(key, result.newValue);
@@ -5851,7 +6600,7 @@ var TopGunClient = class {
5851
6600
  return this.journalReader;
5852
6601
  }
5853
6602
  // ============================================
5854
- // Conflict Resolver API (Phase 5.05)
6603
+ // Conflict Resolver API
5855
6604
  // ============================================
5856
6605
  /**
5857
6606
  * Get the conflict resolver client for registering custom merge resolvers.
@@ -6110,7 +6859,7 @@ var TopGun = class {
6110
6859
  nodeId: config.nodeId
6111
6860
  });
6112
6861
  this.initPromise = this.client.start().catch((err) => {
6113
- console.error("Failed to start TopGun client:", err);
6862
+ logger.error({ err, context: "client_start" }, "Failed to start TopGun client");
6114
6863
  throw err;
6115
6864
  });
6116
6865
  return new Proxy(this, handler);
@@ -6165,7 +6914,7 @@ var CollectionWrapper = class {
6165
6914
  };
6166
6915
 
6167
6916
  // src/crypto/EncryptionManager.ts
6168
- import { serialize as serialize4, deserialize as deserialize3 } from "@topgunbuild/core";
6917
+ import { serialize as serialize4, deserialize as deserialize4 } from "@topgunbuild/core";
6169
6918
  var _EncryptionManager = class _EncryptionManager {
6170
6919
  /**
6171
6920
  * Encrypts data using AES-GCM.
@@ -6201,9 +6950,9 @@ var _EncryptionManager = class _EncryptionManager {
6201
6950
  key,
6202
6951
  record.data
6203
6952
  );
6204
- return deserialize3(new Uint8Array(plaintextBuffer));
6953
+ return deserialize4(new Uint8Array(plaintextBuffer));
6205
6954
  } catch (err) {
6206
- console.error("Decryption failed", err);
6955
+ logger.error({ err, context: "decryption" }, "Decryption failed");
6207
6956
  throw new Error("Failed to decrypt data: " + err);
6208
6957
  }
6209
6958
  }
@@ -6325,7 +7074,7 @@ var EncryptedStorageAdapter = class {
6325
7074
  };
6326
7075
 
6327
7076
  // src/index.ts
6328
- import { LWWMap as LWWMap3, Predicates } from "@topgunbuild/core";
7077
+ import { LWWMap as LWWMap4, Predicates } from "@topgunbuild/core";
6329
7078
  export {
6330
7079
  BackpressureError,
6331
7080
  ChangeTracker,
@@ -6338,7 +7087,7 @@ export {
6338
7087
  EventJournalReader,
6339
7088
  HybridQueryHandle,
6340
7089
  IDBAdapter,
6341
- LWWMap3 as LWWMap,
7090
+ LWWMap4 as LWWMap,
6342
7091
  PNCounterHandle,
6343
7092
  PartitionRouter,
6344
7093
  Predicates,