@rljson/server 0.0.4 → 0.0.5

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.
@@ -8,4 +8,12 @@ found in the LICENSE file in the root of this package.
8
8
 
9
9
  # Blog
10
10
 
11
- Add latest posts at the end.
11
+ Add posts as Markdown entries in this file (newest last). Keep each post small and link to source code or PRs when helpful. Template:
12
+
13
+ ```md
14
+ ## YYYY-MM-DD — Title
15
+
16
+ - What changed (1–3 bullets)
17
+ - Why it matters
18
+ - Links: PRs, docs, demos
19
+ ```
@@ -8,25 +8,59 @@ found in the LICENSE file in the root of this package.
8
8
 
9
9
  # Contributors Guide
10
10
 
11
- - [Prepare](#prepare)
12
- - [Develop](#develop)
13
- - [Administrate](#administrate)
14
- - [Fast Coding](#fast-coding)
11
+ - [Prerequisites](#prerequisites)
12
+ - [Setup](#setup)
13
+ - [Everyday development](#everyday-development)
14
+ - [Publishing](#publishing)
15
+ - [More docs](#more-docs)
15
16
 
16
- ## Prepare
17
+ ## Prerequisites
17
18
 
18
- Read [prepare.md](doc/prepare.md)
19
+ - Node.js v22.14.0+
20
+ - pnpm v10 (see [install-node-mac.md](doc/install-node-mac.md) or [install-node-win.md](doc/install-node-win.md))
21
+ - Optional: sibling repos `rljson-io`, `rljson-bs`, `rljson-db`, `rljson` if you prefer linking for local development
19
22
 
20
- <!-- ........................................................................-->
23
+ ## Setup
21
24
 
22
- ## Develop
25
+ ```sh
26
+ pnpm install
27
+ ```
23
28
 
24
- Read [develop.md](doc/develop.md)
29
+ By default we consume published npm versions. If you want to work against local packages instead, `pnpm link` them in sibling folders and add temporary overrides as needed.
25
30
 
26
- ## Administrate
31
+ ## Everyday development
27
32
 
28
- Read [create-new-repo.md](doc/create-new-repo.md)
33
+ - Run tests + lint (default CI path):
29
34
 
30
- ## Fast Coding
35
+ ```sh
36
+ pnpm test
37
+ ```
31
38
 
32
- Read [fast-coding-guide.md](doc/fast-coding-guide.md)
39
+ - Lint only:
40
+
41
+ ```sh
42
+ pnpm lint
43
+ ```
44
+
45
+ - Build the package (emits dist, copies README):
46
+
47
+ ```sh
48
+ pnpm build
49
+ ```
50
+
51
+ - Update goldens for snapshot-like tests:
52
+
53
+ ```sh
54
+ pnpm updateGoldens
55
+ ```
56
+
57
+ ## Publishing
58
+
59
+ `pnpm build` runs `pnpm test` via `prebuild`. `pnpm publish` (or npm publish) will trigger `prepublishOnly` and uses the built `dist` folder. Keep the changelog and versioning in sync with repo guidelines.
60
+
61
+ ## More docs
62
+
63
+ - [doc/prepare.md](doc/prepare.md)
64
+ - [doc/develop.md](doc/develop.md)
65
+ - [doc/create-new-repo.md](doc/create-new-repo.md)
66
+ - [doc/fast-coding-guide.md](doc/fast-coding-guide.md)
package/dist/README.md CHANGED
@@ -8,17 +8,76 @@ found in the LICENSE file in the root of this package.
8
8
 
9
9
  # @rljson/server
10
10
 
11
- ## Users
11
+ Local-first, pull-by-reference server layer for Rljson. Clients keep writes local, pull data on demand through multis, and let the server proxy references without duplicating client data.
12
12
 
13
- | File | Purpose |
14
- | ------------------------------------ | --------------------------- |
15
- | [README.public.md](README.public.md) | Install and use the package |
13
+ - Writes stay local; reads cascade: local ➜ server ➜ peers
14
+ - References (hashes) flow; data is pulled on demand
15
+ - Server aggregates sockets and multicasts refs, but only stores what you explicitly import
16
16
 
17
- ## Contributors
17
+ ## Quick start
18
18
 
19
- | File | Purpose |
20
- | ------------------------------------------------ | ----------------------------- |
21
- | [README.contributors.md](README.contributors.md) | Run, debug, build and publish |
22
- | [README.architecture.md](README.architecture.md) | Software architecture guide |
23
- | [README.trouble.md](README.trouble.md) | Errors & solutions |
24
- | [README.blog.md](README.blog.md) | Blog |
19
+ Install:
20
+
21
+ ```sh
22
+ pnpm add @rljson/server
23
+ ```
24
+
25
+ Minimal server:
26
+
27
+ ```ts
28
+ import { BsMem } from '@rljson/bs';
29
+ import { IoMem } from '@rljson/io';
30
+ import { Route } from '@rljson/rljson';
31
+ import { Server, SocketIoBridge } from '@rljson/server';
32
+
33
+ const route = Route.fromFlat('my.app');
34
+ const serverIo = new IoMem();
35
+ await serverIo.init();
36
+ await serverIo.isReady();
37
+
38
+ const server = new Server(route, serverIo, new BsMem());
39
+ await server.init();
40
+
41
+ // When your runtime yields sockets, wrap them:
42
+ // await server.addSocket(new SocketIoBridge(serverSocket));
43
+ ```
44
+
45
+ Minimal client:
46
+
47
+ ```ts
48
+ import { BsMem } from '@rljson/bs';
49
+ import { IoMem } from '@rljson/io';
50
+ import { Client, SocketIoBridge } from '@rljson/server';
51
+
52
+ const client = new Client(new SocketIoBridge(clientSocket), new IoMem(), new BsMem());
53
+ await client.init();
54
+
55
+ const io = client.io; // IoMulti merged interface
56
+ const bs = client.bs; // BsMulti merged interface
57
+ ```
58
+
59
+ Run tests and lint:
60
+
61
+ ```sh
62
+ pnpm test
63
+ ```
64
+
65
+ Build distribution:
66
+
67
+ ```sh
68
+ pnpm build
69
+ ```
70
+
71
+ ## Documentation map
72
+
73
+ | Audience | File | Highlights |
74
+ | --------------- | ------------------------------------------------ | ------------------------------------------------- |
75
+ | Users | [README.public.md](README.public.md) | Install, usage, networking model, examples |
76
+ | Contributors | [README.contributors.md](README.contributors.md) | Setup, dev workflow, publishing, fast coding tips |
77
+ | Architecture | [README.architecture.md](README.architecture.md) | Deep dive into multis, peer bridges, data flows |
78
+ | Troubleshooting | [README.trouble.md](README.trouble.md) | Known issues and fixes |
79
+ | Blog | [README.blog.md](README.blog.md) | Writing and collecting project blog entries |
80
+
81
+ ## Example code
82
+
83
+ See [src/example.ts](src/example.ts) for a runnable end-to-end demo and [test/server.spec.ts](test/server.spec.ts) for broader integration cases.
@@ -10,6 +10,19 @@ found in the LICENSE file in the root of this package.
10
10
 
11
11
  @rljson/server provides a lightweight client/server layer for Rljson storage. It wires Io (row/table data) and Bs (blob storage) over sockets so clients can read from server storage while still keeping their own local storage.
12
12
 
13
+ ## Prerequisites
14
+
15
+ - Node.js v22.14.0 or newer
16
+ - A socket runtime (examples use Socket.IO)
17
+ - `Io`/`Bs` implementations (in-memory examples use `IoMem` and `BsMem`)
18
+
19
+ ## Design pillars
20
+
21
+ - **Local-first, read-through**: Writes stay on the caller; reads walk priorities (local first, then peers via server).
22
+ - **Pull by reference**: Only references (hashes) travel over the wire; data is pulled on demand through `IoMulti`/`BsMulti`.
23
+ - **Server as proxy**: The server aggregates and multicasts refs, but does not duplicate client data unless you intentionally store it there.
24
+ - **Single abstraction surface**: `Client.io`/`Client.bs` and `Server.io`/`Server.bs` expose merged multis so you do not handle peers manually.
25
+
13
26
  ## What it does (quick overview)
14
27
 
15
28
  - **Server** hosts Io + Bs and exposes them over sockets.
@@ -22,6 +35,50 @@ found in the LICENSE file in the root of this package.
22
35
  pnpm add @rljson/server
23
36
  ```
24
37
 
38
+ ## Quick start (Socket.IO example)
39
+
40
+ Server setup:
41
+
42
+ ```ts
43
+ import { createServer } from 'node:http';
44
+ import { Server as SocketIoServer } from 'socket.io';
45
+ import { BsMem } from '@rljson/bs';
46
+ import { IoMem } from '@rljson/io';
47
+ import { Route } from '@rljson/rljson';
48
+ import { Server, SocketIoBridge } from '@rljson/server';
49
+
50
+ const httpServer = createServer();
51
+ const socketIo = new SocketIoServer(httpServer);
52
+
53
+ const route = Route.fromFlat('my.app');
54
+ const server = new Server(route, new IoMem(), new BsMem());
55
+ await server.init();
56
+
57
+ socketIo.on('connection', async (socket) => {
58
+ await server.addSocket(new SocketIoBridge(socket));
59
+ });
60
+
61
+ httpServer.listen(0);
62
+ ```
63
+
64
+ Client setup:
65
+
66
+ ```ts
67
+ import { io as socketIoClient } from 'socket.io-client';
68
+ import { BsMem } from '@rljson/bs';
69
+ import { IoMem } from '@rljson/io';
70
+ import { Db } from '@rljson/db';
71
+ import { Client, SocketIoBridge } from '@rljson/server';
72
+
73
+ const socket = socketIoClient('http://localhost:3000', { forceNew: true });
74
+
75
+ const client = new Client(new SocketIoBridge(socket), new IoMem(), new BsMem());
76
+ await client.init();
77
+
78
+ const db = new Db(client.io!);
79
+ // db.get/insert now cascade local ➜ server automatically
80
+ ```
81
+
25
82
  ## Basic usage
26
83
 
27
84
  ### Server
@@ -46,7 +103,7 @@ await server.init();
46
103
  // await server.addSocket(new SocketIoBridge(serverSocket));
47
104
  ```
48
105
 
49
- ### Client
106
+ ### Client API
50
107
 
51
108
  ```ts
52
109
  import { BsMem } from '@rljson/bs';
@@ -87,7 +144,7 @@ This is implemented with `IoMulti` and `BsMulti` internally, but the public API
87
144
  - `io` – Io interface (multi-layer)
88
145
  - `bs` – Bs interface (multi-layer)
89
146
 
90
- ### Server
147
+ ### Server API
91
148
 
92
149
  - `init()` – initializes server multis
93
150
  - `ready()` – resolves when Io is ready
@@ -127,3 +184,148 @@ The same pattern is used for Bs (blob storage).
127
184
 
128
185
  - `Client.io` and `Client.bs` are already merged interfaces. No need to access multis directly.
129
186
  - `Server.addSocket()` batches refreshes to reduce rebuild overhead when multiple sockets connect.
187
+ - Multicast includes `__origin` markers plus a `_multicastedRefs` set to prevent ref echo loops.
188
+
189
+ ## Architecture Overview
190
+
191
+ ### Pull-Based Reference Architecture
192
+
193
+ @rljson/server implements a **pull-based architecture** where data is retrieved on-demand using content-addressed references (hashes), not automatically pushed between clients. This fundamentally differs from traditional sync systems:
194
+
195
+ ### Key principle: references flow, data is pulled
196
+
197
+ ```text
198
+ Reference Flow: Client A → Server → Client B (broadcast)
199
+ Data Flow: Client A ← Server ← Client B (pulled on query)
200
+ ```
201
+
202
+ ### How It Works
203
+
204
+ 1. **Client A stores data locally** (writes to priority 1 layer)
205
+
206
+ ```ts
207
+ const results = await db.insert(route, [data]);
208
+ const ref = results[0]._hash;
209
+ ```
210
+
211
+ 2. **Client A broadcasts reference** (not the data)
212
+
213
+ ```ts
214
+ socket.emit(route.flat, ref);
215
+ ```
216
+
217
+ 3. **Client B receives reference** via multicast
218
+
219
+ ```ts
220
+ socket.on(route.flat, (ref) => { /* ... */ });
221
+ ```
222
+
223
+ 4. **Client B queries by reference**
224
+
225
+ ```ts
226
+ const result = await db.get(route, { _hash: ref });
227
+ ```
228
+
229
+ 5. **Server automatically pulls from Client A**
230
+ - Client B's query goes to its IoMulti (priority 1: local, not found)
231
+ - Falls back to IoPeer → Server (priority 2)
232
+ - Server's IoMulti cascades: priority 1 (local cache), then priority 2 (IoPeer[Client A])
233
+ - Data flows back: Client A → Server → Client B
234
+
235
+ **This cascade happens automatically** - no explicit pull operation needed.
236
+
237
+ ### Three Storage Types
238
+
239
+ #### 1. Io Data (Tables)
240
+
241
+ - **What**: Relational tables (Cake, Cell, custom content types)
242
+ - **Storage**: IoMulti (local Io + IoPeer instances)
243
+ - **Query**: `db.get(route, { _hash: ref })`
244
+ - **Use Cases**: Structured data, records, metadata
245
+
246
+ #### 2. Bs Data (Blobs)
247
+
248
+ - **What**: Binary blobs (files, images, videos)
249
+ - **Storage**: BsMulti (local Bs + BsPeer instances)
250
+ - **Query**: `bs.get(blobHash)` after getting ref from Io table
251
+ - **Use Cases**: Large files, media content
252
+ - **Pattern**: Store blob → get hash → store hash in Io table → others query by hash
253
+
254
+ #### 3. Tree Data (Hierarchical)
255
+
256
+ - **What**: JSON objects converted to tree structures
257
+ - **Storage**: In Io layer with special 'trees' content type
258
+ - **Conversion**: `treeFromObject(jsObject)` creates Tree[] array
259
+ - **Root**: Last element in array (`trees[trees.length - 1]._hash`)
260
+ - **Query**: `db.get(route, { _hash: rootHash })` returns ALL related nodes
261
+ - **Use Cases**: Configuration objects, nested data structures
262
+
263
+ ### Client-to-Client Communication
264
+
265
+ ### Pattern: Insert on Client A, Get on Client B
266
+
267
+ ```ts
268
+ // Setup: All parties create table definitions
269
+ await server.createTables({ withInsertHistory: [tableCfg] });
270
+ await clientA.createTables({ withInsertHistory: [tableCfg] });
271
+ await clientB.createTables({ withInsertHistory: [tableCfg] });
272
+
273
+ // Client A: Insert data locally
274
+ const result = await dbA.insert(route, [{ name: 'Tesla', model: 'Model S' }]);
275
+ const ref = result[0]._hash;
276
+
277
+ // Client A: Broadcast reference
278
+ clientA.socket.emit(route.flat, ref);
279
+
280
+ // Client B: Listen and pull data
281
+ clientB.socket.on(route.flat, async (ref) => {
282
+ // This query automatically cascades through server to Client A
283
+ const data = await dbB.get(route, { _hash: ref });
284
+ console.log(data.rljson.cars._data[0]); // { name: 'Tesla', ... }
285
+ });
286
+ ```
287
+
288
+ **Server never stores the car data** - it only proxies the query from Client B to Client A.
289
+
290
+ ### Why Pull-Based?
291
+
292
+ | Aspect | Pull-Based (@rljson/server) | Push-Based (Traditional) |
293
+ | ------------------- | ----------------------------- | ---------------------------- |
294
+ | **Network Traffic** | Minimal (only refs) | High (all data replicated) |
295
+ | **Data Freshness** | Always latest (pull on query) | Can be stale (cached copies) |
296
+ | **Storage** | Single source of truth | Multiple copies to sync |
297
+ | **Bandwidth** | Low (on-demand only) | High (push all changes) |
298
+ | **Offline** | Works fully offline | Needs sync when reconnected |
299
+ | **Conflicts** | None (read from source) | Requires resolution logic |
300
+
301
+ ### When to Use @rljson/server
302
+
303
+ ✅ **Good fit:**
304
+
305
+ - Local-first applications with occasional sharing
306
+ - Collaborative tools where users own their data
307
+ - Media sharing apps (store locally, share by reference)
308
+ - Configuration management (pull config by root hash)
309
+ - Document collaboration (pull latest version by ref)
310
+
311
+ ❌ **Not ideal for:**
312
+
313
+ - Real-time collaborative editing (character-by-character)
314
+ - Systems requiring strong consistency guarantees
315
+ - Centralized storage where server must have all data
316
+ - Automatic background sync without references
317
+
318
+ ### Key Design Principles
319
+
320
+ 1. **Local-First**: All writes go to local storage only
321
+ 2. **Content-Addressed**: Everything referenced by hash
322
+ 3. **Reference-Based Discovery**: Need a reference to query data
323
+ 4. **Automatic Cascade**: IoMulti/BsMulti handle priority traversal
324
+ 5. **Server as Proxy**: Server doesn't store client data, only routes queries
325
+ 6. **Pull on Demand**: Data retrieved only when explicitly queried
326
+
327
+ ### Next Steps
328
+
329
+ - See [README.architecture.md](README.architecture.md) for detailed architecture documentation
330
+ - See [test/server.spec.ts](test/server.spec.ts) for comprehensive integration examples
331
+ - See [src/example.ts](src/example.ts) for a basic usage example
@@ -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,6 +1,7 @@
1
1
  import { Bs } from '@rljson/bs';
2
- import { Io, IoMulti, Socket } from '@rljson/io';
2
+ import { Io, IoMulti } from '@rljson/io';
3
3
  import { BaseNode } from './base-node.ts';
4
+ import { SocketLike } from './socket-bundle.ts';
4
5
  export declare class Client extends BaseNode {
5
6
  private _socketToServer;
6
7
  protected _localIo: Io;
@@ -11,11 +12,11 @@ export declare class Client extends BaseNode {
11
12
  private _bsMulti?;
12
13
  /**
13
14
  * Creates a Client instance
14
- * @param _socketToServer - Socket to connect to server
15
+ * @param _socketToServer - Socket or namespace bundle to connect to server
15
16
  * @param _localIo - Local Io for local storage
16
17
  * @param _localBs - Local Bs for local blob storage
17
18
  */
18
- constructor(_socketToServer: Socket, _localIo: Io, _localBs: Bs);
19
+ constructor(_socketToServer: SocketLike, _localIo: Io, _localBs: Bs);
19
20
  /**
20
21
  * Initializes Io and Bs multis and their peer bridges.
21
22
  * @returns The initialized Io implementation.
@@ -47,10 +48,12 @@ export declare class Client extends BaseNode {
47
48
  private _setupBs;
48
49
  /**
49
50
  * Creates and initializes a downstream Io peer.
51
+ * @param socket - Downstream socket to the server Io namespace.
50
52
  */
51
53
  private _createIoPeer;
52
54
  /**
53
55
  * Creates and initializes a downstream Bs peer.
56
+ * @param socket - Downstream socket to the server Bs namespace.
54
57
  */
55
58
  private _createBsPeer;
56
59
  }
package/dist/server.d.ts CHANGED
@@ -2,6 +2,7 @@ import { Bs, BsPeer } from '@rljson/bs';
2
2
  import { Io, IoPeer, Socket } from '@rljson/io';
3
3
  import { Route } from '@rljson/rljson';
4
4
  import { BaseNode } from './base-node.ts';
5
+ import { SocketLike } from './socket-bundle.ts';
5
6
  export type SocketWithClientId = Socket & {
6
7
  __clientId?: string;
7
8
  };
@@ -33,7 +34,7 @@ export declare class Server extends BaseNode {
33
34
  * @param socket - Client socket to register.
34
35
  * @returns The server instance.
35
36
  */
36
- addSocket(socket: Socket): Promise<this>;
37
+ addSocket(socket: SocketLike): Promise<this>;
37
38
  /**
38
39
  * Removes all listeners from all connected clients.
39
40
  */
@@ -56,7 +57,10 @@ export declare class Server extends BaseNode {
56
57
  * Returns the connected clients map.
57
58
  */
58
59
  get clients(): Map<string, {
59
- socket: SocketWithClientId;
60
+ ioUp: SocketWithClientId;
61
+ ioDown: SocketWithClientId;
62
+ bsUp: SocketWithClientId;
63
+ bsDown: SocketWithClientId;
60
64
  io: IoPeer;
61
65
  bs: BsPeer;
62
66
  }>;
@@ -73,7 +77,7 @@ export declare class Server extends BaseNode {
73
77
  /**
74
78
  * Registers the client socket and peers.
75
79
  * @param clientId - Stable client identifier.
76
- * @param socket - Client socket to register.
80
+ * @param sockets - Directional sockets to register.
77
81
  * @param io - Io peer associated with the client.
78
82
  * @param bs - Bs peer associated with the client.
79
83
  */