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 +532 -0
- package/dist/index.d.mts +167 -0
- package/dist/index.d.ts +167 -0
- package/dist/index.js +18 -0
- package/dist/index.mjs +0 -0
- package/package.json +51 -0
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
|
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|