@rljson/server 0.0.10 → 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.
@@ -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
@@ -637,6 +637,84 @@ await server.removeSocket(clientIds[0]);
637
637
  // Automatic: clients are removed when their socket emits 'disconnect'
638
638
  ```
639
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
+
640
718
  ## Architecture Overview
641
719
 
642
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.
@@ -637,6 +637,84 @@ await server.removeSocket(clientIds[0]);
637
637
  // Automatic: clients are removed when their socket emits 'disconnect'
638
638
  ```
639
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
+
640
718
  ## Architecture Overview
641
719
 
642
720
  ### Pull-Based Reference Architecture
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
+ }