zen-fs-config 0.5.24 → 0.5.25
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 +56 -77
- package/README.md +112 -36
- package/README.zh-CN.md +107 -29
- package/dist/backend-registry-VHPBA57D.mjs +24 -0
- package/dist/chunk-R65B37S2.mjs +303 -0
- package/dist/index.d.mts +104 -72
- package/dist/index.d.ts +104 -72
- package/dist/index.js +623 -526
- package/dist/index.mjs +308 -522
- package/dist/zen-fs-config.js +661 -558
- package/package.json +15 -3
package/DESIGN.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# zen-fs-config — Design Document
|
|
2
2
|
|
|
3
|
+
> **Architecture direction (REQUIREMENTS §9 D2)**: the core uses a **GenericSyncGroup base with two type implementations** — `config-sync` (versioning/tombstone/conflict + hosts `.meta/app-data-groups/`) and `data-sync` (plain file sync, always governed as an app-data-group under config-sync via `ConfigRepo.createAppDataGroup`); **backend-type management and data-backend info persistence stay in the core `zen-fs-config` (UI is presentation-only)**. This document describes the **current implementation (two classes)**; the target architecture follows D2.
|
|
4
|
+
|
|
3
5
|
## 1. Overview
|
|
4
6
|
|
|
5
7
|
zen-fs-config is a distributed configuration management library built on top of:
|
|
@@ -12,7 +14,7 @@ It allows multiple application instances (programs) running on different nodes t
|
|
|
12
14
|
zen-fs-config supports two types of **sync groups**, each backed by a multi-backend sync network but serving different purposes:
|
|
13
15
|
|
|
14
16
|
- **Config-sync group** — Synchronizes the configuration repository itself: backend topology, app configs, shared configs, node-local configs. This is the "meta layer."
|
|
15
|
-
- **Data-sync group** — Synchronizes application data only. A data-sync group
|
|
17
|
+
- **Data-sync group** — Synchronizes application data only. A data-sync group is always referenced and managed by a config-sync group as an app's data storage layer (via `ConfigRepo.createAppDataGroup`).
|
|
16
18
|
|
|
17
19
|
A config-sync group can reference one or more data-sync groups per app, allowing apps to store bulk data on separate backends (e.g., a different repo/branch under the same account) while keeping configuration management unified.
|
|
18
20
|
|
|
@@ -86,8 +88,7 @@ A **sync group** is a set of backends that synchronize with each other. There ar
|
|
|
86
88
|
**Data-sync group**:
|
|
87
89
|
- Synced content: application data files only (no config meta layer)
|
|
88
90
|
- Backends: full credentials + storage location, but typically reusing the same account as a config-sync backend with a different storage target (e.g., same token/owner, different repo/branch)
|
|
89
|
-
- Can be
|
|
90
|
-
- Can be referenced by a config-sync group as an app's data storage
|
|
91
|
+
- Can be referenced by a config-sync group as an app's data storage (via `ConfigRepo.createAppDataGroup`)
|
|
91
92
|
|
|
92
93
|
**Group type detection**: When connecting to a backend, the library reads `.meta/group-type` to determine the group type. If the file is absent, the backend is treated as a new empty group and the caller decides which type to create.
|
|
93
94
|
|
|
@@ -130,7 +131,7 @@ A **sync group** is a set of backends that synchronize with each other. There ar
|
|
|
130
131
|
│ ├─ api-version.json
|
|
131
132
|
│ └─ .feature-flags.json.version
|
|
132
133
|
│
|
|
133
|
-
└─ nodes/ [
|
|
134
|
+
└─ nodes/ [synced — bidirectional, not node-scoped]
|
|
134
135
|
├─ {nodeId}/
|
|
135
136
|
│ ├─ local.json Node-local config
|
|
136
137
|
│ └─ env.json
|
|
@@ -252,17 +253,6 @@ The `accountFields` metadata for each backend type (registered via `BackendMetad
|
|
|
252
253
|
| WebDAV | `url`, `username`, `password` | `rootPath` |
|
|
253
254
|
| RemoteStorage | `userAddress`, `token` | (none) |
|
|
254
255
|
|
|
255
|
-
### Standalone Data-Sync Group
|
|
256
|
-
|
|
257
|
-
A data-sync group can also be used **without** a config-sync group. In this case:
|
|
258
|
-
|
|
259
|
-
1. The user provides a single backend configuration (e.g., Gitee: token + owner + repo + branch)
|
|
260
|
-
2. The library connects and reads `.meta/group-type` → `"data-sync"`
|
|
261
|
-
3. The app directly reads/writes data files on these backends
|
|
262
|
-
4. No config meta layer, no version sidecars, no tombstones — just raw data sync
|
|
263
|
-
|
|
264
|
-
Multiple data-sync backends can be registered within a single data-sync group, providing redundancy and multi-device sync for app data.
|
|
265
|
-
|
|
266
256
|
## 5. Sync Rules (`.meta/sync-rules.json`)
|
|
267
257
|
|
|
268
258
|
```json
|
|
@@ -301,7 +291,8 @@ Multiple data-sync backends can be registered within a single data-sync group, p
|
|
|
301
291
|
|
|
302
292
|
- Private app directories (`/{appId}/`): one-way push, no conflict possible
|
|
303
293
|
- Shared directory (`/shared/`): bi-directional, conflict possible, merge strategy
|
|
304
|
-
- Nodes
|
|
294
|
+
- Nodes directory (`/nodes/{nodeId}/`): bi-directional via the main sync pair (not node-scoped — every node's config syncs to every replica)
|
|
295
|
+
- Meta directory (`/.meta/`): bi-directional (topology, tombstones, conflict archives)
|
|
305
296
|
|
|
306
297
|
## 6. Versioning & Change Detection
|
|
307
298
|
|
|
@@ -391,11 +382,11 @@ zen-fs-sync emits a `conflict` event with full conflict details. Application can
|
|
|
391
382
|
|
|
392
383
|
## 8. Node-Local Configuration
|
|
393
384
|
|
|
394
|
-
Some configs are specific to a single node and
|
|
385
|
+
Some configs are specific to a single node and live under `/nodes/{nodeId}/`. They are synced to backends by the main bidirectional sync pair (there is no `direction: "none"` exclusion).
|
|
395
386
|
|
|
396
387
|
### 8.1 Storage
|
|
397
388
|
|
|
398
|
-
Node-local configs live under `/nodes/{nodeId}/`.
|
|
389
|
+
Node-local configs live under `/nodes/{nodeId}/`. There is no `direction: "none"` exclusion for `/nodes/` — the main bidirectional sync pair (`fullFS` ↔ replica, root `/`, no filter) replicates it to every replica exactly like `/{appId}/` or `/shared/` (sync is not node-scoped).
|
|
399
390
|
|
|
400
391
|
```
|
|
401
392
|
/nodes/server-1/
|
|
@@ -413,7 +404,7 @@ Priority order:
|
|
|
413
404
|
### 8.3 API
|
|
414
405
|
|
|
415
406
|
```typescript
|
|
416
|
-
// Write node-local config (
|
|
407
|
+
// Write node-local config (writes local primary; synced to replicas on next poll/flush)
|
|
417
408
|
repo.setNodeConfig('server-1', '/local.json', { ip: '10.0.0.1' });
|
|
418
409
|
|
|
419
410
|
// Read node-local config
|
|
@@ -431,9 +422,9 @@ const otherConfig = repo.peekNodeConfig<{ ip: string }>('server-2', '/local.json
|
|
|
431
422
|
| API | Write Target | Persisted | Synced | Purpose |
|
|
432
423
|
|---|---|---|---|---|
|
|
433
424
|
| `getConfig` / `setConfig` | CachedFS → auto-sync to replicas | Yes | Yes | Normal config |
|
|
434
|
-
| `getNodeConfig` / `setNodeConfig` |
|
|
435
|
-
| `publishNodeConfig` |
|
|
436
|
-
| `peekNodeConfig` |
|
|
425
|
+
| `getNodeConfig` / `setNodeConfig` | Local primary → synced to replicas via main pair (eventual) | Yes | Yes (all `/nodes/*`, not node-scoped) | Node config, namespaced by nodeId |
|
|
426
|
+
| `publishNodeConfig` | Explicit one-shot push to replicas | Yes | Yes (one-shot) | Force immediate sync of node files |
|
|
427
|
+
| `peekNodeConfig` | Read local (synced-in) copy | N/A | N/A | Read another node's config (after it has synced in) |
|
|
437
428
|
|
|
438
429
|
## 9. ConfigRepo Interface
|
|
439
430
|
|
|
@@ -458,7 +449,7 @@ interface ConfigRepo {
|
|
|
458
449
|
/** Read node-local config */
|
|
459
450
|
getNodeConfig<T>(nodeId: string, path: string): T;
|
|
460
451
|
|
|
461
|
-
/** Write node-local config (
|
|
452
|
+
/** Write node-local config (writes local primary; synced to replicas via main pair) */
|
|
462
453
|
setNodeConfig(nodeId: string, path: string, data: any): void;
|
|
463
454
|
|
|
464
455
|
/** Publish node-local config to sync backends (one-time, for debugging) */
|
|
@@ -561,7 +552,7 @@ interface AppDataBackendDescriptor {
|
|
|
561
552
|
|
|
562
553
|
## 10. Initialization
|
|
563
554
|
|
|
564
|
-
The recommended entry point is `connect`, which auto-detects the
|
|
555
|
+
The recommended entry point is `connect`, which always anchors on a config-sync repo (the host for data groups) and auto-detects the backend type. The lower-level `createConfigRepo` is also available; data groups are always created via `ConfigRepo.createAppDataGroup` (decision A / T5).
|
|
565
556
|
|
|
566
557
|
### `connect` (recommended — auto-detect)
|
|
567
558
|
|
|
@@ -577,8 +568,8 @@ const result = await connect('my-app', {
|
|
|
577
568
|
});
|
|
578
569
|
|
|
579
570
|
// result.groupType → "config-sync" or "data-sync"
|
|
580
|
-
// result.repo → ConfigRepo (
|
|
581
|
-
// result.dataGroup →
|
|
571
|
+
// result.repo → ConfigRepo (always — it hosts the data groups)
|
|
572
|
+
// result.dataGroup → config-managed data group (default group on first launch / data-sync connect)
|
|
582
573
|
```
|
|
583
574
|
|
|
584
575
|
### Zero-parameter (offline-first)
|
|
@@ -634,18 +625,22 @@ const backends = await repo.getBackends();
|
|
|
634
625
|
// backends.backends = [{ id: 'local-idb', ... }, { id: 's3-backup', ... }]
|
|
635
626
|
```
|
|
636
627
|
|
|
637
|
-
###
|
|
628
|
+
### Data-Sync Group via Config-Sync (recommended)
|
|
629
|
+
|
|
630
|
+
> Data groups are always managed by a config-sync repo via `createAppDataGroup` (decision A / T5). The snippet below shows the supported path.
|
|
638
631
|
|
|
639
632
|
```typescript
|
|
640
|
-
import {
|
|
633
|
+
import { connect } from 'zen-fs-config';
|
|
641
634
|
|
|
642
|
-
//
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
635
|
+
// connect always returns a config-sync repo; data groups live under it
|
|
636
|
+
const result = await connect('my-app', { nodeId: 'node-1' });
|
|
637
|
+
const repo = result.repo!;
|
|
638
|
+
|
|
639
|
+
// Create a data group and attach a data backend — config-sync stores the
|
|
640
|
+
// data group's backend topology in .meta/app-data-groups/{appId}/{id}.json
|
|
641
|
+
const dataGroup = await repo.createAppDataGroup('notes', [
|
|
642
|
+
{ id: 'gitee-data', type: 'Gitee', options: { token: '...', owner: '...', repo: 'my-app-data', branch: 'main' } },
|
|
643
|
+
]);
|
|
649
644
|
|
|
650
645
|
// Read/write data files directly
|
|
651
646
|
await dataGroup.fs.promises.writeFile('/notes/todo.json', JSON.stringify({ task: 'buy milk' }));
|
|
@@ -658,6 +653,7 @@ await dataGroup.addBackend('gitee-backup', 'Gitee', {
|
|
|
658
653
|
|
|
659
654
|
// Cleanup
|
|
660
655
|
await dataGroup.dispose();
|
|
656
|
+
await repo.dispose();
|
|
661
657
|
```
|
|
662
658
|
|
|
663
659
|
### Config-Sync with App Data Group (account reuse)
|
|
@@ -905,51 +901,32 @@ watch() triggers:
|
|
|
905
901
|
|
|
906
902
|
**Key difference from previous design**: The old approach merged source and target snapshots into a single map (`source ∪ target`), which lost information about which filesystem a file belonged to. The new approach keeps them separate, enabling precise per-side change detection and bidirectional deletion propagation (see §11.10).
|
|
907
903
|
|
|
908
|
-
### 11.8 Standalone Data-Sync
|
|
904
|
+
### 11.8 Standalone Data-Sync Entry — REMOVED
|
|
909
905
|
|
|
910
|
-
|
|
911
|
-
createDataSyncGroup('my-app', options?)
|
|
912
|
-
│
|
|
913
|
-
├─ 1. Connect to user-provided backend (options.backendInfo)
|
|
914
|
-
│
|
|
915
|
-
├─ 2. Read /.meta/group-type
|
|
916
|
-
│ ├─ "data-sync" → existing group, read .meta/backends/ for all data backends
|
|
917
|
-
│ ├─ absent → new group, write /.meta/group-type = "data-sync"
|
|
918
|
-
│ └─ "config-sync" → error: this is a config-sync backend, use createConfigRepo()
|
|
919
|
-
│
|
|
920
|
-
├─ 3. Create IndexedDB as local primary (for offline access)
|
|
921
|
-
│
|
|
922
|
-
├─ 4. Setup sync: IndexedDB ↔ each data backend (bi-directional)
|
|
923
|
-
│ NOTE: Does NOT watch yet — sync first
|
|
924
|
-
│
|
|
925
|
-
├─ 5. syncAll() — pull data from remote backends
|
|
926
|
-
│
|
|
927
|
-
├─ 6. watchAll() — start monitoring AFTER sync completes
|
|
928
|
-
│
|
|
929
|
-
└─ 7. Return DataSyncGroup handle with direct fs access
|
|
930
|
-
```
|
|
906
|
+
The standalone data-sync entry has been **removed** from the public API (decision A / T5). Data groups are created exclusively via `ConfigRepo.createAppDataGroup` and are always owned by a config-sync repo, whose `.meta/app-data-groups/{appId}/{id}.json` is the authoritative source of the data group's backend topology.
|
|
931
907
|
|
|
932
908
|
### 11.9 Unified Entry Point (`connect`)
|
|
933
909
|
|
|
934
|
-
`createConfigRepo`
|
|
910
|
+
`createConfigRepo` is the lower-level factory for a config-sync repo. The recommended entry point is `connect`, which always anchors on a config-sync repo and dispatches the backend as follows:
|
|
935
911
|
|
|
936
912
|
```
|
|
937
913
|
connect('my-app', options?)
|
|
938
914
|
│
|
|
939
|
-
├─ 1.
|
|
915
|
+
├─ 1. Determine config-sync repo (create or reuse) — this always hosts the data groups
|
|
940
916
|
│
|
|
941
|
-
├─ 2.
|
|
917
|
+
├─ 2. If options.backendInfo provided, read /.meta/group-type
|
|
942
918
|
│
|
|
943
|
-
├─ "config-sync" →
|
|
944
|
-
│
|
|
919
|
+
├─ "config-sync" → connect the repo to that backend; then load every app
|
|
920
|
+
│ data group from .meta/app-data-groups/{appId}/*.json (UC2)
|
|
921
|
+
│ return { groupType: "config-sync", repo, appDataGroups }
|
|
945
922
|
│
|
|
946
|
-
├─ "data-sync" →
|
|
947
|
-
│
|
|
923
|
+
├─ "data-sync" → keep the config repo local-only; ensure the default data
|
|
924
|
+
│ group exists and attach the backend to it (UC3). The backend
|
|
925
|
+
│ info is written back into app-data-groups by addBackend.
|
|
926
|
+
│ return { groupType: "data-sync", repo, dataGroup }
|
|
948
927
|
│
|
|
949
|
-
└─ absent
|
|
950
|
-
|
|
951
|
-
├─ options.groupType === "config-sync" (or omitted) → dispatch to createConfigRepo()
|
|
952
|
-
└─ default: config-sync
|
|
928
|
+
└─ absent (no backendInfo) → local-only config repo + a default data group
|
|
929
|
+
└─ return { groupType: "config-sync" (or options.groupType), repo, dataGroup }
|
|
953
930
|
```
|
|
954
931
|
|
|
955
932
|
**Usage**:
|
|
@@ -970,7 +947,7 @@ if (result.groupType === 'config-sync') {
|
|
|
970
947
|
const repo = result.repo;
|
|
971
948
|
repo.setConfig('/db/host', { hostname: 'localhost' });
|
|
972
949
|
} else {
|
|
973
|
-
// result.dataGroup is a
|
|
950
|
+
// result.dataGroup is a config-managed data group (result.repo is its host)
|
|
974
951
|
const dataGroup = result.dataGroup;
|
|
975
952
|
await dataGroup.fs.promises.writeFile('/data.json', '{"key":"value"}');
|
|
976
953
|
}
|
|
@@ -988,14 +965,16 @@ const result = await connect('my-app', {
|
|
|
988
965
|
interface ConnectResult {
|
|
989
966
|
/** Detected or forced group type */
|
|
990
967
|
groupType: 'config-sync' | 'data-sync';
|
|
991
|
-
/**
|
|
968
|
+
/** The config-sync repo — always present; it hosts the data groups */
|
|
992
969
|
repo?: ConfigRepo;
|
|
993
|
-
/**
|
|
994
|
-
dataGroup?:
|
|
970
|
+
/** The default app data group (config-managed); present on first launch / data-sync connect */
|
|
971
|
+
dataGroup?: AppDataGroup;
|
|
972
|
+
/** All app data groups loaded for this app (may be empty) */
|
|
973
|
+
appDataGroups?: AppDataGroup[];
|
|
995
974
|
}
|
|
996
975
|
```
|
|
997
976
|
|
|
998
|
-
**Offline / zero-parameter mode**: When no `backendInfo` is provided, `connect` defaults to `config-sync
|
|
977
|
+
**Offline / zero-parameter mode**: When no `backendInfo` is provided, `connect` defaults to `config-sync`, creates an IndexedDB-only config repo, and also creates a default app data group under it via `createAppDataGroup` (same as `createConfigRepo` with no options, plus the default data group).
|
|
999
978
|
|
|
1000
979
|
### 11.10 Snapshot Optimization Design
|
|
1001
980
|
|
|
@@ -1127,13 +1106,13 @@ Application
|
|
|
1127
1106
|
→ Sync to replicas per sync-rules
|
|
1128
1107
|
```
|
|
1129
1108
|
|
|
1130
|
-
### Write Path (node-local,
|
|
1109
|
+
### Write Path (node-local, synced)
|
|
1131
1110
|
```
|
|
1132
1111
|
Application
|
|
1133
1112
|
→ repo.setNodeConfig('server-1', '/local.json', { ip: '10.0.0.1' })
|
|
1134
|
-
→ Serialize + write to /nodes/server-1/local.json
|
|
1135
|
-
→
|
|
1136
|
-
→ File
|
|
1113
|
+
→ Serialize + write to /nodes/server-1/local.json (local primary)
|
|
1114
|
+
→ Main bidirectional sync pair replicates /nodes/ to replicas on next poll/flush
|
|
1115
|
+
→ File is also present on other replicas (sync is not node-scoped)
|
|
1137
1116
|
```
|
|
1138
1117
|
|
|
1139
1118
|
### Publish (one-time sync)
|
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Distributed configuration management library built on [ZenFS](https://github.com/weijia/zen-fs), with IndexedDB as the offline-first local primary backend and user-provided remote backends as replicas that sync automatically. Supports app isolation, shared spaces, node-local config, and conflict-safe operations.
|
|
4
4
|
|
|
5
|
+
> **Architecture direction (REQUIREMENTS §9 D2)**: the core uses a **GenericSyncGroup base with two type implementations** — `config-sync` (versioning/tombstone/conflict + hosts `.meta/app-data-groups/`) and `data-sync` (plain file sync, optionally governed as an app-data-group under config-sync); **backend-type management and data-backend info persistence stay in the core `zen-fs-config` (UI is presentation-only)**. This document describes the **current implementation (two classes)**; the target architecture follows D2.
|
|
6
|
+
|
|
5
7
|
**GitHub**: https://github.com/weijia/zen-fs-config
|
|
6
8
|
**NPM**: `zen-fs-config`
|
|
7
9
|
**Design doc**: [DESIGN.md](./DESIGN.md)
|
|
@@ -12,7 +14,7 @@ Distributed configuration management library built on [ZenFS](https://github.com
|
|
|
12
14
|
- **Multi-backend sync** — Add any number of remote replicas (Gitee, GitHub, RemoteStorage, WebDAV, etc.) with automatic bi-directional sync
|
|
13
15
|
- **App isolation** — Each app gets its own namespace under `/{appId}/`
|
|
14
16
|
- **Shared spaces** — Cross-app shared config under `/shared/`
|
|
15
|
-
- **Node-local config** — Per-device settings under `/nodes/{nodeId}
|
|
17
|
+
- **Node-local config** — Per-device settings under `/nodes/{nodeId}/`, synced to backends by the main bidirectional pair (each node isolated by its `nodeId` subdirectory)
|
|
16
18
|
- **Self-describing topology** — Backend configuration is stored as files in `.meta/backends/`, so re-opening a repo restores everything automatically
|
|
17
19
|
- **Conflict safety** — Conflicts are archived instead of silently overwritten; JSON deep-merge is available as a strategy
|
|
18
20
|
- **Version tracking** — Every config file has a version sidecar with version number and SHA-256 hash
|
|
@@ -21,23 +23,31 @@ Distributed configuration management library built on [ZenFS](https://github.com
|
|
|
21
23
|
## Installation
|
|
22
24
|
|
|
23
25
|
```bash
|
|
26
|
+
# Core dependencies (always required)
|
|
24
27
|
npm install zen-fs-config @zenfs/core @zenfs/dom zen-fs-sync
|
|
28
|
+
|
|
29
|
+
# Optional cloud backends — peer dependencies, install only the ones you actually use
|
|
30
|
+
npm install zen-fs-github # GitHub (repo/branch) replica
|
|
31
|
+
npm install zen-fs-gitee # Gitee (repo/branch) replica
|
|
32
|
+
npm install zen-fs-remotestoragejs # RemoteStorage replica
|
|
25
33
|
```
|
|
26
34
|
|
|
27
35
|
> `@zenfs/dom` provides the IndexedDB backend (required in browser environments). `zen-fs-cache` is an optional dependency for remote request caching.
|
|
36
|
+
>
|
|
37
|
+
> **Cloud backends are optional peer dependencies, installed by you (the consumer), not bundled by `zen-fs-config`.** The core *pre-registers* the `GitHub`, `Gitee`, and `RemoteStorage` backend **types** (so you can pass `type: 'Gitee'`, etc. straight to `addBackend`/`connect` without calling `registerBackend`), but it does **not** ship their implementations. You must `npm install` the matching package yourself; otherwise `createBackend()` throws a clear "package not installed — run `npm install <pkg>`" hint. This keeps the core bundle small and avoids forcing an internet dependency on everyone.
|
|
28
38
|
|
|
29
39
|
## Usage via `<script>` tag (no build step)
|
|
30
40
|
|
|
31
|
-
A self-contained browser bundle is published at `dist/zen-fs-config.js`. It bundles **
|
|
41
|
+
A self-contained browser bundle is published at `dist/zen-fs-config.js`. It bundles the **core** dependencies (`@zenfs/core`, `@zenfs/dom`, `zen-fs-sync`, `zen-fs-cache`) and exposes the library on the global `window.ZenFSConfig`. No npm install, no bundler — just drop it into any HTML page. **Cloud backends (GitHub / Gitee / RemoteStorage) are NOT bundled** — see the section below to enable them in `<script>` mode with **no import map required**.
|
|
32
42
|
|
|
33
43
|
```html
|
|
34
44
|
<script src="https://unpkg.com/zen-fs-config/dist/zen-fs-config.js"></script>
|
|
35
45
|
<script>
|
|
36
46
|
(async () => {
|
|
37
|
-
const {
|
|
47
|
+
const { connect } = window.ZenFSConfig;
|
|
38
48
|
|
|
39
49
|
// IndexedDB primary backend is created automatically
|
|
40
|
-
const repo = await
|
|
50
|
+
const { repo } = await connect('my-app');
|
|
41
51
|
|
|
42
52
|
repo.setConfig('greeting.json', { msg: 'hello' });
|
|
43
53
|
const cfg = await repo.getConfig('greeting.json');
|
|
@@ -48,42 +58,70 @@ A self-contained browser bundle is published at `dist/zen-fs-config.js`. It bund
|
|
|
48
58
|
|
|
49
59
|
> The browser bundle includes a pure-JS SHA-256 fallback, so version tracking works even in non-secure contexts (plain HTTP) where `crypto.subtle` is unavailable.
|
|
50
60
|
|
|
61
|
+
### Enabling cloud backends (GitHub / Gitee / RemoteStorage) in `<script>` mode — **no import map required**
|
|
62
|
+
|
|
63
|
+
The self-contained core bundle does **not** include the cloud backend implementations — they are optional peer packages kept out of the core to keep the bundle small. The good news: **all three cloud packages now ship a browser global (UMD) build**, so in a no-build `<script>` page you can enable them with plain `<script>` tags — **no import map and no bundler needed**:
|
|
64
|
+
|
|
65
|
+
```html
|
|
66
|
+
<script src="https://unpkg.com/zen-fs-gitee/dist/zen-fs-gitee.global.js"></script>
|
|
67
|
+
<script src="https://unpkg.com/zen-fs-github/dist/zen-fs-github.global.js"></script>
|
|
68
|
+
<script src="https://unpkg.com/zen-fs-remotestoragejs/dist/zen-fs-remotestoragejs.global.js"></script>
|
|
69
|
+
<script src="https://unpkg.com/zen-fs-config/dist/zen-fs-config.js"></script>
|
|
70
|
+
<script>
|
|
71
|
+
(async () => {
|
|
72
|
+
const { connect } = window.ZenFSConfig;
|
|
73
|
+
const { repo } = await connect('my-app', {
|
|
74
|
+
backendInfo: { type: 'Gitee', options: { token, owner, repo, branch } },
|
|
75
|
+
});
|
|
76
|
+
})();
|
|
77
|
+
</script>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Global build → global variable mapping:
|
|
81
|
+
- `zen-fs-gitee` → `window.ZenFSGitee` (build: `dist/zen-fs-gitee.global.js`)
|
|
82
|
+
- `zen-fs-github` → `window.ZenFSGitHub` (build: `dist/zen-fs-github.global.js`; v1.1.3+)
|
|
83
|
+
- `zen-fs-remotestoragejs` → `window.ZenFSRemoteStorage` (build: `dist/zen-fs-remotestoragejs.global.js`)
|
|
84
|
+
|
|
85
|
+
`zen-fs-config` auto-detects these globals (`window.ZenFSGitee` / `window.ZenFSGitHub` / `window.ZenFSRemoteStorage`) and uses them directly, skipping the bare `import()`. Because all three packages declare a `browser`/`unpkg` field, a bare package URL (e.g. `https://unpkg.com/zen-fs-github`) also returns the global build — so you may shorten the three `<script>` lines to bare URLs if you prefer.
|
|
86
|
+
|
|
51
87
|
## Quick Start
|
|
52
88
|
|
|
53
89
|
### 1. Initialize (zero-configuration)
|
|
54
90
|
|
|
55
91
|
```typescript
|
|
56
|
-
import {
|
|
92
|
+
import { connect } from 'zen-fs-config';
|
|
57
93
|
|
|
58
94
|
// Creates an IndexedDB primary backend automatically
|
|
59
|
-
const repo = await
|
|
95
|
+
const { repo } = await connect('my-app');
|
|
60
96
|
|
|
61
97
|
// Read/write config (synchronous API, served from IndexedDB)
|
|
62
98
|
repo.setConfig('/database', { host: 'localhost', port: 5432 });
|
|
63
99
|
const db = repo.getConfig<{ host: string; port: number }>('/database');
|
|
64
100
|
```
|
|
65
101
|
|
|
66
|
-
### 2.
|
|
102
|
+
### 2. Write config values
|
|
67
103
|
|
|
68
104
|
```typescript
|
|
69
|
-
|
|
70
|
-
|
|
105
|
+
// You can write as many config files as you like under the app root
|
|
106
|
+
repo.setConfig('/cache', { ttl: 3600, maxSize: '100MB' });
|
|
107
|
+
repo.setConfig('/feature-flags', { newUI: true, beta: false });
|
|
108
|
+
```
|
|
71
109
|
|
|
72
|
-
|
|
73
|
-
registerBackend('Gitee', async (options) => {
|
|
74
|
-
return Gitee.create(options);
|
|
75
|
-
});
|
|
110
|
+
### 3. Add a remote replica backend
|
|
76
111
|
|
|
77
|
-
|
|
112
|
+
```typescript
|
|
113
|
+
// GitHub / Gitee / RemoteStorage are pre-registered by the core — no registerBackend needed.
|
|
114
|
+
// Install the package first (e.g. `npm install zen-fs-gitee` — see Installation), then:
|
|
78
115
|
await repo.addBackend('gitee-prod', 'Gitee', {
|
|
79
116
|
token: 'your-token',
|
|
80
117
|
owner: 'your-name',
|
|
81
118
|
repo: 'config-repo',
|
|
82
119
|
branch: 'main',
|
|
83
120
|
}, 'Production Gitee config repo');
|
|
121
|
+
// ↑ auto-syncs bi-directionally with the local IndexedDB primary
|
|
84
122
|
```
|
|
85
123
|
|
|
86
|
-
###
|
|
124
|
+
### 4. Automatic sync
|
|
87
125
|
|
|
88
126
|
```typescript
|
|
89
127
|
// Writing to IndexedDB triggers auto-sync to all replicas
|
|
@@ -93,11 +131,11 @@ repo.setConfig('/database', { host: 'new-host', port: 5432 });
|
|
|
93
131
|
await repo.flush();
|
|
94
132
|
```
|
|
95
133
|
|
|
96
|
-
###
|
|
134
|
+
### 5. Re-open on next page load
|
|
97
135
|
|
|
98
136
|
```typescript
|
|
99
137
|
// Just pass the appId — IndexedDB restores everything
|
|
100
|
-
const repo = await
|
|
138
|
+
const { repo } = await connect('my-app');
|
|
101
139
|
|
|
102
140
|
// Config is readable immediately (offline)
|
|
103
141
|
const db = repo.getConfig<{ host: string; port: number }>('/database');
|
|
@@ -107,10 +145,10 @@ const backends = await repo.getBackends();
|
|
|
107
145
|
console.log(backends?.backends.map(b => b.id)); // ['local-idb', 'gitee-prod', ...]
|
|
108
146
|
```
|
|
109
147
|
|
|
110
|
-
### Initialize with a backend from the start
|
|
148
|
+
### 6. Initialize with a backend from the start
|
|
111
149
|
|
|
112
150
|
```typescript
|
|
113
|
-
const repo = await
|
|
151
|
+
const { repo } = await connect('my-app', {
|
|
114
152
|
primaryBackendId: 'gitee-prod',
|
|
115
153
|
backendInfo: {
|
|
116
154
|
type: 'Gitee',
|
|
@@ -120,16 +158,56 @@ const repo = await createConfigRepo('my-app', {
|
|
|
120
158
|
});
|
|
121
159
|
|
|
122
160
|
// Next time you don't need to pass backend info again
|
|
123
|
-
const repo2 = await
|
|
161
|
+
const { repo: repo2 } = await connect('my-app');
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Unified entry point: `connect()`
|
|
165
|
+
|
|
166
|
+
If you don't know in advance whether a backend holds a config-sync repo or a
|
|
167
|
+
data-sync group, use the unified `connect()` entry point. It always anchors on
|
|
168
|
+
a config-sync repo (which hosts the data groups) and, when the backend is a
|
|
169
|
+
data-sync backend, attaches it to the default app data group. There is no
|
|
170
|
+
separate standalone data-sync entry — data groups are always
|
|
171
|
+
managed by a config-sync repo via `createAppDataGroup` (decision A / T5).
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
import { connect } from 'zen-fs-config';
|
|
175
|
+
|
|
176
|
+
// 1. No backendInfo → local-only, defaults to config-sync
|
|
177
|
+
const { groupType, repo } = await connect('my-app');
|
|
178
|
+
|
|
179
|
+
// 2. With a remote backend → detect group type automatically
|
|
180
|
+
const result = await connect('my-app', {
|
|
181
|
+
backendInfo: { type: 'Gitee', options: { token, owner, repo, branch } },
|
|
182
|
+
});
|
|
183
|
+
if (result.groupType === 'data-sync') {
|
|
184
|
+
// result.dataGroup — a config-managed data group (result.repo is its host)
|
|
185
|
+
} else {
|
|
186
|
+
// result.repo — a ConfigRepo
|
|
187
|
+
}
|
|
124
188
|
```
|
|
125
189
|
|
|
190
|
+
Detection & dispatch rules:
|
|
191
|
+
|
|
192
|
+
- Always creates (or reuses) a **config-sync `ConfigRepo`** as the anchor.
|
|
193
|
+
- Reads `/.meta/group-type` from the backend: `config-sync` → connect the repo to that backend; `data-sync` → attach the backend to the default app data group inside the config-sync repo.
|
|
194
|
+
- If that file is absent (a brand-new backend), falls back to `options.groupType` (default `config-sync`).
|
|
195
|
+
- If you pass `options.groupType` and it conflicts with the backend's actual type, `connect()` throws a `Group type mismatch` error.
|
|
196
|
+
- The returned `ConnectResult` always carries `groupType` and `repo` (the config-sync repo). It also carries `dataGroup` (the default app data group) on first launch and when a data-sync backend was connected.
|
|
197
|
+
|
|
198
|
+
| Method | Description |
|
|
199
|
+
|--------|-------------|
|
|
200
|
+
| `connect(appId, options?)` | Unified entry: always returns a `ConfigRepo`, optionally with a default data group |
|
|
201
|
+
|
|
202
|
+
`connect()` accepts the same options as `createConfigRepo` (`backendInfo`, `primaryBackendId`, `idbStoreName`, `nodeId`, `folderPath`, `cache`, `serializer`, `onConflict`, `syncPollIntervalMs`) plus `groupType`. On Node.js, pass `folderPath` (or set `ZEN_FS_CONFIG_HOME`) to enable local disk persistence.
|
|
203
|
+
|
|
126
204
|
## Directory Structure
|
|
127
205
|
|
|
128
206
|
```
|
|
129
207
|
/
|
|
130
208
|
├── {appId}/ # App-private config (auto-synced to replicas)
|
|
131
209
|
├── shared/ # Cross-app shared config (bi-directional sync)
|
|
132
|
-
├── nodes/{nodeId}/ # Node-local config (
|
|
210
|
+
├── nodes/{nodeId}/ # Node-local config (synced; namespaced by nodeId)
|
|
133
211
|
└── .meta/
|
|
134
212
|
├── backends/ # Backend topology (one file per backend)
|
|
135
213
|
│ ├── local-idb.json
|
|
@@ -143,20 +221,21 @@ Each config file has a sidecar version file: `db.json` → `.db.json.version` (v
|
|
|
143
221
|
|
|
144
222
|
## Core API
|
|
145
223
|
|
|
224
|
+
> **Entry point**: use the unified `connect(appId, options?)` (see above) to obtain a `ConfigRepo`. `createConfigRepo` is still exported as a lower-level factory but `connect` is recommended — it also handles data-sync groups and Node.js local persistence (`folderPath` / `ZEN_FS_CONFIG_HOME`).
|
|
225
|
+
|
|
146
226
|
### ConfigRepo
|
|
147
227
|
|
|
148
228
|
| Method | Description |
|
|
149
229
|
|--------|-------------|
|
|
150
|
-
| `createConfigRepo(appId, options?)` | Create a config repo. IndexedDB is always primary; `backendInfo` becomes a replica. |
|
|
151
230
|
| `getConfig<T>(path)` | Synchronously read app config (from IndexedDB) |
|
|
152
231
|
| `setConfig(path, data)` | Synchronously write app config (async persistence + auto-sync) |
|
|
153
232
|
| `addBackend(id, type, options, desc?)` | Dynamically add a replica backend with auto bi-directional sync |
|
|
154
233
|
| `removeBackend(id)` | Remove a replica backend and stop syncing |
|
|
155
234
|
| `getBackends()` | Read backend topology (aggregated from `.meta/backends/*.json`) |
|
|
156
235
|
| `getNodeConfig<T>(nodeId, path)` | Asynchronously read node-local config |
|
|
157
|
-
| `setNodeConfig(nodeId, path, data)` |
|
|
158
|
-
| `publishNodeConfig(nodeId)` |
|
|
159
|
-
| `peekNodeConfig<T>(nodeId, path)` | Read
|
|
236
|
+
| `setNodeConfig(nodeId, path, data)` | Write node-local config (local primary; synced to replicas via main pair) |
|
|
237
|
+
| `publishNodeConfig(nodeId)` | Explicit one-shot push of node config to all backends (auto-sync also covers it) |
|
|
238
|
+
| `peekNodeConfig<T>(nodeId, path)` | Read another node's config (from the synced-in local copy) |
|
|
160
239
|
| `flush()` | Manually trigger all pending syncs |
|
|
161
240
|
| `listConflicts()` | List all archived conflicts |
|
|
162
241
|
| `resolveConflict(id, merged)` | Resolve a conflict with merged content |
|
|
@@ -165,26 +244,20 @@ Each config file has a sidecar version file: `db.json` → `.db.json.version` (v
|
|
|
165
244
|
|
|
166
245
|
### Backend Registration
|
|
167
246
|
|
|
168
|
-
zen-fs-config
|
|
169
|
-
- **IndexedDB** — local primary backend (based on `@zenfs/dom`)
|
|
247
|
+
zen-fs-config bundles two built-in backends (no install needed):
|
|
248
|
+
- **IndexedDB** — local primary backend (based on `@zenfs/dom`)
|
|
170
249
|
- **InMemory** — in-memory backend (based on `@zenfs/core`), useful for testing
|
|
171
250
|
|
|
172
|
-
|
|
251
|
+
Three **cloud backend types are pre-registered by the core** (lazy-loaded on first use): `GitHub` (`zen-fs-github`), `Gitee` (`zen-fs-gitee`), and `RemoteStorage` (`zen-fs-remotestoragejs`). You do **not** need to call `registerBackend` for them — just install the matching package (see Installation) and pass `type: 'Gitee'`, etc. to `addBackend`/`connect`. `registerBackend` is only needed to add a *fully custom* backend or to override a built-in registration:
|
|
173
252
|
|
|
174
253
|
```typescript
|
|
175
254
|
import { registerBackend } from 'zen-fs-config';
|
|
176
255
|
|
|
177
|
-
//
|
|
256
|
+
// Example: override the built-in Gitee registration with a custom factory
|
|
178
257
|
registerBackend('Gitee', async (options) => {
|
|
179
258
|
const { Gitee } = await import('zen-fs-gitee');
|
|
180
259
|
return Gitee.create(options);
|
|
181
260
|
});
|
|
182
|
-
|
|
183
|
-
// Register RemoteStorage backend
|
|
184
|
-
registerBackend('RemoteStorage', async (options) => {
|
|
185
|
-
const { createRemoteStorageFileSystem } = await import('zen-fs-remotestoragejs');
|
|
186
|
-
return createRemoteStorageFileSystem(options);
|
|
187
|
-
});
|
|
188
261
|
```
|
|
189
262
|
|
|
190
263
|
## Architecture
|
|
@@ -202,7 +275,7 @@ ConfigRepo
|
|
|
202
275
|
```
|
|
203
276
|
|
|
204
277
|
- **IndexedDB is the only primary backend** — all reads and writes go directly to IndexedDB, guaranteeing offline availability
|
|
205
|
-
- **Remote backends are replicas** — added via `addBackend()` or `
|
|
278
|
+
- **Remote backends are replicas** — added via `addBackend()` or `connect({ backendInfo })`
|
|
206
279
|
- **Auto-sync** — changes to IndexedDB automatically propagate to all replicas
|
|
207
280
|
- **Self-describing topology** — backend configuration lives in `.meta/backends/`, one JSON file per backend
|
|
208
281
|
|
|
@@ -214,6 +287,9 @@ ConfigRepo
|
|
|
214
287
|
| `@zenfs/dom >=1.0.0` | IndexedDB backend (browser) | Yes (browser) |
|
|
215
288
|
| `zen-fs-sync >=0.4.7` | Cross-backend sync engine | Yes |
|
|
216
289
|
| `zen-fs-cache >=1.0.0` | ETag/TTL caching layer | No (optional) |
|
|
290
|
+
| `zen-fs-github` | GitHub replica backend (**optional peer**) | No |
|
|
291
|
+
| `zen-fs-gitee` | Gitee replica backend (**optional peer**) | No |
|
|
292
|
+
| `zen-fs-remotestoragejs` | RemoteStorage replica backend (**optional peer**) | No |
|
|
217
293
|
|
|
218
294
|
## License
|
|
219
295
|
|