@rljson/server 0.0.5 → 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.
- package/README.architecture.md +247 -11
- package/README.md +11 -3
- package/README.public.md +401 -10
- package/dist/README.architecture.md +247 -11
- package/dist/README.md +11 -3
- package/dist/README.public.md +401 -10
- package/dist/client.d.ts +70 -1
- package/dist/index.d.ts +6 -0
- package/dist/logger.d.ts +115 -0
- package/dist/server.d.ts +141 -4
- package/dist/server.js +809 -101
- package/package.json +17 -17
package/dist/README.public.md
CHANGED
|
@@ -54,6 +54,10 @@ const route = Route.fromFlat('my.app');
|
|
|
54
54
|
const server = new Server(route, new IoMem(), new BsMem());
|
|
55
55
|
await server.init();
|
|
56
56
|
|
|
57
|
+
// Or with logging enabled:
|
|
58
|
+
// import { ConsoleLogger } from '@rljson/server';
|
|
59
|
+
// const server = new Server(route, new IoMem(), new BsMem(), { logger: new ConsoleLogger() });
|
|
60
|
+
|
|
57
61
|
socketIo.on('connection', async (socket) => {
|
|
58
62
|
await server.addSocket(new SocketIoBridge(socket));
|
|
59
63
|
});
|
|
@@ -67,16 +71,22 @@ Client setup:
|
|
|
67
71
|
import { io as socketIoClient } from 'socket.io-client';
|
|
68
72
|
import { BsMem } from '@rljson/bs';
|
|
69
73
|
import { IoMem } from '@rljson/io';
|
|
70
|
-
import {
|
|
74
|
+
import { Route } from '@rljson/rljson';
|
|
71
75
|
import { Client, SocketIoBridge } from '@rljson/server';
|
|
72
76
|
|
|
73
77
|
const socket = socketIoClient('http://localhost:3000', { forceNew: true });
|
|
74
78
|
|
|
75
|
-
|
|
79
|
+
// Pass the same route as the server to automatically create Db and Connector
|
|
80
|
+
const route = Route.fromFlat('my.app');
|
|
81
|
+
const client = new Client(new SocketIoBridge(socket), new IoMem(), new BsMem(), route);
|
|
76
82
|
await client.init();
|
|
77
83
|
|
|
78
|
-
|
|
79
|
-
//
|
|
84
|
+
// Or with logging enabled:
|
|
85
|
+
// import { ConsoleLogger } from '@rljson/server';
|
|
86
|
+
// const client = new Client(socket, io, bs, route, { logger: new ConsoleLogger() });
|
|
87
|
+
|
|
88
|
+
// client.db and client.connector are ready to use
|
|
89
|
+
// client.db.get/insert now cascade local ➜ server automatically
|
|
80
90
|
```
|
|
81
91
|
|
|
82
92
|
## Basic usage
|
|
@@ -108,6 +118,7 @@ await server.init();
|
|
|
108
118
|
```ts
|
|
109
119
|
import { BsMem } from '@rljson/bs';
|
|
110
120
|
import { IoMem } from '@rljson/io';
|
|
121
|
+
import { Route } from '@rljson/rljson';
|
|
111
122
|
|
|
112
123
|
import { Client } from '@rljson/server';
|
|
113
124
|
|
|
@@ -117,14 +128,20 @@ await localIo.isReady();
|
|
|
117
128
|
|
|
118
129
|
const localBs = new BsMem();
|
|
119
130
|
|
|
120
|
-
|
|
131
|
+
// With route: Db and Connector are created automatically
|
|
132
|
+
const route = Route.fromFlat('my.app.route');
|
|
133
|
+
const client = new Client(new SocketIoBridge(clientSocket), localIo, localBs, route);
|
|
121
134
|
await client.init();
|
|
122
135
|
|
|
123
136
|
// Unified interfaces
|
|
124
|
-
const io = client.io;
|
|
125
|
-
const bs = client.bs;
|
|
137
|
+
const io = client.io; // IoMulti (local + server)
|
|
138
|
+
const bs = client.bs; // BsMulti (local + server)
|
|
139
|
+
const db = client.db; // Db (wraps IoMulti)
|
|
140
|
+
const connector = client.connector; // Connector (wired to route + socket)
|
|
126
141
|
```
|
|
127
142
|
|
|
143
|
+
The `route` parameter is optional. Without it, the client only sets up `io` and `bs`, and `db`/`connector` will be `undefined`.
|
|
144
|
+
|
|
128
145
|
## How the layering works
|
|
129
146
|
|
|
130
147
|
Both client and server use a **multi-layer** approach:
|
|
@@ -138,19 +155,28 @@ This is implemented with `IoMulti` and `BsMulti` internally, but the public API
|
|
|
138
155
|
|
|
139
156
|
### Client
|
|
140
157
|
|
|
141
|
-
- `init()` – builds Io/Bs multis
|
|
158
|
+
- `init()` – builds Io/Bs multis, starts peer bridges, and (if route was provided) creates Db and Connector
|
|
142
159
|
- `ready()` – resolves once Io is ready
|
|
143
160
|
- `tearDown()` – closes and clears local state
|
|
144
161
|
- `io` – Io interface (multi-layer)
|
|
145
162
|
- `bs` – Bs interface (multi-layer)
|
|
163
|
+
- `db` – Db instance wrapping IoMulti (available when route was provided)
|
|
164
|
+
- `connector` – Connector wired to the route and socket (available when route was provided)
|
|
165
|
+
- `route` – the Route passed to the constructor
|
|
166
|
+
- `logger` – the `ServerLogger` instance (defaults to `noopLogger`)
|
|
146
167
|
|
|
147
168
|
### Server API
|
|
148
169
|
|
|
149
170
|
- `init()` – initializes server multis
|
|
150
171
|
- `ready()` – resolves when Io is ready
|
|
151
|
-
- `addSocket(socket)` – registers a client socket and refreshes multis
|
|
172
|
+
- `addSocket(socket)` – registers a client socket, sets up disconnect handling, and refreshes multis
|
|
173
|
+
- `removeSocket(clientId)` – removes a client, cleans up peers/listeners, and rebuilds multis
|
|
174
|
+
- `tearDown()` – gracefully shuts down: stops timers, clears all clients, closes storage
|
|
152
175
|
- `io` – Io interface used by server
|
|
153
176
|
- `bs` – Bs interface used by server
|
|
177
|
+
- `clients` – `Map` of connected clients (keyed by internal clientId)
|
|
178
|
+
- `isTornDown` – whether the server has been shut down
|
|
179
|
+
- `logger` – the `ServerLogger` instance (defaults to `noopLogger`)
|
|
154
180
|
|
|
155
181
|
## Example
|
|
156
182
|
|
|
@@ -184,7 +210,372 @@ The same pattern is used for Bs (blob storage).
|
|
|
184
210
|
|
|
185
211
|
- `Client.io` and `Client.bs` are already merged interfaces. No need to access multis directly.
|
|
186
212
|
- `Server.addSocket()` batches refreshes to reduce rebuild overhead when multiple sockets connect.
|
|
187
|
-
- Multicast
|
|
213
|
+
- Multicast uses `__origin` markers plus a two-generation ref set to prevent echo loops. Stale refs are automatically evicted (configurable via `refEvictionIntervalMs`).
|
|
214
|
+
- Disconnected sockets are auto-detected and cleaned up — dead peers are removed and multis rebuilt.
|
|
215
|
+
- Peer initialization is guarded by a configurable timeout (`peerInitTimeoutMs`, default 30 s) on both server and client. On the server it prevents `addSocket()` from hanging on unresponsive clients; on the client it prevents `init()` from hanging when the server is unreachable.
|
|
216
|
+
- Logging is opt-in via `{ logger }` options. Use `ConsoleLogger` for development, `BufferedLogger` for testing, `FilteredLogger` for production. Default is `NoopLogger` (zero overhead).
|
|
217
|
+
|
|
218
|
+
## Logging
|
|
219
|
+
|
|
220
|
+
Both `Server` and `Client` support structured logging via an injectable `ServerLogger` interface. Logging is opt-in — by default a zero-overhead `NoopLogger` is used.
|
|
221
|
+
|
|
222
|
+
### Logger implementations
|
|
223
|
+
|
|
224
|
+
| Class | Purpose |
|
|
225
|
+
| ---------------- | ------------------------------------------------------------- |
|
|
226
|
+
| `NoopLogger` | Default. All methods are empty — zero overhead in production. |
|
|
227
|
+
| `ConsoleLogger` | Logs to `console.log`/`warn`/`error`. Good for development. |
|
|
228
|
+
| `BufferedLogger` | Stores entries in memory. Ideal for test assertions. |
|
|
229
|
+
| `FilteredLogger` | Wraps another logger, filtering by level and/or source. |
|
|
230
|
+
|
|
231
|
+
### Injecting a logger
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
import { Server, Client, ConsoleLogger, BufferedLogger, FilteredLogger } from '@rljson/server';
|
|
235
|
+
|
|
236
|
+
// Console logging (development)
|
|
237
|
+
const server = new Server(route, io, bs, { logger: new ConsoleLogger() });
|
|
238
|
+
const client = new Client(socket, io, bs, route, { logger: new ConsoleLogger() });
|
|
239
|
+
|
|
240
|
+
// Buffered logging (testing)
|
|
241
|
+
const logger = new BufferedLogger();
|
|
242
|
+
const server = new Server(route, io, bs, { logger });
|
|
243
|
+
// After operations:
|
|
244
|
+
logger.entries; // All log entries
|
|
245
|
+
logger.byLevel('error'); // Only errors
|
|
246
|
+
logger.bySource('Server.Multicast'); // Only multicast entries
|
|
247
|
+
logger.clear(); // Reset
|
|
248
|
+
|
|
249
|
+
// Filtered logging (production — errors and warnings only)
|
|
250
|
+
const filtered = new FilteredLogger(new ConsoleLogger(), {
|
|
251
|
+
levels: ['error', 'warn'],
|
|
252
|
+
});
|
|
253
|
+
const server = new Server(route, io, bs, { logger: filtered });
|
|
254
|
+
|
|
255
|
+
// Filtered by source (only multicast traffic)
|
|
256
|
+
const trafficOnly = new FilteredLogger(new ConsoleLogger(), {
|
|
257
|
+
levels: ['traffic'],
|
|
258
|
+
sources: ['Server.Multicast'],
|
|
259
|
+
});
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Log levels
|
|
263
|
+
|
|
264
|
+
| Level | Method | What it captures |
|
|
265
|
+
| --------- | ------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
266
|
+
| `info` | `logger.info(source, message, data?)` | Lifecycle events: construction, init, tearDown, peer creation, multi rebuilds |
|
|
267
|
+
| `warn` | `logger.warn(source, message, data?)` | Duplicate ref suppression, loop prevention |
|
|
268
|
+
| `error` | `logger.error(source, message, error?, data?)` | Failures during init, peer creation, multicast, multi rebuilds |
|
|
269
|
+
| `traffic` | `logger.traffic(direction, source, event, data?)` | Socket traffic: inbound refs from clients, outbound multicasts to clients |
|
|
270
|
+
|
|
271
|
+
### Log sources
|
|
272
|
+
|
|
273
|
+
Each log entry includes a `source` field identifying the component:
|
|
274
|
+
|
|
275
|
+
| Source | Component |
|
|
276
|
+
| ------------------ | ----------------------------------------------------- |
|
|
277
|
+
| `Server` | Server lifecycle (init, addSocket, rebuild, refresh) |
|
|
278
|
+
| `Server.Io` | Server Io peer creation |
|
|
279
|
+
| `Server.Bs` | Server Bs peer creation |
|
|
280
|
+
| `Server.Multicast` | Ref broadcasting between clients |
|
|
281
|
+
| `Client` | Client lifecycle (init, tearDown, Db/Connector setup) |
|
|
282
|
+
| `Client.Io` | Client Io multi setup, peer bridge, peer creation |
|
|
283
|
+
| `Client.Bs` | Client Bs multi setup, peer bridge, peer creation |
|
|
284
|
+
|
|
285
|
+
### Custom logger
|
|
286
|
+
|
|
287
|
+
Implement the `ServerLogger` interface to integrate with any logging framework:
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
import type { ServerLogger } from '@rljson/server';
|
|
291
|
+
|
|
292
|
+
const myLogger: ServerLogger = {
|
|
293
|
+
info(source, message, data?) { /* your logging framework */ },
|
|
294
|
+
warn(source, message, data?) { /* ... */ },
|
|
295
|
+
error(source, message, error?, data?) { /* ... */ },
|
|
296
|
+
traffic(direction, source, event, data?) { /* ... */ },
|
|
297
|
+
};
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Server options
|
|
301
|
+
|
|
302
|
+
`ServerOptions` configures production behavior:
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
const server = new Server(route, io, bs, {
|
|
306
|
+
logger: new ConsoleLogger(), // Structured logging (default: NoopLogger)
|
|
307
|
+
refEvictionIntervalMs: 60_000, // Ref dedup sweep interval (default: 60 s, 0 = disable)
|
|
308
|
+
peerInitTimeoutMs: 30_000, // Peer handshake timeout (default: 30 s, 0 = disable)
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
| Option | Default | Description |
|
|
313
|
+
| ----------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
314
|
+
| `logger` | NoopLogger | Structured logger for lifecycle, traffic, and error events. |
|
|
315
|
+
| `refEvictionIntervalMs` | 60 000 | Two-generation sweep interval for multicast ref dedup. Refs older than two intervals are forgotten, preventing unbounded memory growth. |
|
|
316
|
+
| `peerInitTimeoutMs` | 30 000 | Maximum time `addSocket()` waits for a peer to initialize. Prevents hanging on unresponsive clients. |
|
|
317
|
+
| `syncConfig` | undefined | Sync protocol configuration (see below). Enables ACK aggregation, gap-fill, and enriched payloads. |
|
|
318
|
+
| `refLogSize` | 1 000 | Maximum number of recent payloads retained in the ref log for gap-fill responses. |
|
|
319
|
+
| `ackTimeoutMs` | 10 000 | Timeout for collecting individual client ACKs before emitting the aggregated ACK. Falls back to `syncConfig.ackTimeoutMs`. |
|
|
320
|
+
|
|
321
|
+
## Client options
|
|
322
|
+
|
|
323
|
+
`ClientOptions` configures the client:
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
const client = new Client(socket, io, bs, route, {
|
|
327
|
+
logger: new ConsoleLogger(), // Structured logging (default: NoopLogger)
|
|
328
|
+
peerInitTimeoutMs: 30_000, // Peer handshake timeout (default: 30 s, 0 = disable)
|
|
329
|
+
syncConfig, // Sync protocol config (default: undefined)
|
|
330
|
+
clientIdentity: 'my-client-id', // Stable client identity (default: auto-generated)
|
|
331
|
+
});
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
| Option | Default | Description |
|
|
335
|
+
| ------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
336
|
+
| `logger` | NoopLogger | Structured logger for lifecycle, traffic, and error events. |
|
|
337
|
+
| `peerInitTimeoutMs` | 30 000 | Maximum time `init()` waits for Io/Bs peers to initialize. Prevents hanging when the server is unreachable. Set to 0 to disable. |
|
|
338
|
+
| `syncConfig` | undefined | Sync protocol configuration (see below). Passed through to the Connector for enriched payloads. |
|
|
339
|
+
| `clientIdentity` | undefined | Stable client identity passed to the Connector. Auto-generated when `syncConfig.includeClientIdentity` is true and this is omitted. |
|
|
340
|
+
|
|
341
|
+
## Sync protocol
|
|
342
|
+
|
|
343
|
+
The sync protocol is **opt-in** and backward-compatible. When `syncConfig` is provided to the server (and/or client), the system activates enriched payload forwarding, ACK aggregation, and gap-fill support.
|
|
344
|
+
|
|
345
|
+
### Enabling sync
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
import { Server, Client, ConsoleLogger } from '@rljson/server';
|
|
349
|
+
import type { SyncConfig } from '@rljson/server';
|
|
350
|
+
|
|
351
|
+
const syncConfig: SyncConfig = {
|
|
352
|
+
causalOrdering: true, // Attach seq numbers, detect gaps, serve gap-fill
|
|
353
|
+
requireAck: true, // Collect client ACKs, emit aggregated AckPayload
|
|
354
|
+
includeClientIdentity: true, // Attach stable ClientId and timestamp to payloads
|
|
355
|
+
ackTimeoutMs: 5_000, // Per-ref ACK timeout (default: 10 s)
|
|
356
|
+
maxDedupSetSize: 10_000, // Max refs per dedup generation (default: 10 000)
|
|
357
|
+
bootstrapHeartbeatMs: 30_000, // Periodic bootstrap heartbeat interval (optional)
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// Server — enables ref log, ACK aggregation, gap-fill responder
|
|
361
|
+
const server = new Server(route, io, bs, { syncConfig });
|
|
362
|
+
await server.init();
|
|
363
|
+
|
|
364
|
+
// Client — passes SyncConfig to the Connector for enriched payloads
|
|
365
|
+
const client = new Client(socket, localIo, localBs, route, { syncConfig });
|
|
366
|
+
await client.init();
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### What each flag does
|
|
370
|
+
|
|
371
|
+
| Flag | Server effect | Client (Connector) effect |
|
|
372
|
+
| ----------------------- | ------------------------------------------------ | ------------------------------------------------------------ |
|
|
373
|
+
| `causalOrdering` | Stores payloads in ref log; responds to gap-fill | Attaches `seq` + `p`; detects gaps; requests gap-fill |
|
|
374
|
+
| `requireAck` | Collects per-client ACKs; emits aggregated ACK | Awaits ACK via `sendWithAck()`; emits client ACK |
|
|
375
|
+
| `includeClientIdentity` | Forwards `c` and `t` transparently | Attaches stable `ClientId` and wall-clock timestamp |
|
|
376
|
+
| `ackTimeoutMs` | Controls server-side ACK collection timeout | Controls client-side ACK wait timeout |
|
|
377
|
+
| `maxDedupSetSize` | — | Caps dedup set size per generation (two-generation eviction) |
|
|
378
|
+
| `bootstrapHeartbeatMs` | Sends latest ref to all clients periodically | — (handled server-side only) |
|
|
379
|
+
|
|
380
|
+
### ACK flow
|
|
381
|
+
|
|
382
|
+
```text
|
|
383
|
+
Client A ──emit(ref)──► Server ──forward──► Client B, C
|
|
384
|
+
Server ◄──ackClient── Client B
|
|
385
|
+
Server ◄──ackClient── Client C
|
|
386
|
+
Client A ◄──ack──────── Server (ok: true, receivedBy: 2, totalClients: 2)
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
If not all clients ACK within the timeout, the server emits a partial ACK (`ok: false`).
|
|
390
|
+
|
|
391
|
+
### Gap-fill flow
|
|
392
|
+
|
|
393
|
+
```text
|
|
394
|
+
Client B detects seq gap (expected 6, got 8)
|
|
395
|
+
Client B ──gapfill:req──► Server (afterSeq: 5)
|
|
396
|
+
Client B ◄──gapfill:res── Server (refs with seq 6, 7)
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
The server maintains a bounded ref log (ring buffer) of recent payloads. When a client detects a sequence gap, it requests the missing refs. The server filters the ref log and responds with matching entries.
|
|
400
|
+
|
|
401
|
+
### Bootstrap (late joiner support)
|
|
402
|
+
|
|
403
|
+
When a new client connects, the server immediately sends the **latest known ref** via the `${route}:bootstrap` event. This ensures late-joining clients catch up without waiting for the next write.
|
|
404
|
+
|
|
405
|
+
```text
|
|
406
|
+
Client A ──emit(ref)──► Server (Server tracks latestRef)
|
|
407
|
+
... time passes ...
|
|
408
|
+
Client B ──addSocket──► Server
|
|
409
|
+
Client B ◄──bootstrap── Server ({ o: '__server__', r: latestRef })
|
|
410
|
+
Client B ──db.get(ref)──► Server ► A (pull data on demand)
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
For additional resilience, configure `bootstrapHeartbeatMs` in `SyncConfig` to periodically broadcast the latest ref to all clients. Each client's dedup pipeline automatically filters refs it has already seen.
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
const syncConfig: SyncConfig = {
|
|
417
|
+
bootstrapHeartbeatMs: 30_000, // Send latest ref to all clients every 30s
|
|
418
|
+
};
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
**Key details:**
|
|
422
|
+
|
|
423
|
+
- Bootstrap payload uses origin `'__server__'` (not a real client)
|
|
424
|
+
- The Connector's `_processIncoming()` handles dedup automatically
|
|
425
|
+
- If no ref has been seen yet (empty server), no bootstrap is sent
|
|
426
|
+
- Heartbeat timer calls `.unref()` so it doesn't keep the process alive
|
|
427
|
+
|
|
428
|
+
### Sync event names
|
|
429
|
+
|
|
430
|
+
All sync events are route-specific, generated by `syncEvents(route)`:
|
|
431
|
+
|
|
432
|
+
| Event | Direction | Purpose |
|
|
433
|
+
| ---------------------- | --------------- | ------------------------------------------ |
|
|
434
|
+
| `${route}` | Bidirectional | Ref broadcast (existing) |
|
|
435
|
+
| `${route}:ack` | Server → Client | Aggregated delivery acknowledgment |
|
|
436
|
+
| `${route}:ack:client` | Client → Server | Individual client receipt confirmation |
|
|
437
|
+
| `${route}:gapfill:req` | Client → Server | Request missing refs after detected gap |
|
|
438
|
+
| `${route}:gapfill:res` | Server → Client | Supply missing refs from ref log |
|
|
439
|
+
| `${route}:bootstrap` | Server → Client | Latest ref on connect / periodic heartbeat |
|
|
440
|
+
|
|
441
|
+
### Wire format reference
|
|
442
|
+
|
|
443
|
+
All payloads are JSON objects transmitted via socket events. The two required fields (`o`, `r`) provide backward-compatible self-echo filtering and ref identification. All other fields activate only when the corresponding `SyncConfig` flags are set.
|
|
444
|
+
|
|
445
|
+
#### ConnectorPayload
|
|
446
|
+
|
|
447
|
+
The main message transmitted between Connector and Server. Sent on event `${route}`.
|
|
448
|
+
|
|
449
|
+
| Field | Type | Required | Activated by | Purpose |
|
|
450
|
+
| ------- | ----------------------- | -------- | ----------------------- | ---------------------------------------------------- |
|
|
451
|
+
| `r` | `string` | ✅ | always | The ref (InsertHistory timeId) being announced |
|
|
452
|
+
| `o` | `string` | ✅ | always | Ephemeral origin of the sender (self-echo filtering) |
|
|
453
|
+
| `c` | `ClientId` | ❌ | `includeClientIdentity` | Stable client identity (survives reconnections) |
|
|
454
|
+
| `t` | `number` | ❌ | `includeClientIdentity` | Client-side wall-clock timestamp (ms since epoch) |
|
|
455
|
+
| `seq` | `number` | ❌ | `causalOrdering` | Monotonic counter per (client, route) pair |
|
|
456
|
+
| `p` | `InsertHistoryTimeId[]` | ❌ | `causalOrdering` | Causal predecessor timeIds |
|
|
457
|
+
| `cksum` | `string` | ❌ | — | Content checksum for ACK verification |
|
|
458
|
+
|
|
459
|
+
**Minimal** (backward-compatible, no SyncConfig):
|
|
460
|
+
|
|
461
|
+
```json
|
|
462
|
+
{ "o": "1700000000000:AbCd", "r": "1700000000001:EfGh" }
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
**Fully populated** (all SyncConfig flags enabled):
|
|
466
|
+
|
|
467
|
+
```json
|
|
468
|
+
{
|
|
469
|
+
"o": "1700000000000:AbCd",
|
|
470
|
+
"r": "1700000000001:EfGh",
|
|
471
|
+
"c": "client_V1StGXR8_Z5j",
|
|
472
|
+
"t": 1700000000001,
|
|
473
|
+
"seq": 42,
|
|
474
|
+
"p": ["1700000000000:XyZw"]
|
|
475
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
#### AckPayload
|
|
479
|
+
|
|
480
|
+
Server → Client acknowledgment. Sent on event `${route}:ack` after the server has collected individual client ACKs (or after a timeout).
|
|
481
|
+
|
|
482
|
+
| Field | Type | Required | Purpose |
|
|
483
|
+
| -------------- | --------- | -------- | ------------------------------------------------------------- |
|
|
484
|
+
| `r` | `string` | ✅ | The ref being acknowledged |
|
|
485
|
+
| `ok` | `boolean` | ✅ | `true` if all clients confirmed; `false` on timeout / partial |
|
|
486
|
+
| `receivedBy` | `number` | ❌ | Count of clients that confirmed receipt |
|
|
487
|
+
| `totalClients` | `number` | ❌ | Total receiver clients at broadcast time |
|
|
488
|
+
|
|
489
|
+
**Full ACK example:**
|
|
490
|
+
|
|
491
|
+
```json
|
|
492
|
+
{ "r": "1700000000001:EfGh", "ok": true, "receivedBy": 3, "totalClients": 3 }
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
**Partial / timed-out ACK:**
|
|
496
|
+
|
|
497
|
+
```json
|
|
498
|
+
{ "r": "1700000000001:EfGh", "ok": false, "receivedBy": 1, "totalClients": 3 }
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
#### GapFillRequest
|
|
502
|
+
|
|
503
|
+
Client → Server request for missing refs. Sent on event `${route}:gapfill:req` when a Connector detects a sequence gap.
|
|
504
|
+
|
|
505
|
+
| Field | Type | Required | Purpose |
|
|
506
|
+
| ------------- | --------------------- | -------- | ------------------------------------------------------ |
|
|
507
|
+
| `route` | `string` | ✅ | The route for which refs are missing |
|
|
508
|
+
| `afterSeq` | `number` | ✅ | Last sequence number the client successfully processed |
|
|
509
|
+
| `afterTimeId` | `InsertHistoryTimeId` | ❌ | Alternative anchor if sequence numbers are unavailable |
|
|
510
|
+
|
|
511
|
+
```json
|
|
512
|
+
{ "route": "/sharedTree", "afterSeq": 5, "afterTimeId": "1700000000000:AbCd" }
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
#### GapFillResponse
|
|
516
|
+
|
|
517
|
+
Server → Client response containing missing refs. Sent on event `${route}:gapfill:res`, ordered chronologically (oldest first).
|
|
518
|
+
|
|
519
|
+
| Field | Type | Required | Purpose |
|
|
520
|
+
| ------- | -------------------- | -------- | ----------------------------------------------- |
|
|
521
|
+
| `route` | `string` | ✅ | The route this response corresponds to |
|
|
522
|
+
| `refs` | `ConnectorPayload[]` | ✅ | Ordered list of missing payloads (oldest first) |
|
|
523
|
+
|
|
524
|
+
```json
|
|
525
|
+
{
|
|
526
|
+
"route": "/sharedTree",
|
|
527
|
+
"refs": [
|
|
528
|
+
{ "o": "1700000000000:AbCd", "r": "1700000000006:MnOp", "seq": 6 },
|
|
529
|
+
{ "o": "1700000000000:AbCd", "r": "1700000000007:QrSt", "seq": 7 }
|
|
530
|
+
]
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
#### SyncConfig flag → field activation summary
|
|
535
|
+
|
|
536
|
+
| SyncConfig flag | Payload fields activated | Events activated |
|
|
537
|
+
| ----------------------- | ------------------------------ | ------------------------------ |
|
|
538
|
+
| _(none / default)_ | `o`, `r` | `${route}` only |
|
|
539
|
+
| `causalOrdering` | + `seq`, `p` | + `gapfill:req`, `gapfill:res` |
|
|
540
|
+
| `requireAck` | _(no extra fields)_ | + `ack`, `ack:client` |
|
|
541
|
+
| `includeClientIdentity` | + `c`, `t` | _(no extra events)_ |
|
|
542
|
+
| All flags combined | `o`, `r`, `c`, `t`, `seq`, `p` | All 5 events |
|
|
543
|
+
|
|
544
|
+
#### ClientId format
|
|
545
|
+
|
|
546
|
+
A `ClientId` is a 12-character [nanoid](https://github.com/ai/nanoid) prefixed with `"client_"` for easy identification in logs:
|
|
547
|
+
|
|
548
|
+
```
|
|
549
|
+
client_V1StGXR8_Z5j
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
Unlike a Connector's ephemeral `origin` (which changes on every instantiation), a `ClientId` should be generated once and stored (e.g. in localStorage) so it persists across reconnections.
|
|
553
|
+
|
|
554
|
+
## Lifecycle management
|
|
555
|
+
|
|
556
|
+
### Graceful shutdown
|
|
557
|
+
|
|
558
|
+
```ts
|
|
559
|
+
// Server
|
|
560
|
+
await server.tearDown();
|
|
561
|
+
// Stops eviction timer, removes all listeners, clears clients, closes IoMulti.
|
|
562
|
+
console.log(server.isTornDown); // true
|
|
563
|
+
|
|
564
|
+
// Client
|
|
565
|
+
await client.tearDown();
|
|
566
|
+
// Calls connector.tearDown() (removes socket listeners),
|
|
567
|
+
// closes IoMulti, clears Bs references, resets Db/Connector.
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### Removing a client
|
|
571
|
+
|
|
572
|
+
```ts
|
|
573
|
+
// Manual removal by clientId
|
|
574
|
+
const clientIds = Array.from(server.clients.keys());
|
|
575
|
+
await server.removeSocket(clientIds[0]);
|
|
576
|
+
|
|
577
|
+
// Automatic: clients are removed when their socket emits 'disconnect'
|
|
578
|
+
```
|
|
188
579
|
|
|
189
580
|
## Architecture Overview
|
|
190
581
|
|
package/dist/client.d.ts
CHANGED
|
@@ -1,22 +1,59 @@
|
|
|
1
1
|
import { Bs } from '@rljson/bs';
|
|
2
|
+
import { Connector, Db } from '@rljson/db';
|
|
2
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';
|
|
4
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
|
+
}
|
|
5
33
|
export declare class Client extends BaseNode {
|
|
6
34
|
private _socketToServer;
|
|
7
35
|
protected _localIo: Io;
|
|
8
36
|
protected _localBs: Bs;
|
|
37
|
+
private _route?;
|
|
9
38
|
private _ioMultiIos;
|
|
10
39
|
private _ioMulti?;
|
|
11
40
|
private _bsMultiBss;
|
|
12
41
|
private _bsMulti?;
|
|
42
|
+
private _db?;
|
|
43
|
+
private _connector?;
|
|
44
|
+
private _logger;
|
|
45
|
+
private _syncConfig?;
|
|
46
|
+
private _clientIdentity?;
|
|
47
|
+
private _peerInitTimeoutMs;
|
|
13
48
|
/**
|
|
14
49
|
* Creates a Client instance
|
|
15
50
|
* @param _socketToServer - Socket or namespace bundle to connect to server
|
|
16
51
|
* @param _localIo - Local Io for local storage
|
|
17
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
|
|
18
55
|
*/
|
|
19
|
-
constructor(_socketToServer: SocketLike, _localIo: Io, _localBs: Bs);
|
|
56
|
+
constructor(_socketToServer: SocketLike, _localIo: Io, _localBs: Bs, _route?: Route | undefined, options?: ClientOptions);
|
|
20
57
|
/**
|
|
21
58
|
* Initializes Io and Bs multis and their peer bridges.
|
|
22
59
|
* @returns The initialized Io implementation.
|
|
@@ -38,6 +75,27 @@ export declare class Client extends BaseNode {
|
|
|
38
75
|
* Returns the Bs implementation.
|
|
39
76
|
*/
|
|
40
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;
|
|
41
99
|
/**
|
|
42
100
|
* Builds the Io multi with local and peer layers.
|
|
43
101
|
*/
|
|
@@ -56,4 +114,15 @@ export declare class Client extends BaseNode {
|
|
|
56
114
|
* @param socket - Downstream socket to the server Bs namespace.
|
|
57
115
|
*/
|
|
58
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;
|
|
59
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';
|