@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Rljson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -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.
package/README.blog.md ADDED
@@ -0,0 +1,11 @@
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
+ # Blog
10
+
11
+ Add latest posts at the end.
@@ -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/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 |