@topgunbuild/client 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { LWWRecord, ORMapRecord, PredicateNode, LWWMap, ORMap, Timestamp, HLC } from '@topgunbuild/core';
1
+ import { LWWRecord, ORMapRecord, PredicateNode, ConflictResolverDef, MergeRejection, Timestamp, LWWMap, ORMap, HLC, EntryProcessorDef, EntryProcessorResult, PNCounter, PNCounterState, JournalEvent, JournalEventType, NodeHealth, ConnectionPoolConfig, PartitionRouterConfig, PartitionMap, ClusterClientConfig } from '@topgunbuild/core';
2
2
  export { LWWMap, LWWRecord, PredicateNode, Predicates } from '@topgunbuild/core';
3
3
  import pino from 'pino';
4
4
 
@@ -35,6 +35,65 @@ interface IStorageAdapter {
35
35
  getAllKeys(): Promise<string[]>;
36
36
  }
37
37
 
38
+ /**
39
+ * Represents a change event for tracking data mutations.
40
+ */
41
+ interface ChangeEvent<T> {
42
+ /** Type of change: 'add' for new entries, 'update' for modified entries, 'remove' for deleted entries */
43
+ type: 'add' | 'update' | 'remove';
44
+ /** The key of the changed entry */
45
+ key: string;
46
+ /** New value (present for 'add' and 'update') */
47
+ value?: T;
48
+ /** Previous value (present for 'update' and 'remove') */
49
+ previousValue?: T;
50
+ /** HLC timestamp of the change */
51
+ timestamp: number;
52
+ }
53
+ /**
54
+ * ChangeTracker computes differences between snapshots of a Map.
55
+ * Used to track add/update/remove changes for subscription notifications.
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * const tracker = new ChangeTracker<Todo>();
60
+ *
61
+ * // First snapshot
62
+ * const changes1 = tracker.computeChanges(
63
+ * new Map([['a', { title: 'Todo A' }]]),
64
+ * Date.now()
65
+ * );
66
+ * // changes1 = [{ type: 'add', key: 'a', value: { title: 'Todo A' }, timestamp: ... }]
67
+ *
68
+ * // Second snapshot with update
69
+ * const changes2 = tracker.computeChanges(
70
+ * new Map([['a', { title: 'Todo A Updated' }]]),
71
+ * Date.now()
72
+ * );
73
+ * // changes2 = [{ type: 'update', key: 'a', value: { title: 'Todo A Updated' }, previousValue: { title: 'Todo A' }, timestamp: ... }]
74
+ * ```
75
+ */
76
+ declare class ChangeTracker<T> {
77
+ private previousSnapshot;
78
+ /**
79
+ * Computes changes between previous and current state.
80
+ * Updates internal snapshot after computation.
81
+ *
82
+ * @param current - Current state as a Map
83
+ * @param timestamp - HLC timestamp for the changes
84
+ * @returns Array of change events (may be empty if no changes)
85
+ */
86
+ computeChanges(current: Map<string, T>, timestamp: number): ChangeEvent<T>[];
87
+ /**
88
+ * Reset tracker (e.g., on query change or reconnect)
89
+ */
90
+ reset(): void;
91
+ /**
92
+ * Get current snapshot size for debugging/metrics
93
+ */
94
+ get size(): number;
95
+ }
96
+
38
97
  interface QueryFilter {
39
98
  where?: Record<string, any>;
40
99
  predicate?: PredicateNode;
@@ -55,6 +114,9 @@ declare class QueryHandle<T> {
55
114
  private filter;
56
115
  private listeners;
57
116
  private currentResults;
117
+ private changeTracker;
118
+ private pendingChanges;
119
+ private changeListeners;
58
120
  constructor(syncEngine: SyncEngine, mapName: string, filter?: QueryFilter);
59
121
  subscribe(callback: (results: QueryResultItem<T>[]) => void): () => void;
60
122
  private loadInitialLocalData;
@@ -79,6 +141,47 @@ declare class QueryHandle<T> {
79
141
  * Called by SyncEngine when server sends a live update
80
142
  */
81
143
  onUpdate(key: string, value: T | null): void;
144
+ /**
145
+ * Subscribe to change events (Phase 5.1).
146
+ * Returns an unsubscribe function.
147
+ *
148
+ * @example
149
+ * ```typescript
150
+ * const unsubscribe = handle.onChanges((changes) => {
151
+ * for (const change of changes) {
152
+ * if (change.type === 'add') {
153
+ * console.log('Added:', change.key, change.value);
154
+ * }
155
+ * }
156
+ * });
157
+ * ```
158
+ */
159
+ onChanges(listener: (changes: ChangeEvent<T>[]) => void): () => void;
160
+ /**
161
+ * Get and clear pending changes (Phase 5.1).
162
+ * Call this to retrieve all changes since the last consume.
163
+ */
164
+ consumeChanges(): ChangeEvent<T>[];
165
+ /**
166
+ * Get last change without consuming (Phase 5.1).
167
+ * Returns null if no pending changes.
168
+ */
169
+ getLastChange(): ChangeEvent<T> | null;
170
+ /**
171
+ * Get all pending changes without consuming (Phase 5.1).
172
+ */
173
+ getPendingChanges(): ChangeEvent<T>[];
174
+ /**
175
+ * Clear all pending changes (Phase 5.1).
176
+ */
177
+ clearChanges(): void;
178
+ /**
179
+ * Reset change tracker (Phase 5.1).
180
+ * Use when query filter changes or on reconnect.
181
+ */
182
+ resetChangeTracker(): void;
183
+ private computeAndNotifyChanges;
184
+ private notifyChangeListeners;
82
185
  private notify;
83
186
  private getSortedResults;
84
187
  getFilter(): QueryFilter;
@@ -304,6 +407,238 @@ interface OperationDroppedEvent {
304
407
  key: string;
305
408
  }
306
409
 
410
+ /**
411
+ * Connection Provider Types
412
+ *
413
+ * IConnectionProvider abstracts WebSocket connection handling to support
414
+ * both single-server and cluster modes.
415
+ */
416
+ /**
417
+ * Events emitted by IConnectionProvider.
418
+ */
419
+ type ConnectionProviderEvent = 'connected' | 'disconnected' | 'reconnected' | 'message' | 'partitionMapUpdated' | 'error';
420
+ /**
421
+ * Connection event handler type.
422
+ */
423
+ type ConnectionEventHandler = (...args: any[]) => void;
424
+ /**
425
+ * Abstract interface for WebSocket connection providers.
426
+ *
427
+ * Implementations:
428
+ * - SingleServerProvider: Direct connection to a single server
429
+ * - ClusterClient: Multi-node connection pool with partition routing
430
+ */
431
+ interface IConnectionProvider {
432
+ /**
433
+ * Connect to the server(s).
434
+ * In cluster mode, connects to all seed nodes.
435
+ */
436
+ connect(): Promise<void>;
437
+ /**
438
+ * Get connection for a specific key.
439
+ * In cluster mode: routes to partition owner based on key hash.
440
+ * In single-server mode: returns the only connection.
441
+ *
442
+ * @param key - The key to route (used for partition-aware routing)
443
+ * @throws Error if not connected
444
+ */
445
+ getConnection(key: string): WebSocket;
446
+ /**
447
+ * Get any available connection.
448
+ * Used for subscriptions, metadata requests, and non-key-specific operations.
449
+ *
450
+ * @throws Error if not connected
451
+ */
452
+ getAnyConnection(): WebSocket;
453
+ /**
454
+ * Check if at least one connection is active and ready.
455
+ */
456
+ isConnected(): boolean;
457
+ /**
458
+ * Get all connected node IDs.
459
+ * Single-server mode returns ['default'].
460
+ * Cluster mode returns actual node IDs.
461
+ */
462
+ getConnectedNodes(): string[];
463
+ /**
464
+ * Subscribe to connection events.
465
+ *
466
+ * Events:
467
+ * - 'connected': A connection was established (nodeId?: string)
468
+ * - 'disconnected': A connection was lost (nodeId?: string)
469
+ * - 'reconnected': A connection was re-established after disconnect (nodeId?: string)
470
+ * - 'message': A message was received (nodeId: string, data: any)
471
+ * - 'partitionMapUpdated': Partition map was updated (cluster mode only)
472
+ * - 'error': An error occurred (error: Error)
473
+ */
474
+ on(event: ConnectionProviderEvent, handler: ConnectionEventHandler): void;
475
+ /**
476
+ * Unsubscribe from connection events.
477
+ */
478
+ off(event: ConnectionProviderEvent, handler: ConnectionEventHandler): void;
479
+ /**
480
+ * Send a message via the appropriate connection.
481
+ * In cluster mode, routes based on key if provided.
482
+ *
483
+ * @param data - Serialized message data
484
+ * @param key - Optional key for routing (cluster mode)
485
+ */
486
+ send(data: ArrayBuffer | Uint8Array, key?: string): void;
487
+ /**
488
+ * Close all connections gracefully.
489
+ */
490
+ close(): Promise<void>;
491
+ }
492
+ /**
493
+ * Configuration for SingleServerProvider.
494
+ */
495
+ interface SingleServerProviderConfig {
496
+ /** WebSocket URL to connect to */
497
+ url: string;
498
+ /** Maximum reconnection attempts (default: 10) */
499
+ maxReconnectAttempts?: number;
500
+ /** Initial reconnect delay in ms (default: 1000) */
501
+ reconnectDelayMs?: number;
502
+ /** Backoff multiplier for reconnect delay (default: 2) */
503
+ backoffMultiplier?: number;
504
+ /** Maximum reconnect delay in ms (default: 30000) */
505
+ maxReconnectDelayMs?: number;
506
+ }
507
+
508
+ /**
509
+ * Registered resolver info returned from server.
510
+ */
511
+ interface ResolverInfo {
512
+ mapName: string;
513
+ name: string;
514
+ priority?: number;
515
+ keyPattern?: string;
516
+ }
517
+ /**
518
+ * Registration result from server.
519
+ */
520
+ interface RegisterResult {
521
+ success: boolean;
522
+ error?: string;
523
+ }
524
+ /**
525
+ * Client-side manager for conflict resolvers.
526
+ *
527
+ * Provides API for:
528
+ * - Registering conflict resolvers on server
529
+ * - Unregistering resolvers
530
+ * - Listing registered resolvers
531
+ * - Subscribing to merge rejection events
532
+ */
533
+ declare class ConflictResolverClient {
534
+ private readonly syncEngine;
535
+ private readonly rejectionListeners;
536
+ private readonly pendingRequests;
537
+ private static readonly REQUEST_TIMEOUT;
538
+ constructor(syncEngine: SyncEngine);
539
+ /**
540
+ * Register a conflict resolver on the server.
541
+ *
542
+ * @param mapName The map to register the resolver for
543
+ * @param resolver The resolver definition
544
+ * @returns Promise resolving to registration result
545
+ *
546
+ * @example
547
+ * ```typescript
548
+ * // Register a first-write-wins resolver for bookings
549
+ * await client.resolvers.register('bookings', {
550
+ * name: 'first-write-wins',
551
+ * code: `
552
+ * if (context.localValue !== undefined) {
553
+ * return { action: 'reject', reason: 'Slot already booked' };
554
+ * }
555
+ * return { action: 'accept', value: context.remoteValue };
556
+ * `,
557
+ * priority: 100,
558
+ * });
559
+ * ```
560
+ */
561
+ register<V>(mapName: string, resolver: Omit<ConflictResolverDef<V>, 'fn'>): Promise<RegisterResult>;
562
+ /**
563
+ * Unregister a conflict resolver from the server.
564
+ *
565
+ * @param mapName The map the resolver is registered for
566
+ * @param resolverName The name of the resolver to unregister
567
+ * @returns Promise resolving to unregistration result
568
+ */
569
+ unregister(mapName: string, resolverName: string): Promise<RegisterResult>;
570
+ /**
571
+ * List registered conflict resolvers on the server.
572
+ *
573
+ * @param mapName Optional - filter by map name
574
+ * @returns Promise resolving to list of resolver info
575
+ */
576
+ list(mapName?: string): Promise<ResolverInfo[]>;
577
+ /**
578
+ * Subscribe to merge rejection events.
579
+ *
580
+ * @param listener Callback for rejection events
581
+ * @returns Unsubscribe function
582
+ *
583
+ * @example
584
+ * ```typescript
585
+ * const unsubscribe = client.resolvers.onRejection((rejection) => {
586
+ * console.log(`Merge rejected for ${rejection.key}: ${rejection.reason}`);
587
+ * // Optionally refresh the local value
588
+ * });
589
+ *
590
+ * // Later...
591
+ * unsubscribe();
592
+ * ```
593
+ */
594
+ onRejection(listener: (rejection: MergeRejection) => void): () => void;
595
+ /**
596
+ * Handle REGISTER_RESOLVER_RESPONSE from server.
597
+ * Called by SyncEngine.
598
+ */
599
+ handleRegisterResponse(message: {
600
+ requestId: string;
601
+ success: boolean;
602
+ error?: string;
603
+ }): void;
604
+ /**
605
+ * Handle UNREGISTER_RESOLVER_RESPONSE from server.
606
+ * Called by SyncEngine.
607
+ */
608
+ handleUnregisterResponse(message: {
609
+ requestId: string;
610
+ success: boolean;
611
+ error?: string;
612
+ }): void;
613
+ /**
614
+ * Handle LIST_RESOLVERS_RESPONSE from server.
615
+ * Called by SyncEngine.
616
+ */
617
+ handleListResponse(message: {
618
+ requestId: string;
619
+ resolvers: ResolverInfo[];
620
+ }): void;
621
+ /**
622
+ * Handle MERGE_REJECTED from server.
623
+ * Called by SyncEngine.
624
+ */
625
+ handleMergeRejected(message: {
626
+ mapName: string;
627
+ key: string;
628
+ attemptedValue: unknown;
629
+ reason: string;
630
+ timestamp: Timestamp;
631
+ }): void;
632
+ /**
633
+ * Clear all pending requests (e.g., on disconnect).
634
+ */
635
+ clearPending(): void;
636
+ /**
637
+ * Get the number of registered rejection listeners.
638
+ */
639
+ get rejectionListenerCount(): number;
640
+ }
641
+
307
642
  interface HeartbeatConfig {
308
643
  intervalMs: number;
309
644
  timeoutMs: number;
@@ -323,7 +658,10 @@ interface BackoffConfig {
323
658
  }
324
659
  interface SyncEngineConfig {
325
660
  nodeId: string;
326
- serverUrl: string;
661
+ /** @deprecated Use connectionProvider instead */
662
+ serverUrl?: string;
663
+ /** Connection provider (preferred over serverUrl) */
664
+ connectionProvider?: IConnectionProvider;
327
665
  storageAdapter: IStorageAdapter;
328
666
  reconnectInterval?: number;
329
667
  heartbeat?: Partial<HeartbeatConfig>;
@@ -337,6 +675,8 @@ declare class SyncEngine {
337
675
  private readonly hlc;
338
676
  private readonly stateMachine;
339
677
  private readonly backoffConfig;
678
+ private readonly connectionProvider;
679
+ private readonly useConnectionProvider;
340
680
  private websocket;
341
681
  private opLog;
342
682
  private maps;
@@ -358,6 +698,7 @@ declare class SyncEngine {
358
698
  private highWaterMarkEmitted;
359
699
  private backpressureListeners;
360
700
  private pendingWriteConcernPromises;
701
+ private readonly conflictResolverClient;
361
702
  constructor(config: SyncEngineConfig);
362
703
  /**
363
704
  * Get the current connection state
@@ -384,6 +725,14 @@ declare class SyncEngine {
384
725
  * Check if fully connected and synced
385
726
  */
386
727
  private isConnected;
728
+ /**
729
+ * Initialize connection using IConnectionProvider (Phase 4.5 cluster mode).
730
+ * Sets up event handlers for the connection provider.
731
+ */
732
+ private initConnectionProvider;
733
+ /**
734
+ * Initialize connection using direct WebSocket (legacy single-server mode).
735
+ */
387
736
  private initConnection;
388
737
  private scheduleReconnect;
389
738
  private calculateBackoffDelay;
@@ -391,6 +740,18 @@ declare class SyncEngine {
391
740
  * Reset backoff counter (called on successful connection)
392
741
  */
393
742
  private resetBackoff;
743
+ /**
744
+ * Send a message through the current connection.
745
+ * Uses connectionProvider if in cluster mode, otherwise uses direct websocket.
746
+ * @param message Message object to serialize and send
747
+ * @param key Optional key for routing (cluster mode only)
748
+ * @returns true if message was sent, false otherwise
749
+ */
750
+ private sendMessage;
751
+ /**
752
+ * Check if we can send messages (connection is ready).
753
+ */
754
+ private canSend;
394
755
  private loadOpLog;
395
756
  private saveOpLog;
396
757
  registerMap(mapName: string, map: LWWMap<any, any> | ORMap<any, any>): void;
@@ -439,6 +800,43 @@ declare class SyncEngine {
439
800
  * Use after fatal errors to start fresh.
440
801
  */
441
802
  resetConnection(): void;
803
+ /**
804
+ * Wait for a partition map update from the connection provider.
805
+ * Used when an operation fails with NOT_OWNER error and needs
806
+ * to wait for an updated partition map before retrying.
807
+ *
808
+ * @param timeoutMs - Maximum time to wait (default: 5000ms)
809
+ * @returns Promise that resolves when partition map is updated or times out
810
+ */
811
+ waitForPartitionMapUpdate(timeoutMs?: number): Promise<void>;
812
+ /**
813
+ * Wait for the connection to be available.
814
+ * Used when an operation fails due to connection issues and needs
815
+ * to wait for reconnection before retrying.
816
+ *
817
+ * @param timeoutMs - Maximum time to wait (default: 10000ms)
818
+ * @returns Promise that resolves when connected or rejects on timeout
819
+ */
820
+ waitForConnection(timeoutMs?: number): Promise<void>;
821
+ /**
822
+ * Wait for a specific sync state.
823
+ * Useful for waiting until fully connected and synced.
824
+ *
825
+ * @param targetState - The state to wait for
826
+ * @param timeoutMs - Maximum time to wait (default: 30000ms)
827
+ * @returns Promise that resolves when state is reached or rejects on timeout
828
+ */
829
+ waitForState(targetState: SyncState, timeoutMs?: number): Promise<void>;
830
+ /**
831
+ * Check if the connection provider is connected.
832
+ * Convenience method for failover logic.
833
+ */
834
+ isProviderConnected(): boolean;
835
+ /**
836
+ * Get the connection provider for direct access.
837
+ * Use with caution - prefer using SyncEngine methods.
838
+ */
839
+ getConnectionProvider(): IConnectionProvider;
442
840
  private resetMap;
443
841
  /**
444
842
  * Starts the heartbeat mechanism after successful connection.
@@ -540,6 +938,99 @@ declare class SyncEngine {
540
938
  * Cancel all pending Write Concern promises (e.g., on disconnect).
541
939
  */
542
940
  private cancelAllWriteConcernPromises;
941
+ /** Counter update listeners by name */
942
+ private counterUpdateListeners;
943
+ /**
944
+ * Subscribe to counter updates from server.
945
+ * @param name Counter name
946
+ * @param listener Callback when counter state is updated
947
+ * @returns Unsubscribe function
948
+ */
949
+ onCounterUpdate(name: string, listener: (state: any) => void): () => void;
950
+ /**
951
+ * Request initial counter state from server.
952
+ * @param name Counter name
953
+ */
954
+ requestCounter(name: string): void;
955
+ /**
956
+ * Sync local counter state to server.
957
+ * @param name Counter name
958
+ * @param state Counter state to sync
959
+ */
960
+ syncCounter(name: string, state: any): void;
961
+ /**
962
+ * Handle incoming counter update from server.
963
+ * Called by handleServerMessage for COUNTER_UPDATE messages.
964
+ */
965
+ private handleCounterUpdate;
966
+ /** Pending entry processor requests by requestId */
967
+ private pendingProcessorRequests;
968
+ /** Pending batch entry processor requests by requestId */
969
+ private pendingBatchProcessorRequests;
970
+ /** Default timeout for entry processor requests (ms) */
971
+ private static readonly PROCESSOR_TIMEOUT;
972
+ /**
973
+ * Execute an entry processor on a single key atomically.
974
+ *
975
+ * @param mapName Name of the map
976
+ * @param key Key to process
977
+ * @param processor Processor definition
978
+ * @returns Promise resolving to the processor result
979
+ */
980
+ executeOnKey<V, R = V>(mapName: string, key: string, processor: EntryProcessorDef<V, R>): Promise<EntryProcessorResult<R>>;
981
+ /**
982
+ * Execute an entry processor on multiple keys.
983
+ *
984
+ * @param mapName Name of the map
985
+ * @param keys Keys to process
986
+ * @param processor Processor definition
987
+ * @returns Promise resolving to a map of key -> result
988
+ */
989
+ executeOnKeys<V, R = V>(mapName: string, keys: string[], processor: EntryProcessorDef<V, R>): Promise<Map<string, EntryProcessorResult<R>>>;
990
+ /**
991
+ * Handle entry processor response from server.
992
+ * Called by handleServerMessage for ENTRY_PROCESS_RESPONSE messages.
993
+ */
994
+ private handleEntryProcessResponse;
995
+ /**
996
+ * Handle entry processor batch response from server.
997
+ * Called by handleServerMessage for ENTRY_PROCESS_BATCH_RESPONSE messages.
998
+ */
999
+ private handleEntryProcessBatchResponse;
1000
+ /** Message listeners for journal and other generic messages */
1001
+ private messageListeners;
1002
+ /**
1003
+ * Subscribe to all incoming messages.
1004
+ * Used by EventJournalReader to receive journal events.
1005
+ *
1006
+ * @param event Event type (currently only 'message')
1007
+ * @param handler Message handler
1008
+ */
1009
+ on(event: 'message', handler: (message: any) => void): void;
1010
+ /**
1011
+ * Unsubscribe from incoming messages.
1012
+ *
1013
+ * @param event Event type (currently only 'message')
1014
+ * @param handler Message handler to remove
1015
+ */
1016
+ off(event: 'message', handler: (message: any) => void): void;
1017
+ /**
1018
+ * Send a message to the server.
1019
+ * Public method for EventJournalReader and other components.
1020
+ *
1021
+ * @param message Message object to send
1022
+ */
1023
+ send(message: any): void;
1024
+ /**
1025
+ * Emit message to all listeners.
1026
+ * Called internally when a message is received.
1027
+ */
1028
+ private emitMessage;
1029
+ /**
1030
+ * Get the conflict resolver client for registering custom resolvers
1031
+ * and subscribing to merge rejection events.
1032
+ */
1033
+ getConflictResolverClient(): ConflictResolverClient;
543
1034
  }
544
1035
 
545
1036
  interface ILock {
@@ -558,95 +1049,353 @@ declare class DistributedLock implements ILock {
558
1049
  isLocked(): boolean;
559
1050
  }
560
1051
 
561
- declare class TopGunClient {
562
- private readonly nodeId;
1052
+ /**
1053
+ * Client-side handle for a PN Counter.
1054
+ *
1055
+ * Wraps the core PNCounterImpl and integrates with SyncEngine for:
1056
+ * - Automatic sync to server when counter changes
1057
+ * - Receiving remote updates from other clients
1058
+ * - Local persistence via IStorageAdapter (IndexedDB in browser)
1059
+ *
1060
+ * @example
1061
+ * ```typescript
1062
+ * const counter = client.getPNCounter('likes:post-123');
1063
+ * counter.increment(); // Immediate local update + sync to server
1064
+ *
1065
+ * counter.subscribe((value) => {
1066
+ * console.log('Current likes:', value);
1067
+ * });
1068
+ * ```
1069
+ */
1070
+ declare class PNCounterHandle implements PNCounter {
1071
+ private readonly counter;
1072
+ private readonly name;
563
1073
  private readonly syncEngine;
564
- private readonly maps;
565
- private readonly storageAdapter;
566
- private readonly topicHandles;
567
- constructor(config: {
568
- nodeId?: string;
569
- serverUrl: string;
570
- storage: IStorageAdapter;
571
- backoff?: Partial<BackoffConfig>;
572
- backpressure?: Partial<BackpressureConfig>;
573
- });
574
- start(): Promise<void>;
575
- setAuthToken(token: string): void;
576
- setAuthTokenProvider(provider: () => Promise<string | null>): void;
1074
+ private readonly storageAdapter?;
1075
+ private syncScheduled;
1076
+ private persistScheduled;
1077
+ private unsubscribeFromUpdates?;
1078
+ constructor(name: string, nodeId: string, syncEngine: SyncEngine, storageAdapter?: IStorageAdapter);
577
1079
  /**
578
- * Creates a live query subscription for a map.
1080
+ * Restore counter state from local storage.
1081
+ * Called during construction to recover offline state.
579
1082
  */
580
- query<T>(mapName: string, filter: QueryFilter): QueryHandle<T>;
1083
+ private restoreFromStorage;
581
1084
  /**
582
- * Retrieves a distributed lock instance.
583
- * @param name The name of the lock.
1085
+ * Persist counter state to local storage.
1086
+ * Debounced to avoid excessive writes during rapid operations.
584
1087
  */
585
- getLock(name: string): DistributedLock;
1088
+ private schedulePersist;
586
1089
  /**
587
- * Retrieves a topic handle for Pub/Sub messaging.
588
- * @param name The name of the topic.
1090
+ * Actually persist state to storage.
589
1091
  */
590
- topic(name: string): TopicHandle;
1092
+ private persistToStorage;
591
1093
  /**
592
- * Retrieves an LWWMap instance. If the map doesn't exist locally, it's created.
593
- * @param name The name of the map.
594
- * @returns An LWWMap instance.
1094
+ * Get current counter value.
595
1095
  */
596
- getMap<K, V>(name: string): LWWMap<K, V>;
1096
+ get(): number;
597
1097
  /**
598
- * Retrieves an ORMap instance. If the map doesn't exist locally, it's created.
599
- * @param name The name of the map.
600
- * @returns An ORMap instance.
1098
+ * Increment by 1 and return new value.
601
1099
  */
602
- getORMap<K, V>(name: string): ORMap<K, V>;
603
- private restoreORMap;
604
- private persistORMapKey;
605
- private persistORMapTombstones;
1100
+ increment(): number;
606
1101
  /**
607
- * Closes the client, disconnecting from the server and cleaning up resources.
1102
+ * Decrement by 1 and return new value.
608
1103
  */
609
- close(): void;
1104
+ decrement(): number;
610
1105
  /**
611
- * Get the current connection state
1106
+ * Add delta (positive or negative) and return new value.
612
1107
  */
613
- getConnectionState(): SyncState;
1108
+ addAndGet(delta: number): number;
614
1109
  /**
615
- * Subscribe to connection state changes
616
- * @param listener Callback function called on each state change
617
- * @returns Unsubscribe function
1110
+ * Get state for sync.
618
1111
  */
619
- onConnectionStateChange(listener: (event: StateChangeEvent) => void): () => void;
1112
+ getState(): PNCounterState;
620
1113
  /**
621
- * Get state machine history for debugging
622
- * @param limit Maximum number of entries to return
1114
+ * Merge remote state.
623
1115
  */
624
- getStateHistory(limit?: number): StateChangeEvent[];
1116
+ merge(remote: PNCounterState): void;
625
1117
  /**
626
- * Reset the connection and state machine.
627
- * Use after fatal errors to start fresh.
1118
+ * Subscribe to value changes.
628
1119
  */
629
- resetConnection(): void;
1120
+ subscribe(listener: (value: number) => void): () => void;
630
1121
  /**
631
- * Get the current number of pending (unacknowledged) operations.
1122
+ * Get the counter name.
632
1123
  */
633
- getPendingOpsCount(): number;
1124
+ getName(): string;
634
1125
  /**
635
- * Get the current backpressure status.
1126
+ * Cleanup resources.
636
1127
  */
637
- getBackpressureStatus(): BackpressureStatus;
1128
+ dispose(): void;
638
1129
  /**
639
- * Returns true if writes are currently paused due to backpressure.
1130
+ * Schedule sync to server with debouncing.
1131
+ * Batches rapid increments to avoid network spam.
640
1132
  */
641
- isBackpressurePaused(): boolean;
1133
+ private scheduleSync;
1134
+ }
1135
+
1136
+ /**
1137
+ * Serialized journal event from network (bigint as string).
1138
+ */
1139
+ interface JournalEventData {
1140
+ sequence: string;
1141
+ type: JournalEventType;
1142
+ mapName: string;
1143
+ key: string;
1144
+ value?: unknown;
1145
+ previousValue?: unknown;
1146
+ timestamp: {
1147
+ millis: number;
1148
+ counter: number;
1149
+ nodeId: string;
1150
+ };
1151
+ nodeId: string;
1152
+ metadata?: Record<string, unknown>;
1153
+ }
1154
+ /**
1155
+ * Options for journal subscription.
1156
+ */
1157
+ interface JournalSubscribeOptions {
1158
+ /** Start from specific sequence */
1159
+ fromSequence?: bigint;
1160
+ /** Filter by map name */
1161
+ mapName?: string;
1162
+ /** Filter by event types */
1163
+ types?: JournalEventType[];
1164
+ }
1165
+ /**
1166
+ * Client-side Event Journal Reader.
1167
+ * Communicates with server to read and subscribe to journal events.
1168
+ */
1169
+ declare class EventJournalReader {
1170
+ private readonly syncEngine;
1171
+ private readonly listeners;
1172
+ private subscriptionCounter;
1173
+ constructor(syncEngine: SyncEngine);
642
1174
  /**
643
- * Subscribe to backpressure events.
1175
+ * Read events from sequence with optional limit.
644
1176
  *
645
- * Available events:
646
- * - 'backpressure:high': Emitted when pending ops reach high water mark
647
- * - 'backpressure:low': Emitted when pending ops drop below low water mark
648
- * - 'backpressure:paused': Emitted when writes are paused (pause strategy)
649
- * - 'backpressure:resumed': Emitted when writes resume after being paused
1177
+ * @param sequence Starting sequence (inclusive)
1178
+ * @param limit Maximum events to return (default: 100)
1179
+ * @returns Promise resolving to array of events
1180
+ */
1181
+ readFrom(sequence: bigint, limit?: number): Promise<JournalEvent[]>;
1182
+ /**
1183
+ * Read events for a specific map.
1184
+ *
1185
+ * @param mapName Map name to filter
1186
+ * @param sequence Starting sequence (default: 0n)
1187
+ * @param limit Maximum events to return (default: 100)
1188
+ */
1189
+ readMapEvents(mapName: string, sequence?: bigint, limit?: number): Promise<JournalEvent[]>;
1190
+ /**
1191
+ * Subscribe to new journal events.
1192
+ *
1193
+ * @param listener Callback for each event
1194
+ * @param options Subscription options
1195
+ * @returns Unsubscribe function
1196
+ */
1197
+ subscribe(listener: (event: JournalEvent) => void, options?: JournalSubscribeOptions): () => void;
1198
+ /**
1199
+ * Get the latest sequence number from server.
1200
+ */
1201
+ getLatestSequence(): Promise<bigint>;
1202
+ /**
1203
+ * Parse network event data to JournalEvent.
1204
+ */
1205
+ private parseEvent;
1206
+ /**
1207
+ * Generate unique request ID.
1208
+ */
1209
+ private generateRequestId;
1210
+ }
1211
+
1212
+ /**
1213
+ * Cluster mode configuration for TopGunClient.
1214
+ * When provided, the client connects to multiple nodes with partition-aware routing.
1215
+ */
1216
+ interface TopGunClusterConfig {
1217
+ /** Initial seed nodes (at least one required) */
1218
+ seeds: string[];
1219
+ /** Connection pool size per node (default: 1) */
1220
+ connectionsPerNode?: number;
1221
+ /** Enable smart routing to partition owner (default: true) */
1222
+ smartRouting?: boolean;
1223
+ /** Partition map refresh interval in ms (default: 30000) */
1224
+ partitionMapRefreshMs?: number;
1225
+ /** Connection timeout per node in ms (default: 5000) */
1226
+ connectionTimeoutMs?: number;
1227
+ /** Retry attempts for failed operations (default: 3) */
1228
+ retryAttempts?: number;
1229
+ }
1230
+ /**
1231
+ * Default values for cluster configuration
1232
+ */
1233
+ declare const DEFAULT_CLUSTER_CONFIG: Required<Omit<TopGunClusterConfig, 'seeds'>>;
1234
+ /**
1235
+ * TopGunClient configuration options
1236
+ */
1237
+ interface TopGunClientConfig {
1238
+ /** Unique node identifier (auto-generated if not provided) */
1239
+ nodeId?: string;
1240
+ /** Single-server mode: WebSocket URL to connect to */
1241
+ serverUrl?: string;
1242
+ /** Cluster mode: Configuration for multi-node routing */
1243
+ cluster?: TopGunClusterConfig;
1244
+ /** Storage adapter for local persistence */
1245
+ storage: IStorageAdapter;
1246
+ /** Backoff configuration for reconnection */
1247
+ backoff?: Partial<BackoffConfig>;
1248
+ /** Backpressure configuration */
1249
+ backpressure?: Partial<BackpressureConfig>;
1250
+ }
1251
+ declare class TopGunClient {
1252
+ private readonly nodeId;
1253
+ private readonly syncEngine;
1254
+ private readonly maps;
1255
+ private readonly storageAdapter;
1256
+ private readonly topicHandles;
1257
+ private readonly counters;
1258
+ private readonly clusterClient?;
1259
+ private readonly isClusterMode;
1260
+ private readonly clusterConfig?;
1261
+ constructor(config: TopGunClientConfig);
1262
+ start(): Promise<void>;
1263
+ setAuthToken(token: string): void;
1264
+ setAuthTokenProvider(provider: () => Promise<string | null>): void;
1265
+ /**
1266
+ * Creates a live query subscription for a map.
1267
+ */
1268
+ query<T>(mapName: string, filter: QueryFilter): QueryHandle<T>;
1269
+ /**
1270
+ * Retrieves a distributed lock instance.
1271
+ * @param name The name of the lock.
1272
+ */
1273
+ getLock(name: string): DistributedLock;
1274
+ /**
1275
+ * Retrieves a topic handle for Pub/Sub messaging.
1276
+ * @param name The name of the topic.
1277
+ */
1278
+ topic(name: string): TopicHandle;
1279
+ /**
1280
+ * Retrieves a PN Counter instance. If the counter doesn't exist locally, it's created.
1281
+ * PN Counters support increment and decrement operations that work offline
1282
+ * and sync to server when connected.
1283
+ *
1284
+ * @param name The name of the counter (e.g., 'likes:post-123')
1285
+ * @returns A PNCounterHandle instance
1286
+ *
1287
+ * @example
1288
+ * ```typescript
1289
+ * const likes = client.getPNCounter('likes:post-123');
1290
+ * likes.increment(); // +1
1291
+ * likes.decrement(); // -1
1292
+ * likes.addAndGet(10); // +10
1293
+ *
1294
+ * likes.subscribe((value) => {
1295
+ * console.log('Current likes:', value);
1296
+ * });
1297
+ * ```
1298
+ */
1299
+ getPNCounter(name: string): PNCounterHandle;
1300
+ /**
1301
+ * Retrieves an LWWMap instance. If the map doesn't exist locally, it's created.
1302
+ * @param name The name of the map.
1303
+ * @returns An LWWMap instance.
1304
+ */
1305
+ getMap<K, V>(name: string): LWWMap<K, V>;
1306
+ /**
1307
+ * Retrieves an ORMap instance. If the map doesn't exist locally, it's created.
1308
+ * @param name The name of the map.
1309
+ * @returns An ORMap instance.
1310
+ */
1311
+ getORMap<K, V>(name: string): ORMap<K, V>;
1312
+ private restoreORMap;
1313
+ private persistORMapKey;
1314
+ private persistORMapTombstones;
1315
+ /**
1316
+ * Closes the client, disconnecting from the server and cleaning up resources.
1317
+ */
1318
+ close(): void;
1319
+ /**
1320
+ * Check if running in cluster mode
1321
+ */
1322
+ isCluster(): boolean;
1323
+ /**
1324
+ * Get list of connected cluster nodes (cluster mode only)
1325
+ * @returns Array of connected node IDs, or empty array in single-server mode
1326
+ */
1327
+ getConnectedNodes(): string[];
1328
+ /**
1329
+ * Get the current partition map version (cluster mode only)
1330
+ * @returns Partition map version, or 0 in single-server mode
1331
+ */
1332
+ getPartitionMapVersion(): number;
1333
+ /**
1334
+ * Check if direct routing is active (cluster mode only)
1335
+ * Direct routing sends operations directly to partition owners.
1336
+ * @returns true if routing is active, false otherwise
1337
+ */
1338
+ isRoutingActive(): boolean;
1339
+ /**
1340
+ * Get health status for all cluster nodes (cluster mode only)
1341
+ * @returns Map of node IDs to their health status
1342
+ */
1343
+ getClusterHealth(): Map<string, NodeHealth>;
1344
+ /**
1345
+ * Force refresh of partition map (cluster mode only)
1346
+ * Use this after detecting routing errors.
1347
+ */
1348
+ refreshPartitionMap(): Promise<void>;
1349
+ /**
1350
+ * Get cluster router statistics (cluster mode only)
1351
+ */
1352
+ getClusterStats(): {
1353
+ mapVersion: number;
1354
+ partitionCount: number;
1355
+ nodeCount: number;
1356
+ lastRefresh: number;
1357
+ isStale: boolean;
1358
+ } | null;
1359
+ /**
1360
+ * Get the current connection state
1361
+ */
1362
+ getConnectionState(): SyncState;
1363
+ /**
1364
+ * Subscribe to connection state changes
1365
+ * @param listener Callback function called on each state change
1366
+ * @returns Unsubscribe function
1367
+ */
1368
+ onConnectionStateChange(listener: (event: StateChangeEvent) => void): () => void;
1369
+ /**
1370
+ * Get state machine history for debugging
1371
+ * @param limit Maximum number of entries to return
1372
+ */
1373
+ getStateHistory(limit?: number): StateChangeEvent[];
1374
+ /**
1375
+ * Reset the connection and state machine.
1376
+ * Use after fatal errors to start fresh.
1377
+ */
1378
+ resetConnection(): void;
1379
+ /**
1380
+ * Get the current number of pending (unacknowledged) operations.
1381
+ */
1382
+ getPendingOpsCount(): number;
1383
+ /**
1384
+ * Get the current backpressure status.
1385
+ */
1386
+ getBackpressureStatus(): BackpressureStatus;
1387
+ /**
1388
+ * Returns true if writes are currently paused due to backpressure.
1389
+ */
1390
+ isBackpressurePaused(): boolean;
1391
+ /**
1392
+ * Subscribe to backpressure events.
1393
+ *
1394
+ * Available events:
1395
+ * - 'backpressure:high': Emitted when pending ops reach high water mark
1396
+ * - 'backpressure:low': Emitted when pending ops drop below low water mark
1397
+ * - 'backpressure:paused': Emitted when writes are paused (pause strategy)
1398
+ * - 'backpressure:resumed': Emitted when writes resume after being paused
650
1399
  * - 'operation:dropped': Emitted when an operation is dropped (drop-oldest strategy)
651
1400
  *
652
1401
  * @param event Event name
@@ -669,6 +1418,144 @@ declare class TopGunClient {
669
1418
  * ```
670
1419
  */
671
1420
  onBackpressure(event: 'backpressure:high' | 'backpressure:low' | 'backpressure:paused' | 'backpressure:resumed' | 'operation:dropped', listener: (data?: BackpressureThresholdEvent | OperationDroppedEvent) => void): () => void;
1421
+ /**
1422
+ * Execute an entry processor on a single key atomically.
1423
+ *
1424
+ * Entry processors solve the read-modify-write race condition by executing
1425
+ * user-defined logic atomically on the server where the data lives.
1426
+ *
1427
+ * @param mapName Name of the map
1428
+ * @param key Key to process
1429
+ * @param processor Processor definition with name, code, and optional args
1430
+ * @returns Promise resolving to the processor result
1431
+ *
1432
+ * @example
1433
+ * ```typescript
1434
+ * // Increment a counter atomically
1435
+ * const result = await client.executeOnKey('stats', 'pageViews', {
1436
+ * name: 'increment',
1437
+ * code: `
1438
+ * const current = value ?? 0;
1439
+ * return { value: current + 1, result: current + 1 };
1440
+ * `,
1441
+ * });
1442
+ *
1443
+ * // Using built-in processor
1444
+ * import { BuiltInProcessors } from '@topgunbuild/core';
1445
+ * const result = await client.executeOnKey(
1446
+ * 'stats',
1447
+ * 'pageViews',
1448
+ * BuiltInProcessors.INCREMENT(1)
1449
+ * );
1450
+ * ```
1451
+ */
1452
+ executeOnKey<V, R = V>(mapName: string, key: string, processor: EntryProcessorDef<V, R>): Promise<EntryProcessorResult<R>>;
1453
+ /**
1454
+ * Execute an entry processor on multiple keys.
1455
+ *
1456
+ * Each key is processed atomically. The operation returns when all keys
1457
+ * have been processed.
1458
+ *
1459
+ * @param mapName Name of the map
1460
+ * @param keys Keys to process
1461
+ * @param processor Processor definition
1462
+ * @returns Promise resolving to a map of key -> result
1463
+ *
1464
+ * @example
1465
+ * ```typescript
1466
+ * // Reset multiple counters
1467
+ * const results = await client.executeOnKeys(
1468
+ * 'stats',
1469
+ * ['pageViews', 'uniqueVisitors', 'bounceRate'],
1470
+ * {
1471
+ * name: 'reset',
1472
+ * code: `return { value: 0, result: value };`, // Returns old value
1473
+ * }
1474
+ * );
1475
+ *
1476
+ * for (const [key, result] of results) {
1477
+ * console.log(`${key}: was ${result.result}, now 0`);
1478
+ * }
1479
+ * ```
1480
+ */
1481
+ executeOnKeys<V, R = V>(mapName: string, keys: string[], processor: EntryProcessorDef<V, R>): Promise<Map<string, EntryProcessorResult<R>>>;
1482
+ /** Cached EventJournalReader instance */
1483
+ private journalReader?;
1484
+ /**
1485
+ * Get the Event Journal reader for subscribing to and reading
1486
+ * map change events.
1487
+ *
1488
+ * The Event Journal provides:
1489
+ * - Append-only log of all map changes (PUT, UPDATE, DELETE)
1490
+ * - Subscription to real-time events
1491
+ * - Historical event replay
1492
+ * - Audit trail for compliance
1493
+ *
1494
+ * @returns EventJournalReader instance
1495
+ *
1496
+ * @example
1497
+ * ```typescript
1498
+ * const journal = client.getEventJournal();
1499
+ *
1500
+ * // Subscribe to all events
1501
+ * const unsubscribe = journal.subscribe((event) => {
1502
+ * console.log(`${event.type} on ${event.mapName}:${event.key}`);
1503
+ * });
1504
+ *
1505
+ * // Subscribe to specific map
1506
+ * journal.subscribe(
1507
+ * (event) => console.log('User changed:', event.key),
1508
+ * { mapName: 'users' }
1509
+ * );
1510
+ *
1511
+ * // Read historical events
1512
+ * const events = await journal.readFrom(0n, 100);
1513
+ * ```
1514
+ */
1515
+ getEventJournal(): EventJournalReader;
1516
+ /**
1517
+ * Get the conflict resolver client for registering custom merge resolvers.
1518
+ *
1519
+ * Conflict resolvers allow you to customize how merge conflicts are handled
1520
+ * on the server. You can implement business logic like:
1521
+ * - First-write-wins for booking systems
1522
+ * - Numeric constraints (non-negative, min/max)
1523
+ * - Owner-only modifications
1524
+ * - Custom merge strategies
1525
+ *
1526
+ * @returns ConflictResolverClient instance
1527
+ *
1528
+ * @example
1529
+ * ```typescript
1530
+ * const resolvers = client.getConflictResolvers();
1531
+ *
1532
+ * // Register a first-write-wins resolver
1533
+ * await resolvers.register('bookings', {
1534
+ * name: 'first-write-wins',
1535
+ * code: `
1536
+ * if (context.localValue !== undefined) {
1537
+ * return { action: 'reject', reason: 'Slot already booked' };
1538
+ * }
1539
+ * return { action: 'accept', value: context.remoteValue };
1540
+ * `,
1541
+ * priority: 100,
1542
+ * });
1543
+ *
1544
+ * // Subscribe to merge rejections
1545
+ * resolvers.onRejection((rejection) => {
1546
+ * console.log(`Merge rejected: ${rejection.reason}`);
1547
+ * // Optionally refresh local state
1548
+ * });
1549
+ *
1550
+ * // List registered resolvers
1551
+ * const registered = await resolvers.list('bookings');
1552
+ * console.log('Active resolvers:', registered);
1553
+ *
1554
+ * // Unregister when done
1555
+ * await resolvers.unregister('bookings', 'first-write-wins');
1556
+ * ```
1557
+ */
1558
+ getConflictResolvers(): ConflictResolverClient;
672
1559
  }
673
1560
 
674
1561
  interface TopGunConfig {
@@ -797,6 +1684,574 @@ declare class BackpressureError extends Error {
797
1684
  constructor(pendingCount: number, maxPending: number);
798
1685
  }
799
1686
 
1687
+ /**
1688
+ * ConnectionPool - Manages WebSocket connections to multiple cluster nodes
1689
+ *
1690
+ * Phase 4: Partition-Aware Client Routing
1691
+ *
1692
+ * Features:
1693
+ * - Maintains connections to all known cluster nodes
1694
+ * - Automatic reconnection with exponential backoff
1695
+ * - Health monitoring and status tracking
1696
+ * - Connection lifecycle management
1697
+ */
1698
+
1699
+ interface ConnectionPoolEvents {
1700
+ 'node:connected': (nodeId: string) => void;
1701
+ 'node:disconnected': (nodeId: string, reason: string) => void;
1702
+ 'node:healthy': (nodeId: string) => void;
1703
+ 'node:unhealthy': (nodeId: string, reason: string) => void;
1704
+ 'message': (nodeId: string, message: any) => void;
1705
+ 'error': (nodeId: string, error: Error) => void;
1706
+ }
1707
+ declare class ConnectionPool {
1708
+ private readonly listeners;
1709
+ private readonly config;
1710
+ private readonly connections;
1711
+ private primaryNodeId;
1712
+ private healthCheckTimer;
1713
+ private authToken;
1714
+ constructor(config?: Partial<ConnectionPoolConfig>);
1715
+ on(event: string, listener: (...args: any[]) => void): this;
1716
+ off(event: string, listener: (...args: any[]) => void): this;
1717
+ emit(event: string, ...args: any[]): boolean;
1718
+ removeAllListeners(event?: string): this;
1719
+ /**
1720
+ * Set authentication token for all connections
1721
+ */
1722
+ setAuthToken(token: string): void;
1723
+ /**
1724
+ * Add a node to the connection pool
1725
+ */
1726
+ addNode(nodeId: string, endpoint: string): Promise<void>;
1727
+ /**
1728
+ * Remove a node from the connection pool
1729
+ */
1730
+ removeNode(nodeId: string): Promise<void>;
1731
+ /**
1732
+ * Get connection for a specific node
1733
+ */
1734
+ getConnection(nodeId: string): WebSocket | null;
1735
+ /**
1736
+ * Get primary connection (first/seed node)
1737
+ */
1738
+ getPrimaryConnection(): WebSocket | null;
1739
+ /**
1740
+ * Get any healthy connection
1741
+ */
1742
+ getAnyHealthyConnection(): {
1743
+ nodeId: string;
1744
+ socket: WebSocket;
1745
+ } | null;
1746
+ /**
1747
+ * Send message to a specific node
1748
+ */
1749
+ send(nodeId: string, message: any): boolean;
1750
+ /**
1751
+ * Send message to primary node
1752
+ */
1753
+ sendToPrimary(message: any): boolean;
1754
+ /**
1755
+ * Get health status for all nodes
1756
+ */
1757
+ getHealthStatus(): Map<string, NodeHealth>;
1758
+ /**
1759
+ * Get list of connected node IDs
1760
+ */
1761
+ getConnectedNodes(): string[];
1762
+ /**
1763
+ * Get all node IDs
1764
+ */
1765
+ getAllNodes(): string[];
1766
+ /**
1767
+ * Check if node is connected and authenticated
1768
+ */
1769
+ isNodeConnected(nodeId: string): boolean;
1770
+ /**
1771
+ * Check if connected to a specific node.
1772
+ * Alias for isNodeConnected() for IConnectionProvider compatibility.
1773
+ */
1774
+ isConnected(nodeId: string): boolean;
1775
+ /**
1776
+ * Start health monitoring
1777
+ */
1778
+ startHealthCheck(): void;
1779
+ /**
1780
+ * Stop health monitoring
1781
+ */
1782
+ stopHealthCheck(): void;
1783
+ /**
1784
+ * Close all connections and cleanup
1785
+ */
1786
+ close(): void;
1787
+ private connect;
1788
+ private sendAuth;
1789
+ private handleMessage;
1790
+ private flushPendingMessages;
1791
+ private scheduleReconnect;
1792
+ private performHealthCheck;
1793
+ }
1794
+
1795
+ /**
1796
+ * PartitionRouter - Routes operations to the correct cluster node
1797
+ *
1798
+ * Phase 4: Partition-Aware Client Routing
1799
+ *
1800
+ * Features:
1801
+ * - Maintains local copy of partition map
1802
+ * - Routes keys to owner nodes using consistent hashing
1803
+ * - Handles stale routing with automatic refresh
1804
+ * - Supports fallback to server-side forwarding
1805
+ */
1806
+
1807
+ interface RoutingResult {
1808
+ nodeId: string;
1809
+ partitionId: number;
1810
+ isOwner: boolean;
1811
+ isBackup: boolean;
1812
+ }
1813
+ interface PartitionRouterEvents {
1814
+ 'partitionMap:updated': (version: number, changesCount: number) => void;
1815
+ 'partitionMap:stale': (currentVersion: number, lastRefresh: number) => void;
1816
+ 'routing:miss': (key: string, expectedOwner: string, actualOwner: string) => void;
1817
+ }
1818
+ declare class PartitionRouter {
1819
+ private readonly listeners;
1820
+ private readonly config;
1821
+ private readonly connectionPool;
1822
+ private partitionMap;
1823
+ private lastRefreshTime;
1824
+ private refreshTimer;
1825
+ private pendingRefresh;
1826
+ constructor(connectionPool: ConnectionPool, config?: Partial<PartitionRouterConfig>);
1827
+ on(event: string, listener: (...args: any[]) => void): this;
1828
+ off(event: string, listener: (...args: any[]) => void): this;
1829
+ once(event: string, listener: (...args: any[]) => void): this;
1830
+ emit(event: string, ...args: any[]): boolean;
1831
+ removeListener(event: string, listener: (...args: any[]) => void): this;
1832
+ removeAllListeners(event?: string): this;
1833
+ /**
1834
+ * Get the partition ID for a given key
1835
+ */
1836
+ getPartitionId(key: string): number;
1837
+ /**
1838
+ * Route a key to the owner node
1839
+ */
1840
+ route(key: string): RoutingResult | null;
1841
+ /**
1842
+ * Route a key and get the WebSocket connection to use
1843
+ */
1844
+ routeToConnection(key: string): {
1845
+ nodeId: string;
1846
+ socket: WebSocket;
1847
+ } | null;
1848
+ /**
1849
+ * Get routing info for multiple keys (batch routing)
1850
+ */
1851
+ routeBatch(keys: string[]): Map<string, RoutingResult[]>;
1852
+ /**
1853
+ * Get all partitions owned by a specific node
1854
+ */
1855
+ getPartitionsForNode(nodeId: string): number[];
1856
+ /**
1857
+ * Get current partition map version
1858
+ */
1859
+ getMapVersion(): number;
1860
+ /**
1861
+ * Check if partition map is available
1862
+ */
1863
+ hasPartitionMap(): boolean;
1864
+ /**
1865
+ * Get owner node for a key.
1866
+ * Returns null if partition map is not available.
1867
+ */
1868
+ getOwner(key: string): string | null;
1869
+ /**
1870
+ * Get backup nodes for a key.
1871
+ * Returns empty array if partition map is not available.
1872
+ */
1873
+ getBackups(key: string): string[];
1874
+ /**
1875
+ * Get the full partition map.
1876
+ * Returns null if not available.
1877
+ */
1878
+ getMap(): PartitionMap | null;
1879
+ /**
1880
+ * Update entire partition map.
1881
+ * Only accepts newer versions.
1882
+ */
1883
+ updateMap(map: PartitionMap): boolean;
1884
+ /**
1885
+ * Update a single partition (for delta updates).
1886
+ */
1887
+ updatePartition(partitionId: number, owner: string, backups: string[]): void;
1888
+ /**
1889
+ * Check if partition map is stale
1890
+ */
1891
+ isMapStale(): boolean;
1892
+ /**
1893
+ * Request fresh partition map from server
1894
+ */
1895
+ refreshPartitionMap(): Promise<void>;
1896
+ /**
1897
+ * Start periodic partition map refresh
1898
+ */
1899
+ startPeriodicRefresh(): void;
1900
+ /**
1901
+ * Stop periodic refresh
1902
+ */
1903
+ stopPeriodicRefresh(): void;
1904
+ /**
1905
+ * Handle NOT_OWNER error from server
1906
+ */
1907
+ handleNotOwnerError(key: string, actualOwner: string, newMapVersion: number): void;
1908
+ /**
1909
+ * Get statistics about routing
1910
+ */
1911
+ getStats(): {
1912
+ mapVersion: number;
1913
+ partitionCount: number;
1914
+ nodeCount: number;
1915
+ lastRefresh: number;
1916
+ isStale: boolean;
1917
+ };
1918
+ /**
1919
+ * Cleanup resources
1920
+ */
1921
+ close(): void;
1922
+ private handlePartitionMap;
1923
+ private handlePartitionMapDelta;
1924
+ private applyPartitionChange;
1925
+ private updateConnectionPool;
1926
+ private doRefreshPartitionMap;
1927
+ }
1928
+
1929
+ /**
1930
+ * ClusterClient - Cluster-aware client wrapper
1931
+ *
1932
+ * Phase 4: Partition-Aware Client Routing
1933
+ * Phase 4.5: Implements IConnectionProvider for SyncEngine abstraction
1934
+ *
1935
+ * Wraps the standard TopGunClient with cluster-aware routing capabilities.
1936
+ * Coordinates between ConnectionPool and PartitionRouter for optimal
1937
+ * request routing in a clustered environment.
1938
+ */
1939
+
1940
+ interface ClusterClientEvents {
1941
+ 'connected': () => void;
1942
+ 'disconnected': (reason: string) => void;
1943
+ 'partitionMap:ready': (version: number) => void;
1944
+ 'routing:active': () => void;
1945
+ 'error': (error: Error) => void;
1946
+ 'circuit:open': (nodeId: string) => void;
1947
+ 'circuit:closed': (nodeId: string) => void;
1948
+ 'circuit:half-open': (nodeId: string) => void;
1949
+ }
1950
+ /**
1951
+ * Circuit breaker state for a node.
1952
+ */
1953
+ interface CircuitState {
1954
+ /** Number of consecutive failures */
1955
+ failures: number;
1956
+ /** Timestamp of last failure */
1957
+ lastFailure: number;
1958
+ /** Current circuit state */
1959
+ state: 'closed' | 'open' | 'half-open';
1960
+ }
1961
+ /**
1962
+ * Routing metrics for monitoring smart routing effectiveness.
1963
+ */
1964
+ interface RoutingMetrics {
1965
+ /** Operations routed directly to partition owner */
1966
+ directRoutes: number;
1967
+ /** Operations falling back to any node (owner unavailable) */
1968
+ fallbackRoutes: number;
1969
+ /** Operations when partition map is missing/stale */
1970
+ partitionMisses: number;
1971
+ /** Total routing decisions made */
1972
+ totalRoutes: number;
1973
+ }
1974
+ type ClusterRoutingMode = 'direct' | 'forward';
1975
+ /**
1976
+ * ClusterClient implements IConnectionProvider for multi-node cluster mode.
1977
+ * It provides partition-aware routing and connection management.
1978
+ */
1979
+ declare class ClusterClient implements IConnectionProvider {
1980
+ private readonly listeners;
1981
+ private readonly connectionPool;
1982
+ private readonly partitionRouter;
1983
+ private readonly config;
1984
+ private initialized;
1985
+ private routingActive;
1986
+ private readonly routingMetrics;
1987
+ private readonly circuits;
1988
+ private readonly circuitBreakerConfig;
1989
+ constructor(config: ClusterClientConfig);
1990
+ on(event: string, listener: (...args: any[]) => void): this;
1991
+ off(event: string, listener: (...args: any[]) => void): this;
1992
+ emit(event: string, ...args: any[]): boolean;
1993
+ removeAllListeners(event?: string): this;
1994
+ /**
1995
+ * Connect to cluster nodes (IConnectionProvider interface).
1996
+ * Alias for start() method.
1997
+ */
1998
+ connect(): Promise<void>;
1999
+ /**
2000
+ * Get connection for a specific key (IConnectionProvider interface).
2001
+ * Routes to partition owner based on key hash when smart routing is enabled.
2002
+ * @throws Error if not connected
2003
+ */
2004
+ getConnection(key: string): WebSocket;
2005
+ /**
2006
+ * Get fallback connection when owner is unavailable.
2007
+ * @throws Error if no connection available
2008
+ */
2009
+ private getFallbackConnection;
2010
+ /**
2011
+ * Request a partition map refresh in the background.
2012
+ * Called when routing to an unknown/disconnected owner.
2013
+ */
2014
+ private requestPartitionMapRefresh;
2015
+ /**
2016
+ * Request partition map from a specific node.
2017
+ * Called on first node connection.
2018
+ */
2019
+ private requestPartitionMapFromNode;
2020
+ /**
2021
+ * Check if at least one connection is active (IConnectionProvider interface).
2022
+ */
2023
+ isConnected(): boolean;
2024
+ /**
2025
+ * Send data via the appropriate connection (IConnectionProvider interface).
2026
+ * Routes based on key if provided.
2027
+ */
2028
+ send(data: ArrayBuffer | Uint8Array, key?: string): void;
2029
+ /**
2030
+ * Send data with automatic retry and rerouting on failure.
2031
+ * @param data - Data to send
2032
+ * @param key - Optional key for routing
2033
+ * @param options - Retry options
2034
+ * @throws Error after max retries exceeded
2035
+ */
2036
+ sendWithRetry(data: ArrayBuffer | Uint8Array, key?: string, options?: {
2037
+ maxRetries?: number;
2038
+ retryDelayMs?: number;
2039
+ retryOnNotOwner?: boolean;
2040
+ }): Promise<void>;
2041
+ /**
2042
+ * Check if an error is retryable.
2043
+ */
2044
+ private isRetryableError;
2045
+ /**
2046
+ * Wait for partition map update.
2047
+ */
2048
+ private waitForPartitionMapUpdateInternal;
2049
+ /**
2050
+ * Wait for at least one connection to be available.
2051
+ */
2052
+ private waitForConnectionInternal;
2053
+ /**
2054
+ * Helper delay function.
2055
+ */
2056
+ private delay;
2057
+ /**
2058
+ * Initialize cluster connections
2059
+ */
2060
+ start(): Promise<void>;
2061
+ /**
2062
+ * Set authentication token
2063
+ */
2064
+ setAuthToken(token: string): void;
2065
+ /**
2066
+ * Send operation with automatic routing (legacy API for cluster operations).
2067
+ * @deprecated Use send(data, key) for IConnectionProvider interface
2068
+ */
2069
+ sendMessage(key: string, message: any): boolean;
2070
+ /**
2071
+ * Send directly to partition owner
2072
+ */
2073
+ sendDirect(key: string, message: any): boolean;
2074
+ /**
2075
+ * Send to primary node for server-side forwarding
2076
+ */
2077
+ sendForward(message: any): boolean;
2078
+ /**
2079
+ * Send batch of operations with routing
2080
+ */
2081
+ sendBatch(operations: Array<{
2082
+ key: string;
2083
+ message: any;
2084
+ }>): Map<string, boolean>;
2085
+ /**
2086
+ * Get connection pool health status
2087
+ */
2088
+ getHealthStatus(): Map<string, NodeHealth>;
2089
+ /**
2090
+ * Get partition router stats
2091
+ */
2092
+ getRouterStats(): ReturnType<PartitionRouter['getStats']>;
2093
+ /**
2094
+ * Get routing metrics for monitoring smart routing effectiveness.
2095
+ */
2096
+ getRoutingMetrics(): RoutingMetrics;
2097
+ /**
2098
+ * Reset routing metrics counters.
2099
+ * Useful for monitoring intervals.
2100
+ */
2101
+ resetRoutingMetrics(): void;
2102
+ /**
2103
+ * Check if cluster routing is active
2104
+ */
2105
+ isRoutingActive(): boolean;
2106
+ /**
2107
+ * Get list of connected nodes
2108
+ */
2109
+ getConnectedNodes(): string[];
2110
+ /**
2111
+ * Check if cluster client is initialized
2112
+ */
2113
+ isInitialized(): boolean;
2114
+ /**
2115
+ * Force refresh of partition map
2116
+ */
2117
+ refreshPartitionMap(): Promise<void>;
2118
+ /**
2119
+ * Shutdown cluster client (IConnectionProvider interface).
2120
+ */
2121
+ close(): Promise<void>;
2122
+ /**
2123
+ * Get the connection pool (for internal use)
2124
+ */
2125
+ getConnectionPool(): ConnectionPool;
2126
+ /**
2127
+ * Get the partition router (for internal use)
2128
+ */
2129
+ getPartitionRouter(): PartitionRouter;
2130
+ /**
2131
+ * Get any healthy WebSocket connection (IConnectionProvider interface).
2132
+ * @throws Error if not connected
2133
+ */
2134
+ getAnyConnection(): WebSocket;
2135
+ /**
2136
+ * Get any healthy WebSocket connection, or null if none available.
2137
+ * Use this for optional connection checks.
2138
+ */
2139
+ getAnyConnectionOrNull(): WebSocket | null;
2140
+ /**
2141
+ * Get circuit breaker state for a node.
2142
+ */
2143
+ getCircuit(nodeId: string): CircuitState;
2144
+ /**
2145
+ * Check if a node can be used (circuit not open).
2146
+ */
2147
+ canUseNode(nodeId: string): boolean;
2148
+ /**
2149
+ * Record a successful operation to a node.
2150
+ * Resets circuit breaker on success.
2151
+ */
2152
+ recordSuccess(nodeId: string): void;
2153
+ /**
2154
+ * Record a failed operation to a node.
2155
+ * Opens circuit breaker after threshold failures.
2156
+ */
2157
+ recordFailure(nodeId: string): void;
2158
+ /**
2159
+ * Get all circuit breaker states.
2160
+ */
2161
+ getCircuitStates(): Map<string, CircuitState>;
2162
+ /**
2163
+ * Reset circuit breaker for a specific node.
2164
+ */
2165
+ resetCircuit(nodeId: string): void;
2166
+ /**
2167
+ * Reset all circuit breakers.
2168
+ */
2169
+ resetAllCircuits(): void;
2170
+ private setupEventHandlers;
2171
+ private waitForPartitionMap;
2172
+ }
2173
+
2174
+ /**
2175
+ * SingleServerProvider implements IConnectionProvider for single-server mode.
2176
+ *
2177
+ * This is an adapter that wraps direct WebSocket connection handling,
2178
+ * providing the same interface used by ClusterClient for multi-node mode.
2179
+ */
2180
+ declare class SingleServerProvider implements IConnectionProvider {
2181
+ private readonly url;
2182
+ private readonly config;
2183
+ private ws;
2184
+ private reconnectAttempts;
2185
+ private reconnectTimer;
2186
+ private isClosing;
2187
+ private listeners;
2188
+ constructor(config: SingleServerProviderConfig);
2189
+ /**
2190
+ * Connect to the WebSocket server.
2191
+ */
2192
+ connect(): Promise<void>;
2193
+ /**
2194
+ * Get connection for a specific key.
2195
+ * In single-server mode, key is ignored.
2196
+ */
2197
+ getConnection(_key: string): WebSocket;
2198
+ /**
2199
+ * Get any available connection.
2200
+ */
2201
+ getAnyConnection(): WebSocket;
2202
+ /**
2203
+ * Check if connected.
2204
+ */
2205
+ isConnected(): boolean;
2206
+ /**
2207
+ * Get connected node IDs.
2208
+ * Single-server mode returns ['default'] when connected.
2209
+ */
2210
+ getConnectedNodes(): string[];
2211
+ /**
2212
+ * Subscribe to connection events.
2213
+ */
2214
+ on(event: ConnectionProviderEvent, handler: ConnectionEventHandler): void;
2215
+ /**
2216
+ * Unsubscribe from connection events.
2217
+ */
2218
+ off(event: ConnectionProviderEvent, handler: ConnectionEventHandler): void;
2219
+ /**
2220
+ * Send data via the WebSocket connection.
2221
+ * In single-server mode, key parameter is ignored.
2222
+ */
2223
+ send(data: ArrayBuffer | Uint8Array, _key?: string): void;
2224
+ /**
2225
+ * Close the WebSocket connection.
2226
+ */
2227
+ close(): Promise<void>;
2228
+ /**
2229
+ * Emit an event to all listeners.
2230
+ */
2231
+ private emit;
2232
+ /**
2233
+ * Schedule a reconnection attempt with exponential backoff.
2234
+ */
2235
+ private scheduleReconnect;
2236
+ /**
2237
+ * Calculate backoff delay with exponential increase.
2238
+ */
2239
+ private calculateBackoffDelay;
2240
+ /**
2241
+ * Get the WebSocket URL this provider connects to.
2242
+ */
2243
+ getUrl(): string;
2244
+ /**
2245
+ * Get current reconnection attempt count.
2246
+ */
2247
+ getReconnectAttempts(): number;
2248
+ /**
2249
+ * Reset reconnection counter.
2250
+ * Called externally after successful authentication.
2251
+ */
2252
+ resetReconnectAttempts(): void;
2253
+ }
2254
+
800
2255
  declare const logger: pino.Logger<never, boolean>;
801
2256
 
802
- export { type BackoffConfig, type BackpressureConfig, BackpressureError, type BackpressureStatus, type BackpressureStrategy, type BackpressureThresholdEvent, DEFAULT_BACKPRESSURE_CONFIG, EncryptedStorageAdapter, type HeartbeatConfig, IDBAdapter, type IStorageAdapter, type OpLogEntry, type OperationDroppedEvent, type QueryFilter, QueryHandle, type QueryResultItem, type QueryResultSource, type StateChangeEvent, type StateChangeListener, SyncEngine, type SyncEngineConfig, SyncState, SyncStateMachine, type SyncStateMachineConfig, TopGun, TopGunClient, type TopicCallback, TopicHandle, VALID_TRANSITIONS, isValidTransition, logger };
2257
+ export { type BackoffConfig, type BackpressureConfig, BackpressureError, type BackpressureStatus, type BackpressureStrategy, type BackpressureThresholdEvent, type ChangeEvent, ChangeTracker, type CircuitState, ClusterClient, type ClusterClientEvents, type ClusterRoutingMode, ConflictResolverClient, type ConnectionEventHandler, ConnectionPool, type ConnectionPoolEvents, type ConnectionProviderEvent, DEFAULT_BACKPRESSURE_CONFIG, DEFAULT_CLUSTER_CONFIG, EncryptedStorageAdapter, EventJournalReader, type HeartbeatConfig, type IConnectionProvider, IDBAdapter, type IStorageAdapter, type JournalEventData, type JournalSubscribeOptions, type OpLogEntry, type OperationDroppedEvent, PNCounterHandle, PartitionRouter, type PartitionRouterEvents, type QueryFilter, QueryHandle, type QueryResultItem, type QueryResultSource, type RegisterResult, type ResolverInfo, type RoutingMetrics, type RoutingResult, SingleServerProvider, type SingleServerProviderConfig, type StateChangeEvent, type StateChangeListener, SyncEngine, type SyncEngineConfig, SyncState, SyncStateMachine, type SyncStateMachineConfig, TopGun, TopGunClient, type TopGunClientConfig, type TopGunClusterConfig, type TopicCallback, TopicHandle, VALID_TRANSITIONS, isValidTransition, logger };