zen-fs-config 0.4.6 → 0.5.1

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
@@ -9,6 +9,13 @@ zen-fs-config is a distributed configuration management library built on top of:
9
9
 
10
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
11
 
12
+ zen-fs-config supports two types of **sync groups**, each backed by a multi-backend sync network but serving different purposes:
13
+
14
+ - **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.
16
+
17
+ 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
+
12
19
  ## 2. Architecture
13
20
 
14
21
  ### 2.1 Three-Layer Stack
@@ -47,18 +54,67 @@ Backend topology and sync rules are stored **inside** the configuration reposito
47
54
 
48
55
  External input at startup is limited to: **which backend to connect to** and optionally **bootstrap data** (if the repo doesn't exist yet).
49
56
 
57
+ ### 2.4 Sync Group Types
58
+
59
+ A **sync group** is a set of backends that synchronize with each other. There are two types:
60
+
61
+ ```
62
+ ┌─────────────────────────────────────────────────────────────────┐
63
+ │ Config-Sync Group │
64
+ │ ┌──────────────────────────────────────────────────────┐ │
65
+ │ │ .meta/backends/ ← config-sync backends │ │
66
+ │ │ .meta/app-data-groups/ ← references to data-sync │ │
67
+ │ │ /{appId}/ ← app config data │ │
68
+ │ │ /shared/ ← shared config │ │
69
+ │ │ /nodes/ ← node-local config │ │
70
+ │ └──────────────────────────────────────────────────────┘ │
71
+ │ │ references │
72
+ │ ▼ │
73
+ │ ┌──────────────────────────────────────────────────────┐ │
74
+ │ │ Data-Sync Group (per-app) │ │
75
+ │ │ .meta/backends/ ← data-sync backends │ │
76
+ │ │ / ← app data files │ │
77
+ │ └──────────────────────────────────────────────────────┘ │
78
+ └─────────────────────────────────────────────────────────────────┘
79
+ ```
80
+
81
+ **Config-sync group**:
82
+ - Synced content: `.meta/` (topology), `/{appId}/` (app config), `/shared/`, `/nodes/`
83
+ - Backends: full credentials + storage location (e.g., Gitee: token + owner + repo + branch)
84
+ - Can reference data-sync groups via `.meta/app-data-groups/{appId}/`
85
+
86
+ **Data-sync group**:
87
+ - Synced content: application data files only (no config meta layer)
88
+ - 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
+
92
+ **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
+ | `.meta/group-type` | Behavior |
95
+ |---|---|
96
+ | `config-sync` | Full system: IndexedDB primary + config replicas + optional data-sync groups |
97
+ | `data-sync` | Lightweight: data backends only, direct read/write, no config meta layer |
98
+ | absent | New empty backend; caller decides group type at creation time |
99
+
50
100
  ## 3. File System Structure
51
101
 
102
+ ### 3.1 Config-Sync Group
103
+
52
104
  ```
53
105
  /
54
106
  ├─ .meta/ [synced to replicas]
55
- │ ├─ backends/ Backend topology (one file per backend)
56
- ├─ local-idb.json { id, type, options, description }
107
+ │ ├─ group-type Group type marker: "config-sync"
108
+ │ ├─ backends/ Backend topology (one file per backend)
109
+ │ │ ├─ local-idb.json { id, type, options, description }
57
110
  │ │ ├─ gitee-prod.json
58
111
  │ │ └─ ...
59
- │ ├─ .deleted/ Tombstones for deletion propagation
112
+ │ ├─ app-data-groups/ References to data-sync groups (per app)
113
+ │ │ └─ {appId}/
114
+ │ │ └─ {dataGroupId}.json { groupType: "data-sync", backends: [...] }
115
+ │ ├─ .deleted/ Tombstones for deletion propagation
60
116
  │ │ └─ {encoded-path}.json
61
- │ └─ .conflicts/ Conflict archives (safekeeping)
117
+ │ └─ .conflicts/ Conflict archives (safekeeping)
62
118
  │ └─ {timestamp}_{path}/
63
119
  │ ├─ meta.json
64
120
  │ ├─ source
@@ -81,7 +137,27 @@ External input at startup is limited to: **which backend to connect to** and opt
81
137
  └─ .node-id Current node's ID (auto-generated)
82
138
  ```
83
139
 
84
- ### 3.1 Directory Semantics
140
+ ### 3.2 Data-Sync Group
141
+
142
+ ```
143
+ /
144
+ ├─ .meta/ [synced to replicas]
145
+ │ ├─ group-type Group type marker: "data-sync"
146
+ │ └─ backends/ Data backend topology (one file per backend)
147
+ │ ├─ gitee-data-1.json { id, type, options, description }
148
+ │ └─ gitee-data-2.json
149
+
150
+ └─ (application data files) [synced: bi-directional]
151
+ ├─ documents/
152
+ │ ├─ note-1.json
153
+ │ └─ note-2.json
154
+ └─ media/
155
+ └─ config.json
156
+ ```
157
+
158
+ A data-sync group has a much simpler structure: no version sidecars, no tombstones, no conflict archives — just raw data files and a minimal `.meta/` for backend topology and group type identification.
159
+
160
+ ### 3.3 Directory Semantics (Config-Sync Group)
85
161
 
86
162
  | Directory | Sync Direction | Conflict Risk | Purpose |
87
163
  |---|---|---|---|
@@ -90,7 +166,7 @@ External input at startup is limited to: **which backend to connect to** and opt
90
166
  | `/nodes/` | None (by default) | None | Per-node local config |
91
167
  | `/.meta/` | Bi-directional | None (topology files) | Backend topology, tombstones, conflict archives |
92
168
 
93
- ### 3.2 Config-to-File Mapping
169
+ ### 3.4 Config-to-File Mapping
94
170
 
95
171
  Each config key maps to one file. The mapping is straightforward:
96
172
 
@@ -99,7 +175,7 @@ Each config key maps to one file. The mapping is straightforward:
99
175
  - Path is relative to the app's root (`/{appId}/`), with `.json` extension appended automatically
100
176
  - If path already has an extension (e.g., `/readme.md`), the extension is preserved
101
177
 
102
- ### 3.3 Serialization
178
+ ### 3.5 Serialization
103
179
 
104
180
  The serializer is determined by file extension:
105
181
 
@@ -140,6 +216,53 @@ The local IndexedDB backend (`local-idb`) is always the primary — all config o
140
216
 
141
217
  **Migration**: If a legacy `.meta/backends.json` file exists (pre-0.4.0), it is automatically migrated to individual files on startup.
142
218
 
219
+ ## 4.1 App Data Groups (`.meta/app-data-groups/{appId}/`)
220
+
221
+ A config-sync group can reference data-sync groups on behalf of specific apps. Each reference is stored as a JSON file under `.meta/app-data-groups/{appId}/`.
222
+
223
+ **`.meta/app-data-groups/my-app/data-store-1.json`**:
224
+ ```json
225
+ {
226
+ "id": "data-store-1",
227
+ "groupType": "data-sync",
228
+ "backends": [
229
+ {
230
+ "id": "gitee-data",
231
+ "type": "Gitee",
232
+ "options": { "token": "...", "owner": "...", "repo": "my-app-data", "branch": "main" },
233
+ "accountBackendId": "gitee-prod",
234
+ "description": "App data on Gitee (reuses gitee-prod account)"
235
+ }
236
+ ]
237
+ }
238
+ ```
239
+
240
+ ### Account Reuse
241
+
242
+ The `accountBackendId` field (optional) references a config-sync backend whose account fields (e.g., `token`, `owner`, `baseUrl` for Gitee/GitHub) are reused. When present, the data-sync backend's `options` only need to specify the **storage location** fields (e.g., `repo`, `branch`); account fields are merged in from the referenced config-sync backend at creation time.
243
+
244
+ When `accountBackendId` is absent or null, the data-sync backend must provide a complete set of options (including credentials).
245
+
246
+ The `accountFields` metadata for each backend type (registered via `BackendMetadata.accountFields`) determines which fields are "account" vs "storage location":
247
+
248
+ | Backend Type | Account Fields | Storage Location Fields |
249
+ |---|---|---|
250
+ | Gitee | `token`, `owner`, `baseUrl` | `repo`, `branch` |
251
+ | GitHub | `token`, `owner`, `baseUrl` | `repo`, `branch` |
252
+ | WebDAV | `url`, `username`, `password` | `rootPath` |
253
+ | RemoteStorage | `userAddress`, `token` | (none) |
254
+
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
+
143
266
  ## 5. Sync Rules (`.meta/sync-rules.json`)
144
267
 
145
268
  ```json
@@ -376,13 +499,88 @@ interface ConfigRepo {
376
499
  /** Sync .meta/ files to all replicas */
377
500
  syncMetaToReplicas(): Promise<void>;
378
501
 
502
+ // --- App Data Storage (data-sync groups) ---
503
+
504
+ /**
505
+ * Create a data-sync group for this app, referencing a config-sync backend's account.
506
+ * The data-sync group gets its own set of backends (typically reusing an account
507
+ * from a config-sync backend but with different storage location like repo/branch).
508
+ *
509
+ * @param id Data group ID (e.g., "data-store-1")
510
+ * @param backends Array of backend descriptors for the data-sync group.
511
+ * Each can optionally specify `accountBackendId` to reuse credentials.
512
+ */
513
+ createAppDataGroup(
514
+ id: string,
515
+ backends: AppDataBackendDescriptor[],
516
+ ): Promise<AppDataGroup>;
517
+
518
+ /**
519
+ * Get an existing data-sync group for this app.
520
+ * Returns a handle with its own fs for reading/writing data files.
521
+ */
522
+ getAppDataGroup(id: string): Promise<AppDataGroup>;
523
+
524
+ /** List all data-sync groups registered for this app. */
525
+ listAppDataGroups(): Promise<AppDataGroupDescriptor[]>;
526
+
527
+ /** Remove a data-sync group (stops sync, removes descriptor). */
528
+ removeAppDataGroup(id: string): Promise<void>;
529
+
379
530
  /** Dispose: stop sync, release resources */
380
531
  dispose(): Promise<void>;
381
532
  }
533
+
534
+ /**
535
+ * A data-sync group handle. Provides direct file system access
536
+ * to the app's data storage, independent of the config-sync layer.
537
+ */
538
+ interface AppDataGroup {
539
+ readonly groupId: string;
540
+ readonly appId: string;
541
+ /** Direct fs for reading/writing data files (chroot to this group's root) */
542
+ readonly fs: typeof import('node:fs');
543
+ /** Get sync status for this data group's sync pairs */
544
+ getSyncStatuses(): Map<string, SyncPairStatus>;
545
+ /** Manually flush pending sync */
546
+ flush(): Promise<SyncResult[]>;
547
+ /** Stop sync and release resources */
548
+ dispose(): Promise<void>;
549
+ }
550
+
551
+ /** Descriptor for a backend within a data-sync group. */
552
+ interface AppDataBackendDescriptor {
553
+ id: string;
554
+ type: string;
555
+ options: Record<string, unknown>;
556
+ /** Optional: reuse account fields from a config-sync backend */
557
+ accountBackendId?: string;
558
+ description?: string;
559
+ }
382
560
  ```
383
561
 
384
562
  ## 10. Initialization
385
563
 
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.
565
+
566
+ ### `connect` (recommended — auto-detect)
567
+
568
+ ```typescript
569
+ import { connect } from 'zen-fs-config';
570
+
571
+ // User provides a backend — connect auto-detects config-sync vs data-sync
572
+ const result = await connect('my-app', {
573
+ backendInfo: {
574
+ type: 'Gitee',
575
+ options: { token: '...', owner: '...', repo: '...', branch: 'main' },
576
+ },
577
+ });
578
+
579
+ // result.groupType → "config-sync" or "data-sync"
580
+ // result.repo → ConfigRepo (if config-sync)
581
+ // result.dataGroup → DataSyncGroup (if data-sync)
582
+ ```
583
+
386
584
  ### Zero-parameter (offline-first)
387
585
 
388
586
  ```typescript
@@ -436,8 +634,62 @@ const backends = await repo.getBackends();
436
634
  // backends.backends = [{ id: 'local-idb', ... }, { id: 's3-backup', ... }]
437
635
  ```
438
636
 
637
+ ### Standalone Data-Sync Group (no config layer)
638
+
639
+ ```typescript
640
+ import { createDataSyncGroup } from 'zen-fs-config';
641
+
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
+ });
649
+
650
+ // Read/write data files directly
651
+ await dataGroup.fs.promises.writeFile('/notes/todo.json', JSON.stringify({ task: 'buy milk' }));
652
+ const data = JSON.parse(await dataGroup.fs.promises.readFile('/notes/todo.json', 'utf-8'));
653
+
654
+ // Add more data backends later (multi-backend sync)
655
+ await dataGroup.addBackend('gitee-backup', 'Gitee', {
656
+ token: '...', owner: '...', repo: 'my-app-data-backup', branch: 'main',
657
+ });
658
+
659
+ // Cleanup
660
+ await dataGroup.dispose();
661
+ ```
662
+
663
+ ### Config-Sync with App Data Group (account reuse)
664
+
665
+ ```typescript
666
+ const repo = await createConfigRepo('my-app', {
667
+ backendInfo: {
668
+ type: 'Gitee',
669
+ options: { token: '...', owner: '...', repo: 'configs', branch: 'main' },
670
+ },
671
+ });
672
+
673
+ // Create a data-sync group that reuses the config backend's account
674
+ // but stores data in a different repo
675
+ await repo.createAppDataGroup('data-store-1', [
676
+ {
677
+ id: 'gitee-data',
678
+ type: 'Gitee',
679
+ accountBackendId: 'gitee-prod', // reuse token + owner from this config backend
680
+ options: { repo: 'my-app-data', branch: 'main' }, // only storage location
681
+ },
682
+ ]);
683
+
684
+ // Get the data group handle for direct file access
685
+ const dataGroup = await repo.getAppDataGroup('data-store-1');
686
+ await dataGroup.fs.promises.writeFile('/cache.json', '{"key":"value"}');
687
+ ```
688
+
439
689
  ## 11. Initialization Flow
440
690
 
691
+ ### 11.1 Config-Sync Group (`createConfigRepo`)
692
+
441
693
  ```
442
694
  createConfigRepo('my-app', options?)
443
695
 
@@ -446,9 +698,9 @@ createConfigRepo('my-app', options?)
446
698
 
447
699
  ├─ 2. Ensure /.meta/ directory exists
448
700
 
449
- ├─ 3. Migrate legacy .meta/backends.json .meta/backends/*.json (if exists)
701
+ ├─ 3. Write /.meta/group-type = "config-sync" (if not exists)
450
702
 
451
- ├─ 4. Ensure local-idb descriptor exists in .meta/backends/
703
+ ├─ 4. Migrate legacy .meta/backends.json .meta/backends/*.json (if exists)
452
704
 
453
705
  ├─ 5. If options.backendInfo provided:
454
706
  │ ├─ Generate replica ID (options.primaryBackendId or auto)
@@ -456,7 +708,7 @@ createConfigRepo('my-app', options?)
456
708
 
457
709
  ├─ 6. Read all backend descriptors from .meta/backends/
458
710
 
459
- ├─ 7. Determine nodeId (explicit > env > auto-generated .node-id file)
711
+ ├─ 7. Determine nodeId (explicit > localStorage > auto-generated)
460
712
 
461
713
  ├─ 8. Create ConfigRepo with primary = 'local-idb'
462
714
 
@@ -465,11 +717,104 @@ createConfigRepo('my-app', options?)
465
717
  │ ├─ Create SyncPair(IndexedDB, replica, bi-directional)
466
718
  │ └─ syncEngine.watch(pairId)
467
719
 
468
- ├─ 10. syncMetaToReplicas: push .meta/ changes to all replicas
720
+ ├─ 10. Load app data group references from .meta/app-data-groups/{appId}/
721
+ │ For each referenced data-sync group:
722
+ │ ├─ Create data-sync backends (merge account fields if accountBackendId set)
723
+ │ ├─ Create SyncPair for each data backend
724
+ │ └─ syncEngine.watch(pairId)
725
+
726
+ ├─ 11. syncMetaToReplicas: push .meta/ changes to all replicas (background)
727
+
728
+ └─ 12. Load config cache from IndexedDB
729
+ ```
730
+
731
+ ### 11.2 Standalone Data-Sync Group (`createDataSyncGroup`)
732
+
733
+ ```
734
+ createDataSyncGroup('my-app', options?)
735
+
736
+ ├─ 1. Connect to user-provided backend (options.backendInfo)
469
737
 
470
- └─ 11. Load config cache from IndexedDB
738
+ ├─ 2. Read /.meta/group-type
739
+ │ ├─ "data-sync" → existing group, read .meta/backends/ for all data backends
740
+ │ ├─ absent → new group, write /.meta/group-type = "data-sync"
741
+ │ └─ "config-sync" → error: this is a config-sync backend, use createConfigRepo()
742
+
743
+ ├─ 3. Create IndexedDB as local primary (for offline access)
744
+
745
+ ├─ 4. Setup sync: IndexedDB ↔ each data backend (bi-directional)
746
+
747
+ └─ 5. Return DataSyncGroup handle with direct fs access
471
748
  ```
472
749
 
750
+ ### 11.3 Unified Entry Point (`connect`)
751
+
752
+ `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:
753
+
754
+ ```
755
+ connect('my-app', options?)
756
+
757
+ ├─ 1. Connect to user-provided backend (options.backendInfo)
758
+
759
+ ├─ 2. Read /.meta/group-type
760
+
761
+ ├─ "config-sync" → dispatch to createConfigRepo()
762
+ │ return { groupType: "config-sync", repo }
763
+
764
+ ├─ "data-sync" → dispatch to createDataSyncGroup()
765
+ │ return { groupType: "data-sync", dataGroup }
766
+
767
+ └─ absent → new empty backend
768
+ ├─ options.groupType === "data-sync" → dispatch to createDataSyncGroup()
769
+ ├─ options.groupType === "config-sync" (or omitted) → dispatch to createConfigRepo()
770
+ └─ default: config-sync
771
+ ```
772
+
773
+ **Usage**:
774
+
775
+ ```typescript
776
+ import { connect } from 'zen-fs-config';
777
+
778
+ // Auto-detect: connects to backend, reads group-type, dispatches accordingly
779
+ const result = await connect('my-app', {
780
+ backendInfo: {
781
+ type: 'Gitee',
782
+ options: { token: '...', owner: '...', repo: '...', branch: 'main' },
783
+ },
784
+ });
785
+
786
+ if (result.groupType === 'config-sync') {
787
+ // result.repo is a ConfigRepo — full config system
788
+ const repo = result.repo;
789
+ repo.setConfig('/db/host', { hostname: 'localhost' });
790
+ } else {
791
+ // result.dataGroup is a DataSyncGroup — lightweight data-only system
792
+ const dataGroup = result.dataGroup;
793
+ await dataGroup.fs.promises.writeFile('/data.json', '{"key":"value"}');
794
+ }
795
+
796
+ // Explicit override (skip detection, force a specific group type)
797
+ const result = await connect('my-app', {
798
+ backendInfo: { type: 'Gitee', options: {...} },
799
+ groupType: 'data-sync', // force data-sync even if backend has no group-type yet
800
+ });
801
+ ```
802
+
803
+ **Return type**:
804
+
805
+ ```typescript
806
+ interface ConnectResult {
807
+ /** Detected or forced group type */
808
+ groupType: 'config-sync' | 'data-sync';
809
+ /** Present when groupType === "config-sync" */
810
+ repo?: ConfigRepo;
811
+ /** Present when groupType === "data-sync" */
812
+ dataGroup?: DataSyncGroup;
813
+ }
814
+ ```
815
+
816
+ **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).
817
+
473
818
  ## 12. Data Flow
474
819
 
475
820
  ### Read Path