@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.
- package/README.architecture.md +1116 -0
- package/README.blog.md +9 -1
- package/README.contributors.md +47 -13
- package/README.md +70 -11
- package/README.public.md +204 -2
- package/README.trouble.md +50 -0
- package/dist/README.architecture.md +1116 -0
- package/dist/README.blog.md +9 -1
- package/dist/README.contributors.md +47 -13
- package/dist/README.md +70 -11
- package/dist/README.public.md +204 -2
- package/dist/README.trouble.md +50 -0
- package/dist/client.d.ts +6 -3
- package/dist/server.d.ts +7 -3
- package/dist/server.js +69 -37
- package/dist/socket-bundle.d.ts +16 -0
- package/package.json +11 -11
package/README.architecture.md
CHANGED
|
@@ -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
|