@rljson/fs-agent 0.0.2
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/LICENSE +21 -0
- package/README.architecture.md +449 -0
- package/README.blog.md +11 -0
- package/README.contributors.md +32 -0
- package/README.md +24 -0
- package/README.public.md +781 -0
- package/README.trouble.md +23 -0
- package/dist/README.architecture.md +449 -0
- package/dist/README.blog.md +11 -0
- package/dist/README.contributors.md +32 -0
- package/dist/README.md +24 -0
- package/dist/README.public.md +781 -0
- package/dist/README.trouble.md +23 -0
- package/dist/client-server/client-server-setup.d.ts +17 -0
- package/dist/client-server/live-client-server.d.ts +1 -0
- package/dist/example.d.ts +1 -0
- package/dist/fs-agent.d.ts +178 -0
- package/dist/fs-agent.js +4049 -0
- package/dist/fs-blob-adapter.d.ts +92 -0
- package/dist/fs-db-adapter.d.ts +34 -0
- package/dist/fs-scanner.d.ts +104 -0
- package/dist/index.d.ts +5 -0
- package/dist/src/example.ts +101 -0
- package/package.json +59 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@license
|
|
3
|
+
Copyright (c) 2025 Rljson
|
|
4
|
+
|
|
5
|
+
Use of this source code is governed by terms that can be
|
|
6
|
+
found in the LICENSE file in the root of this package.
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
# Trouble shooting
|
|
10
|
+
|
|
11
|
+
## Table of contents <!-- omit in toc -->
|
|
12
|
+
|
|
13
|
+
- [Vscode Windows: Debugging is not working](#vscode-windows-debugging-is-not-working)
|
|
14
|
+
|
|
15
|
+
## Vscode Windows: Debugging is not working
|
|
16
|
+
|
|
17
|
+
Date: 2025-03-08
|
|
18
|
+
|
|
19
|
+
⚠️ IMPORTANT: On Windows, please check out the repo on drive C. There is a bug
|
|
20
|
+
in the VS Code Vitest extension (v1.14.4), which prevents test debugging from
|
|
21
|
+
working: <https://github.com/vitest-dev/vscode/issues/548> Please check from
|
|
22
|
+
time to time if the issue has been fixed and remove this note once it is
|
|
23
|
+
resolved.
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@license
|
|
3
|
+
Copyright (c) 2025 Rljson
|
|
4
|
+
|
|
5
|
+
Use of this source code is governed by terms that can be
|
|
6
|
+
found in the LICENSE file in the root of this package.
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
# Architecture
|
|
10
|
+
|
|
11
|
+
## Pull-Based Reference Architecture
|
|
12
|
+
|
|
13
|
+
### Why References Are Required
|
|
14
|
+
|
|
15
|
+
The @rljson/server architecture implements a **pull-based reference system** where data cannot be retrieved without a reference (hash). This is a fundamental design principle.
|
|
16
|
+
|
|
17
|
+
**Query Chain When Client B Pulls from Client A:**
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Client B: db.get(route, { _hash: rootHash })
|
|
21
|
+
↓
|
|
22
|
+
Db constructs where clause: { _hash: rootHash }
|
|
23
|
+
↓
|
|
24
|
+
IoMulti.readRows(table, { _hash: rootHash })
|
|
25
|
+
↓
|
|
26
|
+
Priority 2: IoPeer.readRows({ table, where: { _hash: rootHash } })
|
|
27
|
+
↓
|
|
28
|
+
Socket emits: 'readRows' with { table, where: { _hash: rootHash } }
|
|
29
|
+
↓
|
|
30
|
+
Server's IoPeerBridge receives and forwards to Client A
|
|
31
|
+
↓
|
|
32
|
+
Client A's Io.readRows(table, { _hash: rootHash })
|
|
33
|
+
↓
|
|
34
|
+
Returns matching rows → Server → Client B
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Key Point**: `IoPeer.readRows()` requires a `where` clause with the reference:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// From rljson-io/src/io-peer.ts
|
|
41
|
+
readRows(request: {
|
|
42
|
+
table: string;
|
|
43
|
+
where: { [column: string]: JsonValue | null }; // ← REQUIRED!
|
|
44
|
+
}): Promise<Rljson>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**You cannot query without knowing what to look for:**
|
|
48
|
+
- ❌ `io.readRows('sharedTree', {})` - No way to identify what to pull
|
|
49
|
+
- ✅ `io.readRows('sharedTree', { _hash: 'abc123' })` - Specific reference
|
|
50
|
+
|
|
51
|
+
**This is why Connector notifications are essential:**
|
|
52
|
+
1. Client A stores tree locally
|
|
53
|
+
2. Client A broadcasts root hash via Connector
|
|
54
|
+
3. Client B receives hash and uses it: `db.get(route, { _hash: receivedHash })`
|
|
55
|
+
4. Without the hash, Client B cannot pull the data
|
|
56
|
+
|
|
57
|
+
## Architectural Rules (DO NOT VIOLATE)
|
|
58
|
+
|
|
59
|
+
### CRITICAL: Socket-Only Communication Between Client and Server
|
|
60
|
+
|
|
61
|
+
**Clients MUST communicate with the server ONLY through socket connections. Direct access to server resources (Io, Bs, Db) is ABSOLUTELY FORBIDDEN.**
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// ✅ CORRECT: Client uses its own resources, communicates via socket
|
|
65
|
+
const server = new Server(route, serverIo, serverBs);
|
|
66
|
+
await server.init();
|
|
67
|
+
|
|
68
|
+
const socketA = new SocketMock();
|
|
69
|
+
socketA.connect();
|
|
70
|
+
await server.addSocket(socketA);
|
|
71
|
+
|
|
72
|
+
const localIoA = new IoMem();
|
|
73
|
+
await localIoA.init();
|
|
74
|
+
const localBsA = new BsMem();
|
|
75
|
+
const clientA = new Client(socketA, localIoA, localBsA);
|
|
76
|
+
await clientA.init();
|
|
77
|
+
|
|
78
|
+
// Use client's own Bs and Io - data syncs through socket automatically
|
|
79
|
+
const agentA = new FsAgent(folderA, clientA.bs);
|
|
80
|
+
const clientDbA = new Db(clientA.io);
|
|
81
|
+
await agentA.syncToDb(clientDbA, connectorA, 'sharedTree');
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// ❌ ABSOLUTELY FORBIDDEN: Client directly accessing server's Bs
|
|
86
|
+
const server = new Server(route, serverIo, serverBs);
|
|
87
|
+
const clientA = new Client(socketA, localIoA, localBsA);
|
|
88
|
+
|
|
89
|
+
// WRONG! This violates the client-server boundary
|
|
90
|
+
const agentA = new FsAgent(folderA, serverBs); // Using server's Bs directly
|
|
91
|
+
|
|
92
|
+
// WRONG! Sharing server's Io or Db
|
|
93
|
+
const clientDbA = new Db(serverIo); // Using server's Io
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Rationale**: The entire architecture is built on the Client-Server pattern where:
|
|
97
|
+
|
|
98
|
+
- Each client has its own local Io and Bs
|
|
99
|
+
- The Client class automatically syncs data with the server through the socket
|
|
100
|
+
- Direct access to server resources bypasses this architecture and breaks distributed scenarios
|
|
101
|
+
- This pattern is ESSENTIAL for the library to work in real-world distributed deployments
|
|
102
|
+
|
|
103
|
+
**This is the MOST IMPORTANT architectural rule. Violating it makes the entire implementation meaningless.**
|
|
104
|
+
|
|
105
|
+
### Connector and Server Route Matching (CRITICAL)
|
|
106
|
+
|
|
107
|
+
**Connector routes MUST match the Server route for message routing to work. The route MUST be based on the tree table name (treeKey), not arbitrary application names.**
|
|
108
|
+
|
|
109
|
+
The route represents the data structure path in the database - it's not an application identifier. When creating routes for tree synchronization:
|
|
110
|
+
|
|
111
|
+
1. The route must derive from the tree table name (treeKey)
|
|
112
|
+
2. The Server and all Connectors must use the exact same route
|
|
113
|
+
3. Using arbitrary names like 'myapp.sync' or 'fsagent.demo' breaks the data path
|
|
114
|
+
|
|
115
|
+
When creating Connectors, use the **same route** that was used to initialize the Server. The Server's multicast logic listens on `server.route.flat`, and Connectors send/receive on `connector.route.flat`. If these don't match, messages will never be routed between clients.
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// ✅ CORRECT: Connector routes match server route (based on tree table name)
|
|
119
|
+
const treeKey = 'sharedTree';
|
|
120
|
+
const route = Route.fromFlat(`/${treeKey}`);
|
|
121
|
+
const server = new Server(route, serverIo, serverBs);
|
|
122
|
+
await server.init();
|
|
123
|
+
|
|
124
|
+
// Both connectors use the SAME route as the server
|
|
125
|
+
const connectorA = new Connector(clientDbA, route, socketA);
|
|
126
|
+
const connectorB = new Connector(clientDbB, route, socketB);
|
|
127
|
+
|
|
128
|
+
// Now messages flow: A sends → Server multicasts → B receives
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// ❌ WRONG: Connector routes differ from server route
|
|
133
|
+
const treeKey = 'sharedTree';
|
|
134
|
+
const serverRoute = Route.fromFlat('myapp.sync'); // Wrong! Not based on treeKey
|
|
135
|
+
const server = new Server(serverRoute, serverIo, serverBs);
|
|
136
|
+
|
|
137
|
+
// WRONG! These routes don't match the server route
|
|
138
|
+
const connectorA = new Connector(clientDbA, Route.fromFlat('/dataSync'), socketA);
|
|
139
|
+
const connectorB = new Connector(clientDbB, Route.fromFlat('/dataSync'), socketB);
|
|
140
|
+
|
|
141
|
+
// Messages will NOT be routed! Server listens on '/sharedTree' but connectors use other routes
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Why This Matters:**
|
|
145
|
+
- The Server's `_multicastRefs()` method registers socket listeners on `this._route.flat`
|
|
146
|
+
- When Connector A calls `connector.send(ref)`, it emits on `connector.route.flat`
|
|
147
|
+
- If the routes don't match, the Server never receives the message
|
|
148
|
+
- Cross-client communication completely breaks
|
|
149
|
+
|
|
150
|
+
**Best Practice:** Pass the server route to client setup functions or use a shared constant.
|
|
151
|
+
|
|
152
|
+
### Self-Broadcast Behavior and Filtering
|
|
153
|
+
|
|
154
|
+
**Connectors receive their own messages via local socket echo. This is EXPECTED behavior.**
|
|
155
|
+
|
|
156
|
+
When a Connector sends a message via `connector.send(ref)`, two things happen:
|
|
157
|
+
|
|
158
|
+
1. **Local Socket Echo**: The connector's own `listen()` callback is immediately triggered because sockets emit to all listeners (standard EventEmitter behavior)
|
|
159
|
+
2. **Server Multicast**: The server receives the message and broadcasts it to OTHER clients (sender is filtered out via `clientIdA !== clientIdB` in `@rljson/server@0.0.4+`)
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// This is NORMAL behavior:
|
|
163
|
+
const connector = new Connector(clientDb, route, socket);
|
|
164
|
+
|
|
165
|
+
connector.listen((ref) => {
|
|
166
|
+
console.log('Received ref:', ref);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
connector.send('my-ref-123');
|
|
170
|
+
// Output: "Received ref: my-ref-123" ← Local echo happens IMMEDIATELY
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Server-Side Filtering (v0.0.4+):**
|
|
174
|
+
|
|
175
|
+
The `@rljson/server` package (v0.0.4 and later) correctly filters out the sender when multicasting:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
// Inside Server._multicastRefs():
|
|
179
|
+
for (const [clientIdB, { socket: socketB }] of this._clients.entries()) {
|
|
180
|
+
if (clientIdA !== clientIdB) { // ← Sender is excluded from multicast
|
|
181
|
+
const forwarded = Object.assign({}, payload, { __origin: clientIdA });
|
|
182
|
+
socketB.emit(this._route.flat, forwarded);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
This means Client A will NOT receive its message back from the server, but it WILL receive it via local socket echo.
|
|
188
|
+
|
|
189
|
+
**Application-Level Self-Filtering:**
|
|
190
|
+
|
|
191
|
+
Because of local socket echo, **application code MUST filter out its own broadcasts** to prevent infinite loops. This is done in FsAgent using the `_lastSentRef` property:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// In FsAgent:
|
|
195
|
+
private _lastSentRef?: string;
|
|
196
|
+
|
|
197
|
+
// When sending:
|
|
198
|
+
this._lastSentRef = ref;
|
|
199
|
+
connector.send(ref);
|
|
200
|
+
|
|
201
|
+
// When receiving:
|
|
202
|
+
if (treeRef === this._lastSentRef) {
|
|
203
|
+
console.log('[syncFromDb] Skipping self-broadcast');
|
|
204
|
+
return; // Don't process own message
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Why This Architecture:**
|
|
209
|
+
- Socket echo is unavoidable with EventEmitter-based implementations (SocketMock, real sockets)
|
|
210
|
+
- Server-side filtering prevents network round-trips but can't prevent local echo
|
|
211
|
+
- Application-level filtering is defensive programming and works regardless of socket implementation
|
|
212
|
+
- This pattern is necessary for all real-time sync systems
|
|
213
|
+
|
|
214
|
+
### Client-Server Pattern
|
|
215
|
+
|
|
216
|
+
**ALWAYS use `Server` and `Client` classes from `@rljson/server` directly.**
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
// ✅ CORRECT: Let Server and Client handle internal BsMulti/BsPeer setup
|
|
220
|
+
const server = new Server(route, serverIo, serverBs);
|
|
221
|
+
await server.init();
|
|
222
|
+
|
|
223
|
+
const socket = new SocketMock();
|
|
224
|
+
socket.connect();
|
|
225
|
+
await server.addSocket(socket);
|
|
226
|
+
|
|
227
|
+
const localIo = new IoMem();
|
|
228
|
+
await localIo.init();
|
|
229
|
+
const localBs = new BsMem();
|
|
230
|
+
|
|
231
|
+
const client = new Client(socket, localIo, localBs);
|
|
232
|
+
await client.init();
|
|
233
|
+
|
|
234
|
+
// Use client.bs for all operations
|
|
235
|
+
const agent = new FsAgent(folderPath, client.bs);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// ❌ WRONG: Never manually construct BsMulti with BsPeer
|
|
240
|
+
// This works around library issues instead of fixing them at the source
|
|
241
|
+
const localBs = new BsMem();
|
|
242
|
+
const peerBs = new BsPeer(socket);
|
|
243
|
+
const clientBs = new BsMulti([
|
|
244
|
+
{ bs: localBs, priority: 1, read: true, write: true },
|
|
245
|
+
{ bs: peerBs, priority: 2, read: true, write: true },
|
|
246
|
+
]);
|
|
247
|
+
const client = new Client(socket, localIo, clientBs);
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Rationale**: If the `Server` or `Client` classes don't work correctly for our use case, we must fix the issue in `@rljson/server` package, not work around it in tests or application code. Tests should reflect real-world usage patterns, not paper over library deficiencies.
|
|
251
|
+
|
|
252
|
+
### Database Access Pattern
|
|
253
|
+
|
|
254
|
+
**ALWAYS create client-specific `Db` instances using `client.io`, never share server's `Db`.**
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// ✅ CORRECT: Each client creates its own Db with client.io
|
|
258
|
+
const server = new Server(route, serverIo, serverBs);
|
|
259
|
+
await server.init();
|
|
260
|
+
|
|
261
|
+
// Server creates table structure (one-time setup)
|
|
262
|
+
const serverDb = new Db(serverIo);
|
|
263
|
+
await serverDb.core.createTableWithInsertHistory(treeCfg);
|
|
264
|
+
|
|
265
|
+
// Each client gets its own Db
|
|
266
|
+
const clientA = new Client(socketA, localIoA, localBsA);
|
|
267
|
+
await clientA.init();
|
|
268
|
+
const clientDbA = new Db(clientA.io); // Uses client.io, not serverIo
|
|
269
|
+
|
|
270
|
+
const clientB = new Client(socketB, localIoB, localBsB);
|
|
271
|
+
await clientB.init();
|
|
272
|
+
const clientDbB = new Db(clientB.io); // Uses client.io, not serverIo
|
|
273
|
+
|
|
274
|
+
// Use client-specific Db instances
|
|
275
|
+
await agentA.storeInDb(clientDbA, 'sharedTree');
|
|
276
|
+
await agentB.loadFromDb(clientDbB, 'sharedTree', rootRef);
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// ❌ WRONG: Sharing server's Db directly with clients
|
|
281
|
+
const serverDb = new Db(serverIo);
|
|
282
|
+
await serverDb.core.createTableWithInsertHistory(treeCfg);
|
|
283
|
+
|
|
284
|
+
// Both clients use the same Db - bypasses Client/Server architecture
|
|
285
|
+
await agentA.storeInDb(serverDb, 'sharedTree');
|
|
286
|
+
await agentB.loadFromDb(serverDb, 'sharedTree', rootRef);
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Rationale**: The `Client` class creates an internal `IoMulti` that combines local `Io` with a server peer. By using `client.io`, database operations automatically go through this multi-layer structure, maintaining proper client-server separation. Sharing the server's `Db` directly violates this architecture and bypasses the Client/Server pattern entirely.
|
|
290
|
+
|
|
291
|
+
## Data Synchronization Flow (How It Works)
|
|
292
|
+
|
|
293
|
+
### The Peer-to-Peer Architecture with Central Server Coordination
|
|
294
|
+
|
|
295
|
+
The fs-agent implements a distributed peer-to-peer synchronization pattern where:
|
|
296
|
+
|
|
297
|
+
1. **Each client stores data locally** in its own `Io` (database) and `Bs` (blob storage)
|
|
298
|
+
2. **References are broadcast** through the server via Connector
|
|
299
|
+
3. **Data is pulled on-demand** when a client needs data it doesn't have
|
|
300
|
+
4. **The server coordinates** but doesn't own the data - it routes requests between clients
|
|
301
|
+
|
|
302
|
+
### Step-by-Step Sync Flow: Client A → Client B
|
|
303
|
+
|
|
304
|
+
**Message Routing via Connector:**
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
Client A Server Client B
|
|
308
|
+
-------- ------ --------
|
|
309
|
+
|
|
310
|
+
1. File changes detected
|
|
311
|
+
↓
|
|
312
|
+
2. FsAgent extracts tree
|
|
313
|
+
↓
|
|
314
|
+
3. connector.send(treeRef)
|
|
315
|
+
│
|
|
316
|
+
├─→ Local Socket Echo 6. Server._multicastRefs()
|
|
317
|
+
│ (Client A's listener triggered) filters sender
|
|
318
|
+
│ ↓
|
|
319
|
+
└─→ socket.emit(route, {r: ref}) 7. Checks: clientIdA !== clientIdB
|
|
320
|
+
↓ ↓
|
|
321
|
+
Server receives on 8. Broadcasts to OTHER clients
|
|
322
|
+
socket.on(route, ...) (Client A excluded)
|
|
323
|
+
↓
|
|
324
|
+
socketB.emit(route, {
|
|
325
|
+
r: ref,
|
|
326
|
+
__origin: clientIdA
|
|
327
|
+
})
|
|
328
|
+
↓
|
|
329
|
+
→ Client B
|
|
330
|
+
↓
|
|
331
|
+
9. Client B's connector
|
|
332
|
+
.listen() triggered
|
|
333
|
+
↓
|
|
334
|
+
10. syncFromDb callback
|
|
335
|
+
processes ref
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**Key Points:**
|
|
339
|
+
- Client A's connector receives its own message via **local socket echo** (step 1 branch)
|
|
340
|
+
- FsAgent's `_lastSentRef` filtering prevents processing this echo
|
|
341
|
+
- Server receives the message and broadcasts to **all OTHER clients** (step 6-8)
|
|
342
|
+
- The `__origin` field prevents infinite forwarding loops in the server
|
|
343
|
+
- Client B receives the message and pulls data via IoMulti/BsMulti (see below)
|
|
344
|
+
|
|
345
|
+
**Data Pull Flow (when Client B needs data):**
|
|
346
|
+
|
|
347
|
+
```
|
|
348
|
+
Client A Server Client B
|
|
349
|
+
-------- ------ --------
|
|
350
|
+
|
|
351
|
+
1. File changes detected
|
|
352
|
+
↓
|
|
353
|
+
2. FsAgent extracts tree
|
|
354
|
+
↓
|
|
355
|
+
3. Blobs stored in clientA.bs (local BsMem)
|
|
356
|
+
↓
|
|
357
|
+
4. Tree stored in clientDbA (local IoMem)
|
|
358
|
+
via storeInDb()
|
|
359
|
+
↓
|
|
360
|
+
5. connector.send(treeRootRef)
|
|
361
|
+
↓ socket →→→
|
|
362
|
+
6. Server receives ref
|
|
363
|
+
↓
|
|
364
|
+
7. Multicasts to all clients
|
|
365
|
+
↓ socket →→→
|
|
366
|
+
8. connectorB receives ref
|
|
367
|
+
↓
|
|
368
|
+
9. syncFromDb callback triggered
|
|
369
|
+
↓
|
|
370
|
+
10. loadFromDb(treeRef) called
|
|
371
|
+
↓
|
|
372
|
+
11. Query clientDbB for tree data
|
|
373
|
+
↓
|
|
374
|
+
12. clientDbB.io (IoMulti) checks:
|
|
375
|
+
- localIoB: NOT FOUND
|
|
376
|
+
- IoPeer: Query server
|
|
377
|
+
← socket ←←
|
|
378
|
+
13. Server routes to Client A
|
|
379
|
+
← socket ←←
|
|
380
|
+
14. Client A's Io returns tree data
|
|
381
|
+
→ socket →→
|
|
382
|
+
15. Data flows back to Server
|
|
383
|
+
→ socket →→
|
|
384
|
+
16. Tree data arrives at Client B
|
|
385
|
+
↓
|
|
386
|
+
17. Tree data stored in localIoB
|
|
387
|
+
↓
|
|
388
|
+
18. For each file in tree:
|
|
389
|
+
clientB.bs.getBlob(blobId)
|
|
390
|
+
↓
|
|
391
|
+
19. clientB.bs (BsMulti) checks:
|
|
392
|
+
- localBsB: NOT FOUND
|
|
393
|
+
- BsPeer: Query server
|
|
394
|
+
← socket ←←
|
|
395
|
+
20. Server routes to Client A
|
|
396
|
+
← socket ←←
|
|
397
|
+
21. Client A's Bs returns blob
|
|
398
|
+
→ socket →→
|
|
399
|
+
22. Blob flows back to Server
|
|
400
|
+
→ socket →→
|
|
401
|
+
23. Blob arrives at Client B
|
|
402
|
+
↓
|
|
403
|
+
24. Blob stored in localBsB
|
|
404
|
+
↓
|
|
405
|
+
25. File written to filesystem
|
|
406
|
+
↓
|
|
407
|
+
26. Sync complete!
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Key Architectural Components
|
|
411
|
+
|
|
412
|
+
**IoMulti (inside client.io):**
|
|
413
|
+
|
|
414
|
+
- Combines local IoMem with IoPeer (server connection)
|
|
415
|
+
- When data is requested: first checks local, then queries peer via socket
|
|
416
|
+
- Automatically caches retrieved data locally
|
|
417
|
+
- Transparent to the application - just use `client.io`
|
|
418
|
+
|
|
419
|
+
**BsMulti (inside client.bs):**
|
|
420
|
+
|
|
421
|
+
- Combines local BsMem with BsPeer (server connection)
|
|
422
|
+
- When blob is requested: first checks local, then queries peer via socket
|
|
423
|
+
- Automatically caches retrieved blobs locally
|
|
424
|
+
- Transparent to the application - just use `client.bs`
|
|
425
|
+
|
|
426
|
+
**Connector:**
|
|
427
|
+
|
|
428
|
+
- Broadcasts tree references (not full data) via socket
|
|
429
|
+
- Triggers `syncFromDb` callbacks on receiving clients
|
|
430
|
+
- Minimal bandwidth - only sends references
|
|
431
|
+
|
|
432
|
+
**Server:**
|
|
433
|
+
|
|
434
|
+
- Routes data requests between clients
|
|
435
|
+
- Maintains connections to all clients via sockets
|
|
436
|
+
- Does NOT store client data - purely acts as coordinator/router
|
|
437
|
+
- Has its own serverIo and serverBs for server-specific needs only
|
|
438
|
+
|
|
439
|
+
### Why This Architecture Matters
|
|
440
|
+
|
|
441
|
+
This peer-to-peer pattern with server coordination enables:
|
|
442
|
+
|
|
443
|
+
✅ **Distributed storage**: Each client owns its data locally
|
|
444
|
+
✅ **Bandwidth efficiency**: Only references broadcast, data pulled on-demand
|
|
445
|
+
✅ **Scalability**: Server doesn't store all client data
|
|
446
|
+
✅ **Offline capability**: Clients can work with locally cached data
|
|
447
|
+
✅ **Real-world deployment**: Works across networks, not just in-memory mocks
|
|
448
|
+
|
|
449
|
+
**This is why clients must NEVER access server Io/Bs directly** - it would bypass the entire peer-to-peer mechanism and make the system only work in single-process scenarios.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@license
|
|
3
|
+
Copyright (c) 2025 Rljson
|
|
4
|
+
|
|
5
|
+
Use of this source code is governed by terms that can be
|
|
6
|
+
found in the LICENSE file in the root of this package.
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
# Contributors Guide
|
|
10
|
+
|
|
11
|
+
- [Prepare](#prepare)
|
|
12
|
+
- [Develop](#develop)
|
|
13
|
+
- [Administrate](#administrate)
|
|
14
|
+
- [Fast Coding](#fast-coding)
|
|
15
|
+
|
|
16
|
+
## Prepare
|
|
17
|
+
|
|
18
|
+
Read [prepare.md](doc/prepare.md)
|
|
19
|
+
|
|
20
|
+
<!-- ........................................................................-->
|
|
21
|
+
|
|
22
|
+
## Develop
|
|
23
|
+
|
|
24
|
+
Read [develop.md](doc/develop.md)
|
|
25
|
+
|
|
26
|
+
## Administrate
|
|
27
|
+
|
|
28
|
+
Read [create-new-repo.md](doc/create-new-repo.md)
|
|
29
|
+
|
|
30
|
+
## Fast Coding
|
|
31
|
+
|
|
32
|
+
Read [fast-coding-guide.md](doc/fast-coding-guide.md)
|
package/dist/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@license
|
|
3
|
+
Copyright (c) 2025 Rljson
|
|
4
|
+
|
|
5
|
+
Use of this source code is governed by terms that can be
|
|
6
|
+
found in the LICENSE file in the root of this package.
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
# @rljson/fs-agent
|
|
10
|
+
|
|
11
|
+
## Users
|
|
12
|
+
|
|
13
|
+
| File | Purpose |
|
|
14
|
+
| ------------------------------------ | --------------------------- |
|
|
15
|
+
| [README.public.md](README.public.md) | Install and use the package |
|
|
16
|
+
|
|
17
|
+
## Contributors
|
|
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 |
|