@rljson/server 0.0.4 → 0.0.6

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.
@@ -11,6 +11,7 @@ found in the LICENSE file in the root of this package.
11
11
  ## Table of contents <!-- omit in toc -->
12
12
 
13
13
  - [Vscode Windows: Debugging is not working](#vscode-windows-debugging-is-not-working)
14
+ - [Test Isolation: Socket.IO event listener accumulation](#test-isolation-socketio-event-listener-accumulation)
14
15
 
15
16
  ## Vscode Windows: Debugging is not working
16
17
 
@@ -21,3 +22,52 @@ in the VS Code Vitest extension (v1.14.4), which prevents test debugging from
21
22
  working: <https://github.com/vitest-dev/vscode/issues/548> Please check from
22
23
  time to time if the issue has been fixed and remove this note once it is
23
24
  resolved.
25
+
26
+ ## Test Isolation: Socket.IO event listener accumulation
27
+
28
+ Date: 2025-01-28
29
+
30
+ **Problem:**
31
+
32
+ When running multiple tests that use Socket.IO connections, tests pass individually but fail when run together. This is caused by event listeners from previous tests remaining active on persistent socket instances.
33
+
34
+ **Symptoms:**
35
+
36
+ - Individual tests pass: ✅
37
+ - All tests together fail: ❌
38
+ - Error messages like "received 0 instead of expected number of nodes"
39
+ - Unexpected behavior when sockets receive messages from previous tests
40
+
41
+ **Root Cause:**
42
+
43
+ Socket.IO sockets persist across tests in the `beforeAll` setup. When `SocketIoBridge` instances are created in `beforeEach`, old event listeners accumulate on the underlying sockets, causing interference between tests.
44
+
45
+ **Solution:**
46
+
47
+ Clear all event listeners in `beforeEach` before creating new bridges:
48
+
49
+ ```typescript
50
+ beforeEach(async () => {
51
+ // Remove all event listeners from previous test to prevent interference
52
+ serverSockets.forEach((socket) => socket.removeAllListeners());
53
+ clientSockets.forEach((socket) => socket.removeAllListeners());
54
+
55
+ // Now proceed with test setup...
56
+ server = new Server(route, serverIo, serverBs);
57
+ await server.init();
58
+ // ... rest of setup
59
+ });
60
+ ```
61
+
62
+ **Why This Works:**
63
+
64
+ - `removeAllListeners()` clears accumulated event handlers
65
+ - Each test starts with clean sockets
66
+ - No interference from previous test's `SocketIoBridge` instances
67
+ - Maintains socket connections established in `beforeAll`
68
+
69
+ **Alternative Approaches Considered:**
70
+
71
+ 1. ❌ `tearDown()` in `afterEach`: Caused hook timeouts
72
+ 2. ❌ Creating new socket connections per test: Too slow, defeats purpose of `beforeAll`
73
+ 3. ✅ Clear listeners while reusing connections: Fast and reliable
package/dist/client.d.ts CHANGED
@@ -1,21 +1,59 @@
1
1
  import { Bs } from '@rljson/bs';
2
- import { Io, IoMulti, Socket } from '@rljson/io';
2
+ import { Connector, Db } from '@rljson/db';
3
+ import { Io, IoMulti } from '@rljson/io';
4
+ import { ClientId, Route, SyncConfig } from '@rljson/rljson';
3
5
  import { BaseNode } from './base-node.ts';
6
+ import { ServerLogger } from './logger.ts';
7
+ import { SocketLike } from './socket-bundle.ts';
8
+ /**
9
+ * Options for the Client constructor.
10
+ */
11
+ export interface ClientOptions {
12
+ /** Logger instance for monitoring (defaults to NoopLogger). */
13
+ logger?: ServerLogger;
14
+ /**
15
+ * Sync protocol configuration. When provided, the Connector created
16
+ * by the client will use enriched payloads (sequence numbers, causal
17
+ * ordering, ACK support, client identity).
18
+ */
19
+ syncConfig?: SyncConfig;
20
+ /**
21
+ * Stable client identity. When provided, this identity is passed
22
+ * to the Connector. When omitted but `syncConfig.includeClientIdentity`
23
+ * is true, a new identity is auto-generated.
24
+ */
25
+ clientIdentity?: ClientId;
26
+ /**
27
+ * Timeout in milliseconds for peer initialization during init().
28
+ * If an Io or Bs peer does not respond within this window, init()
29
+ * rejects. Defaults to 30 000 (30 s). Set to 0 to disable the timeout.
30
+ */
31
+ peerInitTimeoutMs?: number;
32
+ }
4
33
  export declare class Client extends BaseNode {
5
34
  private _socketToServer;
6
35
  protected _localIo: Io;
7
36
  protected _localBs: Bs;
37
+ private _route?;
8
38
  private _ioMultiIos;
9
39
  private _ioMulti?;
10
40
  private _bsMultiBss;
11
41
  private _bsMulti?;
42
+ private _db?;
43
+ private _connector?;
44
+ private _logger;
45
+ private _syncConfig?;
46
+ private _clientIdentity?;
47
+ private _peerInitTimeoutMs;
12
48
  /**
13
49
  * Creates a Client instance
14
- * @param _socketToServer - Socket to connect to server
50
+ * @param _socketToServer - Socket or namespace bundle to connect to server
15
51
  * @param _localIo - Local Io for local storage
16
52
  * @param _localBs - Local Bs for local blob storage
53
+ * @param _route - Optional route for automatic Db and Connector creation
54
+ * @param options - Optional configuration including logger for monitoring
17
55
  */
18
- constructor(_socketToServer: Socket, _localIo: Io, _localBs: Bs);
56
+ constructor(_socketToServer: SocketLike, _localIo: Io, _localBs: Bs, _route?: Route | undefined, options?: ClientOptions);
19
57
  /**
20
58
  * Initializes Io and Bs multis and their peer bridges.
21
59
  * @returns The initialized Io implementation.
@@ -37,6 +75,27 @@ export declare class Client extends BaseNode {
37
75
  * Returns the Bs implementation.
38
76
  */
39
77
  get bs(): Bs | undefined;
78
+ /**
79
+ * Returns the Db instance (available when route was provided).
80
+ */
81
+ get db(): Db | undefined;
82
+ /**
83
+ * Returns the Connector instance (available when route was provided).
84
+ */
85
+ get connector(): Connector | undefined;
86
+ /**
87
+ * Returns the route (if provided).
88
+ */
89
+ get route(): Route | undefined;
90
+ /**
91
+ * Returns the logger instance.
92
+ */
93
+ get logger(): ServerLogger;
94
+ /**
95
+ * Creates Db and Connector from the route and IoMulti.
96
+ * Called during init() when a route was provided.
97
+ */
98
+ private _setupDbAndConnector;
40
99
  /**
41
100
  * Builds the Io multi with local and peer layers.
42
101
  */
@@ -47,10 +106,23 @@ export declare class Client extends BaseNode {
47
106
  private _setupBs;
48
107
  /**
49
108
  * Creates and initializes a downstream Io peer.
109
+ * @param socket - Downstream socket to the server Io namespace.
50
110
  */
51
111
  private _createIoPeer;
52
112
  /**
53
113
  * Creates and initializes a downstream Bs peer.
114
+ * @param socket - Downstream socket to the server Bs namespace.
54
115
  */
55
116
  private _createBsPeer;
117
+ /**
118
+ * Returns the configured peer init timeout in milliseconds.
119
+ */
120
+ get peerInitTimeoutMs(): number;
121
+ /**
122
+ * Races a promise against a timeout. Resolves/rejects with the original
123
+ * promise outcome if it settles first, or rejects with a timeout error.
124
+ * @param promise - The promise to race.
125
+ * @param label - Human-readable label for timeout error messages.
126
+ */
127
+ private _withTimeout;
56
128
  }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,9 @@
1
1
  export { Client } from './client.ts';
2
+ export type { ClientOptions } from './client.ts';
3
+ export { BufferedLogger, ConsoleLogger, FilteredLogger, NoopLogger, noopLogger, } from './logger.ts';
4
+ export type { LogEntry, ServerLogger } from './logger.ts';
2
5
  export { Server } from './server.ts';
6
+ export type { ServerOptions } from './server.ts';
3
7
  export { SocketIoBridge } from './socket-io-bridge.ts';
8
+ export type { AckPayload, ConnectorPayload, GapFillRequest, GapFillResponse, SyncConfig, SyncEventNames, } from '@rljson/rljson';
9
+ export { syncEvents } from '@rljson/rljson';
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Logger interface for monitoring Client/Server lifecycle, errors,
3
+ * and network traffic. Implementations can be injected via options.
4
+ *
5
+ * Use `NoopLogger` (default) in production for zero overhead.
6
+ * Use `ConsoleLogger` or `BufferedLogger` for development/testing.
7
+ */
8
+ export interface ServerLogger {
9
+ /**
10
+ * Informational messages (lifecycle events, state changes).
11
+ * @param source - Component identifier (e.g., 'Server', 'Client.Io')
12
+ * @param message - Human-readable message
13
+ * @param data - Optional structured context
14
+ */
15
+ info(source: string, message: string, data?: Record<string, unknown>): void;
16
+ /**
17
+ * Warning messages (suppressed duplicates, loop prevention).
18
+ * @param source - Component identifier
19
+ * @param message - Human-readable message
20
+ * @param data - Optional structured context
21
+ */
22
+ warn(source: string, message: string, data?: Record<string, unknown>): void;
23
+ /**
24
+ * Error messages (failures during init, multicast, peer creation).
25
+ * @param source - Component identifier
26
+ * @param message - Human-readable message
27
+ * @param error - The caught error or unknown value
28
+ * @param data - Optional structured context
29
+ */
30
+ error(source: string, message: string, error?: unknown, data?: Record<string, unknown>): void;
31
+ /**
32
+ * Network traffic messages (socket emit/on events).
33
+ * @param direction - 'in' for received, 'out' for sent
34
+ * @param source - Component identifier
35
+ * @param event - Socket event name
36
+ * @param data - Optional structured context (ref, clientId, etc.)
37
+ */
38
+ traffic(direction: 'in' | 'out', source: string, event: string, data?: Record<string, unknown>): void;
39
+ }
40
+ /**
41
+ * No-op logger. All methods are empty. Zero overhead in production.
42
+ * This is the default logger when none is provided.
43
+ */
44
+ export declare class NoopLogger implements ServerLogger {
45
+ info(..._args: Parameters<ServerLogger['info']>): void;
46
+ warn(..._args: Parameters<ServerLogger['warn']>): void;
47
+ error(..._args: Parameters<ServerLogger['error']>): void;
48
+ traffic(..._args: Parameters<ServerLogger['traffic']>): void;
49
+ }
50
+ /**
51
+ * Logs all events to console. Useful for development and debugging.
52
+ * Data is serialized inline as compact JSON for single-line output.
53
+ */
54
+ export declare class ConsoleLogger implements ServerLogger {
55
+ private _fmt;
56
+ info(source: string, message: string, data?: Record<string, unknown>): void;
57
+ warn(source: string, message: string, data?: Record<string, unknown>): void;
58
+ error(source: string, message: string, error?: unknown, data?: Record<string, unknown>): void;
59
+ traffic(direction: 'in' | 'out', source: string, event: string, data?: Record<string, unknown>): void;
60
+ }
61
+ /**
62
+ * Log entry stored by BufferedLogger.
63
+ */
64
+ export interface LogEntry {
65
+ level: 'info' | 'warn' | 'error' | 'traffic';
66
+ source: string;
67
+ message: string;
68
+ error?: unknown;
69
+ data?: Record<string, unknown>;
70
+ direction?: 'in' | 'out';
71
+ event?: string;
72
+ timestamp: number;
73
+ }
74
+ /**
75
+ * Stores log entries in memory. Useful for test assertions.
76
+ */
77
+ export declare class BufferedLogger implements ServerLogger {
78
+ readonly entries: LogEntry[];
79
+ info(source: string, message: string, data?: Record<string, unknown>): void;
80
+ warn(source: string, message: string, data?: Record<string, unknown>): void;
81
+ error(source: string, message: string, error?: unknown, data?: Record<string, unknown>): void;
82
+ traffic(direction: 'in' | 'out', source: string, event: string, data?: Record<string, unknown>): void;
83
+ /**
84
+ * Returns entries filtered by level.
85
+ * @param level - The log level to filter by
86
+ */
87
+ byLevel(level: LogEntry['level']): LogEntry[];
88
+ /**
89
+ * Returns entries filtered by source (substring match).
90
+ * @param source - The source substring to match
91
+ */
92
+ bySource(source: string): LogEntry[];
93
+ /**
94
+ * Clears all stored entries.
95
+ */
96
+ clear(): void;
97
+ }
98
+ /**
99
+ * Wraps another logger and filters entries by level and/or source.
100
+ */
101
+ export declare class FilteredLogger implements ServerLogger {
102
+ private _inner;
103
+ private _filter;
104
+ constructor(_inner: ServerLogger, _filter?: {
105
+ levels?: Array<'info' | 'warn' | 'error' | 'traffic'>;
106
+ sources?: string[];
107
+ });
108
+ private _shouldLog;
109
+ info(source: string, message: string, data?: Record<string, unknown>): void;
110
+ warn(source: string, message: string, data?: Record<string, unknown>): void;
111
+ error(source: string, message: string, error?: unknown, data?: Record<string, unknown>): void;
112
+ traffic(direction: 'in' | 'out', source: string, event: string, data?: Record<string, unknown>): void;
113
+ }
114
+ /** Shared no-op instance to avoid repeated allocations. */
115
+ export declare const noopLogger: ServerLogger;
package/dist/server.d.ts CHANGED
@@ -1,10 +1,47 @@
1
1
  import { Bs, BsPeer } from '@rljson/bs';
2
2
  import { Io, IoPeer, Socket } from '@rljson/io';
3
- import { Route } from '@rljson/rljson';
3
+ import { ConnectorPayload, Route, SyncConfig, SyncEventNames } from '@rljson/rljson';
4
4
  import { BaseNode } from './base-node.ts';
5
+ import { ServerLogger } from './logger.ts';
6
+ import { SocketLike } from './socket-bundle.ts';
5
7
  export type SocketWithClientId = Socket & {
6
8
  __clientId?: string;
7
9
  };
10
+ /**
11
+ * Options for the Server constructor.
12
+ */
13
+ export interface ServerOptions {
14
+ /** Logger instance for monitoring (defaults to NoopLogger). */
15
+ logger?: ServerLogger;
16
+ /**
17
+ * Interval in milliseconds for evicting stale multicast ref entries.
18
+ * Uses a two-generation sweep: refs older than two intervals are discarded.
19
+ * Defaults to 60 000 (60 s). Set to 0 to disable automatic eviction.
20
+ */
21
+ refEvictionIntervalMs?: number;
22
+ /**
23
+ * Timeout in milliseconds for peer initialization during addSocket().
24
+ * If a peer does not respond within this window, addSocket() rejects.
25
+ * Defaults to 30 000 (30 s). Set to 0 to disable the timeout.
26
+ */
27
+ peerInitTimeoutMs?: number;
28
+ /**
29
+ * Sync protocol configuration. When provided, the server activates
30
+ * ACK aggregation, gap-fill response, and enriched payload forwarding.
31
+ */
32
+ syncConfig?: SyncConfig;
33
+ /**
34
+ * Maximum number of recent payloads to retain in the ref log
35
+ * for gap-fill responses. Defaults to 1000.
36
+ */
37
+ refLogSize?: number;
38
+ /**
39
+ * Timeout in milliseconds for collecting individual client ACKs
40
+ * before emitting the aggregated AckPayload back to the sender.
41
+ * Defaults to the SyncConfig's ackTimeoutMs (or 10 000 ms).
42
+ */
43
+ ackTimeoutMs?: number;
44
+ }
8
45
  export declare class Server extends BaseNode {
9
46
  private _route;
10
47
  protected _localIo: Io;
@@ -16,10 +53,23 @@ export declare class Server extends BaseNode {
16
53
  private _bss;
17
54
  private _bsMulti;
18
55
  private _bsServer;
19
- private _multicastedRefs;
56
+ private _multicastedRefsCurrent;
57
+ private _multicastedRefsPrevious;
58
+ private _refEvictionTimer?;
20
59
  private _refreshPromise?;
21
60
  private _pendingSockets;
22
- constructor(_route: Route, _localIo: Io, _localBs: Bs);
61
+ private _logger;
62
+ private _peerInitTimeoutMs;
63
+ private _disconnectCleanups;
64
+ private _syncConfig;
65
+ private _events;
66
+ private _refLog;
67
+ private _refLogSize;
68
+ private _ackTimeoutMs;
69
+ private _latestRef;
70
+ private _bootstrapHeartbeatTimer?;
71
+ private _tornDown;
72
+ constructor(_route: Route, _localIo: Io, _localBs: Bs, options?: ServerOptions);
23
73
  /**
24
74
  * Initializes Io and Bs multis on the server.
25
75
  */
@@ -33,17 +83,22 @@ export declare class Server extends BaseNode {
33
83
  * @param socket - Client socket to register.
34
84
  * @returns The server instance.
35
85
  */
36
- addSocket(socket: Socket): Promise<this>;
86
+ addSocket(socket: SocketLike): Promise<this>;
37
87
  /**
38
88
  * Removes all listeners from all connected clients.
39
89
  */
40
90
  private _removeAllListeners;
41
91
  /**
42
92
  * Broadcasts incoming payloads from any client to all other connected clients.
43
- * Ensures the sender is filtered out when broadcasting.
93
+ * Enriched with ref log, ACK aggregation, and gap-fill support when
94
+ * syncConfig is provided.
44
95
  */
45
96
  private _multicastRefs;
46
97
  get route(): Route;
98
+ /**
99
+ * Returns the logger instance.
100
+ */
101
+ get logger(): ServerLogger;
47
102
  /**
48
103
  * Returns the Io implementation.
49
104
  */
@@ -56,24 +111,82 @@ export declare class Server extends BaseNode {
56
111
  * Returns the connected clients map.
57
112
  */
58
113
  get clients(): Map<string, {
59
- socket: SocketWithClientId;
114
+ ioUp: SocketWithClientId;
115
+ ioDown: SocketWithClientId;
116
+ bsUp: SocketWithClientId;
117
+ bsDown: SocketWithClientId;
60
118
  io: IoPeer;
61
119
  bs: BsPeer;
62
120
  }>;
121
+ /**
122
+ * Returns the sync configuration, if any.
123
+ */
124
+ get syncConfig(): SyncConfig | undefined;
125
+ /**
126
+ * Returns the typed sync event names.
127
+ */
128
+ get events(): SyncEventNames;
129
+ /**
130
+ * Returns the current ref log contents (for diagnostics / testing).
131
+ */
132
+ get refLog(): readonly ConnectorPayload[];
133
+ /**
134
+ * Returns the latest ref tracked by the server (for bootstrap / diagnostics).
135
+ */
136
+ get latestRef(): string | undefined;
137
+ /**
138
+ * Appends a payload to the bounded ref log (ring buffer).
139
+ * Drops the oldest entry when the log exceeds `_refLogSize`.
140
+ * @param payload - The ConnectorPayload to append.
141
+ */
142
+ private _appendToRefLog;
143
+ /**
144
+ * Sends the latest ref to a specific client socket as a bootstrap message.
145
+ * If no ref has been seen yet, this is a no-op.
146
+ * @param ioDown - The downstream socket to send the bootstrap message on.
147
+ */
148
+ private _sendBootstrap;
149
+ /**
150
+ * Broadcasts the latest ref to all connected clients as a heartbeat.
151
+ * Each client's dedup pipeline will filter out refs it already has.
152
+ */
153
+ private _broadcastBootstrapHeartbeat;
154
+ /**
155
+ * Starts the periodic bootstrap heartbeat timer if configured
156
+ * and not already running.
157
+ */
158
+ private _startBootstrapHeartbeat;
159
+ /**
160
+ * Sets up ACK collection listeners before broadcast.
161
+ * Returns a cleanup function that emits an immediate ACK
162
+ * (used when there are no receivers).
163
+ * @param senderClientId - The internal client ID of the sender.
164
+ * @param ref - The ref being acknowledged.
165
+ * @returns A function to call for immediate ACK (zero receivers).
166
+ */
167
+ private _setupAckCollection;
168
+ /**
169
+ * Registers a gap-fill request listener for a specific client socket.
170
+ * @param clientId - The internal client ID.
171
+ * @param socket - The upstream socket to listen on.
172
+ */
173
+ private _registerGapFillListener;
63
174
  /**
64
175
  * Creates and initializes a downstream Io peer for a socket.
65
176
  * @param socket - Client socket to bind the peer to.
177
+ * @param clientId - Client identifier for logging.
66
178
  */
67
179
  private _createIoPeer;
68
180
  /**
69
181
  * Creates and initializes a downstream Bs peer for a socket.
70
182
  * @param socket - Client socket to bind the peer to.
183
+ * @param clientId - Client identifier for logging.
71
184
  */
72
185
  private _createBsPeer;
73
186
  /**
74
187
  * Registers the client socket and peers.
75
188
  * @param clientId - Stable client identifier.
76
- * @param socket - Client socket to register.
189
+ * @param sockets - Directional sockets to register.
77
190
  * @param io - Io peer associated with the client.
78
191
  * @param bs - Bs peer associated with the client.
79
192
  */
@@ -100,6 +213,34 @@ export declare class Server extends BaseNode {
100
213
  * Batches multi/server refreshes into a single queued task.
101
214
  */
102
215
  private _queueRefresh;
216
+ /**
217
+ * Removes a connected client by its internal client ID.
218
+ * Cleans up listeners, peers, and rebuilds multis.
219
+ * @param clientId - The client identifier (from server.clients keys).
220
+ */
221
+ removeSocket(clientId: string): Promise<void>;
222
+ /**
223
+ * Gracefully shuts down the server: stops timers, removes listeners,
224
+ * clears all client state, and closes storage layers.
225
+ */
226
+ tearDown(): Promise<void>;
227
+ /**
228
+ * Whether the server has been torn down.
229
+ */
230
+ get isTornDown(): boolean;
231
+ /**
232
+ * Registers a listener that auto-removes the client on socket disconnect.
233
+ * @param clientId - Client identifier.
234
+ * @param socket - The upstream socket to listen on.
235
+ */
236
+ private _registerDisconnectHandler;
237
+ /**
238
+ * Races a promise against a timeout. Resolves/rejects with the original
239
+ * promise outcome if it settles first, or rejects with a timeout error.
240
+ * @param promise - The promise to race.
241
+ * @param label - Human-readable label for timeout error messages.
242
+ */
243
+ private _withTimeout;
103
244
  /** Example instance for test purposes */
104
245
  static example(): Promise<Server>;
105
246
  }