zen-fs-config 0.1.1 → 0.1.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/README.md +129 -0
- package/dist/index.d.mts +2 -19
- package/dist/index.d.ts +2 -19
- package/dist/index.js +50 -7
- package/dist/index.mjs +50 -7
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# zen-fs-config
|
|
2
|
+
|
|
3
|
+
基于 ZenFS 的分布式配置管理库。多个应用实例通过任意 ZenFS 后端共享配置,支持应用隔离、共享空间、节点本地配置和冲突安全。
|
|
4
|
+
|
|
5
|
+
**GitHub**: https://github.com/weijia/zen-fs-config
|
|
6
|
+
**NPM**: `zen-fs-config`
|
|
7
|
+
**设计文档**: [DESIGN.md](./DESIGN.md)
|
|
8
|
+
|
|
9
|
+
## 安装
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install zen-fs-config @zenfs/core zen-fs-cache zen-fs-sync
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 快速开始
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { createConfigRepo, registerBackend } from 'zen-fs-config';
|
|
19
|
+
import { InMemory } from '@zenfs/core';
|
|
20
|
+
|
|
21
|
+
// 1. 注册自定义后端(可选,InMemory 已内置)
|
|
22
|
+
registerBackend('InMemory', async (options) => {
|
|
23
|
+
return InMemory.create({ maxSize: options.maxSize as number ?? 100 * 1024 * 1024 });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// 2. 创建配置仓库
|
|
27
|
+
const repo = await createConfigRepo('my-app', {
|
|
28
|
+
primaryBackendId: 'local-memory',
|
|
29
|
+
backendInfo: {
|
|
30
|
+
type: 'InMemory',
|
|
31
|
+
options: { label: 'my-app-config' },
|
|
32
|
+
},
|
|
33
|
+
cache: { storeType: 'MemoryCacheStore', ttlMs: 60_000 },
|
|
34
|
+
bootstrap: {
|
|
35
|
+
backends: [
|
|
36
|
+
{ id: 'local-memory', type: 'InMemory', options: { label: 'primary' } },
|
|
37
|
+
],
|
|
38
|
+
syncRules: [
|
|
39
|
+
{ prefix: '/my-app/', direction: 'one-way', conflictStrategy: 'source-wins', replicas: ['local-memory'] },
|
|
40
|
+
{ prefix: '/shared/', direction: 'bi-directional', conflictStrategy: 'merge', replicas: ['local-memory'] },
|
|
41
|
+
{ prefix: '/nodes/', direction: 'none' },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// 3. 读写配置(同步 API,从内存缓存读取)
|
|
47
|
+
repo.setConfig('/database', { host: 'localhost', port: 5432 });
|
|
48
|
+
const db = repo.getConfig<{ host: string; port: number }>('/database');
|
|
49
|
+
|
|
50
|
+
// 4. 节点本地配置(异步 API,不自动同步)
|
|
51
|
+
await repo.setNodeConfig('node-1', '/debug', { level: 'verbose' });
|
|
52
|
+
const debug = await repo.getNodeConfig('node-1', '/debug');
|
|
53
|
+
|
|
54
|
+
// 5. 发布节点配置到同步后端(用于调试)
|
|
55
|
+
await repo.publishNodeConfig('node-1');
|
|
56
|
+
|
|
57
|
+
// 6. 清理
|
|
58
|
+
await repo.dispose();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 目录结构
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
/
|
|
65
|
+
├── {appId}/ # 应用私有配置(单向同步,每个应用只能读写自己的)
|
|
66
|
+
├── shared/ # 跨应用共享配置(双向同步,merge 冲突策略)
|
|
67
|
+
├── nodes/{nodeId}/ # 节点本地配置(不同步,除非手动 publish)
|
|
68
|
+
└── .meta/
|
|
69
|
+
├── backends.json # 后端拓扑(自描述,任意后端可引导)
|
|
70
|
+
├── sync-rules.json
|
|
71
|
+
└── .conflicts/ # 冲突归档(双方内容都保存,永不丢失)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
每个配置文件有 sidecar 版本文件:`db.json` → `.db.json.version`(版本号 + SHA-256 哈希)。
|
|
75
|
+
|
|
76
|
+
## 核心 API
|
|
77
|
+
|
|
78
|
+
| 方法 | 说明 |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `getConfig<T>(path)` | 同步读取应用配置(从内存缓存) |
|
|
81
|
+
| `setConfig(path, data)` | 同步写入应用配置(异步持久化 + 自动同步) |
|
|
82
|
+
| `getNodeConfig<T>(nodeId, path)` | 异步读取节点本地配置 |
|
|
83
|
+
| `setNodeConfig(nodeId, path, data)` | 异步写入节点本地配置(不同步) |
|
|
84
|
+
| `publishNodeConfig(nodeId)` | 将节点配置一次性同步到所有后端 |
|
|
85
|
+
| `peekNodeConfig<T>(nodeId, path)` | 只读查看其他节点的已发布配置 |
|
|
86
|
+
| `flush()` | 手动触发所有同步 |
|
|
87
|
+
| `listConflicts()` | 列出所有冲突归档 |
|
|
88
|
+
| `resolveConflict(id, merged)` | 用合并内容解决冲突 |
|
|
89
|
+
| `fs.promises.*` | 标准 fs API,chroot 隔离到 `/{appId}/` 和 `/shared/` |
|
|
90
|
+
| `dispose()` | 停止同步、释放资源 |
|
|
91
|
+
|
|
92
|
+
## 后端注册
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { registerBackend } from 'zen-fs-config';
|
|
96
|
+
|
|
97
|
+
// 注册 S3 后端
|
|
98
|
+
registerBackend('S3Bucket', async (options) => {
|
|
99
|
+
const { S3Bucket } = await import('@zenfs/core');
|
|
100
|
+
return S3Bucket.create(options);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// 注册自定义后端
|
|
104
|
+
registerBackend('my-custom', async (options) => {
|
|
105
|
+
return {
|
|
106
|
+
readFile: (path) => { /* ... */ },
|
|
107
|
+
writeFile: (path, data) => { /* ... */ },
|
|
108
|
+
readdir: (path) => { /* ... */ },
|
|
109
|
+
stat: (path) => { /* ... */ },
|
|
110
|
+
exists: (path) => { /* ... */ },
|
|
111
|
+
mkdir: (path) => { /* ... */ },
|
|
112
|
+
unlink: (path) => { /* ... */ },
|
|
113
|
+
rmdir: (path) => { /* ... */ },
|
|
114
|
+
rename: (old, newPath) => { /* ... */ },
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## 依赖
|
|
120
|
+
|
|
121
|
+
| 包 | 说明 |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `@zenfs/core >=2.3.0` | ZenFS 虚拟文件系统 |
|
|
124
|
+
| `zen-fs-cache >=1.0.0` | ETag/TTL 缓存层 |
|
|
125
|
+
| `zen-fs-sync >=0.1.0` | 跨后端同步引擎 |
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT
|
package/dist/index.d.mts
CHANGED
|
@@ -267,15 +267,10 @@ declare function createConfigRepo(appId: string, options: ConfigRepoOptions): Pr
|
|
|
267
267
|
* Users can register custom backends via `registerBackend()`.
|
|
268
268
|
*/
|
|
269
269
|
|
|
270
|
-
/**
|
|
271
|
-
* A factory function that creates a file system instance from options.
|
|
272
|
-
* The returned value must satisfy SyncableFS (and ideally CacheableFileSystem).
|
|
273
|
-
*/
|
|
274
270
|
type BackendFactory = (options: Record<string, unknown>) => Promise<BackendInstance>;
|
|
275
271
|
/**
|
|
276
272
|
* The minimal interface a backend instance must satisfy.
|
|
277
|
-
*
|
|
278
|
-
* needed by CachedFileSystem.
|
|
273
|
+
* Matches zen-fs-cache's CacheableFileSystem requirements.
|
|
279
274
|
*/
|
|
280
275
|
interface BackendInstance {
|
|
281
276
|
readFile(path: string, ...args: any[]): Promise<any>;
|
|
@@ -285,26 +280,14 @@ interface BackendInstance {
|
|
|
285
280
|
exists(path: string): Promise<boolean>;
|
|
286
281
|
mkdir(path: string, options?: any): Promise<any>;
|
|
287
282
|
unlink(path: string): Promise<void>;
|
|
288
|
-
rmdir
|
|
283
|
+
rmdir(path: string): Promise<void>;
|
|
289
284
|
rename?(oldPath: string, newPath: string): Promise<void>;
|
|
290
285
|
readFileMeta?(path: string, opts?: any): Promise<any>;
|
|
291
286
|
getRevision?(path: string): Promise<string | number | undefined>;
|
|
292
287
|
}
|
|
293
|
-
/**
|
|
294
|
-
* Register a backend factory by type name.
|
|
295
|
-
*/
|
|
296
288
|
declare function registerBackend(type: string, factory: BackendFactory): void;
|
|
297
|
-
/**
|
|
298
|
-
* Create a backend instance from a descriptor.
|
|
299
|
-
*/
|
|
300
289
|
declare function createBackend(descriptor: Pick<BackendDescriptor, 'type' | 'options'>): Promise<BackendInstance>;
|
|
301
|
-
/**
|
|
302
|
-
* Check if a backend type is registered.
|
|
303
|
-
*/
|
|
304
290
|
declare function hasBackend(type: string): boolean;
|
|
305
|
-
/**
|
|
306
|
-
* List all registered backend type names.
|
|
307
|
-
*/
|
|
308
291
|
declare function listBackends(): string[];
|
|
309
292
|
|
|
310
293
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -267,15 +267,10 @@ declare function createConfigRepo(appId: string, options: ConfigRepoOptions): Pr
|
|
|
267
267
|
* Users can register custom backends via `registerBackend()`.
|
|
268
268
|
*/
|
|
269
269
|
|
|
270
|
-
/**
|
|
271
|
-
* A factory function that creates a file system instance from options.
|
|
272
|
-
* The returned value must satisfy SyncableFS (and ideally CacheableFileSystem).
|
|
273
|
-
*/
|
|
274
270
|
type BackendFactory = (options: Record<string, unknown>) => Promise<BackendInstance>;
|
|
275
271
|
/**
|
|
276
272
|
* The minimal interface a backend instance must satisfy.
|
|
277
|
-
*
|
|
278
|
-
* needed by CachedFileSystem.
|
|
273
|
+
* Matches zen-fs-cache's CacheableFileSystem requirements.
|
|
279
274
|
*/
|
|
280
275
|
interface BackendInstance {
|
|
281
276
|
readFile(path: string, ...args: any[]): Promise<any>;
|
|
@@ -285,26 +280,14 @@ interface BackendInstance {
|
|
|
285
280
|
exists(path: string): Promise<boolean>;
|
|
286
281
|
mkdir(path: string, options?: any): Promise<any>;
|
|
287
282
|
unlink(path: string): Promise<void>;
|
|
288
|
-
rmdir
|
|
283
|
+
rmdir(path: string): Promise<void>;
|
|
289
284
|
rename?(oldPath: string, newPath: string): Promise<void>;
|
|
290
285
|
readFileMeta?(path: string, opts?: any): Promise<any>;
|
|
291
286
|
getRevision?(path: string): Promise<string | number | undefined>;
|
|
292
287
|
}
|
|
293
|
-
/**
|
|
294
|
-
* Register a backend factory by type name.
|
|
295
|
-
*/
|
|
296
288
|
declare function registerBackend(type: string, factory: BackendFactory): void;
|
|
297
|
-
/**
|
|
298
|
-
* Create a backend instance from a descriptor.
|
|
299
|
-
*/
|
|
300
289
|
declare function createBackend(descriptor: Pick<BackendDescriptor, 'type' | 'options'>): Promise<BackendInstance>;
|
|
301
|
-
/**
|
|
302
|
-
* Check if a backend type is registered.
|
|
303
|
-
*/
|
|
304
290
|
declare function hasBackend(type: string): boolean;
|
|
305
|
-
/**
|
|
306
|
-
* List all registered backend type names.
|
|
307
|
-
*/
|
|
308
291
|
declare function listBackends(): string[];
|
|
309
292
|
|
|
310
293
|
/**
|
package/dist/index.js
CHANGED
|
@@ -308,12 +308,7 @@ async function createBackend(descriptor) {
|
|
|
308
308
|
`Unknown backend type: "${descriptor.type}". Available types: ${Array.from(registry.keys()).join(", ")}. Use registerBackend() to register a custom backend.`
|
|
309
309
|
);
|
|
310
310
|
}
|
|
311
|
-
|
|
312
|
-
if (!instance.rmdir) {
|
|
313
|
-
instance.rmdir = async (_path) => {
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
return instance;
|
|
311
|
+
return factory(descriptor.options);
|
|
317
312
|
}
|
|
318
313
|
function hasBackend(type) {
|
|
319
314
|
return registry.has(type);
|
|
@@ -321,11 +316,59 @@ function hasBackend(type) {
|
|
|
321
316
|
function listBackends() {
|
|
322
317
|
return Array.from(registry.keys());
|
|
323
318
|
}
|
|
319
|
+
function syncToAsync(backend) {
|
|
320
|
+
return {
|
|
321
|
+
readFile(path, ...args) {
|
|
322
|
+
const result = backend.readFile(path, ...args);
|
|
323
|
+
return Promise.resolve(result);
|
|
324
|
+
},
|
|
325
|
+
writeFile(path, data, options) {
|
|
326
|
+
backend.writeFile(path, data, options);
|
|
327
|
+
return Promise.resolve();
|
|
328
|
+
},
|
|
329
|
+
readdir(path) {
|
|
330
|
+
const entries = backend.readdir(path);
|
|
331
|
+
return Promise.resolve(entries.map((e) => typeof e === "string" ? e : e.name));
|
|
332
|
+
},
|
|
333
|
+
stat(path, ...args) {
|
|
334
|
+
return Promise.resolve(backend.stat(path, ...args));
|
|
335
|
+
},
|
|
336
|
+
exists(path) {
|
|
337
|
+
try {
|
|
338
|
+
backend.stat(path);
|
|
339
|
+
return Promise.resolve(true);
|
|
340
|
+
} catch {
|
|
341
|
+
return Promise.resolve(false);
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
mkdir(path, options) {
|
|
345
|
+
backend.mkdir(path, options);
|
|
346
|
+
return Promise.resolve();
|
|
347
|
+
},
|
|
348
|
+
unlink(path) {
|
|
349
|
+
backend.unlink(path);
|
|
350
|
+
return Promise.resolve();
|
|
351
|
+
},
|
|
352
|
+
rmdir(path) {
|
|
353
|
+
if (typeof backend.rmdir === "function") {
|
|
354
|
+
backend.rmdir(path);
|
|
355
|
+
}
|
|
356
|
+
return Promise.resolve();
|
|
357
|
+
},
|
|
358
|
+
rename(oldPath, newPath) {
|
|
359
|
+
if (typeof backend.rename === "function") {
|
|
360
|
+
backend.rename(oldPath, newPath);
|
|
361
|
+
}
|
|
362
|
+
return Promise.resolve();
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
}
|
|
324
366
|
registerBackend("InMemory", async (options) => {
|
|
325
367
|
const { InMemory } = await import("@zenfs/core");
|
|
326
368
|
const maxSize = options.maxSize ?? 100 * 1024 * 1024;
|
|
327
369
|
const label = options.label ?? "zen-fs-config";
|
|
328
|
-
|
|
370
|
+
const fs = InMemory.create({ maxSize, label });
|
|
371
|
+
return syncToAsync(fs);
|
|
329
372
|
});
|
|
330
373
|
|
|
331
374
|
// src/version.ts
|
package/dist/index.mjs
CHANGED
|
@@ -261,12 +261,7 @@ async function createBackend(descriptor) {
|
|
|
261
261
|
`Unknown backend type: "${descriptor.type}". Available types: ${Array.from(registry.keys()).join(", ")}. Use registerBackend() to register a custom backend.`
|
|
262
262
|
);
|
|
263
263
|
}
|
|
264
|
-
|
|
265
|
-
if (!instance.rmdir) {
|
|
266
|
-
instance.rmdir = async (_path) => {
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
return instance;
|
|
264
|
+
return factory(descriptor.options);
|
|
270
265
|
}
|
|
271
266
|
function hasBackend(type) {
|
|
272
267
|
return registry.has(type);
|
|
@@ -274,11 +269,59 @@ function hasBackend(type) {
|
|
|
274
269
|
function listBackends() {
|
|
275
270
|
return Array.from(registry.keys());
|
|
276
271
|
}
|
|
272
|
+
function syncToAsync(backend) {
|
|
273
|
+
return {
|
|
274
|
+
readFile(path, ...args) {
|
|
275
|
+
const result = backend.readFile(path, ...args);
|
|
276
|
+
return Promise.resolve(result);
|
|
277
|
+
},
|
|
278
|
+
writeFile(path, data, options) {
|
|
279
|
+
backend.writeFile(path, data, options);
|
|
280
|
+
return Promise.resolve();
|
|
281
|
+
},
|
|
282
|
+
readdir(path) {
|
|
283
|
+
const entries = backend.readdir(path);
|
|
284
|
+
return Promise.resolve(entries.map((e) => typeof e === "string" ? e : e.name));
|
|
285
|
+
},
|
|
286
|
+
stat(path, ...args) {
|
|
287
|
+
return Promise.resolve(backend.stat(path, ...args));
|
|
288
|
+
},
|
|
289
|
+
exists(path) {
|
|
290
|
+
try {
|
|
291
|
+
backend.stat(path);
|
|
292
|
+
return Promise.resolve(true);
|
|
293
|
+
} catch {
|
|
294
|
+
return Promise.resolve(false);
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
mkdir(path, options) {
|
|
298
|
+
backend.mkdir(path, options);
|
|
299
|
+
return Promise.resolve();
|
|
300
|
+
},
|
|
301
|
+
unlink(path) {
|
|
302
|
+
backend.unlink(path);
|
|
303
|
+
return Promise.resolve();
|
|
304
|
+
},
|
|
305
|
+
rmdir(path) {
|
|
306
|
+
if (typeof backend.rmdir === "function") {
|
|
307
|
+
backend.rmdir(path);
|
|
308
|
+
}
|
|
309
|
+
return Promise.resolve();
|
|
310
|
+
},
|
|
311
|
+
rename(oldPath, newPath) {
|
|
312
|
+
if (typeof backend.rename === "function") {
|
|
313
|
+
backend.rename(oldPath, newPath);
|
|
314
|
+
}
|
|
315
|
+
return Promise.resolve();
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
}
|
|
277
319
|
registerBackend("InMemory", async (options) => {
|
|
278
320
|
const { InMemory } = await import("@zenfs/core");
|
|
279
321
|
const maxSize = options.maxSize ?? 100 * 1024 * 1024;
|
|
280
322
|
const label = options.label ?? "zen-fs-config";
|
|
281
|
-
|
|
323
|
+
const fs = InMemory.create({ maxSize, label });
|
|
324
|
+
return syncToAsync(fs);
|
|
282
325
|
});
|
|
283
326
|
|
|
284
327
|
// src/version.ts
|