@rljson/server 0.0.9 → 0.0.11
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 +35 -0
- package/README.public.md +135 -0
- package/dist/README.architecture.md +35 -0
- package/dist/README.public.md +135 -0
- package/dist/client.d.ts +27 -0
- package/dist/index.d.ts +3 -1
- package/dist/node.d.ts +174 -0
- package/dist/server.js +350 -940
- package/package.json +2 -1
package/README.architecture.md
CHANGED
|
@@ -77,6 +77,41 @@ In default setups you can reuse a single socket for all four channels; the code
|
|
|
77
77
|
|
|
78
78
|
## Core Components
|
|
79
79
|
|
|
80
|
+
### 0. Node (Self-Organizing Orchestrator)
|
|
81
|
+
|
|
82
|
+
The `Node` class sits above `Server` and `Client`, bridging `@rljson/network` topology events into role transitions. It:
|
|
83
|
+
|
|
84
|
+
1. **Owns storage**: Creates a single `IoMem`/`BsMem` pair at `start()`, reused across all role transitions. Data survives hub↔client switches because `IoMem.close()` only flips `_isOpen` — the in-memory data is never cleared.
|
|
85
|
+
2. **Reacts to topology**: Subscribes to `NetworkManager`'s `role-changed` event. On `'hub'`, tears down any Client and creates a Server. On `'client'`, tears down any Server and creates a Client.
|
|
86
|
+
3. **Manages transport**: Uses injectable factories (`CreateHubTransport`/`CreateClientTransport`) to create the transport layer, keeping the Node class transport-agnostic.
|
|
87
|
+
4. **Agent lifecycle**: An optional `createAgent` factory in `NodeDeps` is called on every `ready` event. The returned `AgentHandle.stop()` is called before the next role transition. This enables application-level wiring (e.g. FsAgent) without circular dependencies.
|
|
88
|
+
5. **Serialized transitions**: Role transitions are queued — a new `role-changed` event waits for the previous transition to complete before starting. This prevents race conditions between teardown and setup.
|
|
89
|
+
6. **Error resilience**: Errors in user-provided code (agent factories, transport factories) are caught and logged. The node continues functioning — a failed transport degrades connectivity but doesn't crash, a failed agent leaves the node's core intact.
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
┌─────────────────────────────────────────┐
|
|
93
|
+
│ Node │
|
|
94
|
+
│ ┌──────┐ ┌──────┐ │
|
|
95
|
+
│ │IoMem │ │BsMem │ ← owned by Node │
|
|
96
|
+
│ └──┬───┘ └──┬───┘ │
|
|
97
|
+
│ │ │ │
|
|
98
|
+
│ ┌──▼────────▼───┐ ┌────────────────┐ │
|
|
99
|
+
│ │ Server/Client │──│ HubTransport │ │
|
|
100
|
+
│ │ (role-based) │ │ or ClientSocket│ │
|
|
101
|
+
│ └───────┬───────┘ └────────────────┘ │
|
|
102
|
+
│ │ │
|
|
103
|
+
│ ┌───────▼───────┐ │
|
|
104
|
+
│ │ AgentHandle │ ← optional, wired │
|
|
105
|
+
│ │ (e.g. FsAgent)│ via createAgent │
|
|
106
|
+
│ └───────────────┘ │
|
|
107
|
+
│ ▲ │
|
|
108
|
+
│ │ role-changed │
|
|
109
|
+
│ ┌──┴───────────┐ │
|
|
110
|
+
│ │NetworkManager│ │
|
|
111
|
+
│ └──────────────┘ │
|
|
112
|
+
└─────────────────────────────────────────┘
|
|
113
|
+
```
|
|
114
|
+
|
|
80
115
|
### 1. Client
|
|
81
116
|
|
|
82
117
|
The `Client` class provides a unified interface for data access by combining local storage with server storage.
|
package/README.public.md
CHANGED
|
@@ -164,6 +164,9 @@ This is implemented with `IoMulti` and `BsMulti` internally, but the public API
|
|
|
164
164
|
- `connector` – Connector wired to the route and socket (available when route was provided)
|
|
165
165
|
- `route` – the Route passed to the constructor
|
|
166
166
|
- `logger` – the `ServerLogger` instance (defaults to `noopLogger`)
|
|
167
|
+
- `isConnected` – whether the socket is currently connected (tracks `disconnect`/`connect` events)
|
|
168
|
+
- `onDisconnect(callback)` – registers a callback that fires when the socket disconnects (receives the reason string)
|
|
169
|
+
- `onReconnect(callback)` – registers a callback that fires when the socket reconnects after a previous disconnect
|
|
167
170
|
|
|
168
171
|
### Server API
|
|
169
172
|
|
|
@@ -216,6 +219,60 @@ The same pattern is used for Bs (blob storage).
|
|
|
216
219
|
- 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.
|
|
217
220
|
- Logging is opt-in via `{ logger }` options. Use `ConsoleLogger` for development, `BufferedLogger` for testing, `FilteredLogger` for production. Default is `NoopLogger` (zero overhead).
|
|
218
221
|
|
|
222
|
+
## Reconnect handling
|
|
223
|
+
|
|
224
|
+
The `Client` class tracks socket connection state and provides hooks for upper layers to react to disconnects and reconnections.
|
|
225
|
+
|
|
226
|
+
### How it works
|
|
227
|
+
|
|
228
|
+
Socket.IO auto-reconnects at the transport level by default. When the connection drops and is restored:
|
|
229
|
+
|
|
230
|
+
1. The client-side Socket.IO socket emits `'disconnect'` then (later) `'connect'`.
|
|
231
|
+
2. The `Client` class listens for these events and updates `isConnected`.
|
|
232
|
+
3. Registered `onDisconnect` / `onReconnect` callbacks fire.
|
|
233
|
+
4. The server auto-detects the dropped socket (`removeSocket`) and re-registers the reconnected socket via a new `'connection'` event.
|
|
234
|
+
5. The server sends a bootstrap ref to the reconnected client, triggering a re-sync via the Connector's bootstrap handler.
|
|
235
|
+
|
|
236
|
+
### Usage
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
import { Client } from '@rljson/server';
|
|
240
|
+
|
|
241
|
+
const client = new Client(socket, io, bs, route, {
|
|
242
|
+
logger: new ConsoleLogger(),
|
|
243
|
+
});
|
|
244
|
+
await client.init();
|
|
245
|
+
|
|
246
|
+
// Track connection state
|
|
247
|
+
console.log(client.isConnected); // true
|
|
248
|
+
|
|
249
|
+
// React to disconnects
|
|
250
|
+
client.onDisconnect((reason) => {
|
|
251
|
+
console.warn(`Disconnected: ${reason}`);
|
|
252
|
+
// Pause user-facing sync indicators, queue local changes, etc.
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// React to reconnections
|
|
256
|
+
client.onReconnect(() => {
|
|
257
|
+
console.info('Reconnected to server');
|
|
258
|
+
// Resume sync, trigger re-fetch, update UI, etc.
|
|
259
|
+
});
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### What survives a reconnect
|
|
263
|
+
|
|
264
|
+
| Layer | Survives? | Details |
|
|
265
|
+
| ----------------- | --------- | ----------------------------------------------------------------- |
|
|
266
|
+
| Socket.IO | ✅ | Auto-reconnects, reuses the same client-side socket object |
|
|
267
|
+
| SocketIoBridge | ✅ | Wraps the same socket — event listeners are preserved |
|
|
268
|
+
| IoPeer / BsPeer | ✅ | Listeners remain on the client socket; server re-creates its side |
|
|
269
|
+
| Connector | ✅ | Listeners remain; server bootstrap re-syncs state |
|
|
270
|
+
| IoMulti / BsMulti | ✅ | Multi layers are unaffected; local Io/Bs always available |
|
|
271
|
+
|
|
272
|
+
### Cleanup
|
|
273
|
+
|
|
274
|
+
All connection handlers are cleaned up automatically on `tearDown()`. After tearDown, no callbacks will fire.
|
|
275
|
+
|
|
219
276
|
## Logging
|
|
220
277
|
|
|
221
278
|
Both `Server` and `Client` support structured logging via an injectable `ServerLogger` interface. Logging is opt-in — by default a zero-overhead `NoopLogger` is used.
|
|
@@ -580,6 +637,84 @@ await server.removeSocket(clientIds[0]);
|
|
|
580
637
|
// Automatic: clients are removed when their socket emits 'disconnect'
|
|
581
638
|
```
|
|
582
639
|
|
|
640
|
+
## Self-Organizing Node
|
|
641
|
+
|
|
642
|
+
The `Node` class bridges `@rljson/network` peer discovery with `Server`/`Client` role transitions. Every node runs the same code — on startup it discovers peers, elects a hub, and automatically assumes the correct role.
|
|
643
|
+
|
|
644
|
+
### Quick start
|
|
645
|
+
|
|
646
|
+
```ts
|
|
647
|
+
import { Node } from '@rljson/server';
|
|
648
|
+
import { Route } from '@rljson/rljson';
|
|
649
|
+
|
|
650
|
+
const node = new Node(
|
|
651
|
+
{
|
|
652
|
+
domain: 'my-app',
|
|
653
|
+
port: 3000,
|
|
654
|
+
route: Route.fromFlat('/sharedTree'),
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
createHubTransport: async (port) => { /* start HTTP+Socket.IO server */ },
|
|
658
|
+
createClientTransport: async (hubAddress) => { /* connect to hub */ },
|
|
659
|
+
},
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
await node.start();
|
|
663
|
+
|
|
664
|
+
// The node auto-discovers peers and assumes a role:
|
|
665
|
+
node.on('ready', ({ role, client, server, socket }) => {
|
|
666
|
+
console.log(`Role: ${role}`); // 'hub' or 'client'
|
|
667
|
+
console.log(`Server: ${server}`); // Server instance (hub only)
|
|
668
|
+
console.log(`Client: ${client}`); // Client instance (client only)
|
|
669
|
+
console.log(`Socket: ${socket}`); // Socket to hub (client only)
|
|
670
|
+
});
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### Data preservation across role transitions
|
|
674
|
+
|
|
675
|
+
The `Node` owns a single `IoMem`/`BsMem` pair, created once at `start()` and reused across all role transitions. When a role changes (e.g., hub → client → hub), the underlying storage is re-initialized without losing data — `IoMem.close()` only sets `_isOpen = false`, the data in memory is preserved.
|
|
676
|
+
|
|
677
|
+
This means:
|
|
678
|
+
- Data written while hub survives a transition to client
|
|
679
|
+
- Data written while client survives a transition to hub
|
|
680
|
+
- A re-elected hub still serves all previously stored blobs and tables
|
|
681
|
+
|
|
682
|
+
### Agent lifecycle
|
|
683
|
+
|
|
684
|
+
The `createAgent` factory in `NodeDeps` is called on every role transition. The returned `AgentHandle.stop()` is called before the next transition or when the node stops. This is how you wire application-level agents (e.g. `FsAgent`) without creating circular dependencies:
|
|
685
|
+
|
|
686
|
+
```ts
|
|
687
|
+
const node = new Node(config, {
|
|
688
|
+
createHubTransport: ...,
|
|
689
|
+
createClientTransport: ...,
|
|
690
|
+
createAgent: async ({ role, client, server, socket }) => {
|
|
691
|
+
// Wire your agent here — called on every role transition
|
|
692
|
+
const stopSync = await startMySync(role, client, server);
|
|
693
|
+
return { stop: () => stopSync() };
|
|
694
|
+
},
|
|
695
|
+
});
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
The Node serializes transitions — a new transition waits for the previous one to complete, ensuring agents are always stopped cleanly.
|
|
699
|
+
|
|
700
|
+
Errors in `createAgent` or `agentHandle.stop()` are caught and logged — they never crash the node. Similarly, transport factory failures degrade connectivity but leave the node functional locally.
|
|
701
|
+
|
|
702
|
+
### API
|
|
703
|
+
|
|
704
|
+
| Property / Method | Description |
|
|
705
|
+
| --------------------- | ----------------------------------------------------- |
|
|
706
|
+
| `node.start()` | Begin peer discovery and role assignment |
|
|
707
|
+
| `node.stop()` | Tear down current role and stop discovery |
|
|
708
|
+
| `node.role` | Current role: `'hub'`, `'client'`, or `'unassigned'` |
|
|
709
|
+
| `node.io` | IoMulti from active Server or Client |
|
|
710
|
+
| `node.bs` | BsMulti from active Server or Client |
|
|
711
|
+
| `node.server` | Server instance (when hub), else `undefined` |
|
|
712
|
+
| `node.client` | Client instance (when client), else `undefined` |
|
|
713
|
+
| `node.socket` | Socket to hub (when client), else `undefined` |
|
|
714
|
+
| `node.topology` | Current network topology snapshot |
|
|
715
|
+
| `node.on(event, cb)` | Subscribe to `'ready'`, `'role-changed'`, `'stopped'` |
|
|
716
|
+
| `node.off(event, cb)` | Unsubscribe |
|
|
717
|
+
|
|
583
718
|
## Architecture Overview
|
|
584
719
|
|
|
585
720
|
### Pull-Based Reference Architecture
|
|
@@ -77,6 +77,41 @@ In default setups you can reuse a single socket for all four channels; the code
|
|
|
77
77
|
|
|
78
78
|
## Core Components
|
|
79
79
|
|
|
80
|
+
### 0. Node (Self-Organizing Orchestrator)
|
|
81
|
+
|
|
82
|
+
The `Node` class sits above `Server` and `Client`, bridging `@rljson/network` topology events into role transitions. It:
|
|
83
|
+
|
|
84
|
+
1. **Owns storage**: Creates a single `IoMem`/`BsMem` pair at `start()`, reused across all role transitions. Data survives hub↔client switches because `IoMem.close()` only flips `_isOpen` — the in-memory data is never cleared.
|
|
85
|
+
2. **Reacts to topology**: Subscribes to `NetworkManager`'s `role-changed` event. On `'hub'`, tears down any Client and creates a Server. On `'client'`, tears down any Server and creates a Client.
|
|
86
|
+
3. **Manages transport**: Uses injectable factories (`CreateHubTransport`/`CreateClientTransport`) to create the transport layer, keeping the Node class transport-agnostic.
|
|
87
|
+
4. **Agent lifecycle**: An optional `createAgent` factory in `NodeDeps` is called on every `ready` event. The returned `AgentHandle.stop()` is called before the next role transition. This enables application-level wiring (e.g. FsAgent) without circular dependencies.
|
|
88
|
+
5. **Serialized transitions**: Role transitions are queued — a new `role-changed` event waits for the previous transition to complete before starting. This prevents race conditions between teardown and setup.
|
|
89
|
+
6. **Error resilience**: Errors in user-provided code (agent factories, transport factories) are caught and logged. The node continues functioning — a failed transport degrades connectivity but doesn't crash, a failed agent leaves the node's core intact.
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
┌─────────────────────────────────────────┐
|
|
93
|
+
│ Node │
|
|
94
|
+
│ ┌──────┐ ┌──────┐ │
|
|
95
|
+
│ │IoMem │ │BsMem │ ← owned by Node │
|
|
96
|
+
│ └──┬───┘ └──┬───┘ │
|
|
97
|
+
│ │ │ │
|
|
98
|
+
│ ┌──▼────────▼───┐ ┌────────────────┐ │
|
|
99
|
+
│ │ Server/Client │──│ HubTransport │ │
|
|
100
|
+
│ │ (role-based) │ │ or ClientSocket│ │
|
|
101
|
+
│ └───────┬───────┘ └────────────────┘ │
|
|
102
|
+
│ │ │
|
|
103
|
+
│ ┌───────▼───────┐ │
|
|
104
|
+
│ │ AgentHandle │ ← optional, wired │
|
|
105
|
+
│ │ (e.g. FsAgent)│ via createAgent │
|
|
106
|
+
│ └───────────────┘ │
|
|
107
|
+
│ ▲ │
|
|
108
|
+
│ │ role-changed │
|
|
109
|
+
│ ┌──┴───────────┐ │
|
|
110
|
+
│ │NetworkManager│ │
|
|
111
|
+
│ └──────────────┘ │
|
|
112
|
+
└─────────────────────────────────────────┘
|
|
113
|
+
```
|
|
114
|
+
|
|
80
115
|
### 1. Client
|
|
81
116
|
|
|
82
117
|
The `Client` class provides a unified interface for data access by combining local storage with server storage.
|
package/dist/README.public.md
CHANGED
|
@@ -164,6 +164,9 @@ This is implemented with `IoMulti` and `BsMulti` internally, but the public API
|
|
|
164
164
|
- `connector` – Connector wired to the route and socket (available when route was provided)
|
|
165
165
|
- `route` – the Route passed to the constructor
|
|
166
166
|
- `logger` – the `ServerLogger` instance (defaults to `noopLogger`)
|
|
167
|
+
- `isConnected` – whether the socket is currently connected (tracks `disconnect`/`connect` events)
|
|
168
|
+
- `onDisconnect(callback)` – registers a callback that fires when the socket disconnects (receives the reason string)
|
|
169
|
+
- `onReconnect(callback)` – registers a callback that fires when the socket reconnects after a previous disconnect
|
|
167
170
|
|
|
168
171
|
### Server API
|
|
169
172
|
|
|
@@ -216,6 +219,60 @@ The same pattern is used for Bs (blob storage).
|
|
|
216
219
|
- 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.
|
|
217
220
|
- Logging is opt-in via `{ logger }` options. Use `ConsoleLogger` for development, `BufferedLogger` for testing, `FilteredLogger` for production. Default is `NoopLogger` (zero overhead).
|
|
218
221
|
|
|
222
|
+
## Reconnect handling
|
|
223
|
+
|
|
224
|
+
The `Client` class tracks socket connection state and provides hooks for upper layers to react to disconnects and reconnections.
|
|
225
|
+
|
|
226
|
+
### How it works
|
|
227
|
+
|
|
228
|
+
Socket.IO auto-reconnects at the transport level by default. When the connection drops and is restored:
|
|
229
|
+
|
|
230
|
+
1. The client-side Socket.IO socket emits `'disconnect'` then (later) `'connect'`.
|
|
231
|
+
2. The `Client` class listens for these events and updates `isConnected`.
|
|
232
|
+
3. Registered `onDisconnect` / `onReconnect` callbacks fire.
|
|
233
|
+
4. The server auto-detects the dropped socket (`removeSocket`) and re-registers the reconnected socket via a new `'connection'` event.
|
|
234
|
+
5. The server sends a bootstrap ref to the reconnected client, triggering a re-sync via the Connector's bootstrap handler.
|
|
235
|
+
|
|
236
|
+
### Usage
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
import { Client } from '@rljson/server';
|
|
240
|
+
|
|
241
|
+
const client = new Client(socket, io, bs, route, {
|
|
242
|
+
logger: new ConsoleLogger(),
|
|
243
|
+
});
|
|
244
|
+
await client.init();
|
|
245
|
+
|
|
246
|
+
// Track connection state
|
|
247
|
+
console.log(client.isConnected); // true
|
|
248
|
+
|
|
249
|
+
// React to disconnects
|
|
250
|
+
client.onDisconnect((reason) => {
|
|
251
|
+
console.warn(`Disconnected: ${reason}`);
|
|
252
|
+
// Pause user-facing sync indicators, queue local changes, etc.
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// React to reconnections
|
|
256
|
+
client.onReconnect(() => {
|
|
257
|
+
console.info('Reconnected to server');
|
|
258
|
+
// Resume sync, trigger re-fetch, update UI, etc.
|
|
259
|
+
});
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### What survives a reconnect
|
|
263
|
+
|
|
264
|
+
| Layer | Survives? | Details |
|
|
265
|
+
| ----------------- | --------- | ----------------------------------------------------------------- |
|
|
266
|
+
| Socket.IO | ✅ | Auto-reconnects, reuses the same client-side socket object |
|
|
267
|
+
| SocketIoBridge | ✅ | Wraps the same socket — event listeners are preserved |
|
|
268
|
+
| IoPeer / BsPeer | ✅ | Listeners remain on the client socket; server re-creates its side |
|
|
269
|
+
| Connector | ✅ | Listeners remain; server bootstrap re-syncs state |
|
|
270
|
+
| IoMulti / BsMulti | ✅ | Multi layers are unaffected; local Io/Bs always available |
|
|
271
|
+
|
|
272
|
+
### Cleanup
|
|
273
|
+
|
|
274
|
+
All connection handlers are cleaned up automatically on `tearDown()`. After tearDown, no callbacks will fire.
|
|
275
|
+
|
|
219
276
|
## Logging
|
|
220
277
|
|
|
221
278
|
Both `Server` and `Client` support structured logging via an injectable `ServerLogger` interface. Logging is opt-in — by default a zero-overhead `NoopLogger` is used.
|
|
@@ -580,6 +637,84 @@ await server.removeSocket(clientIds[0]);
|
|
|
580
637
|
// Automatic: clients are removed when their socket emits 'disconnect'
|
|
581
638
|
```
|
|
582
639
|
|
|
640
|
+
## Self-Organizing Node
|
|
641
|
+
|
|
642
|
+
The `Node` class bridges `@rljson/network` peer discovery with `Server`/`Client` role transitions. Every node runs the same code — on startup it discovers peers, elects a hub, and automatically assumes the correct role.
|
|
643
|
+
|
|
644
|
+
### Quick start
|
|
645
|
+
|
|
646
|
+
```ts
|
|
647
|
+
import { Node } from '@rljson/server';
|
|
648
|
+
import { Route } from '@rljson/rljson';
|
|
649
|
+
|
|
650
|
+
const node = new Node(
|
|
651
|
+
{
|
|
652
|
+
domain: 'my-app',
|
|
653
|
+
port: 3000,
|
|
654
|
+
route: Route.fromFlat('/sharedTree'),
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
createHubTransport: async (port) => { /* start HTTP+Socket.IO server */ },
|
|
658
|
+
createClientTransport: async (hubAddress) => { /* connect to hub */ },
|
|
659
|
+
},
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
await node.start();
|
|
663
|
+
|
|
664
|
+
// The node auto-discovers peers and assumes a role:
|
|
665
|
+
node.on('ready', ({ role, client, server, socket }) => {
|
|
666
|
+
console.log(`Role: ${role}`); // 'hub' or 'client'
|
|
667
|
+
console.log(`Server: ${server}`); // Server instance (hub only)
|
|
668
|
+
console.log(`Client: ${client}`); // Client instance (client only)
|
|
669
|
+
console.log(`Socket: ${socket}`); // Socket to hub (client only)
|
|
670
|
+
});
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### Data preservation across role transitions
|
|
674
|
+
|
|
675
|
+
The `Node` owns a single `IoMem`/`BsMem` pair, created once at `start()` and reused across all role transitions. When a role changes (e.g., hub → client → hub), the underlying storage is re-initialized without losing data — `IoMem.close()` only sets `_isOpen = false`, the data in memory is preserved.
|
|
676
|
+
|
|
677
|
+
This means:
|
|
678
|
+
- Data written while hub survives a transition to client
|
|
679
|
+
- Data written while client survives a transition to hub
|
|
680
|
+
- A re-elected hub still serves all previously stored blobs and tables
|
|
681
|
+
|
|
682
|
+
### Agent lifecycle
|
|
683
|
+
|
|
684
|
+
The `createAgent` factory in `NodeDeps` is called on every role transition. The returned `AgentHandle.stop()` is called before the next transition or when the node stops. This is how you wire application-level agents (e.g. `FsAgent`) without creating circular dependencies:
|
|
685
|
+
|
|
686
|
+
```ts
|
|
687
|
+
const node = new Node(config, {
|
|
688
|
+
createHubTransport: ...,
|
|
689
|
+
createClientTransport: ...,
|
|
690
|
+
createAgent: async ({ role, client, server, socket }) => {
|
|
691
|
+
// Wire your agent here — called on every role transition
|
|
692
|
+
const stopSync = await startMySync(role, client, server);
|
|
693
|
+
return { stop: () => stopSync() };
|
|
694
|
+
},
|
|
695
|
+
});
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
The Node serializes transitions — a new transition waits for the previous one to complete, ensuring agents are always stopped cleanly.
|
|
699
|
+
|
|
700
|
+
Errors in `createAgent` or `agentHandle.stop()` are caught and logged — they never crash the node. Similarly, transport factory failures degrade connectivity but leave the node functional locally.
|
|
701
|
+
|
|
702
|
+
### API
|
|
703
|
+
|
|
704
|
+
| Property / Method | Description |
|
|
705
|
+
| --------------------- | ----------------------------------------------------- |
|
|
706
|
+
| `node.start()` | Begin peer discovery and role assignment |
|
|
707
|
+
| `node.stop()` | Tear down current role and stop discovery |
|
|
708
|
+
| `node.role` | Current role: `'hub'`, `'client'`, or `'unassigned'` |
|
|
709
|
+
| `node.io` | IoMulti from active Server or Client |
|
|
710
|
+
| `node.bs` | BsMulti from active Server or Client |
|
|
711
|
+
| `node.server` | Server instance (when hub), else `undefined` |
|
|
712
|
+
| `node.client` | Client instance (when client), else `undefined` |
|
|
713
|
+
| `node.socket` | Socket to hub (when client), else `undefined` |
|
|
714
|
+
| `node.topology` | Current network topology snapshot |
|
|
715
|
+
| `node.on(event, cb)` | Subscribe to `'ready'`, `'role-changed'`, `'stopped'` |
|
|
716
|
+
| `node.off(event, cb)` | Unsubscribe |
|
|
717
|
+
|
|
583
718
|
## Architecture Overview
|
|
584
719
|
|
|
585
720
|
### Pull-Based Reference Architecture
|
package/dist/client.d.ts
CHANGED
|
@@ -45,6 +45,10 @@ export declare class Client extends BaseNode {
|
|
|
45
45
|
private _syncConfig?;
|
|
46
46
|
private _clientIdentity?;
|
|
47
47
|
private _peerInitTimeoutMs;
|
|
48
|
+
private _isConnected;
|
|
49
|
+
private _disconnectCallbacks;
|
|
50
|
+
private _reconnectCallbacks;
|
|
51
|
+
private _connectionCleanup?;
|
|
48
52
|
/**
|
|
49
53
|
* Creates a Client instance
|
|
50
54
|
* @param _socketToServer - Socket or namespace bundle to connect to server
|
|
@@ -91,11 +95,34 @@ export declare class Client extends BaseNode {
|
|
|
91
95
|
* Returns the logger instance.
|
|
92
96
|
*/
|
|
93
97
|
get logger(): ServerLogger;
|
|
98
|
+
/**
|
|
99
|
+
* Whether the client is currently connected to the server.
|
|
100
|
+
* Tracks socket-level connection state via `disconnect` and `connect` events.
|
|
101
|
+
*/
|
|
102
|
+
get isConnected(): boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Registers a callback that fires when the socket disconnects.
|
|
105
|
+
* The callback receives the disconnect reason string.
|
|
106
|
+
* @param callback - Invoked with the disconnect reason
|
|
107
|
+
*/
|
|
108
|
+
onDisconnect(callback: (reason: string) => void): void;
|
|
109
|
+
/**
|
|
110
|
+
* Registers a callback that fires when the socket reconnects
|
|
111
|
+
* after a previous disconnect.
|
|
112
|
+
* @param callback - Invoked on reconnection
|
|
113
|
+
*/
|
|
114
|
+
onReconnect(callback: () => void): void;
|
|
94
115
|
/**
|
|
95
116
|
* Creates Db and Connector from the route and IoMulti.
|
|
96
117
|
* Called during init() when a route was provided.
|
|
97
118
|
*/
|
|
98
119
|
private _setupDbAndConnector;
|
|
120
|
+
/**
|
|
121
|
+
* Registers socket-level disconnect/connect listeners.
|
|
122
|
+
* Logs state transitions and invokes registered callbacks.
|
|
123
|
+
* The `connect` callback only fires on RE-connections (not the initial connect).
|
|
124
|
+
*/
|
|
125
|
+
private _registerConnectionHandlers;
|
|
99
126
|
/**
|
|
100
127
|
* Builds the Io multi with local and peer layers.
|
|
101
128
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -4,8 +4,10 @@ export { FileLogger } from './file-logger.ts';
|
|
|
4
4
|
export type { FileLoggerOptions } from './file-logger.ts';
|
|
5
5
|
export { BufferedLogger, ConsoleLogger, FilteredLogger, NoopLogger, noopLogger, } from './logger.ts';
|
|
6
6
|
export type { LogEntry, ServerLogger } from './logger.ts';
|
|
7
|
+
export { Node } from './node.ts';
|
|
8
|
+
export type { AgentHandle, CreateAgent, CreateClientTransport, CreateHubTransport, HubTransport, NodeConfig, NodeDeps, NodeEventName, NodeEvents, ReadyContext, } from './node.ts';
|
|
7
9
|
export { Server } from './server.ts';
|
|
8
10
|
export type { ServerOptions } from './server.ts';
|
|
9
11
|
export { SocketIoBridge } from './socket-io-bridge.ts';
|
|
10
|
-
export type { AckPayload, ConnectorPayload, GapFillRequest, GapFillResponse, SyncConfig, SyncEventNames, } from '@rljson/rljson';
|
|
11
12
|
export { syncEvents } from '@rljson/rljson';
|
|
13
|
+
export type { AckPayload, ConnectorPayload, GapFillRequest, GapFillResponse, SyncConfig, SyncEventNames, } from '@rljson/rljson';
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { Bs } from '@rljson/bs';
|
|
2
|
+
import { Io } from '@rljson/io';
|
|
3
|
+
import { NetworkConfig, NetworkManagerOptions, NetworkTopology, NodeRole, RoleChangedEvent, NetworkManager } from '@rljson/network';
|
|
4
|
+
import { Route } from '@rljson/rljson';
|
|
5
|
+
import { Client, ClientOptions } from './client.ts';
|
|
6
|
+
import { ServerLogger } from './logger.ts';
|
|
7
|
+
import { Server, ServerOptions } from './server.ts';
|
|
8
|
+
import { SocketLike } from './socket-bundle.ts';
|
|
9
|
+
/**
|
|
10
|
+
* Transport handle returned by {@link CreateHubTransport}.
|
|
11
|
+
*
|
|
12
|
+
* Abstracts the "server side" of the transport layer so the Node class
|
|
13
|
+
* can accept incoming connections without knowing whether Socket.IO,
|
|
14
|
+
* WebSocket, or mock sockets are being used.
|
|
15
|
+
*/
|
|
16
|
+
export interface HubTransport {
|
|
17
|
+
/** Register a callback for incoming client connections. */
|
|
18
|
+
onConnection: (callback: (socket: SocketLike) => void) => void;
|
|
19
|
+
/** Shut down the transport (close HTTP server, etc.). */
|
|
20
|
+
close: () => Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Factory that creates a hub-side transport (e.g. HTTP + Socket.IO server).
|
|
24
|
+
* Called when this node becomes the hub.
|
|
25
|
+
*/
|
|
26
|
+
export type CreateHubTransport = (port: number) => Promise<HubTransport>;
|
|
27
|
+
/**
|
|
28
|
+
* Factory that creates a client-side socket to connect to the hub.
|
|
29
|
+
* Called when this node becomes a client.
|
|
30
|
+
*/
|
|
31
|
+
export type CreateClientTransport = (hubAddress: string) => Promise<SocketLike>;
|
|
32
|
+
/**
|
|
33
|
+
* Injectable dependencies for the Node class.
|
|
34
|
+
*/
|
|
35
|
+
export interface NodeDeps {
|
|
36
|
+
/** Factory to create hub-side transport. */
|
|
37
|
+
createHubTransport: CreateHubTransport;
|
|
38
|
+
/** Factory to create client-side socket to hub. */
|
|
39
|
+
createClientTransport: CreateClientTransport;
|
|
40
|
+
/** Options passed to NetworkManager (e.g. mock probe function). */
|
|
41
|
+
networkManagerOptions?: NetworkManagerOptions;
|
|
42
|
+
/**
|
|
43
|
+
* Optional factory for application-level agents (e.g. FsAgent).
|
|
44
|
+
* Called on every `ready` event. The returned handle's `stop()`
|
|
45
|
+
* is called before the next role transition or on `node.stop()`.
|
|
46
|
+
*/
|
|
47
|
+
createAgent?: CreateAgent;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Configuration for a self-organizing Node.
|
|
51
|
+
*/
|
|
52
|
+
export interface NodeConfig {
|
|
53
|
+
/** Network domain for peer discovery. */
|
|
54
|
+
domain: string;
|
|
55
|
+
/** Port for this node's hub server / probing. */
|
|
56
|
+
port: number;
|
|
57
|
+
/** Data route for Server/Client sync. */
|
|
58
|
+
route: Route;
|
|
59
|
+
/** Full network configuration (broadcast, cloud, static, probing). */
|
|
60
|
+
network?: Partial<NetworkConfig>;
|
|
61
|
+
/** Options forwarded to the Server constructor. */
|
|
62
|
+
serverOptions?: ServerOptions;
|
|
63
|
+
/** Options forwarded to the Client constructor. */
|
|
64
|
+
clientOptions?: ClientOptions;
|
|
65
|
+
/** Logger shared across Node, Server, and Client. */
|
|
66
|
+
logger?: ServerLogger;
|
|
67
|
+
/** Directory for persistent node identity. */
|
|
68
|
+
identityDir?: string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Context passed to the `ready` event and to {@link CreateAgent}.
|
|
72
|
+
*/
|
|
73
|
+
export interface ReadyContext {
|
|
74
|
+
/** The node's current role. */
|
|
75
|
+
role: NodeRole;
|
|
76
|
+
/** The Client instance (defined when role is `'client'`). */
|
|
77
|
+
client?: Client;
|
|
78
|
+
/** The Server instance (defined when role is `'hub'`). */
|
|
79
|
+
server?: Server;
|
|
80
|
+
/** The socket used to connect to the hub (defined when role is `'client'`). */
|
|
81
|
+
socket?: SocketLike;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Handle returned by {@link CreateAgent}.
|
|
85
|
+
* Node calls `stop()` before every role transition and on `node.stop()`.
|
|
86
|
+
*/
|
|
87
|
+
export interface AgentHandle {
|
|
88
|
+
stop: () => Promise<void> | void;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Factory called after each role transition to wire application-level agents
|
|
92
|
+
* (e.g. FsAgent). Return an {@link AgentHandle} whose `stop()` will be called
|
|
93
|
+
* before the next role transition or when the node stops.
|
|
94
|
+
*/
|
|
95
|
+
export type CreateAgent = (context: ReadyContext) => Promise<AgentHandle>;
|
|
96
|
+
/** Events emitted by Node. */
|
|
97
|
+
export interface NodeEvents {
|
|
98
|
+
/** Emitted when the node has assumed its role and is ready. */
|
|
99
|
+
ready: (context: ReadyContext) => void;
|
|
100
|
+
/** Emitted when the node's role changes. */
|
|
101
|
+
'role-changed': (event: RoleChangedEvent) => void;
|
|
102
|
+
/** Emitted when the node is stopped. */
|
|
103
|
+
stopped: () => void;
|
|
104
|
+
}
|
|
105
|
+
/** Valid event names for Node. */
|
|
106
|
+
export type NodeEventName = keyof NodeEvents;
|
|
107
|
+
/**
|
|
108
|
+
* Self-organizing node that automatically transitions between
|
|
109
|
+
* hub (Server) and client (Client) roles based on network topology.
|
|
110
|
+
*/
|
|
111
|
+
export declare class Node {
|
|
112
|
+
private readonly _config;
|
|
113
|
+
private readonly _deps;
|
|
114
|
+
private _networkManager;
|
|
115
|
+
private _server?;
|
|
116
|
+
private _client?;
|
|
117
|
+
private _hubTransport?;
|
|
118
|
+
private _clientSocket?;
|
|
119
|
+
private _agentHandle?;
|
|
120
|
+
private _ioMem?;
|
|
121
|
+
private _bsMem?;
|
|
122
|
+
private _role;
|
|
123
|
+
private _running;
|
|
124
|
+
private _transitioning?;
|
|
125
|
+
private _listeners;
|
|
126
|
+
private readonly _logger;
|
|
127
|
+
constructor(_config: NodeConfig, _deps: NodeDeps);
|
|
128
|
+
/**
|
|
129
|
+
* Start the node. Begins network discovery and role assignment.
|
|
130
|
+
*/
|
|
131
|
+
start(): Promise<void>;
|
|
132
|
+
/**
|
|
133
|
+
* Stop the node. Tears down Server/Client and network discovery.
|
|
134
|
+
*/
|
|
135
|
+
stop(): Promise<void>;
|
|
136
|
+
/** This node's current role. */
|
|
137
|
+
get role(): NodeRole;
|
|
138
|
+
/** Current network topology snapshot. */
|
|
139
|
+
get topology(): NetworkTopology;
|
|
140
|
+
/** The Io instance (from Server or Client), or undefined if unassigned. */
|
|
141
|
+
get io(): Io | undefined;
|
|
142
|
+
/** The Bs instance (from Server or Client), or undefined if unassigned. */
|
|
143
|
+
get bs(): Bs | undefined;
|
|
144
|
+
/** The Server instance when this node is the hub, or undefined. */
|
|
145
|
+
get server(): Server | undefined;
|
|
146
|
+
/** The Client instance when this node is a client, or undefined. */
|
|
147
|
+
get client(): Client | undefined;
|
|
148
|
+
/** The socket used to connect to the hub (defined when role is `'client'`). */
|
|
149
|
+
get socket(): SocketLike | undefined;
|
|
150
|
+
/** Whether the node is currently running. */
|
|
151
|
+
get isRunning(): boolean;
|
|
152
|
+
/** The underlying NetworkManager. */
|
|
153
|
+
get networkManager(): NetworkManager;
|
|
154
|
+
/**
|
|
155
|
+
* Subscribe to node events.
|
|
156
|
+
* @param event - Event name
|
|
157
|
+
* @param cb - Callback
|
|
158
|
+
*/
|
|
159
|
+
on<E extends NodeEventName>(event: E, cb: NodeEvents[E]): void;
|
|
160
|
+
/**
|
|
161
|
+
* Unsubscribe from node events.
|
|
162
|
+
* @param event - Event name
|
|
163
|
+
* @param cb - Callback
|
|
164
|
+
*/
|
|
165
|
+
off<E extends NodeEventName>(event: E, cb: NodeEvents[E]): void;
|
|
166
|
+
private _onRoleChanged;
|
|
167
|
+
private _performTransition;
|
|
168
|
+
private _becomeHub;
|
|
169
|
+
private _becomeClient;
|
|
170
|
+
private _tearDownCurrentRole;
|
|
171
|
+
private _startAgent;
|
|
172
|
+
private _stopAgent;
|
|
173
|
+
private _emit;
|
|
174
|
+
}
|