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 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 can be used standalone, or referenced by a config-sync group as an app's data storage layer.
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 used standalone (no config-sync group needed)
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/ [not synced by default]
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 and meta: excluded from sync
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 should not be auto-synced.
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}/`. The `/nodes/` directory is excluded from sync rules (`direction: "none"`).
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 (no sync, local only)
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` | CachedFS → no sync | Yes (primary backend only) | No | Node-private config |
435
- | `publishNodeConfig` | One-time manual sync | Yes | Yes (one-time) | Debug: push to other backends |
436
- | `peekNodeConfig` | CachedFS read | N/A | N/A | Read other nodes' published config |
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 (no auto-sync) */
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 group type. The lower-level `createConfigRepo` and `createDataSyncGroup` are also available for explicit control.
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 (if config-sync)
581
- // result.dataGroup → DataSyncGroup (if data-sync)
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
- ### Standalone Data-Sync Group (no config layer)
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 { createDataSyncGroup } from 'zen-fs-config';
633
+ import { connect } from 'zen-fs-config';
641
634
 
642
- // User provides a data backend directly — no config-sync layer needed
643
- const dataGroup = await createDataSyncGroup('my-app', {
644
- backendInfo: {
645
- type: 'Gitee',
646
- options: { token: '...', owner: '...', repo: 'my-app-data', branch: 'main' },
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 Group (`createDataSyncGroup`)
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` and `createDataSyncGroup` are lower-level factory functions. The recommended entry point is `connect`, which auto-detects the group type and dispatches to the appropriate factory:
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. Connect to user-provided backend (options.backendInfo)
915
+ ├─ 1. Determine config-sync repo (create or reuse) — this always hosts the data groups
940
916
  │
941
- ├─ 2. Read /.meta/group-type
917
+ ├─ 2. If options.backendInfo provided, read /.meta/group-type
942
918
  │
943
- ├─ "config-sync" → dispatch to createConfigRepo()
944
- │ return { groupType: "config-sync", repo }
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" → dispatch to createDataSyncGroup()
947
- │ return { groupType: "data-sync", dataGroup }
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 → new empty backend
950
- ├─ options.groupType === "data-sync" → dispatch to createDataSyncGroup()
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 DataSyncGroup — lightweight data-only system
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
- /** Present when groupType === "config-sync" */
968
+ /** The config-sync repo — always present; it hosts the data groups */
992
969
  repo?: ConfigRepo;
993
- /** Present when groupType === "data-sync" */
994
- dataGroup?: DataSyncGroup;
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` and creates an IndexedDB-only repo (same as `createConfigRepo` with no options).
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, no sync)
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
- → zen-fs-sync ignores /nodes/ (direction: "none")
1136
- → File stays local to primary backend only
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}/` that never sync
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 **all** 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:
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 { createConfigRepo } = window.ZenFSConfig;
47
+ const { connect } = window.ZenFSConfig;
38
48
 
39
49
  // IndexedDB primary backend is created automatically
40
- const repo = await createConfigRepo('my-app');
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 { createConfigRepo } from 'zen-fs-config';
92
+ import { connect } from 'zen-fs-config';
57
93
 
58
94
  // Creates an IndexedDB primary backend automatically
59
- const repo = await createConfigRepo('my-app');
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. Add a remote replica backend
102
+ ### 2. Write config values
67
103
 
68
104
  ```typescript
69
- import { registerBackend } from 'zen-fs-config';
70
- import { Gitee } from 'zen-fs-gitee';
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
- // Register the backend type first
73
- registerBackend('Gitee', async (options) => {
74
- return Gitee.create(options);
75
- });
110
+ ### 3. Add a remote replica backend
76
111
 
77
- // Dynamically add a replica — auto-syncs bi-directionally with local IndexedDB
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
- ### 3. Automatic sync
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
- ### 4. Re-open on next page load
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 createConfigRepo('my-app');
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 createConfigRepo('my-app', {
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 createConfigRepo('my-app');
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 (never synced)
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)` | Asynchronously write node-local config (not synced) |
158
- | `publishNodeConfig(nodeId)` | One-time push of node config to all backends |
159
- | `peekNodeConfig<T>(nodeId, path)` | Read-only view of another node's published config |
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 includes two built-in backends:
169
- - **IndexedDB** — local primary backend (based on `@zenfs/dom`), no registration needed
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
- Register custom backends:
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
- // Register Gitee backend
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 `createConfigRepo({ backendInfo })`
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