@rljson/server 0.0.3 → 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.
@@ -7,3 +7,1119 @@ found in the LICENSE file in the root of this package.
7
7
  -->
8
8
 
9
9
  # Architecture
10
+
11
+ ## Overview
12
+
13
+ The `@rljson/server` package implements a distributed, local-first data architecture that enables multiple clients to share data through a central server while maintaining local storage priority. The system uses a multi-layer approach where local data always takes precedence over server data.
14
+
15
+ ### System map (ASCII)
16
+
17
+ ```text
18
+ [ Client A ] [ Client B ]
19
+ ┌────────────────┐ ┌────────────────┐
20
+ │ IoMulti │ │ IoMulti │
21
+ │ BsMulti │ │ BsMulti │
22
+ │ (local first) │ │ (local first) │
23
+ └───────┬────────┘ └────────┬───────┘
24
+ │ IoPeer/BsPeer (pull from server) │
25
+ │ │
26
+ ┌───────▼────────┐ multicast refs ┌──────▼────────┐
27
+ │ Server │◄──────────────────►│ Server │
28
+ │ IoMulti │ │ BsMulti │
29
+ │ (local+peers) │ │ (local+peers)│
30
+ └───────┬────────┘ └────────┬──────┘
31
+ │ IoPeerBridge/BsPeerBridge (pull to clients)
32
+
33
+ ┌───────▼────────┐
34
+ │ Client C (etc) │
35
+ └────────────────┘
36
+ ```
37
+
38
+ - **Refs broadcast**: Clients emit hashes; server multicasts to others.
39
+ - **Data pulls**: Readers query by ref; multis cascade local ➜ server ➜ peers.
40
+ - **No push of payloads**: Only hashes traverse sockets by default.
41
+
42
+ ### Request flow (pull by reference)
43
+
44
+ ```text
45
+ Client B: db.get(route, {_hash: ref})
46
+ ↓ priority walk
47
+ 1) Local Io (miss)
48
+ 2) IoPeer → Server IoMulti
49
+ a) Server Local Io (miss?)
50
+ b) IoPeer[Client A] (hit)
51
+ ↑ data flows back A → Server → B
52
+ ```
53
+
54
+ ### Layer cheat sheet
55
+
56
+ - **Priority 1**: Local Io/Bs (read+write)
57
+ - **Priority 2+**: Peers (read-only), ordered insertion
58
+ - **Servers**: IoServer/BsServer expose multis to clients
59
+ - **Bridges**: IoPeerBridge/BsPeerBridge let server pull from clients
60
+ - **Peers**: IoPeer/BsPeer let clients pull from server
61
+
62
+ ### Socket namespace separation (why)
63
+
64
+ - **Isolation of channels**: Io (tables) and Bs (blobs) have different payload shapes and backpressure behavior. Separate namespaces prevent cross-talk and let us tune each channel independently.
65
+ - **Avoid coupling and event collisions**: Socket.IO treats event names within a namespace; isolating `io` and `bs` avoids accidental handler overlap and makes tracing simpler.
66
+ - **Directional clarity**: We split up/down per layer (`ioUp/ioDown`, `bsUp/bsDown`) so bridges can enforce read-only vs. read/write roles and keep the API symmetrical for server and client wiring.
67
+ - **Transport flexibility**: In environments that support multiple transports or QoS settings, namespaces can be mapped to different priorities or even different sockets without changing higher-level code.
68
+
69
+ In default setups you can reuse a single socket for all four channels; the code normalizes that into a bundle. When you need stricter isolation (e.g., large blob streams vs. small Io refs), use distinct namespaces/sockets to avoid head-of-line blocking and to keep logging/metrics per channel.
70
+
71
+ ### Design Pillars
72
+
73
+ - **Local-first reads, local-only writes**: All mutations stay on the caller; reads walk the priority ladder (local first, then peers through the server).
74
+ - **Pull by reference**: References (hashes) travel over the wire; data is fetched on-demand through `IoMulti`/`BsMulti`.
75
+ - **Server as proxy/aggregator**: The server multicasts refs and aggregates peers but does not duplicate client data unless explicitly imported there.
76
+ - **Unified surface area**: Public APIs expose merged multis (`Client.io/bs`, `Server.io/bs`) so callers never assemble peer lists manually.
77
+
78
+ ## Core Components
79
+
80
+ ### 1. Client
81
+
82
+ The `Client` class provides a unified interface for data access by combining local storage with server storage.
83
+
84
+ **Key Responsibilities:**
85
+
86
+ - Manage local Io (data tables) and Bs (blob storage)
87
+ - Create bidirectional communication with server
88
+ - Merge local and server data layers into single interfaces (IoMulti, BsMulti)
89
+
90
+ **Data Flow Architecture:**
91
+
92
+ ```text
93
+ ┌─────────────────────────────────────────┐
94
+ │ Client Instance │
95
+ ├─────────────────────────────────────────┤
96
+ │ │
97
+ │ ┌───────────────────────────────────┐ │
98
+ │ │ IoMulti (Priority) │ │
99
+ │ ├───────────────────────────────────┤ │
100
+ │ │ 1. Local Io (read/write/dump) │ │ ← Priority 1: Local First
101
+ │ │ 2. IoPeer (read only) │ │ ← Priority 2: Server Read
102
+ │ └───────────────────────────────────┘ │
103
+ │ ▲ ▲ │
104
+ │ │ │ │
105
+ │ IoPeerBridge IoPeer │
106
+ │ (upstream) (downstream) │
107
+ │ │ │ │
108
+ └───────────┼──────────────┼──────────────┘
109
+ │ │
110
+ ▼ ▼
111
+ ┌───────────────────────────┐
112
+ │ Socket to Server │
113
+ └───────────────────────────┘
114
+ ```
115
+
116
+ **Upstream (Client → Server):**
117
+
118
+ - `IoPeerBridge`: Exposes client's local Io to server for reading
119
+ - `BsPeerBridge`: Exposes client's local Bs to server for reading
120
+ - Server can pull data from connected clients
121
+
122
+ **Downstream (Server → Client):**
123
+
124
+ - `IoPeer`: Allows client to read from server's Io
125
+ - `BsPeer`: Allows client to read from server's Bs
126
+ - Client can pull data from server
127
+
128
+ ### 2. Server
129
+
130
+ The `Server` class acts as a central coordination point that:
131
+
132
+ - Manages connections to multiple clients
133
+ - Aggregates data from all clients into unified interfaces
134
+ - Broadcasts notifications between clients
135
+ - Provides read access to its own local storage
136
+
137
+ **Data Flow Architecture:**
138
+
139
+ ```text
140
+ ┌────────────────────────────────────────────────────┐
141
+ │ Server Instance │
142
+ ├────────────────────────────────────────────────────┤
143
+ │ │
144
+ │ ┌──────────────────────────────────────────────┐ │
145
+ │ │ IoMulti (Priority) │ │
146
+ │ ├──────────────────────────────────────────────┤ │
147
+ │ │ 1. Local Io (read/write/dump) │ │ ← Priority 1
148
+ │ │ 2. IoPeer[Client A] (read only) │ │ ← Priority 2
149
+ │ │ 3. IoPeer[Client B] (read only) │ │ ← Priority 2
150
+ │ │ 4. IoPeer[Client C] (read only) │ │ ← Priority 2
151
+ │ └──────────────────────────────────────────────┘ │
152
+ │ │ │
153
+ │ ▼ │
154
+ │ ┌──────────────────────────────────────────────┐ │
155
+ │ │ IoServer │ │
156
+ │ │ (Exposes IoMulti to clients) │ │
157
+ │ └──────────────────────────────────────────────┘ │
158
+ │ │
159
+ │ Connected Clients: │
160
+ │ ┌──────────────────────────────────────────────┐ │
161
+ │ │ Client A → IoPeer A, Socket A │ │
162
+ │ │ Client B → IoPeer B, Socket B │ │
163
+ │ │ Client C → IoPeer C, Socket C │ │
164
+ │ └──────────────────────────────────────────────┘ │
165
+ └────────────────────────────────────────────────────┘
166
+ ```
167
+
168
+ **Lifecycle and controls:**
169
+
170
+ - `addSocket()` attaches a stable `__clientId`, builds `IoPeer`/`BsPeer`, queues them, rebuilds multis once, and refreshes servers in a batch.
171
+ - Multicast uses `__origin` markers plus `_multicastedRefs` to avoid echo loops and duplicate ref forwarding.
172
+ - Pending sockets are refreshed together so multiple joins trigger a single multi rebuild.
173
+
174
+ ### 3. Multi-Layer Priority System
175
+
176
+ Both Client and Server use `IoMulti` and `BsMulti` to merge multiple data sources:
177
+
178
+ **Priority Rules:**
179
+
180
+ - **Priority 1 (Local)**: Read/Write/Dump enabled, always checked first
181
+ - **Priority 2 (Peer)**: Read-only, fallback when data not found locally
182
+
183
+ **Example Flow:**
184
+
185
+ ```text
186
+ Client A reads table "cars":
187
+ 1. Check local IoMem (priority 1) → Not found
188
+ 2. Check IoPeer to server (priority 2) → Found!
189
+ 3. Return data from server
190
+
191
+ Client A writes to table "cars":
192
+ 1. Write to local IoMem (priority 1) only
193
+ 2. IoPeer is read-only, no write to server
194
+ 3. Local data now takes precedence
195
+ ```
196
+
197
+ ### 4. BaseNode (shared helper)
198
+
199
+ `Client` and `Server` both extend `BaseNode`, which enforces an open local Io and provides Db helpers:
200
+
201
+ - `createTables()` seeds table definitions on the local Io (optionally with insert history).
202
+ - `import()` loads rljson payloads into the local Db, keeping writes local-first.
203
+ - A guard throws if the supplied local Io is not initialized/open, catching miswired setups early.
204
+
205
+ ## Synchronization Patterns
206
+
207
+ ### Overview: Pull-Based Reference Architecture
208
+
209
+ The system implements a **pull-based architecture** where data is retrieved on-demand using references (hashes). No data is automatically pushed between clients or to the server. Instead:
210
+
211
+ 1. **Client stores data locally** (write to priority 1 layer)
212
+ 2. **Client exposes data via IoPeerBridge/BsPeerBridge** (read-only upstream)
213
+ 3. **Other clients retrieve data by reference** (pull from priority 2 layer)
214
+ 4. **Server acts as proxy**, pulling from connected clients on-demand
215
+
216
+ ### Key principle: references flow, data is pulled
217
+
218
+ ```text
219
+ Reference Flow: Client A → Server → Client B
220
+ Data Flow: Client A ← Server ← Client B (pulled on-demand)
221
+ ```
222
+
223
+ ### IoMulti and BsMulti Architecture
224
+
225
+ Both Client and Server use multi-layer storage to aggregate data from multiple sources:
226
+
227
+ **IoMulti (Data Tables):**
228
+
229
+ - Priority 1: Local Io (read/write/dump)
230
+ - Priority 2+: IoPeer instances (read-only) to other participants
231
+
232
+ **BsMulti (Blob Storage):**
233
+
234
+ - Priority 1: Local Bs (read/write)
235
+ - Priority 2+: BsPeer instances (read-only) to other blob stores
236
+
237
+ **Multi-Layer Query Flow:**
238
+
239
+ ```text
240
+ Query: db.get(route, { _hash: "abc123" })
241
+
242
+
243
+ 1. Check Local Io (priority 1)
244
+ ├─ Found? → Return data ✓
245
+ └─ Not found? → Continue to priority 2
246
+
247
+
248
+ 2. Check IoPeer to Server (priority 2)
249
+ ├─ Server checks its Local Io (priority 1)
250
+ │ └─ Not found? → Continue to server's priority 2
251
+ │ │
252
+ │ ▼
253
+ │ Server queries IoPeer[Client A] (server priority 2)
254
+ │ └─ Found in Client A! → Return via chain ✓
255
+
256
+ └─ Data flows back: Client A → Server → Client B
257
+ ```
258
+
259
+ ## Data Synchronization Patterns
260
+
261
+ ### Pattern 1: Io Data Sync (Regular Tables)
262
+
263
+ Io data represents regular relational tables (Cake, Cell, etc.) stored in the Io layer.
264
+
265
+ #### Scenario: Client A inserts data, Client B retrieves it
266
+
267
+ ```text
268
+ ┌──────────┐ ┌──────────┐ ┌──────────┐
269
+ │Client A │ │ Server │ │Client B │
270
+ └────┬─────┘ └────┬─────┘ └────┬─────┘
271
+ │ │ │
272
+ │ 1. db.insert(route, data) │ │
273
+ ├──────────────────────► │ │
274
+ │ (writes to local Io) │ │
275
+ │ Returns: [{ _hash }] │ │
276
+ │ │ │
277
+ │ 2. Broadcast ref to server │ │
278
+ │ socket.emit(route, ref) │ │
279
+ ├────────────────────────────►│ │
280
+ │ │ │
281
+ │ │ 3. Multicast ref to Client B│
282
+ │ ├─────────────────────────────►
283
+ │ │ (with __origin marker) │
284
+ │ │ │
285
+ │ │ 4. Client B: db.get(route, {_hash: ref})
286
+ │ │◄────────────────────────────┤
287
+ │ │ │
288
+ │ 5. Server pulls from A │ │
289
+ │◄────────────────────────────┤ │
290
+ │ via IoPeerBridge │ │
291
+ │ │ │
292
+ │────────────────────────────►│ │
293
+ │ Returns data │ │
294
+ │ │ │
295
+ │ │ 6. Server returns to B │
296
+ │ ├─────────────────────────────►
297
+ │ │ Data pulled through chain│
298
+ ```
299
+
300
+ **Implementation Details:**
301
+
302
+ ```typescript
303
+ // Client A: Insert data (writes locally)
304
+ const insertResults = await dbA.insert(route, [cakeData]);
305
+ const dataRef = insertResults[0]._hash;
306
+
307
+ // Client A: Broadcast reference (optional for notifications)
308
+ clientA.socket.emit(route.flat, dataRef);
309
+
310
+ // Client B: Retrieve by reference (pulls data)
311
+ const result = await dbB.get(route, { _hash: dataRef });
312
+ // Query flows: Client B → IoPeer → Server → IoPeer[A] → Client A
313
+ // Data returns: Client A → Server → Client B
314
+ ```
315
+
316
+ **Key Characteristics:**
317
+
318
+ - ✅ Data never leaves Client A's local storage
319
+ - ✅ Server does NOT store the data (acts as proxy)
320
+ - ✅ Client B pulls data on-demand via reference
321
+ - ✅ Works for: Cake tables, Cell tables, custom content types
322
+
323
+ ### Pattern 2: Bs Data Sync (Blob Storage)
324
+
325
+ Bs data represents binary blobs (files, images, videos) stored in the Bs layer.
326
+
327
+ #### Scenario: Client A stores blob, Client B retrieves it
328
+
329
+ ```text
330
+ ┌──────────┐ ┌──────────┐ ┌──────────┐
331
+ │Client A │ │ Server │ │Client B │
332
+ └────┬─────┘ └────┬─────┘ └────┬─────┘
333
+ │ │ │
334
+ │ 1. bsA.put(blob) │ │
335
+ ├──────────────────────► │ │
336
+ │ (writes to local Bs) │ │
337
+ │ Returns: blobHash │ │
338
+ │ │ │
339
+ │ 2. Store ref in Io table │ │
340
+ │ db.insert(route, { │ │
341
+ │ blobRef: blobHash │ │
342
+ │ }) │ │
343
+ │ │ │
344
+ │ 3. Client B gets ref │ │
345
+ │ │◄────────────────────────────┤
346
+ │ │ db.get(route, where) │
347
+ │ │ │
348
+ │ │ 4. Client B pulls blob │
349
+ │ │◄────────────────────────────┤
350
+ │ │ bsB.get(blobHash) │
351
+ │ │ │
352
+ │ 5. Server pulls from A │ │
353
+ │◄────────────────────────────┤ │
354
+ │ via BsPeerBridge │ │
355
+ │ │ │
356
+ │────────────────────────────►│ │
357
+ │ Returns blob data │ │
358
+ │ │ │
359
+ │ │ 6. Server returns to B │
360
+ │ ├─────────────────────────────►
361
+ │ │ Blob data pulled through │
362
+ ```
363
+
364
+ **Implementation Details:**
365
+
366
+ ```typescript
367
+ // Client A: Store blob locally
368
+ const blobData = new Uint8Array([1, 2, 3, 4]);
369
+ const blobHash = await clientA.bs!.put(blobData);
370
+
371
+ // Client A: Store blob reference in Io table
372
+ await dbA.insert(route, [{
373
+ fileName: "example.bin",
374
+ blobRef: blobHash,
375
+ size: blobData.length
376
+ }]);
377
+
378
+ // Client B: Retrieve blob reference from Io
379
+ const fileRecord = await dbB.get(route, { fileName: "example.bin" });
380
+ const blobHash = fileRecord.rljson.files._data[0].blobRef;
381
+
382
+ // Client B: Pull blob by hash
383
+ const blob = await clientB.bs!.get(blobHash);
384
+ // Query flows: Client B → BsPeer → Server → BsPeer[A] → Client A
385
+ // Blob returns: Client A → Server → Client B
386
+ ```
387
+
388
+ **Key Characteristics:**
389
+
390
+ - ✅ Blobs stored separately from Io tables
391
+ - ✅ Io tables store blob references (hashes)
392
+ - ✅ BsMulti provides same priority-based access as IoMulti
393
+ - ✅ Hot-swapping: Downloaded blobs can be cached locally
394
+ - ✅ Deduplication: Same blob hash = same content
395
+
396
+ ### Pattern 3: Tree Data Sync (Tree Structures)
397
+
398
+ Tree data represents hierarchical structures converted from JavaScript objects using `treeFromObject()`.
399
+
400
+ #### Scenario: Client A creates tree, Client B retrieves entire tree
401
+
402
+ ```text
403
+ ┌──────────┐ ┌──────────┐ ┌──────────┐
404
+ │Client A │ │ Server │ │Client B │
405
+ └────┬─────┘ └────┬─────┘ └────┬─────┘
406
+ │ │ │
407
+ │ 1. Create tree from object │ │
408
+ │ const trees = │ │
409
+ │ treeFromObject({ │ │
410
+ │ x: 10, │ │
411
+ │ y: { z: 20 } │ │
412
+ │ }) │ │
413
+ │ │ │
414
+ │ 2. Import tree data │ │
415
+ │ clientA.import({ │ │
416
+ │ treeName: { │ │
417
+ │ _type: 'trees', │ │
418
+ │ _data: trees │ │
419
+ │ } │ │
420
+ │ }) │ │
421
+ │ (writes to local Io) │ │
422
+ │ │ │
423
+ │ 3. Get root ref │ │
424
+ │ rootHash = │ │
425
+ │ trees[trees.length-1] │ │
426
+ │ ._hash │ │
427
+ │ │ │
428
+ │ 4. Broadcast root ref │ │
429
+ │ socket.emit(route, │ │
430
+ │ rootHash) │ │
431
+ ├────────────────────────────►│ │
432
+ │ │ │
433
+ │ │ 5. Multicast to Client B │
434
+ │ ├─────────────────────────────►
435
+ │ │ │
436
+ │ │ 6. Client B: get by root │
437
+ │ │◄────────────────────────────┤
438
+ │ │ db.get(route, { │
439
+ │ │ _hash: rootHash │
440
+ │ │ }) │
441
+ │ │ │
442
+ │ 7. Server pulls tree nodes │ │
443
+ │◄────────────────────────────┤ │
444
+ │ via IoPeerBridge │ │
445
+ │ (pulls ALL related nodes)│ │
446
+ │ │ │
447
+ │────────────────────────────►│ │
448
+ │ Returns tree nodes[] │ │
449
+ │ │ │
450
+ │ │ 8. Server returns to B │
451
+ │ ├─────────────────────────────►
452
+ │ │ Full tree structure │
453
+ ```
454
+
455
+ **Implementation Details:**
456
+
457
+ ```typescript
458
+ // Client A: Convert object to tree structure
459
+ const treeObject = { x: 10, y: { z: 20 } };
460
+ const trees = treeFromObject(treeObject);
461
+ // trees = [
462
+ // { id: 'x', meta: { value: 10 }, ... },
463
+ // { id: 'y', isParent: true, children: ['z'], ... },
464
+ // { id: 'z', meta: { value: 20 }, ... },
465
+ // { id: 'root', isParent: true, children: ['x', 'y'], ... } ← Root
466
+ // ]
467
+
468
+ // Client A: Get root reference (last tree in array)
469
+ const rootTreeHash = trees[trees.length - 1]._hash;
470
+
471
+ // Client A: Create trees table and import
472
+ const treeCfg = createTreesTableCfg('myTree');
473
+ await clientA.createTables({ withInsertHistory: [treeCfg] });
474
+ await clientA.import({
475
+ myTree: { _type: 'trees', _data: trees }
476
+ });
477
+
478
+ // Client B: Setup same table definition
479
+ await clientB.createTables({ withInsertHistory: [treeCfg] });
480
+
481
+ // Client B: Pull entire tree by root hash
482
+ const result = await dbB.get(Route.fromFlat('myTree'), {
483
+ _hash: rootTreeHash
484
+ });
485
+ // Returns ALL tree nodes (x, y, z, root) in result.rljson.myTree._data
486
+ // Query flows: Client B → IoPeer → Server → IoPeer[A] → Client A
487
+ // Tree flows: Client A → Server → Client B (all related nodes)
488
+ ```
489
+
490
+ **Key Characteristics:**
491
+
492
+ - ✅ `treeFromObject()` converts JS objects to Tree[] arrays
493
+ - ✅ Root node is LAST element in trees array
494
+ - ✅ Query by root hash returns ALL related nodes (entire subtree)
495
+ - ✅ Trees table uses `createTreesTableCfg()` configuration
496
+ - ✅ Pull pattern: Server does NOT store tree (proxies to Client A)
497
+ - ✅ Efficient: Single query retrieves complete tree structure
498
+
499
+ **Tree Structure Details:**
500
+
501
+ ```typescript
502
+ interface Tree {
503
+ id: string; // Unique identifier
504
+ _hash: string; // Content hash (reference)
505
+ isParent?: boolean; // Has children?
506
+ children?: string[]; // Child node IDs
507
+ meta?: {
508
+ value?: any; // Leaf value (for non-parent nodes)
509
+ [key: string]: any; // Additional metadata
510
+ };
511
+ }
512
+ ```
513
+
514
+ ## Data Distribution Patterns
515
+
516
+ ### Pattern 1: Client-to-Client via Server (Pull Pattern)
517
+
518
+ When Client A creates/modifies data that other clients need to access:
519
+
520
+ ```text
521
+ ┌──────────┐ ┌──────────┐ ┌──────────┐
522
+ │Client A │ │ Server │ │Client B │
523
+ └────┬─────┘ └────┬─────┘ └────┬─────┘
524
+ │ │ │
525
+ │ 1. insert(route, data) │ │
526
+ ├──────────────────────► │ │
527
+ │ (writes to local Io) │ │
528
+ │ │ │
529
+ │ │ 2. Client B get(route, ref) │
530
+ │ │◄────────────────────────────┤
531
+ │ │ (via IoPeer) │
532
+ │ │ │
533
+ │ 3. Server's IoMulti cascade │ │
534
+ │◄────────────────────────────┤ │
535
+ │ (automatic via priority) │ │
536
+ │ Reads from Client A │ │
537
+ │ via IoPeerBridge │ │
538
+ │ │ │
539
+ │────────────────────────────►│ │
540
+ │ Returns data │ │
541
+ │ │ │
542
+ │ ├─────────────────────────────►
543
+ │ │ 4. Data flows back to B │
544
+ │ │ (Client A → Server → B) │
545
+ ```text
546
+
547
+ ### Pattern 2: Notification Broadcasting
548
+
549
+ For real-time updates, the server multicasts references between clients:
550
+
551
+ ```text
552
+ ┌──────────┐ ┌──────────┐ ┌──────────┐
553
+ │Client A │ │ Server │ │Client B │
554
+ └────┬─────┘ └────┬─────┘ └────┬─────┘
555
+ │ │ │
556
+ │ 1. socket.emit(route, ref) │ │
557
+ ├──────────────────────► │ │
558
+ │ │ │
559
+ │ │ 2. Multicast to others │
560
+ │ │ (adds __origin marker) │
561
+ │ ├─────────────────────────────►
562
+ │ │ │
563
+ │ │ │
564
+ │ │ 3. Client B receives ref │
565
+ │ │ and can fetch data │
566
+ ```text
567
+
568
+ **Multicast Logic:**
569
+
570
+ - Server listens on route for all connected clients
571
+ - When Client A emits on route, server forwards to all OTHER clients
572
+ - `__origin` marker prevents infinite loops
573
+ - Deduplication via `_multicastedRefs` Set
574
+ - **References are broadcast, data is pulled on-demand**
575
+
576
+ ### Pattern 4: Server as Data Proxy (Not Storage)
577
+
578
+ Important: The server does NOT store client data by default.
579
+
580
+ **Incorrect Pattern (Push):**
581
+
582
+ ```typescript
583
+ // ❌ WRONG: Server should NOT import client data
584
+ await server.import(clientData); // Server becomes storage layer
585
+ ```
586
+
587
+ **Correct Pattern (Pull):**
588
+
589
+ ```typescript
590
+ // ✅ CORRECT: Client stores, server proxies on-demand
591
+ await clientA.import(data); // Client A stores locally
592
+ // Server reads from Client A via IoPeerBridge only when Client B requests it
593
+ const result = await dbB.get(route, { _hash: ref });
594
+ // Server pulls from Client A dynamically
595
+ ```
596
+
597
+ **When Server SHOULD Store Data:**
598
+
599
+ - ✅ Shared configuration data all clients need
600
+ - ✅ Reference data (lookup tables, constants)
601
+ - ✅ Bootstrapping data for new clients
602
+ - ❌ NOT for client-specific operational data
603
+
604
+ ### Pattern 5: Reference Passing Between Clients
605
+
606
+ The most efficient pattern for distributed access:
607
+
608
+ ```text
609
+ 1. Client A creates data → Returns references (hashes)
610
+ 2. Client A broadcasts references (not data) to server
611
+ 3. Server multicasts references to Client B
612
+ 4. Client B receives references
613
+ 5. Client B queries by reference when needed
614
+ 6. Server pulls actual data from Client A on-demand
615
+ 7. Data flows: Client A → Server → Client B (only when requested)
616
+ ```
617
+
618
+ **Benefits:**
619
+
620
+ - ✅ Minimal network traffic (only refs broadcast)
621
+ - ✅ Data pulled only when needed
622
+ - ✅ No stale data (always pull latest from source)
623
+ - ✅ Source of truth remains at Client A
624
+
625
+ ## Complete Integration Examples
626
+
627
+ ### Example 1: Io Data (Cake Table) - Complete Flow
628
+
629
+ ```typescript
630
+ // Setup: All parties create table definitions
631
+ const cakeCfg = {
632
+ name: 'carCake',
633
+ cfg: { _type: 'cake', columns: ['brand', 'model'] }
634
+ };
635
+
636
+ await server.createTables({ withInsertHistory: [cakeCfg] });
637
+ await clientA.createTables({ withInsertHistory: [cakeCfg] });
638
+ await clientB.createTables({ withInsertHistory: [cakeCfg] });
639
+
640
+ // Create Db instances
641
+ const dbA = new Db(clientA.io!);
642
+ const dbB = new Db(clientB.io!);
643
+
644
+ // Client A: Insert data (stores locally)
645
+ const route = Route.fromFlat('carCake');
646
+ const insertResult = await dbA.insert(route, [{
647
+ brand: 'Tesla',
648
+ model: 'Model S'
649
+ }]);
650
+ const carRef = insertResult[0]._hash;
651
+
652
+ // Client A: Broadcast reference
653
+ clientA.socket.emit(route.flat, carRef);
654
+
655
+ // Client B: Listen for reference
656
+ clientB.socket.on(route.flat, async (ref) => {
657
+ // Pull data by reference
658
+ const result = await dbB.get(route, { _hash: ref });
659
+ console.log(result.rljson.carCake._data[0]);
660
+ // { brand: 'Tesla', model: 'Model S', _hash: '...' }
661
+ });
662
+ ```
663
+
664
+ ### Example 2: Bs Data (Blob) - Complete Flow
665
+
666
+ ```typescript
667
+ // Setup: All parties initialize blob storage (BsMulti)
668
+ // Already done via client.init() and server.init()
669
+
670
+ // Client A: Store blob locally
671
+ const imageData = new Uint8Array([255, 216, 255, ...]); // JPEG bytes
672
+ const blobHash = await clientA.bs!.put(imageData);
673
+
674
+ // Client A: Store blob reference in Io table
675
+ const fileRoute = Route.fromFlat('images');
676
+ const insertResult = await dbA.insert(fileRoute, [{
677
+ fileName: 'photo.jpg',
678
+ blobRef: blobHash,
679
+ size: imageData.length,
680
+ mimeType: 'image/jpeg'
681
+ }]);
682
+ const fileRecordRef = insertResult[0]._hash;
683
+
684
+ // Client A: Broadcast file record reference
685
+ clientA.socket.emit(fileRoute.flat, fileRecordRef);
686
+
687
+ // Client B: Receive reference and pull blob
688
+ clientB.socket.on(fileRoute.flat, async (ref) => {
689
+ // 1. Get file metadata from Io
690
+ const fileRecord = await dbB.get(fileRoute, { _hash: ref });
691
+ const blobHash = fileRecord.rljson.images._data[0].blobRef;
692
+
693
+ // 2. Pull actual blob from Bs
694
+ const imageData = await clientB.bs!.get(blobHash);
695
+ console.log(`Downloaded ${imageData.length} bytes`);
696
+
697
+ // 3. Optional: Cache locally (hot-swap)
698
+ await clientB.bs!.put(imageData); // Now in Client B's local Bs
699
+ });
700
+ ```
701
+
702
+ ### Example 3: Tree Data - Complete Flow
703
+
704
+ ```typescript
705
+ // Setup: Create trees table configuration
706
+ const treeCfg = createTreesTableCfg('projectTree');
707
+ await server.createTables({ withInsertHistory: [treeCfg] });
708
+ await clientA.createTables({ withInsertHistory: [treeCfg] });
709
+ await clientB.createTables({ withInsertHistory: [treeCfg] });
710
+
711
+ const dbA = new Db(clientA.io!);
712
+ const dbB = new Db(clientB.io!);
713
+
714
+ // Client A: Create tree from object
715
+ const projectData = {
716
+ name: 'MyApp',
717
+ version: '1.0.0',
718
+ dependencies: {
719
+ react: '18.0.0',
720
+ typescript: '5.0.0'
721
+ },
722
+ scripts: {
723
+ build: 'tsc',
724
+ test: 'vitest'
725
+ }
726
+ };
727
+
728
+ const trees = treeFromObject(projectData);
729
+ const rootHash = trees[trees.length - 1]._hash;
730
+
731
+ // Client A: Import tree (stores locally)
732
+ await clientA.import({
733
+ projectTree: { _type: 'trees', _data: trees }
734
+ });
735
+
736
+ // Client A: Broadcast root reference
737
+ const treeRoute = Route.fromFlat('projectTree');
738
+ clientA.socket.emit(treeRoute.flat, rootHash);
739
+
740
+ // Client B: Receive reference and pull entire tree
741
+ clientB.socket.on(treeRoute.flat, async (rootRef) => {
742
+ // Pull entire tree by root hash
743
+ const result = await dbB.get(treeRoute, { _hash: rootRef });
744
+ const treeNodes = result.rljson.projectTree._data;
745
+
746
+ console.log(`Received ${treeNodes.length} tree nodes`);
747
+ // Includes: name, version, dependencies, react, typescript,
748
+ // scripts, build, test, root
749
+
750
+ // Navigate tree structure
751
+ const root = treeNodes.find(n => n._hash === rootRef);
752
+ console.log(`Root children: ${root.children}`);
753
+ });
754
+ ```
755
+
756
+ ## Performance Considerations
757
+
758
+ ### IoMulti/BsMulti Query Optimization
759
+
760
+ **Priority-based short-circuiting:**
761
+
762
+ ```typescript
763
+ // Query: get(route, where)
764
+ // 1. Check priority 1 (local) → Found? Return immediately ✓
765
+ // 2. Check priority 2 (IoPeer) → Found? Return immediately ✓
766
+ // 3. Check priority 3 (additional peers) → And so on...
767
+ ```
768
+
769
+ **Best Practices:**
770
+
771
+ - ✅ Cache frequently accessed data locally (hot-swapping)
772
+ - ✅ Use specific queries ({ _hash: ref }) instead of broad scans
773
+ - ✅ Minimize priority 2+ queries by pre-loading critical data
774
+ - ❌ Avoid scanning large tables without where clauses
775
+
776
+ ### Blob Storage Optimization
777
+
778
+ **Deduplication:**
779
+
780
+ - Same content = same hash
781
+ - Multiple references to same blob = single storage
782
+
783
+ **Streaming (Future):**
784
+
785
+ - Large blobs can be streamed via `getStream()`
786
+ - Partial retrieval via `get(hash, range)`
787
+
788
+ ### Tree Query Optimization
789
+
790
+ **Single Query for Entire Tree:**
791
+
792
+ - Query root hash returns ALL related nodes
793
+ - No need for recursive queries
794
+ - Efficient for hierarchical data
795
+
796
+ **Tree Caching:**
797
+
798
+ ```typescript
799
+ // After first pull, tree is available locally
800
+ await dbB.get(route, { _hash: rootRef }); // Pulls from Client A
801
+ await dbB.get(route, { _hash: rootRef }); // Reads from local cache
802
+ ```
803
+
804
+ ## Consistency Model (Db layer)
805
+
806
+ The `Db` class operates on top of `IoMulti`, providing distributed data access:
807
+
808
+ ```text
809
+ ┌────────────────────────────────────────┐
810
+ │ Client A │
811
+ │ ┌──────────────────────────────────┐ │
812
+ │ │ Db (dbA) │ │
813
+ │ │ ↓ │ │
814
+ │ │ IoMulti │ │
815
+ │ │ ├─ Local Io (priority 1) │ │
816
+ │ │ └─ IoPeer → Server (priority 2)│ │
817
+ │ └──────────────────────────────────┘ │
818
+ └────────────────────────────────────────┘
819
+
820
+ ┌────────────────────────────────────────┐
821
+ │ Server │
822
+ │ ┌──────────────────────────────────┐ │
823
+ │ │ IoMulti │ │
824
+ │ │ ├─ Local Io (priority 1) │ │
825
+ │ │ ├─ IoPeer[A] (priority 2) │ │
826
+ │ │ └─ IoPeer[B] (priority 2) │ │
827
+ │ └──────────────────────────────────┘ │
828
+ └────────────────────────────────────────┘
829
+
830
+ ┌────────────────────────────────────────┐
831
+ │ Client B │
832
+ │ ┌──────────────────────────────────┐ │
833
+ │ │ Db (dbB) │ │
834
+ │ │ ↓ │ │
835
+ │ │ IoMulti │ │
836
+ │ │ ├─ Local Io (priority 1) │ │
837
+ │ │ └─ IoPeer → Server (priority 2)│ │
838
+ │ └──────────────────────────────────┘ │
839
+ └────────────────────────────────────────┘
840
+ ```
841
+
842
+ **Operations:**
843
+
844
+ **db.insert(route, data):**
845
+
846
+ - Writes to local Io only (via IoMulti's priority 1 layer)
847
+ - Returns `InsertHistoryRow[]` with refs
848
+ - Data remains local until server reads it via IoPeerBridge
849
+
850
+ **db.get(route, where):**
851
+
852
+ - Searches local Io first (priority 1)
853
+ - Falls back to server (priority 2) if not found locally
854
+ - Server's IoMulti includes data from all connected clients
855
+ - Returns `Container` with rljson, tree, and cell data
856
+
857
+ ## Consistency Model
858
+
859
+ ### Local-First Guarantees
860
+
861
+ 1. **Writes are local**: All write operations go to local storage only
862
+ 2. **Reads are prioritized**: Local data is always checked first
863
+ 3. **Server as fallback**: Server data accessed when not available locally
864
+ 4. **Hot-swapping**: When data is read from server, it can be cached locally
865
+
866
+ ### Data Visibility and Access Patterns
867
+
868
+ **What Client A can see:**
869
+
870
+ - ✅ Its own local Io data (priority 1)
871
+ - ✅ Its own local Bs blobs (priority 1)
872
+ - ✅ Server's local data (priority 2) if server has any
873
+ - ✅ Other clients' data via server (priority 2) - **pulled automatically on-demand**
874
+ - When Client A queries by reference, server checks its cache (priority 1)
875
+ - If not in server cache, server automatically pulls from Client B (priority 2)
876
+ - This happens transparently through IoMulti's priority system
877
+
878
+ **What Client A cannot see:**
879
+
880
+ - ❌ Data without a valid reference (hash) to query by
881
+ - ❌ Data from disconnected clients (no IoPeer connection)
882
+ - ❌ Data that hasn't been imported/inserted anywhere in the network
883
+
884
+ **What Server can see:**
885
+
886
+ - ✅ Its own local Io data (priority 1)
887
+ - ✅ All connected clients' data (priority 2+) via IoPeerBridge
888
+ - ✅ **Server acts as aggregator** - sees union of all client data
889
+
890
+ ### Data Flow Guarantees
891
+
892
+ **Io Data (Tables):**
893
+
894
+ - Writes: Always to local Io only
895
+ - Reads: Priority 1 (local) → Priority 2 (server/peers)
896
+ - Consistency: Eventually consistent via pull
897
+ - References: Content-addressed by hash
898
+
899
+ **Bs Data (Blobs):**
900
+
901
+ - Writes: Always to local Bs only
902
+ - Reads: Priority 1 (local) → Priority 2 (server/peers)
903
+ - Deduplication: Same hash = same content
904
+ - References: Content-addressed by hash
905
+
906
+ **Tree Data:**
907
+
908
+ - Storage: In Io layer as special 'trees' type
909
+ - Queries: By root hash → returns all related nodes
910
+ - Structure: Hierarchical parent-child relationships
911
+ - References: Root hash identifies entire tree
912
+
913
+ ### Synchronization
914
+
915
+ **No automatic sync**: The system does not automatically replicate writes between clients.
916
+
917
+ **Pull-based sync patterns:**
918
+
919
+ 1. **Via References**: Client A broadcasts ref → Client B pulls data by ref
920
+ 2. **Via Server Proxy**: Client B queries → Server pulls from Client A on-demand
921
+ 3. **Via IoPeerBridge/BsPeerBridge**: Exposing local storage to server for reading
922
+
923
+ **Key Differences from Push-based Sync:**
924
+
925
+ | Aspect | Pull-based (rljson) | Push-based (traditional) |
926
+ | --------------- | --------------------------- | ------------------------- |
927
+ | Data movement | On-demand via query | Automatic replication |
928
+ | Network traffic | Minimal (refs only) | High (all data) |
929
+ | Staleness | Always fresh (pulls latest) | Possible (stale replicas) |
930
+ | Storage | Single source of truth | Multiple copies |
931
+ | Bandwidth | Low (pull when needed) | High (push all changes) |
932
+ | Consistency | Eventually consistent | Strong/eventual |
933
+
934
+ ## Architecture Comparison: Io vs Bs vs Tree
935
+
936
+ | Feature | Io Data | Bs Data | Tree Data |
937
+ | ------------------ | ------------------------- | ------------------------- | ------------------------- |
938
+ | **Storage Layer** | IoMulti (Io + IoPeer[]) | BsMulti (Bs + BsPeer[]) | IoMulti (special type) |
939
+ | **Data Type** | Tables, rows, columns | Binary blobs | Hierarchical nodes |
940
+ | **Content Type** | 'cake', 'cell', custom | Raw bytes | 'trees' |
941
+ | **Query Method** | `db.get(route, where)` | `bs.get(hash)` | `db.get(route, {_hash})` |
942
+ | **Reference Type** | Row hash (_hash) | Blob hash | Root node hash (_hash) |
943
+ | **Write Target** | Priority 1 (local Io) | Priority 1 (local Bs) | Priority 1 (local Io) |
944
+ | **Read Priority** | 1: Local, 2: Server+Peers | 1: Local, 2: Server+Peers | 1: Local, 2: Server+Peers |
945
+ | **Deduplication** | By content hash | By content hash | By content hash |
946
+ | **Query Result** | Matching rows | Single blob | All related nodes |
947
+ | **Table Config** | `createTableCfg()` | N/A | `createTreesTableCfg()` |
948
+ | **Sync Pattern** | Pull by ref | Pull by ref | Pull by root ref |
949
+ | **Use Cases** | Structured data | Files, images, videos | JSON objects, configs |
950
+
951
+ ## Real-World Scenarios
952
+
953
+ ### Scenario 1: Collaborative Document Editing
954
+
955
+ ```text
956
+ Team working on shared documents:
957
+ - Each client has local document storage (Io data)
958
+ - Document edits create new versions (content-addressed)
959
+ - Editor broadcasts document ref to team
960
+ - Team members pull latest version by ref on-demand
961
+ - Server never stores documents (only proxies)
962
+ - Tree data represents document structure (headings, sections)
963
+ ```
964
+
965
+ ### Scenario 2: Media Sharing Application
966
+
967
+ ```text
968
+ Users sharing photos/videos:
969
+ - Photos stored in local Bs (Client A)
970
+ - Photo metadata in Io table (title, tags, blobRef)
971
+ - User A uploads → stores locally, broadcasts ref
972
+ - User B sees notification → pulls blob by ref
973
+ - User B caches blob locally (hot-swap)
974
+ - Server proxies blob from A to B (doesn't store)
975
+ ```
976
+
977
+ ### Scenario 3: Configuration Management
978
+
979
+ ```text
980
+ Application configuration distribution:
981
+ - Config as JSON object → converted to Tree
982
+ - Config stored on admin client (Client A)
983
+ - Root ref broadcast to all clients
984
+ - Clients pull config tree by root ref on-demand
985
+ - Changes create new tree → new root ref
986
+ - Clients update by pulling new root ref
987
+ ```
988
+
989
+ ## Lifecycle
990
+
991
+ ### Client Initialization
992
+
993
+ ```typescript
994
+ const client = new Client(socket, localIo, localBs);
995
+ await client.init(); // Sets up IoMulti and BsMulti
996
+ await client.ready(); // Waits for IoMulti to be ready
997
+
998
+ const db = new Db(client.io!); // Create Db on top of IoMulti
999
+ ```
1000
+
1001
+ ### Server Initialization
1002
+
1003
+ ```typescript
1004
+ const server = new Server(route, serverIo, serverBs);
1005
+ await server.init(); // Sets up IoMulti and BsMulti
1006
+
1007
+ // When clients connect:
1008
+ await server.addSocket(socket); // Rebuilds multis with new IoPeer
1009
+ ```
1010
+
1011
+ ### Adding a Client
1012
+
1013
+ When `server.addSocket(socket)` is called:
1014
+
1015
+ 1. **Create IoPeer/BsPeer**: Establish connection to client
1016
+ 2. **Queue peers**: Add to `_ios` and `_bss` arrays
1017
+ 3. **Rebuild multis**: Recreate IoMulti/BsMulti with all peers
1018
+ 4. **Refresh servers**: Update IoServer/BsServer with new multis
1019
+ 5. **Setup multicast**: Register listeners for route broadcasting
1020
+
1021
+ ### Teardown
1022
+
1023
+ ```typescript
1024
+ await client.tearDown(); // Closes IoMulti, clears state
1025
+ ```
1026
+
1027
+ ## Testing Patterns
1028
+
1029
+ ### Distributed Get Pattern (Server Data)
1030
+
1031
+ ```typescript
1032
+ // Use case: Server has shared reference data
1033
+ await server.createTables({ withInsertHistory: tableCfgs });
1034
+ await server.import(exampleData);
1035
+
1036
+ // Clients need table definitions
1037
+ await clientA.createTables({ withInsertHistory: tableCfgs });
1038
+ await clientB.createTables({ withInsertHistory: tableCfgs });
1039
+
1040
+ // Client A can read server data (priority 2)
1041
+ const dataFromA = await dbA.get(route, where);
1042
+
1043
+ // Client B can read the same server data (priority 2)
1044
+ const dataFromB = await dbB.get(route, where);
1045
+
1046
+ // Both see identical data from server
1047
+ ```
1048
+
1049
+ ### Client-to-Client Pattern (Pull via Server)
1050
+
1051
+ ```typescript
1052
+ // Setup: All parties need table definitions
1053
+ await server.createTables({ withInsertHistory: tableCfgs });
1054
+ await clientA.createTables({ withInsertHistory: tableCfgs });
1055
+ await clientB.createTables({ withInsertHistory: tableCfgs });
1056
+
1057
+ // Client A creates local data
1058
+ await clientA.import(localData);
1059
+
1060
+ // Client A sees its local data (priority 1)
1061
+ const dataFromA = await dbA.get(route, where);
1062
+ const ref = dataFromA.rljson.tableName._data[0]._hash;
1063
+
1064
+ // Client B CAN see Client A's data by reference
1065
+ // Server's IoMulti automatically cascades to Client A
1066
+ const dataFromB = await dbB.get(route, { _hash: ref });
1067
+ // Query: Client B → IoPeer → Server IoMulti → IoPeer[A] → Client A
1068
+ // Data flows back: Client A → Server → Client B
1069
+
1070
+ expect(dataFromB.rljson.tableName._data[0]._hash).toBe(ref);
1071
+ ```
1072
+
1073
+ ### Local-Only Pattern (No Reference Query)
1074
+
1075
+ ```typescript
1076
+ // Client A creates local data
1077
+ await clientA.createTables({ withInsertHistory: tableCfgs });
1078
+ await clientA.import(localData);
1079
+
1080
+ // Client B has no reference to query by
1081
+ await clientB.createTables({ withInsertHistory: tableCfgs });
1082
+
1083
+ // Client B cannot discover Client A's data without a reference
1084
+ // Broad queries won't automatically sync all data
1085
+ await expect(dbB.get(route, {})).rejects.toThrow();
1086
+ // Or returns empty result if table exists but no data locally
1087
+ ```
1088
+
1089
+ ## Key Design Decisions
1090
+
1091
+ ### Why Local-First?
1092
+
1093
+ - **Offline capability**: Clients work without server connection
1094
+ - **Low latency**: Read/write operations are fast (no network)
1095
+ - **Data ownership**: Clients control their own data
1096
+ - **Flexible sync**: Sync on-demand, not automatically
1097
+
1098
+ ### Why Read-Only Peers?
1099
+
1100
+ - **Simplicity**: No conflict resolution needed
1101
+ - **Safety**: Prevents accidental cross-client writes
1102
+ - **Clear semantics**: Local writes, remote reads
1103
+ - **Scalability**: Server doesn't manage write transactions
1104
+
1105
+ ### Why Priority-Based Multi?
1106
+
1107
+ - **Predictable behavior**: Always check local first
1108
+ - **Flexibility**: Add multiple data sources
1109
+ - **Performance**: Short-circuit on local hits
1110
+ - **Composability**: Easy to add new layers
1111
+
1112
+ ## Related Packages
1113
+
1114
+ - **@rljson/io**: Io, IoMulti, IoPeer, IoPeerBridge, IoServer
1115
+ - **@rljson/bs**: Bs, BsMulti, BsPeer, BsPeerBridge, BsServer
1116
+ - **@rljson/db**: Db operations (insert, get, join, etc.)
1117
+ - **@rljson/rljson**: Data structures (Route, TableCfg, etc.)
1118
+
1119
+ ## Future Considerations
1120
+
1121
+ - **Write replication**: Automatically sync writes to server
1122
+ - **Conflict resolution**: Handle concurrent writes
1123
+ - **Change detection**: Notify on data changes
1124
+ - **Batch operations**: Optimize bulk transfers
1125
+ - **Compression**: Reduce network payload size