@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,781 @@
|
|
|
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
|
+
> **A powerful filesystem agent that seamlessly synchronizes your file system with RLJSON databases, providing automatic bidirectional sync, tree structures, and content-addressed blob storage.**
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
`@rljson/fs-agent` bridges the gap between your filesystem and RLJSON databases. It watches for changes, extracts hierarchical tree structures, stores file content efficiently in blob storage with automatic deduplication, and maintains complete version history. With built-in bidirectional synchronization, your filesystem and database stay perfectly in sync—automatically.
|
|
16
|
+
|
|
17
|
+
### Why Use fs-agent?
|
|
18
|
+
|
|
19
|
+
- 🔄 **Automatic Synchronization**: Changes flow seamlessly between filesystem and database
|
|
20
|
+
- 🌳 **Tree Structures**: Represents directories as RLJSON tree structures with parent-child relationships
|
|
21
|
+
- 💾 **Smart Storage**: Content-addressed blob storage eliminates duplicate file content
|
|
22
|
+
- 📜 **Version History**: Every change is tracked with complete insert history
|
|
23
|
+
- 🔁 **Bidirectional Sync**: Changes in either direction are automatically propagated
|
|
24
|
+
- 🛡️ **Loop Prevention**: Intelligent pause/resume mechanism prevents infinite sync loops
|
|
25
|
+
- ✅ **Type-Safe**: Full TypeScript support with comprehensive type definitions
|
|
26
|
+
- 🧪 **Battle-Tested**: 100% test coverage with 139 tests
|
|
27
|
+
- 🧹 **Target Cleanup**: Optional `cleanTarget` restore prunes stale files and directories
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install @rljson/fs-agent
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
### Basic Usage - Automatic Sync
|
|
38
|
+
|
|
39
|
+
The simplest way to get started is with automatic synchronization:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { FsAgent } from '@rljson/fs-agent';
|
|
43
|
+
import { Connector, Db } from '@rljson/db';
|
|
44
|
+
import { IoMem, SocketMock } from '@rljson/io';
|
|
45
|
+
import { BsMem } from '@rljson/bs';
|
|
46
|
+
import { Route, createTreesTableCfg } from '@rljson/rljson';
|
|
47
|
+
|
|
48
|
+
// Setup database
|
|
49
|
+
const io = new IoMem();
|
|
50
|
+
await io.init();
|
|
51
|
+
const db = new Db(io);
|
|
52
|
+
|
|
53
|
+
// Create tree table (must end with "Tree")
|
|
54
|
+
const treeKey = 'projectFilesTree';
|
|
55
|
+
const treeTableCfg = createTreesTableCfg(treeKey);
|
|
56
|
+
await db.core.createTableWithInsertHistory(treeTableCfg);
|
|
57
|
+
|
|
58
|
+
// Create FsAgent
|
|
59
|
+
const agent = new FsAgent('./my-project', new BsMem(), {
|
|
60
|
+
ignore: ['node_modules', '.git', 'dist'],
|
|
61
|
+
maxDepth: 10,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Create Connector for socket-based synchronization
|
|
65
|
+
const socket = new SocketMock();
|
|
66
|
+
const route = Route.fromFlat(`/${treeKey}`);
|
|
67
|
+
const connector = new Connector(db, route, socket);
|
|
68
|
+
|
|
69
|
+
// Start syncing
|
|
70
|
+
const stopSync = await agent.syncToDb(db, connector, treeKey);
|
|
71
|
+
|
|
72
|
+
// The agent now:
|
|
73
|
+
// ✓ Watches your filesystem for changes
|
|
74
|
+
// ✓ Extracts tree structures from directories
|
|
75
|
+
// ✓ Stores file content in blob storage
|
|
76
|
+
// ✓ Broadcasts changes via Connector
|
|
77
|
+
|
|
78
|
+
// When you're done:
|
|
79
|
+
stopSync();
|
|
80
|
+
agent.dispose();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Bidirectional Sync
|
|
84
|
+
|
|
85
|
+
Enable two-way synchronization so changes in the database also update the filesystem:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { Connector } from '@rljson/db';
|
|
89
|
+
import { Route } from '@rljson/rljson';
|
|
90
|
+
import { SocketMock } from '@rljson/io';
|
|
91
|
+
|
|
92
|
+
const agent = new FsAgent('./my-project', new BsMem(), {
|
|
93
|
+
ignore: ['node_modules', '.git', 'dist'],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Create Connector for bidirectional communication
|
|
97
|
+
const socket = new SocketMock();
|
|
98
|
+
const route = Route.fromFlat(`/${treeKey}`);
|
|
99
|
+
const connector = new Connector(db, route, socket);
|
|
100
|
+
|
|
101
|
+
// Start filesystem → database sync
|
|
102
|
+
const stopToDb = await agent.syncToDb(db, connector, treeKey);
|
|
103
|
+
|
|
104
|
+
// Start database → filesystem sync (same Connector)
|
|
105
|
+
const stopFromDb = await agent.syncFromDb(db, connector, treeKey);
|
|
106
|
+
|
|
107
|
+
// Now the agent handles changes in BOTH directions:
|
|
108
|
+
// 1. Filesystem changes → automatically synced to database
|
|
109
|
+
// 2. Database changes → automatically synced to filesystem
|
|
110
|
+
// 3. Loop prevention ensures no infinite sync cycles
|
|
111
|
+
// 4. Both sides stay perfectly synchronized
|
|
112
|
+
|
|
113
|
+
// Stop syncing
|
|
114
|
+
stopToDb();
|
|
115
|
+
stopFromDb();
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Core Concepts
|
|
119
|
+
|
|
120
|
+
### Tree Structures
|
|
121
|
+
|
|
122
|
+
`fs-agent` represents your filesystem as RLJSON tree structures:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// Directory hierarchy:
|
|
126
|
+
// my-project/
|
|
127
|
+
// ├── src/
|
|
128
|
+
// │ ├── index.ts
|
|
129
|
+
// │ └── utils.ts
|
|
130
|
+
// └── package.json
|
|
131
|
+
|
|
132
|
+
// Becomes a tree structure where:
|
|
133
|
+
// - Each file/directory is a tree node
|
|
134
|
+
// - Nodes are content-addressed (identified by hash)
|
|
135
|
+
// - Parent-child relationships are preserved
|
|
136
|
+
// - Changes are tracked via insert history
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Blob Storage
|
|
140
|
+
|
|
141
|
+
Files are stored efficiently using content-addressed blob storage:
|
|
142
|
+
|
|
143
|
+
- **Deduplication**: Identical files are stored only once
|
|
144
|
+
- **Content-Addressed**: Files are identified by their content hash
|
|
145
|
+
- **Efficient**: Only changed files are re-stored
|
|
146
|
+
- **Flexible**: Works with any `Bs` (Blob Storage) implementation
|
|
147
|
+
|
|
148
|
+
### Automatic Watching
|
|
149
|
+
|
|
150
|
+
### Automatic Synchronization (Constructor-based)
|
|
151
|
+
|
|
152
|
+
**Note:** Constructor-based automatic synchronization using `db`, `treeKey`, and `bidirectional` options is deprecated and will throw an error. Use the explicit `syncToDb()` and `syncFromDb()` methods with a `Connector` instance instead (see examples above).
|
|
153
|
+
|
|
154
|
+
The new approach provides better control and uses the Connector pattern for socket-based synchronization:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// ❌ Deprecated (will throw error)
|
|
158
|
+
const agent = new FsAgent('./my-project', new BsMem(), {
|
|
159
|
+
db,
|
|
160
|
+
treeKey,
|
|
161
|
+
bidirectional: true,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ✅ Use this instead
|
|
165
|
+
const agent = new FsAgent('./my-project', new BsMem());
|
|
166
|
+
const connector = new Connector(db, route, socket);
|
|
167
|
+
const stopToDb = await agent.syncToDb(db, connector, treeKey);
|
|
168
|
+
const stopFromDb = await agent.syncFromDb(db, connector, treeKey);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Live Client-Server Demo
|
|
172
|
+
|
|
173
|
+
Run the live in-process demo that mirrors changes between two folders using the same approach as our sync tests (SocketMock, IoMem, BsMem):
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
pnpm exec vite-node src/live-client-server.ts
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
It wipes and recreates `demo/live-client-server/folder-a` and `demo/live-client-server/folder-b`, seeds sample files, and keeps them in sync until you press Ctrl+C. Pass `--keep-existing` to skip the reset.
|
|
180
|
+
|
|
181
|
+
## API Reference
|
|
182
|
+
|
|
183
|
+
### FsAgent
|
|
184
|
+
|
|
185
|
+
Main class that orchestrates filesystem operations.
|
|
186
|
+
|
|
187
|
+
#### Constructor
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
new FsAgent(rootPath: string, bs?: Bs, options?: FsAgentOptions)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Parameters:**
|
|
194
|
+
|
|
195
|
+
- `rootPath` - Root directory to monitor
|
|
196
|
+
- `bs` - Blob storage instance (defaults to `BsMem`)
|
|
197
|
+
- `options` - Configuration options
|
|
198
|
+
|
|
199
|
+
**Options:**
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
interface FsAgentOptions {
|
|
203
|
+
// Scanning options
|
|
204
|
+
ignore?: string[]; // Patterns to ignore (e.g., ['node_modules', '*.log'])
|
|
205
|
+
maxDepth?: number; // Maximum directory depth to scan
|
|
206
|
+
followSymlinks?: boolean; // Whether to follow symbolic links (default: false)
|
|
207
|
+
|
|
208
|
+
// Deprecated options (will throw error if used)
|
|
209
|
+
// Use syncToDb()/syncFromDb() methods with Connector instead
|
|
210
|
+
db?: Db; // DEPRECATED
|
|
211
|
+
treeKey?: string; // DEPRECATED
|
|
212
|
+
bidirectional?: boolean; // DEPRECATED
|
|
213
|
+
storageOptions?: StoreFsTreeOptions; // DEPRECATED
|
|
214
|
+
restoreOptions?: RestoreOptions; // DEPRECATED
|
|
215
|
+
}
|
|
216
|
+
maxDepth?: number; // Maximum directory depth (default: 10)
|
|
217
|
+
followSymlinks?: boolean; // Follow symbolic links (default: false)
|
|
218
|
+
|
|
219
|
+
// Automatic sync options
|
|
220
|
+
db?: Db; // Database instance for auto-sync
|
|
221
|
+
treeKey?: string; // Tree table key for storage
|
|
222
|
+
storageOptions?: {
|
|
223
|
+
// Options for database storage
|
|
224
|
+
includeBlobs?: boolean; // Include blob data in tree (default: true)
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// Bidirectional sync
|
|
228
|
+
bidirectional?: boolean; // Enable database → filesystem sync (default: false)
|
|
229
|
+
|
|
230
|
+
// Restore options applied when syncing from DB (e.g., cleanTarget)
|
|
231
|
+
restoreOptions?: RestoreOptions;
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
#### Properties
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
agent.rootPath: string // Root directory path
|
|
239
|
+
agent.bs: Bs // Blob storage instance
|
|
240
|
+
agent.scanner: FsScanner // File scanner instance
|
|
241
|
+
agent.adapter: FsBlobAdapter // Blob adapter instance
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### Methods
|
|
245
|
+
|
|
246
|
+
##### `extract(): Promise<FsTree>`
|
|
247
|
+
|
|
248
|
+
Extracts the current filesystem as a tree structure.
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
const tree = await agent.extract();
|
|
252
|
+
// Returns: { rootHash: string, trees: Map<string, Tree> }
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
##### `restore(tree: FsTree, targetPath?: string, options?: RestoreOptions): Promise<void>`
|
|
256
|
+
|
|
257
|
+
Restores a tree structure to the filesystem. Use `options.cleanTarget` to remove files and directories that are not part of the tree (helpful for propagating renames or deletions):
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
await agent.restore(tree, './restore-location', { cleanTarget: true });
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
##### `storeInDb(db: Db, treeKey: string, options?: StoreFsTreeOptions): Promise<void>`
|
|
264
|
+
|
|
265
|
+
Manually stores the current filesystem state in the database.
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
await agent.storeInDb(db, 'myFilesTree', { includeBlobs: true });
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
##### `syncToDb(db: Db, connector: Connector, treeKey: string, options?: StoreFsTreeOptions): Promise<() => void>`
|
|
272
|
+
|
|
273
|
+
Starts watching filesystem and syncing to database using Connector for socket-based broadcasts.
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import { Connector } from '@rljson/db';
|
|
277
|
+
import { Route } from '@rljson/rljson';
|
|
278
|
+
import { SocketMock } from '@rljson/io';
|
|
279
|
+
|
|
280
|
+
const socket = new SocketMock();
|
|
281
|
+
const route = Route.fromFlat(`/${treeKey}+`);
|
|
282
|
+
const connector = new Connector(db, route, socket);
|
|
283
|
+
|
|
284
|
+
const stopSync = await agent.syncToDb(db, connector, 'myFilesTree');
|
|
285
|
+
// Later: stopSync();
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
##### `syncFromDb(db: Db, connector: Connector, treeKey: string, restoreOptions?: RestoreOptions): Promise<() => void>`
|
|
289
|
+
|
|
290
|
+
Starts listening to database changes via Connector and syncing to filesystem.
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { Connector } from '@rljson/db';
|
|
294
|
+
import { Route } from '@rljson/rljson';
|
|
295
|
+
import { SocketMock } from '@rljson/io';
|
|
296
|
+
|
|
297
|
+
const socket = new SocketMock();
|
|
298
|
+
const route = Route.fromFlat(`/${treeKey}+`);
|
|
299
|
+
const connector = new Connector(db, route, socket);
|
|
300
|
+
|
|
301
|
+
const stopSync = await agent.syncFromDb(db, connector, 'myFilesTree', {
|
|
302
|
+
cleanTarget: true // Optional: remove files not in tree
|
|
303
|
+
});
|
|
304
|
+
// Later: stopSync();
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
##### `dispose(): void`
|
|
308
|
+
|
|
309
|
+
Stops all syncing and cleans up resources.
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
agent.dispose();
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### FsScanner
|
|
316
|
+
|
|
317
|
+
Low-level filesystem scanner (usually accessed via `agent.scanner`).
|
|
318
|
+
|
|
319
|
+
#### Methods
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
// Scan filesystem once
|
|
323
|
+
const tree = await scanner.scan();
|
|
324
|
+
|
|
325
|
+
// Start watching for changes
|
|
326
|
+
await scanner.watch();
|
|
327
|
+
|
|
328
|
+
// Register change callback
|
|
329
|
+
scanner.onChange(async (change) => {
|
|
330
|
+
console.log(change.type, change.path);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Pause/resume watching (for loop prevention)
|
|
334
|
+
scanner.pauseWatch();
|
|
335
|
+
scanner.resumeWatch();
|
|
336
|
+
|
|
337
|
+
// Get root tree
|
|
338
|
+
const rootTree = scanner.getRootTree();
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Advanced Usage
|
|
342
|
+
|
|
343
|
+
### Manual Sync Control
|
|
344
|
+
|
|
345
|
+
If you need fine-grained control over synchronization:
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
import { Connector } from '@rljson/db';
|
|
349
|
+
import { Route } from '@rljson/rljson';
|
|
350
|
+
import { SocketMock } from '@rljson/io';
|
|
351
|
+
|
|
352
|
+
const agent = new FsAgent('./my-project', new BsMem());
|
|
353
|
+
|
|
354
|
+
// Create Connector for synchronization
|
|
355
|
+
const treeKey = 'myFilesTree';
|
|
356
|
+
const socket = new SocketMock();
|
|
357
|
+
const route = Route.fromFlat(`/${treeKey}`);
|
|
358
|
+
const connector = new Connector(db, route, socket);
|
|
359
|
+
|
|
360
|
+
// Start filesystem → database sync
|
|
361
|
+
const stopToDb = await agent.syncToDb(db, connector, treeKey);
|
|
362
|
+
|
|
363
|
+
// Start database → filesystem sync
|
|
364
|
+
const stopFromDb = await agent.syncFromDb(db, connector, treeKey);
|
|
365
|
+
|
|
366
|
+
// Stop when needed
|
|
367
|
+
stopToDb();
|
|
368
|
+
stopFromDb();
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Custom Blob Storage
|
|
372
|
+
|
|
373
|
+
Use any blob storage implementation:
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
import { BsSql } from '@rljson/bs-sql';
|
|
377
|
+
|
|
378
|
+
// SQL-backed blob storage
|
|
379
|
+
const sqlBs = new BsSql(myDatabase);
|
|
380
|
+
const agent = new FsAgent('./my-project', sqlBs, {
|
|
381
|
+
db,
|
|
382
|
+
treeKey: 'filesTree',
|
|
383
|
+
});
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Ignore Patterns
|
|
387
|
+
|
|
388
|
+
Control what gets scanned and synced:
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
const agent = new FsAgent('./my-project', new BsMem(), {
|
|
392
|
+
db,
|
|
393
|
+
treeKey: 'filesTree',
|
|
394
|
+
ignore: [
|
|
395
|
+
'node_modules',
|
|
396
|
+
'.git',
|
|
397
|
+
'dist',
|
|
398
|
+
'coverage',
|
|
399
|
+
'*.log',
|
|
400
|
+
'.DS_Store',
|
|
401
|
+
'tmp/**',
|
|
402
|
+
],
|
|
403
|
+
});
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Limited Depth Scanning
|
|
407
|
+
|
|
408
|
+
Control how deep to traverse directories:
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
const agent = new FsAgent('./my-project', new BsMem(), {
|
|
412
|
+
db,
|
|
413
|
+
treeKey: 'filesTree',
|
|
414
|
+
maxDepth: 3, // Only scan 3 levels deep
|
|
415
|
+
});
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Extract and Restore
|
|
419
|
+
|
|
420
|
+
Work with tree structures directly:
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
// Extract current state
|
|
424
|
+
const tree = await agent.extract();
|
|
425
|
+
|
|
426
|
+
// Trees are content-addressed
|
|
427
|
+
console.log('Root hash:', tree.rootHash);
|
|
428
|
+
console.log('Total nodes:', tree.trees.size);
|
|
429
|
+
|
|
430
|
+
// Restore to a different location
|
|
431
|
+
await agent.restore(tree, './backup-location');
|
|
432
|
+
|
|
433
|
+
// Or restore from database
|
|
434
|
+
const dbAdapter = new FsDbAdapter(db, 'myFilesTree');
|
|
435
|
+
const treeFromDb = await dbAdapter.loadFsTree('abc123'); // tree hash
|
|
436
|
+
await agent.restore(treeFromDb, './restore-here');
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Version History
|
|
440
|
+
|
|
441
|
+
Access historical versions via insert history:
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
import { Route } from '@rljson/rljson';
|
|
445
|
+
|
|
446
|
+
// Get insert history for the tree
|
|
447
|
+
const route = Route.fromFlat(`${treeKey}/`);
|
|
448
|
+
const historyResult = await db.getInsertHistory(route, {});
|
|
449
|
+
|
|
450
|
+
// historyResult contains all versions with timestamps
|
|
451
|
+
for (const entry of historyResult.history) {
|
|
452
|
+
const treeRef = entry.treeRef;
|
|
453
|
+
const timestamp = entry.insertedAt;
|
|
454
|
+
|
|
455
|
+
// Load and restore specific version
|
|
456
|
+
const tree = await agent.loadFromDb(db, treeKey, treeRef);
|
|
457
|
+
await agent.restore(tree, `./version-${timestamp}`);
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Change Detection
|
|
462
|
+
|
|
463
|
+
React to specific filesystem changes:
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
agent.scanner.onChange(async (change) => {
|
|
467
|
+
switch (change.type) {
|
|
468
|
+
case 'add':
|
|
469
|
+
console.log('File added:', change.path);
|
|
470
|
+
break;
|
|
471
|
+
case 'change':
|
|
472
|
+
console.log('File modified:', change.path);
|
|
473
|
+
break;
|
|
474
|
+
case 'unlink':
|
|
475
|
+
console.log('File deleted:', change.path);
|
|
476
|
+
break;
|
|
477
|
+
case 'addDir':
|
|
478
|
+
console.log('Directory created:', change.path);
|
|
479
|
+
break;
|
|
480
|
+
case 'unlinkDir':
|
|
481
|
+
console.log('Directory deleted:', change.path);
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
## Error Handling
|
|
488
|
+
|
|
489
|
+
`fs-agent` provides robust error handling with clear error messages:
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
try {
|
|
493
|
+
const agent = new FsAgent('./nonexistent', new BsMem(), {
|
|
494
|
+
db,
|
|
495
|
+
treeKey: 'filesTree',
|
|
496
|
+
});
|
|
497
|
+
} catch (error) {
|
|
498
|
+
// Error: Root path "./nonexistent" does not exist. Cannot scan non-existent directory.
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### Common Error Scenarios
|
|
503
|
+
|
|
504
|
+
1. **Missing Root Path**: Clear error if directory doesn't exist
|
|
505
|
+
2. **Database Failures**: Errors include context about what operation failed
|
|
506
|
+
3. **Invalid Tree Data**: Validation errors explain what's wrong
|
|
507
|
+
4. **Sync Failures**: Non-fatal errors logged, syncing continues
|
|
508
|
+
5. **Loop Detection**: Automatic prevention via pause/resume
|
|
509
|
+
|
|
510
|
+
## Examples
|
|
511
|
+
|
|
512
|
+
### Example 1: Simple Project Sync
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
import { FsAgent } from '@rljson/fs-agent';
|
|
516
|
+
import { Connector, Db } from '@rljson/db';
|
|
517
|
+
import { IoMem, SocketMock } from '@rljson/io';
|
|
518
|
+
import { BsMem } from '@rljson/bs';
|
|
519
|
+
import { Route, createTreesTableCfg } from '@rljson/rljson';
|
|
520
|
+
|
|
521
|
+
async function syncProject() {
|
|
522
|
+
// Setup
|
|
523
|
+
const io = new IoMem();
|
|
524
|
+
await io.init();
|
|
525
|
+
const db = new Db(io);
|
|
526
|
+
|
|
527
|
+
const treeKey = 'projectTree';
|
|
528
|
+
const treeTableCfg = createTreesTableCfg(treeKey);
|
|
529
|
+
await db.core.createTableWithInsertHistory(treeTableCfg);
|
|
530
|
+
|
|
531
|
+
// Create agent
|
|
532
|
+
const agent = new FsAgent('./src', new BsMem(), {
|
|
533
|
+
ignore: ['*.tmp'],
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// Create Connector and start syncing
|
|
537
|
+
const socket = new SocketMock();
|
|
538
|
+
const route = Route.fromFlat(`/${treeKey}+`);
|
|
539
|
+
const connector = new Connector(db, route, socket);
|
|
540
|
+
const stopSync = await agent.syncToDb(db, connector, treeKey);
|
|
541
|
+
|
|
542
|
+
// Clean up when done
|
|
543
|
+
process.on('SIGINT', () => {
|
|
544
|
+
stopSync();
|
|
545
|
+
agent.dispose();
|
|
546
|
+
process.exit();
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### Example 2: Backup and Restore
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
async function backupAndRestore() {
|
|
555
|
+
// Create agent
|
|
556
|
+
const agent = new FsAgent('./my-data', new BsMem());
|
|
557
|
+
|
|
558
|
+
// Extract current state
|
|
559
|
+
const backup = await agent.extract();
|
|
560
|
+
console.log('Backed up', backup.trees.size, 'nodes');
|
|
561
|
+
|
|
562
|
+
// ... later, restore from backup
|
|
563
|
+
await agent.restore(backup, './my-data-restored');
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
### Example 2b: Clean Restore (remove stale files)
|
|
568
|
+
|
|
569
|
+
```typescript
|
|
570
|
+
async function cleanRestore() {
|
|
571
|
+
const agent = new FsAgent('./source', new BsMem());
|
|
572
|
+
const snapshot = await agent.extract();
|
|
573
|
+
|
|
574
|
+
// Restore to target and prune anything not in the snapshot
|
|
575
|
+
await agent.restore(snapshot, './target', { cleanTarget: true });
|
|
576
|
+
}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
### Example 3: Bidirectional Sync
|
|
580
|
+
|
|
581
|
+
```typescript
|
|
582
|
+
async function bidirectionalSync() {
|
|
583
|
+
// Setup database
|
|
584
|
+
const io = new IoMem();
|
|
585
|
+
await io.init();
|
|
586
|
+
const db = new Db(io);
|
|
587
|
+
|
|
588
|
+
const treeTableCfg = createTreesTableCfg('sharedTree');
|
|
589
|
+
await db.core.createTableWithInsertHistory(treeTableCfg);
|
|
590
|
+
|
|
591
|
+
// Agent 1: Watches ./alice and syncs to DB
|
|
592
|
+
const alice = new FsAgent('./alice', new BsMem(), {
|
|
593
|
+
db,
|
|
594
|
+
treeKey: 'sharedTree',
|
|
595
|
+
bidirectional: true, // ← Bidirectional
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// Agent 2: Watches ./bob and syncs to DB
|
|
599
|
+
const bob = new FsAgent('./bob', new BsMem(), {
|
|
600
|
+
db,
|
|
601
|
+
treeKey: 'sharedTree',
|
|
602
|
+
bidirectional: true, // ← Bidirectional
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// Now:
|
|
606
|
+
// - Changes in ./alice → sync to DB → appear in ./bob
|
|
607
|
+
// - Changes in ./bob → sync to DB → appear in ./alice
|
|
608
|
+
// - Loop prevention ensures stability
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### Example 4: Custom Change Handling
|
|
613
|
+
|
|
614
|
+
```typescript
|
|
615
|
+
async function customHandling() {
|
|
616
|
+
const agent = new FsAgent('./watched', new BsMem(), {
|
|
617
|
+
db,
|
|
618
|
+
treeKey: 'watchedTree',
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
let changeCount = 0;
|
|
622
|
+
|
|
623
|
+
agent.scanner.onChange(async (change) => {
|
|
624
|
+
changeCount++;
|
|
625
|
+
|
|
626
|
+
if (change.type === 'add' && change.path.endsWith('.ts')) {
|
|
627
|
+
console.log(`New TypeScript file: ${change.path}`);
|
|
628
|
+
// Could trigger build, linting, etc.
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (changeCount % 10 === 0) {
|
|
632
|
+
console.log(`Processed ${changeCount} changes`);
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
## Best Practices
|
|
639
|
+
|
|
640
|
+
### 1. Use Ignore Patterns
|
|
641
|
+
|
|
642
|
+
Always ignore build artifacts, dependencies, and temporary files:
|
|
643
|
+
|
|
644
|
+
```typescript
|
|
645
|
+
{
|
|
646
|
+
ignore: [
|
|
647
|
+
'node_modules',
|
|
648
|
+
'.git',
|
|
649
|
+
'dist',
|
|
650
|
+
'build',
|
|
651
|
+
'coverage',
|
|
652
|
+
'*.log',
|
|
653
|
+
'.DS_Store',
|
|
654
|
+
'tmp',
|
|
655
|
+
];
|
|
656
|
+
}
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### 2. Dispose When Done
|
|
660
|
+
|
|
661
|
+
Always clean up resources:
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
const agent = new FsAgent(path, bs, options);
|
|
665
|
+
|
|
666
|
+
try {
|
|
667
|
+
// Use agent...
|
|
668
|
+
} finally {
|
|
669
|
+
agent.dispose();
|
|
670
|
+
}
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### 3. Use Bidirectional Sync Carefully
|
|
674
|
+
|
|
675
|
+
Bidirectional sync is powerful but consider:
|
|
676
|
+
|
|
677
|
+
- Multiple agents sharing the same `treeKey` will sync to each other
|
|
678
|
+
- Loop prevention is automatic but adds slight latency
|
|
679
|
+
- Best for collaborative scenarios or distributed systems
|
|
680
|
+
|
|
681
|
+
### 4. Monitor Depth
|
|
682
|
+
|
|
683
|
+
Use `maxDepth` for deep directory structures:
|
|
684
|
+
|
|
685
|
+
```typescript
|
|
686
|
+
{
|
|
687
|
+
maxDepth: 5, // Prevents extremely deep recursion
|
|
688
|
+
}
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
### 5. Handle Errors
|
|
692
|
+
|
|
693
|
+
Wrap agent creation in try-catch for better error handling:
|
|
694
|
+
|
|
695
|
+
```typescript
|
|
696
|
+
try {
|
|
697
|
+
const agent = new FsAgent(userProvidedPath, bs, options);
|
|
698
|
+
} catch (error) {
|
|
699
|
+
console.error('Failed to create agent:', error.message);
|
|
700
|
+
}
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
## Performance
|
|
704
|
+
|
|
705
|
+
- **Efficient Scanning**: Only scans changed directories
|
|
706
|
+
- **Deduplication**: Identical files stored once
|
|
707
|
+
- **Content-Addressed**: Fast lookups via hashes
|
|
708
|
+
- **Optimized Watching**: Uses native filesystem events
|
|
709
|
+
- **Smart Sync**: Only syncs when changes detected
|
|
710
|
+
|
|
711
|
+
## Troubleshooting
|
|
712
|
+
|
|
713
|
+
### Agent not syncing changes
|
|
714
|
+
|
|
715
|
+
Make sure you've called `syncToDb()` with a Connector:
|
|
716
|
+
|
|
717
|
+
```typescript
|
|
718
|
+
import { Connector } from '@rljson/db';
|
|
719
|
+
import { Route } from '@rljson/rljson';
|
|
720
|
+
import { SocketMock } from '@rljson/io';
|
|
721
|
+
|
|
722
|
+
const agent = new FsAgent(path, bs);
|
|
723
|
+
|
|
724
|
+
// Create Connector and start syncing
|
|
725
|
+
const socket = new SocketMock();
|
|
726
|
+
const route = Route.fromFlat(`/${treeKey}+`);
|
|
727
|
+
const connector = new Connector(db, route, socket);
|
|
728
|
+
const stopSync = await agent.syncToDb(db, connector, treeKey);
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
### Bidirectional sync not working
|
|
732
|
+
|
|
733
|
+
Ensure you've started both `syncToDb()` and `syncFromDb()` with the same Connector:
|
|
734
|
+
|
|
735
|
+
```typescript
|
|
736
|
+
const connector = new Connector(db, route, socket);
|
|
737
|
+
|
|
738
|
+
// Start both directions
|
|
739
|
+
const stopToDb = await agent.syncToDb(db, connector, treeKey);
|
|
740
|
+
const stopFromDb = await agent.syncFromDb(db, connector, treeKey);
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
### High memory usage
|
|
744
|
+
|
|
745
|
+
Reduce scan depth or add more ignore patterns:
|
|
746
|
+
|
|
747
|
+
```typescript
|
|
748
|
+
{
|
|
749
|
+
maxDepth: 3,
|
|
750
|
+
ignore: ['large-directory/**'],
|
|
751
|
+
}
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
### Files not appearing
|
|
755
|
+
|
|
756
|
+
Check ignore patterns aren't too broad:
|
|
757
|
+
|
|
758
|
+
```typescript
|
|
759
|
+
// Bad: ignores everything
|
|
760
|
+
ignore: ['*'];
|
|
761
|
+
|
|
762
|
+
// Good: specific patterns
|
|
763
|
+
ignore: ['node_modules', '*.log'];
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
## Related Packages
|
|
767
|
+
|
|
768
|
+
- `@rljson/db` - RLJSON database
|
|
769
|
+
- `@rljson/bs` - Blob storage interface
|
|
770
|
+
- `@rljson/rljson` - Core RLJSON library
|
|
771
|
+
- `@rljson/io` - I/O abstractions
|
|
772
|
+
|
|
773
|
+
## License
|
|
774
|
+
|
|
775
|
+
See [LICENSE](LICENSE) file.
|
|
776
|
+
|
|
777
|
+
## More Information
|
|
778
|
+
|
|
779
|
+
- [Example Code](src/example.ts)
|
|
780
|
+
- [Architecture](README.architecture.md)
|
|
781
|
+
- [Contributors](README.contributors.md)
|