@topgunbuild/client 0.1.0 → 0.2.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.ts CHANGED
@@ -113,21 +113,231 @@ declare class TopicHandle {
113
113
  }): void;
114
114
  }
115
115
 
116
+ /**
117
+ * Defines the possible states for the SyncEngine connection state machine.
118
+ */
119
+ declare enum SyncState {
120
+ /** Initial state before any connection attempt */
121
+ INITIAL = "INITIAL",
122
+ /** WebSocket connection is being established */
123
+ CONNECTING = "CONNECTING",
124
+ /** Connected, waiting for authentication response */
125
+ AUTHENTICATING = "AUTHENTICATING",
126
+ /** Authenticated, performing initial data sync */
127
+ SYNCING = "SYNCING",
128
+ /** Fully connected and synchronized */
129
+ CONNECTED = "CONNECTED",
130
+ /** Intentionally or unexpectedly disconnected */
131
+ DISCONNECTED = "DISCONNECTED",
132
+ /** Waiting before retry (exponential backoff) */
133
+ BACKOFF = "BACKOFF",
134
+ /** Fatal error requiring manual intervention or reset */
135
+ ERROR = "ERROR"
136
+ }
137
+ /**
138
+ * Defines valid state transitions for the SyncEngine FSM.
139
+ * Each key is a current state, and the value is an array of valid target states.
140
+ */
141
+ declare const VALID_TRANSITIONS: Record<SyncState, SyncState[]>;
142
+ /**
143
+ * Helper function to check if a transition is valid
144
+ */
145
+ declare function isValidTransition(from: SyncState, to: SyncState): boolean;
146
+
147
+ /**
148
+ * Event emitted when the state machine transitions between states.
149
+ */
150
+ interface StateChangeEvent {
151
+ /** The state before the transition */
152
+ from: SyncState;
153
+ /** The state after the transition */
154
+ to: SyncState;
155
+ /** Unix timestamp (ms) when the transition occurred */
156
+ timestamp: number;
157
+ }
158
+ /**
159
+ * Listener callback for state change events.
160
+ */
161
+ type StateChangeListener = (event: StateChangeEvent) => void;
162
+ /**
163
+ * Configuration options for the state machine.
164
+ */
165
+ interface SyncStateMachineConfig {
166
+ /** Maximum number of state transitions to keep in history (default: 50) */
167
+ maxHistorySize?: number;
168
+ }
169
+ /**
170
+ * A finite state machine for managing SyncEngine connection states.
171
+ *
172
+ * Features:
173
+ * - Validates all state transitions against allowed paths
174
+ * - Emits events on state changes for observability
175
+ * - Maintains a history of transitions for debugging
176
+ * - Logs invalid transition attempts (graceful degradation)
177
+ */
178
+ declare class SyncStateMachine {
179
+ private state;
180
+ private readonly listeners;
181
+ private history;
182
+ private readonly maxHistorySize;
183
+ constructor(config?: SyncStateMachineConfig);
184
+ /**
185
+ * Attempt to transition to a new state.
186
+ * @param to The target state
187
+ * @returns true if the transition was valid and executed, false otherwise
188
+ */
189
+ transition(to: SyncState): boolean;
190
+ /**
191
+ * Get the current state.
192
+ */
193
+ getState(): SyncState;
194
+ /**
195
+ * Check if a transition from the current state to the target state is valid.
196
+ * @param to The target state to check
197
+ */
198
+ canTransition(to: SyncState): boolean;
199
+ /**
200
+ * Subscribe to state change events.
201
+ * @param listener Callback function to be called on each state change
202
+ * @returns An unsubscribe function
203
+ */
204
+ onStateChange(listener: StateChangeListener): () => void;
205
+ /**
206
+ * Get the state transition history.
207
+ * @param limit Maximum number of entries to return (default: all)
208
+ * @returns Array of state change events, oldest first
209
+ */
210
+ getHistory(limit?: number): StateChangeEvent[];
211
+ /**
212
+ * Reset the state machine to INITIAL state.
213
+ * This is a forced reset that bypasses normal transition validation.
214
+ * Use for testing or hard resets after fatal errors.
215
+ * @param clearHistory If true, also clears the transition history (default: true)
216
+ */
217
+ reset(clearHistory?: boolean): void;
218
+ /**
219
+ * Check if the state machine is in a "connected" state
220
+ * (either SYNCING or CONNECTED)
221
+ */
222
+ isConnected(): boolean;
223
+ /**
224
+ * Check if the state machine is in a state where operations can be sent
225
+ * (authenticated and connected)
226
+ */
227
+ isReady(): boolean;
228
+ /**
229
+ * Check if the state machine is currently attempting to connect
230
+ */
231
+ isConnecting(): boolean;
232
+ }
233
+
234
+ /**
235
+ * Backpressure strategy when maxPendingOps is reached.
236
+ */
237
+ type BackpressureStrategy = 'pause' | 'throw' | 'drop-oldest';
238
+ /**
239
+ * Configuration for backpressure control on SyncEngine.
240
+ */
241
+ interface BackpressureConfig {
242
+ /**
243
+ * Maximum number of operations waiting for server acknowledgment.
244
+ * When this limit is reached, the configured strategy will be applied.
245
+ * @default 1000
246
+ */
247
+ maxPendingOps: number;
248
+ /**
249
+ * Strategy when maxPendingOps is reached:
250
+ * - 'pause': Wait for capacity (returns Promise that resolves when space available)
251
+ * - 'throw': Throw BackpressureError immediately
252
+ * - 'drop-oldest': Remove oldest pending op to make room (data loss!)
253
+ * @default 'pause'
254
+ */
255
+ strategy: BackpressureStrategy;
256
+ /**
257
+ * High water mark (percentage of maxPendingOps).
258
+ * Emit 'backpressure:high' event when reached.
259
+ * Value should be between 0 and 1.
260
+ * @default 0.8 (80%)
261
+ */
262
+ highWaterMark: number;
263
+ /**
264
+ * Low water mark (percentage of maxPendingOps).
265
+ * Resume paused writes and emit 'backpressure:low' when pending ops drop below this.
266
+ * Value should be between 0 and 1.
267
+ * @default 0.5 (50%)
268
+ */
269
+ lowWaterMark: number;
270
+ }
271
+ /**
272
+ * Default backpressure configuration.
273
+ */
274
+ declare const DEFAULT_BACKPRESSURE_CONFIG: BackpressureConfig;
275
+ /**
276
+ * Status of backpressure mechanism.
277
+ */
278
+ interface BackpressureStatus {
279
+ /** Current number of pending (unacknowledged) operations */
280
+ pending: number;
281
+ /** Maximum allowed pending operations */
282
+ max: number;
283
+ /** Percentage of capacity used (0-1) */
284
+ percentage: number;
285
+ /** Whether writes are currently paused due to backpressure */
286
+ isPaused: boolean;
287
+ /** Current backpressure strategy */
288
+ strategy: BackpressureStrategy;
289
+ }
290
+ /**
291
+ * Event data for backpressure:high and backpressure:low events.
292
+ */
293
+ interface BackpressureThresholdEvent {
294
+ pending: number;
295
+ max: number;
296
+ }
297
+ /**
298
+ * Event data for operation:dropped event.
299
+ */
300
+ interface OperationDroppedEvent {
301
+ opId: string;
302
+ mapName: string;
303
+ opType: string;
304
+ key: string;
305
+ }
306
+
307
+ interface HeartbeatConfig {
308
+ intervalMs: number;
309
+ timeoutMs: number;
310
+ enabled: boolean;
311
+ }
312
+ interface BackoffConfig {
313
+ /** Initial delay in milliseconds (default: 1000) */
314
+ initialDelayMs: number;
315
+ /** Maximum delay in milliseconds (default: 30000) */
316
+ maxDelayMs: number;
317
+ /** Multiplier for exponential backoff (default: 2) */
318
+ multiplier: number;
319
+ /** Whether to add random jitter to delay (default: true) */
320
+ jitter: boolean;
321
+ /** Maximum number of retry attempts before entering ERROR state (default: 10) */
322
+ maxRetries: number;
323
+ }
116
324
  interface SyncEngineConfig {
117
325
  nodeId: string;
118
326
  serverUrl: string;
119
327
  storageAdapter: IStorageAdapter;
120
328
  reconnectInterval?: number;
329
+ heartbeat?: Partial<HeartbeatConfig>;
330
+ backoff?: Partial<BackoffConfig>;
331
+ backpressure?: Partial<BackpressureConfig>;
121
332
  }
122
333
  declare class SyncEngine {
123
334
  private readonly nodeId;
124
335
  private readonly serverUrl;
125
336
  private readonly storageAdapter;
126
- private readonly reconnectInterval;
127
337
  private readonly hlc;
338
+ private readonly stateMachine;
339
+ private readonly backoffConfig;
128
340
  private websocket;
129
- private isOnline;
130
- private isAuthenticated;
131
341
  private opLog;
132
342
  private maps;
133
343
  private queries;
@@ -137,9 +347,49 @@ declare class SyncEngine {
137
347
  private reconnectTimer;
138
348
  private authToken;
139
349
  private tokenProvider;
350
+ private backoffAttempt;
351
+ private readonly heartbeatConfig;
352
+ private heartbeatInterval;
353
+ private lastPongReceived;
354
+ private lastRoundTripTime;
355
+ private readonly backpressureConfig;
356
+ private backpressurePaused;
357
+ private waitingForCapacity;
358
+ private highWaterMarkEmitted;
359
+ private backpressureListeners;
140
360
  constructor(config: SyncEngineConfig);
361
+ /**
362
+ * Get the current connection state
363
+ */
364
+ getConnectionState(): SyncState;
365
+ /**
366
+ * Subscribe to connection state changes
367
+ * @returns Unsubscribe function
368
+ */
369
+ onConnectionStateChange(listener: (event: StateChangeEvent) => void): () => void;
370
+ /**
371
+ * Get state machine history for debugging
372
+ */
373
+ getStateHistory(limit?: number): StateChangeEvent[];
374
+ /**
375
+ * Check if WebSocket is connected (but may not be authenticated yet)
376
+ */
377
+ private isOnline;
378
+ /**
379
+ * Check if fully authenticated and ready for operations
380
+ */
381
+ private isAuthenticated;
382
+ /**
383
+ * Check if fully connected and synced
384
+ */
385
+ private isConnected;
141
386
  private initConnection;
142
387
  private scheduleReconnect;
388
+ private calculateBackoffDelay;
389
+ /**
390
+ * Reset backoff counter (called on successful connection)
391
+ */
392
+ private resetBackoff;
143
393
  private loadOpLog;
144
394
  private saveOpLog;
145
395
  registerMap(mapName: string, map: LWWMap<any, any> | ORMap<any, any>): void;
@@ -148,7 +398,7 @@ declare class SyncEngine {
148
398
  orRecord?: ORMapRecord<any>;
149
399
  orTag?: string;
150
400
  timestamp: Timestamp;
151
- }): Promise<void>;
401
+ }): Promise<string>;
152
402
  private syncPendingOperations;
153
403
  private startMerkleSync;
154
404
  setAuthToken(token: string): void;
@@ -178,7 +428,92 @@ declare class SyncEngine {
178
428
  * Closes the WebSocket connection and cleans up resources.
179
429
  */
180
430
  close(): void;
431
+ /**
432
+ * Reset the state machine and connection.
433
+ * Use after fatal errors to start fresh.
434
+ */
435
+ resetConnection(): void;
181
436
  private resetMap;
437
+ /**
438
+ * Starts the heartbeat mechanism after successful connection.
439
+ */
440
+ private startHeartbeat;
441
+ /**
442
+ * Stops the heartbeat mechanism.
443
+ */
444
+ private stopHeartbeat;
445
+ /**
446
+ * Sends a PING message to the server.
447
+ */
448
+ private sendPing;
449
+ /**
450
+ * Handles incoming PONG message from server.
451
+ */
452
+ private handlePong;
453
+ /**
454
+ * Checks if heartbeat has timed out and triggers reconnection if needed.
455
+ */
456
+ private checkHeartbeatTimeout;
457
+ /**
458
+ * Returns the last measured round-trip time in milliseconds.
459
+ * Returns null if no PONG has been received yet.
460
+ */
461
+ getLastRoundTripTime(): number | null;
462
+ /**
463
+ * Returns true if the connection is considered healthy based on heartbeat.
464
+ * A connection is healthy if it's online, authenticated, and has received
465
+ * a PONG within the timeout window.
466
+ */
467
+ isConnectionHealthy(): boolean;
468
+ /**
469
+ * Push local ORMap diff to server for the given keys.
470
+ * Sends local records and tombstones that the server might not have.
471
+ */
472
+ private pushORMapDiff;
473
+ /**
474
+ * Get the current number of pending (unsynced) operations.
475
+ */
476
+ getPendingOpsCount(): number;
477
+ /**
478
+ * Get the current backpressure status.
479
+ */
480
+ getBackpressureStatus(): BackpressureStatus;
481
+ /**
482
+ * Returns true if writes are currently paused due to backpressure.
483
+ */
484
+ isBackpressurePaused(): boolean;
485
+ /**
486
+ * Subscribe to backpressure events.
487
+ * @param event Event name: 'backpressure:high', 'backpressure:low', 'backpressure:paused', 'backpressure:resumed', 'operation:dropped'
488
+ * @param listener Callback function
489
+ * @returns Unsubscribe function
490
+ */
491
+ onBackpressure(event: 'backpressure:high' | 'backpressure:low' | 'backpressure:paused' | 'backpressure:resumed' | 'operation:dropped', listener: (data?: BackpressureThresholdEvent | OperationDroppedEvent) => void): () => void;
492
+ /**
493
+ * Emit a backpressure event to all listeners.
494
+ */
495
+ private emitBackpressureEvent;
496
+ /**
497
+ * Check backpressure before adding a new operation.
498
+ * May pause, throw, or drop depending on strategy.
499
+ */
500
+ private checkBackpressure;
501
+ /**
502
+ * Check high water mark and emit event if threshold reached.
503
+ */
504
+ private checkHighWaterMark;
505
+ /**
506
+ * Check low water mark and resume paused writes if threshold reached.
507
+ */
508
+ private checkLowWaterMark;
509
+ /**
510
+ * Wait for capacity to become available (used by 'pause' strategy).
511
+ */
512
+ private waitForCapacity;
513
+ /**
514
+ * Drop the oldest pending operation (used by 'drop-oldest' strategy).
515
+ */
516
+ private dropOldestOp;
182
517
  }
183
518
 
184
519
  interface ILock {
@@ -207,6 +542,8 @@ declare class TopGunClient {
207
542
  nodeId?: string;
208
543
  serverUrl: string;
209
544
  storage: IStorageAdapter;
545
+ backoff?: Partial<BackoffConfig>;
546
+ backpressure?: Partial<BackpressureConfig>;
210
547
  });
211
548
  start(): Promise<void>;
212
549
  setAuthToken(token: string): void;
@@ -244,6 +581,68 @@ declare class TopGunClient {
244
581
  * Closes the client, disconnecting from the server and cleaning up resources.
245
582
  */
246
583
  close(): void;
584
+ /**
585
+ * Get the current connection state
586
+ */
587
+ getConnectionState(): SyncState;
588
+ /**
589
+ * Subscribe to connection state changes
590
+ * @param listener Callback function called on each state change
591
+ * @returns Unsubscribe function
592
+ */
593
+ onConnectionStateChange(listener: (event: StateChangeEvent) => void): () => void;
594
+ /**
595
+ * Get state machine history for debugging
596
+ * @param limit Maximum number of entries to return
597
+ */
598
+ getStateHistory(limit?: number): StateChangeEvent[];
599
+ /**
600
+ * Reset the connection and state machine.
601
+ * Use after fatal errors to start fresh.
602
+ */
603
+ resetConnection(): void;
604
+ /**
605
+ * Get the current number of pending (unacknowledged) operations.
606
+ */
607
+ getPendingOpsCount(): number;
608
+ /**
609
+ * Get the current backpressure status.
610
+ */
611
+ getBackpressureStatus(): BackpressureStatus;
612
+ /**
613
+ * Returns true if writes are currently paused due to backpressure.
614
+ */
615
+ isBackpressurePaused(): boolean;
616
+ /**
617
+ * Subscribe to backpressure events.
618
+ *
619
+ * Available events:
620
+ * - 'backpressure:high': Emitted when pending ops reach high water mark
621
+ * - 'backpressure:low': Emitted when pending ops drop below low water mark
622
+ * - 'backpressure:paused': Emitted when writes are paused (pause strategy)
623
+ * - 'backpressure:resumed': Emitted when writes resume after being paused
624
+ * - 'operation:dropped': Emitted when an operation is dropped (drop-oldest strategy)
625
+ *
626
+ * @param event Event name
627
+ * @param listener Callback function
628
+ * @returns Unsubscribe function
629
+ *
630
+ * @example
631
+ * ```typescript
632
+ * client.onBackpressure('backpressure:high', ({ pending, max }) => {
633
+ * console.warn(`Warning: ${pending}/${max} pending ops`);
634
+ * });
635
+ *
636
+ * client.onBackpressure('backpressure:paused', () => {
637
+ * showLoadingSpinner();
638
+ * });
639
+ *
640
+ * client.onBackpressure('backpressure:resumed', () => {
641
+ * hideLoadingSpinner();
642
+ * });
643
+ * ```
644
+ */
645
+ onBackpressure(event: 'backpressure:high' | 'backpressure:low' | 'backpressure:paused' | 'backpressure:resumed' | 'operation:dropped', listener: (data?: BackpressureThresholdEvent | OperationDroppedEvent) => void): () => void;
247
646
  }
248
647
 
249
648
  interface TopGunConfig {
@@ -362,6 +761,16 @@ declare class EncryptedStorageAdapter implements IStorageAdapter {
362
761
  private isEncryptedRecord;
363
762
  }
364
763
 
764
+ /**
765
+ * Error thrown when backpressure limit is reached and strategy is 'throw'.
766
+ */
767
+ declare class BackpressureError extends Error {
768
+ readonly pendingCount: number;
769
+ readonly maxPending: number;
770
+ readonly name = "BackpressureError";
771
+ constructor(pendingCount: number, maxPending: number);
772
+ }
773
+
365
774
  declare const logger: pino.Logger<never, boolean>;
366
775
 
367
- export { EncryptedStorageAdapter, IDBAdapter, type IStorageAdapter, type OpLogEntry, type QueryFilter, QueryHandle, type QueryResultItem, type QueryResultSource, SyncEngine, TopGun, TopGunClient, type TopicCallback, TopicHandle, logger };
776
+ 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 };