zen-fs-config 0.1.0

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/DESIGN.md ADDED
@@ -0,0 +1,532 @@
1
+ # zen-fs-config — Design Document
2
+
3
+ ## 1. Overview
4
+
5
+ zen-fs-config is a distributed configuration management library built on top of:
6
+ - **ZenFS** (`@zenfs/core`) — Virtual file system with pluggable backends
7
+ - **zen-fs-cache** — Caching layer with ETag/304 revalidation
8
+ - **zen-fs-sync** — Sync engine for mirroring configs across backends
9
+
10
+ It allows multiple application instances (programs) running on different nodes to share configuration through a network of ZenFS backends, with per-app isolation, shared config spaces, node-local config, and conflict safety.
11
+
12
+ ## 2. Architecture
13
+
14
+ ### 2.1 Three-Layer Stack
15
+
16
+ ```
17
+ Application code
18
+ ↓ (reads/writes via standard node:fs API)
19
+ ConfigRepo (this library)
20
+ ├─ zen-fs-cache → CachedFileSystem (ETag/TTL read cache)
21
+ ├─ ZenFS VFS → Context-isolated fs per app (chroot)
22
+ └─ zen-fs-sync → Change detection + conflict resolution
23
+ ├─ Backend X (replica)
24
+ ├─ Backend Y (replica)
25
+ └─ Backend Z (replica)
26
+ ```
27
+
28
+ ### 2.2 No Global Primary
29
+
30
+ Globally, all backends are equal peers — none is the "source of truth."
31
+
32
+ Locally, each program instance has a "primary backend" — the backend it directly reads/writes through. This is a per-instance choice, not a property of the backend itself.
33
+
34
+ ```
35
+ Program A → Primary = Backend X, Replicas = [Y, Z]
36
+ Program B → Primary = Backend Y, Replicas = [X, Z]
37
+ Program C → Primary = Backend Z, Replicas = [X, Y]
38
+ ```
39
+
40
+ ### 2.3 Self-Describing Configuration
41
+
42
+ Backend topology and sync rules are stored **inside** the configuration repository (in `.meta/`), not passed as external parameters. This means any node that can read the config repo can bootstrap the entire sync network.
43
+
44
+ External input at startup is limited to: **which backend to connect to** and optionally **bootstrap data** (if the repo doesn't exist yet).
45
+
46
+ ## 3. File System Structure
47
+
48
+ ```
49
+ /
50
+ ├─ .meta/ [not synced]
51
+ │ ├─ backends.json Backend topology (self-describing)
52
+ │ ├─ sync-rules.json Sync rules
53
+ │ └─ .conflicts/ Conflict archives (safekeeping)
54
+ │ └─ {timestamp}_{path}.from-{a}.to-{b}.json
55
+
56
+ ├─ {appId}/ [one-way: owner → replicas, no conflict]
57
+ │ ├─ db.json
58
+ │ ├─ cache.json
59
+ │ └─ .db.json.version Sidecar version file
60
+
61
+ ├─ shared/ [bi-directional, conflict possible]
62
+ │ ├─ feature-flags.json
63
+ │ ├─ api-version.json
64
+ │ └─ .feature-flags.json.version
65
+
66
+ └─ nodes/ [not synced by default]
67
+ ├─ {nodeId}/
68
+ │ ├─ local.json Node-local config
69
+ │ └─ env.json
70
+ └─ .node-id Current node's ID (auto-generated)
71
+ ```
72
+
73
+ ### 3.1 Directory Semantics
74
+
75
+ | Directory | Sync Direction | Conflict Risk | Purpose |
76
+ |---|---|---|---|
77
+ | `/{appId}/` | One-way (owner → replicas) | None (single writer) | Per-app private config |
78
+ | `/shared/` | Bi-directional | Possible (multiple writers) | Cross-app shared config |
79
+ | `/nodes/` | None (by default) | None | Per-node local config |
80
+ | `/.meta/` | None | None | Topology, rules, conflict archives |
81
+
82
+ ### 3.2 Config-to-File Mapping
83
+
84
+ Each config key maps to one file. The mapping is straightforward:
85
+
86
+ - `setConfig('/db/host', { hostname: 'localhost' })` → writes file `/app-a/db/host.json` with content `{"hostname":"localhost"}`
87
+ - `getConfig('/db/host')` → reads file `/app-a/db/host.json`, parses based on extension
88
+ - Path is relative to the app's root (`/{appId}/`), with `.json` extension appended automatically
89
+ - If path already has an extension (e.g., `/readme.md`), the extension is preserved
90
+
91
+ ### 3.3 Serialization
92
+
93
+ The serializer is determined by file extension:
94
+
95
+ | Extension | Serialize | Deserialize |
96
+ |---|---|---|
97
+ | `.json` (default) | `JSON.stringify` | `JSON.parse` |
98
+ | `.yaml` | YAML dump | YAML parse |
99
+ | `.toml` | TOML dump | TOML parse |
100
+ | `.txt` / no struct extension | `String(data)` | Return as string |
101
+
102
+ Users can inject a custom `ConfigSerializer` for other formats.
103
+
104
+ ## 4. Backend Topology (`.meta/backends.json`)
105
+
106
+ ```json
107
+ {
108
+ "version": 1,
109
+ "backends": [
110
+ {
111
+ "id": "local-idb",
112
+ "type": "IndexedDB",
113
+ "options": { "dbName": "app-config" },
114
+ "description": "Browser local storage"
115
+ },
116
+ {
117
+ "id": "remote-s3",
118
+ "type": "S3Bucket",
119
+ "options": { "bucket": "app-config-bucket", "region": "us-east-1" },
120
+ "description": "Cloud backup"
121
+ }
122
+ ]
123
+ }
124
+ ```
125
+
126
+ Each program instance selects one backend as its primary via `primaryBackendId`. Others become replicas.
127
+
128
+ ## 5. Sync Rules (`.meta/sync-rules.json`)
129
+
130
+ ```json
131
+ {
132
+ "version": 1,
133
+ "rules": [
134
+ {
135
+ "prefix": "/app-a/",
136
+ "direction": "one-way",
137
+ "conflictStrategy": "source-wins",
138
+ "replicas": ["local-idb", "remote-s3"]
139
+ },
140
+ {
141
+ "prefix": "/app-b/",
142
+ "direction": "one-way",
143
+ "conflictStrategy": "source-wins",
144
+ "replicas": ["local-idb", "remote-s3"]
145
+ },
146
+ {
147
+ "prefix": "/shared/",
148
+ "direction": "bi-directional",
149
+ "conflictStrategy": "merge",
150
+ "replicas": ["local-idb", "remote-s3"]
151
+ },
152
+ {
153
+ "prefix": "/nodes/",
154
+ "direction": "none"
155
+ },
156
+ {
157
+ "prefix": "/.meta/",
158
+ "direction": "none"
159
+ }
160
+ ]
161
+ }
162
+ ```
163
+
164
+ - Private app directories (`/{appId}/`): one-way push, no conflict possible
165
+ - Shared directory (`/shared/`): bi-directional, conflict possible, merge strategy
166
+ - Nodes and meta: excluded from sync
167
+
168
+ ## 6. Versioning & Change Detection
169
+
170
+ ### 6.1 Sidecar Version Files
171
+
172
+ Each config file has a companion version file:
173
+
174
+ ```
175
+ /app-a/db.json → Config content
176
+ /app-a/.db.json.version → Version metadata
177
+ ```
178
+
179
+ Version file content:
180
+ ```json
181
+ {
182
+ "version": 5,
183
+ "hash": "sha256:a1b2c3d4...",
184
+ "author": "app-a",
185
+ "timestamp": 1689686400000
186
+ }
187
+ ```
188
+
189
+ ### 6.2 Comparison Logic (extends zen-fs-sync's FileSnapshot)
190
+
191
+ | Condition | Action |
192
+ |---|---|
193
+ | hash same | Skip (content unchanged) |
194
+ | hash different, version different | Higher version wins |
195
+ | hash different, version same | **Conflict** → conflict safety mechanism |
196
+ | version/hash missing | Fall back to mtime+size comparison (backward compat) |
197
+
198
+ ### 6.3 Version Increment
199
+
200
+ On each write:
201
+ 1. Read current version file (if exists)
202
+ 2. Increment version by 1
203
+ 3. Compute SHA-256 hash of new content
204
+ 4. Set author to current instance's `{appId}/{nodeId}`
205
+ 5. Write config file first, then version file
206
+
207
+ Crash recovery: on startup, if hash in version file doesn't match actual file content, auto-increment version and update hash.
208
+
209
+ ## 7. Conflict Safety Mechanism
210
+
211
+ When a conflict is detected (same version, different hash on `/shared/` files):
212
+
213
+ ### 7.1 Archive Both Versions
214
+
215
+ Both conflicting versions are saved to `.meta/.conflicts/` before any resolution:
216
+
217
+ ```
218
+ .meta/.conflicts/1689686400000_shared-feature-flags.from-app-a.to-app-b.json
219
+ ```
220
+
221
+ Archive file content:
222
+ ```json
223
+ {
224
+ "conflictPath": "/shared/feature-flags.json",
225
+ "timestamp": 1689686400000,
226
+ "sourceAuthor": "app-a/server-1",
227
+ "targetAuthor": "app-b/server-2",
228
+ "sourceContent": { "darkMode": true, "newFeature": true },
229
+ "targetContent": { "darkMode": false, "newFeature": false },
230
+ "sourceVersion": 3,
231
+ "targetVersion": 3
232
+ }
233
+ ```
234
+
235
+ ### 7.2 Resolution Strategies
236
+
237
+ After archiving, resolve according to the configured strategy:
238
+
239
+ | Strategy | Behavior |
240
+ |---|---|
241
+ | `source-wins` | Source content overwrites target. Target content archived. |
242
+ | `target-wins` | Target content preserved. Source content archived. |
243
+ | `merge` | JSON deep merge. Both originals archived. Non-JSON falls back to source-wins. |
244
+
245
+ ### 7.3 Event Notification
246
+
247
+ zen-fs-sync emits a `conflict` event with full conflict details. Application can:
248
+ - Accept the auto-resolved result
249
+ - Read `.meta/.conflicts/` archives to manually merge
250
+ - Call `configRepo.resolveConflict(conflictId, mergedContent)` to submit a custom merge
251
+
252
+ **Guarantee**: Neither side's content is ever lost. Recovery is always possible from `.meta/.conflicts/`.
253
+
254
+ ## 8. Node-Local Configuration
255
+
256
+ Some configs are specific to a single node and should not be auto-synced.
257
+
258
+ ### 8.1 Storage
259
+
260
+ Node-local configs live under `/nodes/{nodeId}/`. The `/nodes/` directory is excluded from sync rules (`direction: "none"`).
261
+
262
+ ```
263
+ /nodes/server-1/
264
+ ├─ local.json → { "ip": "10.0.0.1", "cpuCount": 8 }
265
+ └─ env.json → { "NODE_ENV": "production" }
266
+ ```
267
+
268
+ ### 8.2 Node ID Source
269
+
270
+ Priority order:
271
+ 1. Explicit parameter: `createConfigRepo('app-a', { nodeId: 'server-1', ... })`
272
+ 2. Environment variable: `process.env.NODE_ID`
273
+ 3. Auto-generated: random ID written to `/nodes/.node-id` on first startup
274
+
275
+ ### 8.3 API
276
+
277
+ ```typescript
278
+ // Write node-local config (no sync, local only)
279
+ repo.setNodeConfig('server-1', '/local.json', { ip: '10.0.0.1' });
280
+
281
+ // Read node-local config
282
+ const config = repo.getNodeConfig<{ ip: string }>('server-1', '/local.json');
283
+
284
+ // Publish node config to sync backends (one-time, for debugging)
285
+ const result = await repo.publishNodeConfig('server-1');
286
+ // or publish specific files only:
287
+ const result = await repo.publishNodeConfig('server-1', { paths: ['/local.json'] });
288
+
289
+ // Peek at other nodes' published configs (read-only)
290
+ const otherConfig = repo.peekNodeConfig<{ ip: string }>('server-2', '/local.json');
291
+ ```
292
+
293
+ | API | Write Target | Persisted | Synced | Purpose |
294
+ |---|---|---|---|---|
295
+ | `getConfig` / `setConfig` | CachedFS → auto-sync to replicas | Yes | Yes | Normal config |
296
+ | `getNodeConfig` / `setNodeConfig` | CachedFS → no sync | Yes (primary backend only) | No | Node-private config |
297
+ | `publishNodeConfig` | One-time manual sync | Yes | Yes (one-time) | Debug: push to other backends |
298
+ | `peekNodeConfig` | CachedFS read | N/A | N/A | Read other nodes' published config |
299
+
300
+ ## 9. ConfigRepo Interface
301
+
302
+ ```typescript
303
+ interface ConfigRepo {
304
+ /** Application ID (e.g., "app-a") */
305
+ readonly appId: string;
306
+ /** Node ID (e.g., "server-1") */
307
+ readonly nodeId: string;
308
+ /** ZenFS-compatible fs object (node:fs API), context-isolated to own directories */
309
+ readonly fs: typeof import('node:fs');
310
+
311
+ /** Load/reload config from raw string (for initial setup) */
312
+ load(rawConfig: string): Promise<void>;
313
+
314
+ /** Read config value */
315
+ getConfig<T>(path: string): T;
316
+
317
+ /** Write config value (auto-synced) */
318
+ setConfig(path: string, data: any): void;
319
+
320
+ /** Read node-local config */
321
+ getNodeConfig<T>(nodeId: string, path: string): T;
322
+
323
+ /** Write node-local config (no auto-sync) */
324
+ setNodeConfig(nodeId: string, path: string, data: any): void;
325
+
326
+ /** Publish node-local config to sync backends (one-time, for debugging) */
327
+ publishNodeConfig(nodeId: string, options?: {
328
+ paths?: string[];
329
+ }): Promise<SyncResult>;
330
+
331
+ /** Peek at another node's published config (read-only) */
332
+ peekNodeConfig<T>(nodeId: string, path: string): T;
333
+
334
+ /** Manually flush all pending sync */
335
+ flush(): Promise<SyncResult[]>;
336
+
337
+ /** Get sync status for all sync pairs */
338
+ getSyncStatuses(): Map<string, SyncPairStatus>;
339
+
340
+ /** Resolve a conflict with custom merged content */
341
+ resolveConflict(conflictId: string, mergedContent: any): Promise<void>;
342
+
343
+ /** List conflict archives */
344
+ listConflicts(): Promise<ConflictArchive[]>;
345
+
346
+ /** Dispose: stop sync, release cache FS */
347
+ dispose(): Promise<void>;
348
+ }
349
+ ```
350
+
351
+ ## 10. Initialization
352
+
353
+ ```typescript
354
+ import { createConfigRepo } from 'zen-fs-config';
355
+
356
+ const repo = await createConfigRepo('app-a', {
357
+ // The only required external input: which backend to connect to
358
+ primaryBackendId: 'local-idb',
359
+
360
+ // Backend connection info (only for the primary)
361
+ backendInfo: {
362
+ type: 'IndexedDB',
363
+ options: { dbName: 'app-a-config' }
364
+ },
365
+
366
+ // Node ID (optional, see §8.2 for auto-detection)
367
+ nodeId: 'server-1',
368
+
369
+ // Cache configuration (optional, defaults shown)
370
+ cache: {
371
+ storeType: 'MemoryCacheStore',
372
+ ttlMs: 60000
373
+ },
374
+
375
+ // Bootstrap data (only used when .meta/backends.json doesn't exist)
376
+ bootstrap: {
377
+ backends: [
378
+ { id: 'local-idb', type: 'IndexedDB', options: { dbName: 'app-config' } },
379
+ { id: 'remote-s3', type: 'S3Bucket', options: { bucket: 'app-config' } }
380
+ ],
381
+ syncRules: [
382
+ { prefix: '/app-a/', direction: 'one-way', conflictStrategy: 'source-wins', replicas: ['local-idb', 'remote-s3'] },
383
+ { prefix: '/shared/', direction: 'bi-directional', conflictStrategy: 'merge', replicas: ['local-idb', 'remote-s3'] },
384
+ { prefix: '/nodes/', direction: 'none' },
385
+ { prefix: '/.meta/', direction: 'none' }
386
+ ]
387
+ }
388
+ });
389
+
390
+ // Normal config operations
391
+ repo.setConfig('/db/host', { hostname: 'localhost', port: 3306 });
392
+ const dbConfig = repo.getConfig<{ hostname: string; port: number }>('/db/host');
393
+
394
+ // Node-local config
395
+ repo.setNodeConfig('server-1', '/local.json', { ip: '10.0.0.1' });
396
+
397
+ // Publish for debugging (one-time sync)
398
+ await repo.publishNodeConfig('server-1');
399
+
400
+ // Cleanup
401
+ await repo.dispose();
402
+ ```
403
+
404
+ ## 11. Initialization Flow
405
+
406
+ ```
407
+ createConfigRepo('app-a', { primaryBackendId: 'X', backendInfo: {...}, bootstrap: {...} })
408
+
409
+ ├─ 1. Connect to primary backend (backendId = 'X')
410
+
411
+ ├─ 2. Wrap with zen-fs-cache → CachedFileSystem(primaryBackend, cacheStore, { ttlMs })
412
+
413
+ ├─ 3. Configure ZenFS VFS: { '/': cachedFS }
414
+
415
+ ├─ 4. Read .meta/backends.json
416
+ │ ├─ Exists → parse topology, create replica backend instances
417
+ │ └─ Not exists → write bootstrap data to .meta/
418
+
419
+ ├─ 5. Read .meta/sync-rules.json
420
+ │ ├─ Exists → parse rules
421
+ │ └─ Not exists → use bootstrap syncRules
422
+
423
+ ├─ 6. For each rule with direction != 'none':
424
+ │ ├─ Create SyncPair(
425
+ │ │ source: cachedFS,
426
+ │ │ target: replicaBackend,
427
+ │ │ { direction, conflictStrategy, filter: { includePrefixes: [rule.prefix] } }
428
+ │ │ )
429
+ │ └─ syncEngine.watch(pairId)
430
+
431
+ ├─ 7. Determine nodeId (explicit > env > auto-generated .node-id file)
432
+
433
+ ├─ 8. Create ZenFS Context:
434
+ │ ├─ Allowed paths: /{appId}/, /shared/, /nodes/{nodeId}/, /.meta/
435
+ │ ├─ Root chroot: /{appId}/ (for getConfig/setConfig)
436
+ │ └─ Full access for zen-fs-sync (unrestricted)
437
+
438
+ └─ 9. Return ConfigRepo { fs, appId, nodeId, ... }
439
+ ```
440
+
441
+ ## 12. Data Flow
442
+
443
+ ### Read Path
444
+ ```
445
+ Application
446
+ → repo.fs.readFileSync('/db/host.json')
447
+ → ZenFS Context (chroot to /app-a/)
448
+ → CachedFileSystem.readFile('/app-a/db/host.json')
449
+ → Cache hit (TTL)? → return cached bytes (0 network)
450
+ → Cache miss/expired? → 304 revalidate with primary backend
451
+ → Deserialize (JSON.parse for .json files)
452
+ → Return typed object
453
+ ```
454
+
455
+ ### Write Path (auto-synced)
456
+ ```
457
+ Application
458
+ → repo.setConfig('/db/host', { hostname: 'localhost' })
459
+ → Serialize (JSON.stringify)
460
+ → Write config file: /app-a/db/host.json
461
+ → Write version file: /app-a/.db.host.json.version (version++, new hash)
462
+ → CachedFileSystem.writeFile() →穿透 to primary backend → invalidate cache
463
+ → zen-fs-sync watch detects change (poll + debounce)
464
+ → Sync to replicas per sync-rules
465
+ ```
466
+
467
+ ### Write Path (node-local, no sync)
468
+ ```
469
+ Application
470
+ → repo.setNodeConfig('server-1', '/local.json', { ip: '10.0.0.1' })
471
+ → Serialize + write to /nodes/server-1/local.json
472
+ → zen-fs-sync ignores /nodes/ (direction: "none")
473
+ → File stays local to primary backend only
474
+ ```
475
+
476
+ ### Publish (one-time sync)
477
+ ```
478
+ Application
479
+ → repo.publishNodeConfig('server-1')
480
+ → Read /nodes/server-1/**/*
481
+ → Create temporary SyncPair with filter: includePrefixes: ['/nodes/server-1/']
482
+ → Execute one sync() call
483
+ → Files pushed to replicas
484
+ → Dispose temporary SyncPair
485
+ ```
486
+
487
+ ## 13. Peer Dependencies
488
+
489
+ | Package | Role | Version |
490
+ |---|---|---|
491
+ | `@zenfs/core` | Virtual file system, backends, VFS, Context | >=2.3.0 |
492
+ | `zen-fs-cache` | Read caching with ETag/304 revalidation | >=1.0.0 |
493
+ | `zen-fs-sync` | Cross-backend sync engine | >=0.1.0 |
494
+
495
+ ## 14. Extension Points
496
+
497
+ ### Custom Serializer
498
+ ```typescript
499
+ import { createConfigRepo, type ConfigSerializer } from 'zen-fs-config';
500
+
501
+ const yamlSerializer: ConfigSerializer = {
502
+ serialize(data: unknown): Uint8Array { ... },
503
+ deserialize(raw: Uint8Array, path: string): unknown { ... },
504
+ canHandle(path: string): boolean { return path.endsWith('.yaml'); }
505
+ };
506
+ ```
507
+
508
+ ### Custom Conflict Resolver
509
+ ```typescript
510
+ const repo = await createConfigRepo('app-a', {
511
+ ...
512
+ onConflict: async (conflict) => {
513
+ // Custom conflict resolution logic
514
+ // Return merged content, or null to use default strategy
515
+ return customMerge(conflict.sourceContent, conflict.targetContent);
516
+ }
517
+ });
518
+ ```
519
+
520
+ ### Custom Backend Registry
521
+ ```typescript
522
+ import { registerBackend } from 'zen-fs-config';
523
+
524
+ registerBackend('CustomStore', async (options) => {
525
+ const { CustomStoreFS } = await import('custom-store-fs');
526
+ return new CustomStoreFS(options);
527
+ });
528
+ ```
529
+
530
+ ## 15. License
531
+
532
+ MIT
@@ -0,0 +1,167 @@
1
+ import * as node_fs from 'node:fs';
2
+ import { ConflictStrategy, SyncResult, SyncPairStatus } from 'zen-fs-sync';
3
+ export { SyncPairStatus, SyncResult } from 'zen-fs-sync';
4
+
5
+ /** A single backend in the topology. */
6
+ interface BackendDescriptor {
7
+ /** Unique identifier within this config repo (e.g., "local-idb"). */
8
+ id: string;
9
+ /** Backend type name, resolved via the backend registry. */
10
+ type: string;
11
+ /** Options passed to the backend constructor. */
12
+ options: Record<string, unknown>;
13
+ /** Human-readable description. */
14
+ description?: string;
15
+ }
16
+ /** Content of `.meta/backends.json`. */
17
+ interface BackendsMeta {
18
+ version: 1;
19
+ backends: BackendDescriptor[];
20
+ }
21
+ /** Sync direction for a path prefix. */
22
+ type SyncDirection = 'one-way' | 'bi-directional' | 'none';
23
+ /** A single sync rule. */
24
+ interface SyncRule {
25
+ /** Path prefix this rule applies to (e.g., "/app-a/"). */
26
+ prefix: string;
27
+ /** Sync direction. */
28
+ direction: SyncDirection;
29
+ /** Conflict resolution strategy (only relevant for bi-directional). */
30
+ conflictStrategy?: ConflictStrategy;
31
+ /** IDs of replica backends to sync with (from .meta/backends.json). */
32
+ replicas?: string[];
33
+ }
34
+ /** Content of `.meta/sync-rules.json`. */
35
+ interface SyncRulesMeta {
36
+ version: 1;
37
+ rules: SyncRule[];
38
+ }
39
+ /** Content of a sidecar `.version` file. */
40
+ interface VersionMeta {
41
+ /** Monotonically increasing version number. */
42
+ version: number;
43
+ /** SHA-256 hash of the corresponding config file content. */
44
+ hash: string;
45
+ /** Author identifier (e.g., "app-a/server-1"). */
46
+ author: string;
47
+ /** Timestamp when the version was created. */
48
+ timestamp: number;
49
+ }
50
+ /** Content of a conflict archive file in `.meta/.conflicts/`. */
51
+ interface ConflictArchive {
52
+ /** The config file path that conflicted. */
53
+ conflictPath: string;
54
+ /** Timestamp of the conflict. */
55
+ timestamp: number;
56
+ /** Author of the source side. */
57
+ sourceAuthor: string;
58
+ /** Author of the target side. */
59
+ targetAuthor: string;
60
+ /** Source side content. */
61
+ sourceContent: unknown;
62
+ /** Target side content. */
63
+ targetContent: unknown;
64
+ /** Source side version. */
65
+ sourceVersion: number;
66
+ /** Target side version. */
67
+ targetVersion: number;
68
+ /** Strategy that was used to auto-resolve (if any). */
69
+ resolvedStrategy?: ConflictStrategy;
70
+ /** The content that was written as the resolved result (if auto-resolved). */
71
+ resolvedContent?: unknown;
72
+ }
73
+ /** Information passed to conflict event handlers. */
74
+ interface ConflictInfo {
75
+ /** Unique conflict ID (derived from archive filename). */
76
+ conflictId: string;
77
+ /** The config file path that conflicted. */
78
+ path: string;
79
+ /** Source side author. */
80
+ sourceAuthor: string;
81
+ /** Target side author. */
82
+ targetAuthor: string;
83
+ /** Source content. */
84
+ sourceContent: unknown;
85
+ /** Target content. */
86
+ targetContent: unknown;
87
+ }
88
+ /** Pluggable serializer for config files. */
89
+ interface ConfigSerializer {
90
+ /** Serialize a value to bytes. */
91
+ serialize(data: unknown): Uint8Array;
92
+ /** Deserialize bytes to a value. */
93
+ deserialize(raw: Uint8Array, path: string): unknown;
94
+ /** Check if this serializer can handle the given file path. */
95
+ canHandle(path: string): boolean;
96
+ }
97
+ /** Cache configuration. */
98
+ interface CacheOptions {
99
+ /** Type of cache store. */
100
+ storeType?: 'MemoryCacheStore' | 'IdbCacheStore';
101
+ /** Cache store prefix (for IdbCacheStore). */
102
+ storePrefix?: string;
103
+ /** TTL in milliseconds for cache hits without revalidation. Default: 0 (always revalidate). */
104
+ ttlMs?: number;
105
+ }
106
+ /** Bootstrap data, written to .meta/ only on first initialization. */
107
+ interface BootstrapData {
108
+ backends: Omit<BackendDescriptor, 'description'>[];
109
+ syncRules: SyncRule[];
110
+ }
111
+ /** Options for creating a ConfigRepo. */
112
+ interface ConfigRepoOptions {
113
+ /** The backend ID (from .meta/backends.json) to use as this instance's primary. */
114
+ primaryBackendId: string;
115
+ /** Connection info for the primary backend. */
116
+ backendInfo: {
117
+ type: string;
118
+ options: Record<string, unknown>;
119
+ };
120
+ /** Node identifier. Auto-detected if not provided (see DESIGN.md §8.2). */
121
+ nodeId?: string;
122
+ /** Cache configuration. */
123
+ cache?: CacheOptions;
124
+ /** Bootstrap data (only used when .meta/backends.json doesn't exist). */
125
+ bootstrap?: BootstrapData;
126
+ /** Custom serializer. */
127
+ serializer?: ConfigSerializer;
128
+ /** Custom conflict handler. Called before auto-resolution. */
129
+ onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>;
130
+ }
131
+ /** The main configuration repository interface. */
132
+ interface IConfigRepo {
133
+ /** Application ID. */
134
+ readonly appId: string;
135
+ /** Node ID. */
136
+ readonly nodeId: string;
137
+ /** ZenFS-compatible fs object, context-isolated to this app's directories. */
138
+ readonly fs: typeof node_fs;
139
+ /** Load or reload configuration from a raw string. */
140
+ load(rawConfig: string): Promise<void>;
141
+ /** Read a config value. */
142
+ getConfig<T = unknown>(path: string): T;
143
+ /** Write a config value (auto-synced). */
144
+ setConfig(path: string, data: unknown): void;
145
+ /** Read node-local config. */
146
+ getNodeConfig<T = unknown>(nodeId: string, path: string): T;
147
+ /** Write node-local config (no auto-sync). */
148
+ setNodeConfig(nodeId: string, path: string, data: unknown): void;
149
+ /** Publish node-local config to sync backends (one-time, for debugging). */
150
+ publishNodeConfig(nodeId: string, options?: {
151
+ paths?: string[];
152
+ }): Promise<SyncResult>;
153
+ /** Peek at another node's published config (read-only). */
154
+ peekNodeConfig<T = unknown>(nodeId: string, path: string): T;
155
+ /** Manually flush all pending sync operations. */
156
+ flush(): Promise<SyncResult[]>;
157
+ /** Get sync status for all registered sync pairs. */
158
+ getSyncStatuses(): Map<string, SyncPairStatus>;
159
+ /** Resolve a conflict with custom merged content. */
160
+ resolveConflict(conflictId: string, mergedContent: unknown): Promise<void>;
161
+ /** List all conflict archives. */
162
+ listConflicts(): Promise<ConflictArchive[]>;
163
+ /** Dispose: stop all sync, release cache FS and resources. */
164
+ dispose(): Promise<void>;
165
+ }
166
+
167
+ export type { BackendDescriptor, BackendsMeta, BootstrapData, CacheOptions, ConfigRepoOptions, ConfigSerializer, ConflictArchive, ConflictInfo, IConfigRepo, SyncRule, SyncRulesMeta, VersionMeta };
@@ -0,0 +1,167 @@
1
+ import * as node_fs from 'node:fs';
2
+ import { ConflictStrategy, SyncResult, SyncPairStatus } from 'zen-fs-sync';
3
+ export { SyncPairStatus, SyncResult } from 'zen-fs-sync';
4
+
5
+ /** A single backend in the topology. */
6
+ interface BackendDescriptor {
7
+ /** Unique identifier within this config repo (e.g., "local-idb"). */
8
+ id: string;
9
+ /** Backend type name, resolved via the backend registry. */
10
+ type: string;
11
+ /** Options passed to the backend constructor. */
12
+ options: Record<string, unknown>;
13
+ /** Human-readable description. */
14
+ description?: string;
15
+ }
16
+ /** Content of `.meta/backends.json`. */
17
+ interface BackendsMeta {
18
+ version: 1;
19
+ backends: BackendDescriptor[];
20
+ }
21
+ /** Sync direction for a path prefix. */
22
+ type SyncDirection = 'one-way' | 'bi-directional' | 'none';
23
+ /** A single sync rule. */
24
+ interface SyncRule {
25
+ /** Path prefix this rule applies to (e.g., "/app-a/"). */
26
+ prefix: string;
27
+ /** Sync direction. */
28
+ direction: SyncDirection;
29
+ /** Conflict resolution strategy (only relevant for bi-directional). */
30
+ conflictStrategy?: ConflictStrategy;
31
+ /** IDs of replica backends to sync with (from .meta/backends.json). */
32
+ replicas?: string[];
33
+ }
34
+ /** Content of `.meta/sync-rules.json`. */
35
+ interface SyncRulesMeta {
36
+ version: 1;
37
+ rules: SyncRule[];
38
+ }
39
+ /** Content of a sidecar `.version` file. */
40
+ interface VersionMeta {
41
+ /** Monotonically increasing version number. */
42
+ version: number;
43
+ /** SHA-256 hash of the corresponding config file content. */
44
+ hash: string;
45
+ /** Author identifier (e.g., "app-a/server-1"). */
46
+ author: string;
47
+ /** Timestamp when the version was created. */
48
+ timestamp: number;
49
+ }
50
+ /** Content of a conflict archive file in `.meta/.conflicts/`. */
51
+ interface ConflictArchive {
52
+ /** The config file path that conflicted. */
53
+ conflictPath: string;
54
+ /** Timestamp of the conflict. */
55
+ timestamp: number;
56
+ /** Author of the source side. */
57
+ sourceAuthor: string;
58
+ /** Author of the target side. */
59
+ targetAuthor: string;
60
+ /** Source side content. */
61
+ sourceContent: unknown;
62
+ /** Target side content. */
63
+ targetContent: unknown;
64
+ /** Source side version. */
65
+ sourceVersion: number;
66
+ /** Target side version. */
67
+ targetVersion: number;
68
+ /** Strategy that was used to auto-resolve (if any). */
69
+ resolvedStrategy?: ConflictStrategy;
70
+ /** The content that was written as the resolved result (if auto-resolved). */
71
+ resolvedContent?: unknown;
72
+ }
73
+ /** Information passed to conflict event handlers. */
74
+ interface ConflictInfo {
75
+ /** Unique conflict ID (derived from archive filename). */
76
+ conflictId: string;
77
+ /** The config file path that conflicted. */
78
+ path: string;
79
+ /** Source side author. */
80
+ sourceAuthor: string;
81
+ /** Target side author. */
82
+ targetAuthor: string;
83
+ /** Source content. */
84
+ sourceContent: unknown;
85
+ /** Target content. */
86
+ targetContent: unknown;
87
+ }
88
+ /** Pluggable serializer for config files. */
89
+ interface ConfigSerializer {
90
+ /** Serialize a value to bytes. */
91
+ serialize(data: unknown): Uint8Array;
92
+ /** Deserialize bytes to a value. */
93
+ deserialize(raw: Uint8Array, path: string): unknown;
94
+ /** Check if this serializer can handle the given file path. */
95
+ canHandle(path: string): boolean;
96
+ }
97
+ /** Cache configuration. */
98
+ interface CacheOptions {
99
+ /** Type of cache store. */
100
+ storeType?: 'MemoryCacheStore' | 'IdbCacheStore';
101
+ /** Cache store prefix (for IdbCacheStore). */
102
+ storePrefix?: string;
103
+ /** TTL in milliseconds for cache hits without revalidation. Default: 0 (always revalidate). */
104
+ ttlMs?: number;
105
+ }
106
+ /** Bootstrap data, written to .meta/ only on first initialization. */
107
+ interface BootstrapData {
108
+ backends: Omit<BackendDescriptor, 'description'>[];
109
+ syncRules: SyncRule[];
110
+ }
111
+ /** Options for creating a ConfigRepo. */
112
+ interface ConfigRepoOptions {
113
+ /** The backend ID (from .meta/backends.json) to use as this instance's primary. */
114
+ primaryBackendId: string;
115
+ /** Connection info for the primary backend. */
116
+ backendInfo: {
117
+ type: string;
118
+ options: Record<string, unknown>;
119
+ };
120
+ /** Node identifier. Auto-detected if not provided (see DESIGN.md §8.2). */
121
+ nodeId?: string;
122
+ /** Cache configuration. */
123
+ cache?: CacheOptions;
124
+ /** Bootstrap data (only used when .meta/backends.json doesn't exist). */
125
+ bootstrap?: BootstrapData;
126
+ /** Custom serializer. */
127
+ serializer?: ConfigSerializer;
128
+ /** Custom conflict handler. Called before auto-resolution. */
129
+ onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>;
130
+ }
131
+ /** The main configuration repository interface. */
132
+ interface IConfigRepo {
133
+ /** Application ID. */
134
+ readonly appId: string;
135
+ /** Node ID. */
136
+ readonly nodeId: string;
137
+ /** ZenFS-compatible fs object, context-isolated to this app's directories. */
138
+ readonly fs: typeof node_fs;
139
+ /** Load or reload configuration from a raw string. */
140
+ load(rawConfig: string): Promise<void>;
141
+ /** Read a config value. */
142
+ getConfig<T = unknown>(path: string): T;
143
+ /** Write a config value (auto-synced). */
144
+ setConfig(path: string, data: unknown): void;
145
+ /** Read node-local config. */
146
+ getNodeConfig<T = unknown>(nodeId: string, path: string): T;
147
+ /** Write node-local config (no auto-sync). */
148
+ setNodeConfig(nodeId: string, path: string, data: unknown): void;
149
+ /** Publish node-local config to sync backends (one-time, for debugging). */
150
+ publishNodeConfig(nodeId: string, options?: {
151
+ paths?: string[];
152
+ }): Promise<SyncResult>;
153
+ /** Peek at another node's published config (read-only). */
154
+ peekNodeConfig<T = unknown>(nodeId: string, path: string): T;
155
+ /** Manually flush all pending sync operations. */
156
+ flush(): Promise<SyncResult[]>;
157
+ /** Get sync status for all registered sync pairs. */
158
+ getSyncStatuses(): Map<string, SyncPairStatus>;
159
+ /** Resolve a conflict with custom merged content. */
160
+ resolveConflict(conflictId: string, mergedContent: unknown): Promise<void>;
161
+ /** List all conflict archives. */
162
+ listConflicts(): Promise<ConflictArchive[]>;
163
+ /** Dispose: stop all sync, release cache FS and resources. */
164
+ dispose(): Promise<void>;
165
+ }
166
+
167
+ export type { BackendDescriptor, BackendsMeta, BootstrapData, CacheOptions, ConfigRepoOptions, ConfigSerializer, ConflictArchive, ConflictInfo, IConfigRepo, SyncRule, SyncRulesMeta, VersionMeta };
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __copyProps = (to, from, except, desc) => {
7
+ if (from && typeof from === "object" || typeof from === "function") {
8
+ for (let key of __getOwnPropNames(from))
9
+ if (!__hasOwnProp.call(to, key) && key !== except)
10
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
11
+ }
12
+ return to;
13
+ };
14
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
15
+
16
+ // src/index.ts
17
+ var index_exports = {};
18
+ module.exports = __toCommonJS(index_exports);
package/dist/index.mjs ADDED
File without changes
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "zen-fs-config",
3
+ "version": "0.1.0",
4
+ "description": "Distributed config management library built on ZenFS, zen-fs-cache, and zen-fs-sync",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "DESIGN.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup src/index.ts --format cjs,esm --dts",
21
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
22
+ "clean": "rm -rf dist",
23
+ "typecheck": "tsc --noEmit",
24
+ "prepublishOnly": "npm run clean && npm run build"
25
+ },
26
+ "keywords": [
27
+ "zenfs",
28
+ "config",
29
+ "distributed",
30
+ "sync",
31
+ "filesystem",
32
+ "configuration-management"
33
+ ],
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/weijia/zen-fs-config.git"
38
+ },
39
+ "peerDependencies": {
40
+ "@zenfs/core": ">=2.3.0",
41
+ "zen-fs-cache": ">=1.0.0",
42
+ "zen-fs-sync": ">=0.1.0"
43
+ },
44
+ "devDependencies": {
45
+ "@zenfs/core": "^2.5.7",
46
+ "tsup": "^8.5.1",
47
+ "typescript": "^5.9.3",
48
+ "zen-fs-cache": "^1.0.1",
49
+ "zen-fs-sync": "^0.1.0"
50
+ }
51
+ }