zen-fs-config 0.5.4 → 0.5.6
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 +179 -12
- package/dist/index.d.mts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +79 -48
- package/dist/index.mjs +79 -48
- package/package.json +1 -1
package/DESIGN.md
CHANGED
|
@@ -748,12 +748,11 @@ The sync engine (`zen-fs-sync`) uses **snapshot-based change detection**. When `
|
|
|
748
748
|
|
|
749
749
|
1. Builds a snapshot of the source (IndexedDB) — walks all files, records `path`, `size`, `mtimeMs`
|
|
750
750
|
2. Builds a snapshot of the target (remote backend) — same process
|
|
751
|
-
3. **
|
|
752
|
-
4. Caches this merged snapshot as `sourceSnapshots`
|
|
751
|
+
3. Caches **separate** snapshots: `prevSrcSnap` (source) and `prevTgtSnap` (target)
|
|
753
752
|
|
|
754
|
-
On the next `syncAll()`, `syncBidirectional()` compares
|
|
753
|
+
On the next `syncAll()`, `syncBidirectional()` compares each side's current snapshot with its own previous snapshot independently. If both sides are unchanged → **"unchanged" → skip sync entirely**.
|
|
755
754
|
|
|
756
|
-
**The problem**: `buildInitialSnapshots()` only *reads* file metadata — it does NOT copy any files. So if the remote has files that the local doesn't (e.g., duplicate backend descriptors written by another node), the
|
|
755
|
+
**The problem**: `buildInitialSnapshots()` only *reads* file metadata — it does NOT copy any files. So if the remote has files that the local doesn't (e.g., duplicate backend descriptors written by another node), the cached snapshots reflect the un-synced state. The subsequent sync sees "snapshots already match this state" and skips — the file is never actually copied to local, and local-only dedup logic never runs.
|
|
757
756
|
|
|
758
757
|
**The fix**: Always perform a full `syncAll()` **before** `watch()`. With no cached snapshot, `syncBidirectional()` does a complete comparison and copies all missing files. After sync completes, `watch()` builds snapshots from the now-consistent state.
|
|
759
758
|
|
|
@@ -885,8 +884,8 @@ watch() triggers:
|
|
|
885
884
|
│ → triggers debounced sync (default 300ms)
|
|
886
885
|
│
|
|
887
886
|
├─ 2. buildInitialSnapshots()
|
|
888
|
-
│ ├─ BiDirectional:
|
|
889
|
-
│ └─ OneWay:
|
|
887
|
+
│ ├─ BiDirectional: cache separate source and target snapshots
|
|
888
|
+
│ └─ OneWay: cache source snapshot only
|
|
890
889
|
│
|
|
891
890
|
└─ 3. Start poll timers (if backend supports shouldSync)
|
|
892
891
|
├─ Remote backends poll shouldSync() every pollIntervalMs (default 30min)
|
|
@@ -896,12 +895,15 @@ watch() triggers:
|
|
|
896
895
|
**State guard**: If `unwatch()` is called during `buildInitialSnapshots()` (which is async), the snapshots are discarded — they won't be cached. This prevents stale snapshots from causing sync skips.
|
|
897
896
|
|
|
898
897
|
**Snapshot comparison** in `syncBidirectional()`:
|
|
899
|
-
1. Build current snapshots of both sides
|
|
900
|
-
2.
|
|
901
|
-
|
|
902
|
-
-
|
|
903
|
-
-
|
|
904
|
-
|
|
898
|
+
1. Build current snapshots of both sides (via `getSnapshot()`)
|
|
899
|
+
2. Compare each side independently against its own previous snapshot:
|
|
900
|
+
- `srcChanged = !snapshotsEqual(prevSrcSnap, currentSrcSnap)`
|
|
901
|
+
- `tgtChanged = !snapshotsEqual(prevTgtSnap, currentTgtSnap)`
|
|
902
|
+
- If neither changed and both previous snapshots exist → skip sync entirely
|
|
903
|
+
3. Cache current snapshots as `prevSrcSnap` and `prevTgtSnap` for next comparison
|
|
904
|
+
4. If either side changed, proceed with full diff and file operations
|
|
905
|
+
|
|
906
|
+
**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).
|
|
905
907
|
|
|
906
908
|
### 11.8 Standalone Data-Sync Group (`createDataSyncGroup`)
|
|
907
909
|
|
|
@@ -995,6 +997,110 @@ interface ConnectResult {
|
|
|
995
997
|
|
|
996
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).
|
|
997
999
|
|
|
1000
|
+
### 11.10 Snapshot Optimization Design
|
|
1001
|
+
|
|
1002
|
+
This section describes three interrelated optimizations to the sync engine's snapshot mechanism.
|
|
1003
|
+
|
|
1004
|
+
#### 11.10.1 FS-Provided `createSnapshot()`
|
|
1005
|
+
|
|
1006
|
+
The `SyncableFS` interface now includes an optional `createSnapshot()` method:
|
|
1007
|
+
|
|
1008
|
+
```typescript
|
|
1009
|
+
interface SyncableFS {
|
|
1010
|
+
// ... existing methods ...
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Optional: Build a filesystem snapshot.
|
|
1014
|
+
* Returns a map of relative path → {size, mtimeMs} for all files under root.
|
|
1015
|
+
* Returns null if the filesystem is unreachable.
|
|
1016
|
+
*
|
|
1017
|
+
* Backends that can provide a more efficient snapshot than the generic
|
|
1018
|
+
* walkFiles+stat approach should implement this method.
|
|
1019
|
+
*/
|
|
1020
|
+
createSnapshot?(root: string, filter?: SyncFilter): Promise<Map<string, FileSnapshot> | null>;
|
|
1021
|
+
}
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
The sync engine's `getSnapshot()` helper dispatches to the FS-provided method when available, falling back to the generic `buildSnapshot()` (walkFiles + stat) otherwise:
|
|
1025
|
+
|
|
1026
|
+
```typescript
|
|
1027
|
+
private async getSnapshot(fs: SyncableFS): Promise<Map<string, FileSnapshot> | null> {
|
|
1028
|
+
if (fs.createSnapshot) {
|
|
1029
|
+
return fs.createSnapshot(this.root, this.options.filter);
|
|
1030
|
+
}
|
|
1031
|
+
return buildSnapshot(fs, this.root, this.options.filter);
|
|
1032
|
+
}
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
**Optimization examples**:
|
|
1036
|
+
- **Gitee/GitHub**: Use Git tree API to fetch all file metadata in a single request instead of walking files one by one
|
|
1037
|
+
- **IndexedDB**: Use `getAll()` for batch querying instead of individual `stat()` calls
|
|
1038
|
+
- **InMemory**: Directly iterate the internal Map (no async I/O overhead)
|
|
1039
|
+
|
|
1040
|
+
Backends that do not implement `createSnapshot()` are fully supported — the generic fallback produces identical results.
|
|
1041
|
+
|
|
1042
|
+
#### 11.10.2 Separate Source and Target Snapshots
|
|
1043
|
+
|
|
1044
|
+
**Previous design** (merged snapshots):
|
|
1045
|
+
- `buildInitialSnapshots()` merged source and target into a single map: `sourceSnapshots = new Map([...srcSnap, ...tgtSnap])`
|
|
1046
|
+
- `syncBidirectional()` compared `currentMerged` with the cached merged snapshot
|
|
1047
|
+
- **Problem**: The merged map lost which filesystem a file belonged to. A file present on target but not source could be "new on target" or "deleted from source" — the merged snapshot couldn't distinguish.
|
|
1048
|
+
|
|
1049
|
+
**New design** (separate snapshots):
|
|
1050
|
+
- `buildInitialSnapshots()` caches two independent maps: `prevSrcSnap` and `prevTgtSnap`
|
|
1051
|
+
- `syncBidirectional()` compares each side independently:
|
|
1052
|
+
```
|
|
1053
|
+
srcChanged = !snapshotsEqual(prevSrcSnap, currentSrcSnap)
|
|
1054
|
+
tgtChanged = !snapshotsEqual(prevTgtSnap, currentTgtSnap)
|
|
1055
|
+
if (!srcChanged && !tgtChanged && prevSrcSnap && prevTgtSnap) → skip sync
|
|
1056
|
+
```
|
|
1057
|
+
- Each side's change is detected independently, preserving file-location information
|
|
1058
|
+
|
|
1059
|
+
#### 11.10.3 Bidirectional Deletion Propagation
|
|
1060
|
+
|
|
1061
|
+
When a file exists on one side but not the other, the sync engine uses previous snapshots to distinguish "created" from "deleted":
|
|
1062
|
+
|
|
1063
|
+
```
|
|
1064
|
+
File on target, not on source:
|
|
1065
|
+
├─ Was it on source in the previous snapshot (prevSrcSnap)?
|
|
1066
|
+
│ ├─ Yes → file was deleted from source → delete from target too (propagate deletion)
|
|
1067
|
+
│ └─ No → file was created on target → copy to source
|
|
1068
|
+
|
|
1069
|
+
File on source, not on target:
|
|
1070
|
+
├─ Was it on target in the previous snapshot (prevTgtSnap)?
|
|
1071
|
+
│ ├─ Yes → file was deleted from target → delete from source too (propagate deletion)
|
|
1072
|
+
│ └─ No → file was created on source → copy to target
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
**Without this mechanism**, deleting a file on one side would cause the sync engine to see "the other side still has it → copy it back", effectively undoing the deletion.
|
|
1076
|
+
|
|
1077
|
+
**Relationship with tombstones**: The tombstone mechanism (§11.4) and bidirectional deletion propagation operate at different layers and complement each other:
|
|
1078
|
+
|
|
1079
|
+
| Mechanism | Layer | Trigger | How It Works |
|
|
1080
|
+
|-----------|-------|---------|--------------|
|
|
1081
|
+
| Tombstone | `zen-fs-config` (application) | Application calls `deleteFile()` | Writes a `.meta/.deleted/` marker, physically deletes file on all replicas **before** sync runs |
|
|
1082
|
+
| Deletion propagation | `zen-fs-sync` (engine) | Sync detects one-side-only file | Compares with previous snapshot to determine if file was created or deleted |
|
|
1083
|
+
|
|
1084
|
+
Tombstones handle application-initiated deletions (the common case). Deletion propagation handles deletions that bypass the tombstone flow — e.g., external modifications on the remote backend, or files removed by other sync mechanisms.
|
|
1085
|
+
|
|
1086
|
+
#### 11.10.4 `shouldSync()` vs Snapshot Comparison
|
|
1087
|
+
|
|
1088
|
+
These two mechanisms are complementary, not interchangeable:
|
|
1089
|
+
|
|
1090
|
+
| Mechanism | Purpose | Cost | When Used |
|
|
1091
|
+
|-----------|---------|------|-----------|
|
|
1092
|
+
| `shouldSync()` | Fast "has anything changed?" boolean | O(1) for remote (ETag/commit check) | `onRemotePoll()` — decide whether to trigger sync at all |
|
|
1093
|
+
| Snapshot comparison | "What exactly changed?" detail | O(n) filesystem traversal | `syncBidirectional()` — decide what to copy/delete |
|
|
1094
|
+
|
|
1095
|
+
`shouldSync()` is **not** part of the snapshot comparison because:
|
|
1096
|
+
1. `shouldSync()` updates its internal baseline after each call — calling it again during sync would return stale results
|
|
1097
|
+
2. `shouldSync()` returning false doesn't guarantee the snapshot is unchanged — it means the FS's own change detection says nothing changed, which could miss edge cases
|
|
1098
|
+
3. `shouldSync()` returning true doesn't tell us **which** files changed — snapshots are still needed for that
|
|
1099
|
+
|
|
1100
|
+
The two-level optimization works as follows:
|
|
1101
|
+
1. **Level 1**: `shouldSync()` in remote poll → if false, skip sync trigger entirely (saves the O(n) snapshot build)
|
|
1102
|
+
2. **Level 2**: Separate snapshot comparison in sync → if both sides unchanged, skip file operations (saves I/O)
|
|
1103
|
+
|
|
998
1104
|
## 12. Data Flow
|
|
999
1105
|
|
|
1000
1106
|
### Read Path
|
|
@@ -1041,6 +1147,67 @@ Application
|
|
|
1041
1147
|
→ Dispose temporary SyncPair
|
|
1042
1148
|
```
|
|
1043
1149
|
|
|
1150
|
+
### Mtime Preservation During Sync
|
|
1151
|
+
|
|
1152
|
+
**Problem**: When the sync engine copies a file from source to target, it calls `writeFile(path, data)`. The target backend sets its own mtime (typically `Date.now()`), losing the source file's original mtime. This causes the next sync cycle to detect a "modified" file (source mtime ≠ target mtime), triggering unnecessary copies on every sync.
|
|
1153
|
+
|
|
1154
|
+
**Solution**: An optional `writeFileWithMtime` method on the `SyncableFS` interface, with automatic fallback to `writeFile` when not implemented:
|
|
1155
|
+
|
|
1156
|
+
```typescript
|
|
1157
|
+
interface SyncableFS {
|
|
1158
|
+
// ... existing methods ...
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* Optional: write file with precise mtime.
|
|
1162
|
+
* If implemented, the sync engine uses this instead of writeFile,
|
|
1163
|
+
* passing the source file's mtime so the target can preserve it.
|
|
1164
|
+
* Backends that don't support precise mtime should not implement this —
|
|
1165
|
+
* the sync engine falls back to plain writeFile.
|
|
1166
|
+
*/
|
|
1167
|
+
writeFileWithMtime?(path: string, data: string | Uint8Array, mtime: number): Promise<void>;
|
|
1168
|
+
}
|
|
1169
|
+
```
|
|
1170
|
+
|
|
1171
|
+
**Sync engine (`zen-fs-sync`)**: A central helper function handles the fallback:
|
|
1172
|
+
|
|
1173
|
+
```javascript
|
|
1174
|
+
async function writeFileWithMtimeFallback(fs, path, data, mtimeMs) {
|
|
1175
|
+
if (mtimeMs !== undefined && typeof fs.writeFileWithMtime === "function") {
|
|
1176
|
+
await fs.writeFileWithMtime(path, data, mtimeMs);
|
|
1177
|
+
} else {
|
|
1178
|
+
await fs.writeFile(path, data);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
```
|
|
1182
|
+
|
|
1183
|
+
This helper is used in `copyFile()`, `syncOneWay()`, and `writeFileBoth()`. The source file's mtime is obtained via `stat()` before writing, then passed through to the target.
|
|
1184
|
+
|
|
1185
|
+
**Adapters (`zen-fs-config`)**: All three `SyncableFS` adapters implement `writeFileWithMtime`:
|
|
1186
|
+
|
|
1187
|
+
| Adapter | Implementation |
|
|
1188
|
+
|---|---|
|
|
1189
|
+
| `backendToSyncableFS` | Passes `{ mtime }` as options to `backend.writeFile()` — the backend's `writeFile` calls `touch()` with the provided mtime |
|
|
1190
|
+
| `zenfsPromisesToSyncableFS` | Calls `promises.writeFile()` then `promises.utimes()` as a fallback (some VFS backends don't support mtime in writeFile) |
|
|
1191
|
+
| `cachedFSToSyncableFS` | Passes `{ mtime }` as options to `cached.writeFile()` — mtime flows through to the underlying backend |
|
|
1192
|
+
|
|
1193
|
+
**RemoteStorage backend (`zen-fs-remotestoragejs`)**: `writeFileWithMtime` delegates to `writeFile(path, data, { mtime })`, which writes the `.mtime` sidecar file to preserve millisecond-precision mtime (see RemoteStorage DESIGN.md §2 for details).
|
|
1194
|
+
|
|
1195
|
+
**Data flow**:
|
|
1196
|
+
```
|
|
1197
|
+
Source file: /app-a/db.json (mtime=1700000000123)
|
|
1198
|
+
│
|
|
1199
|
+
├─ sync engine: stat("/app-a/db.json") → mtimeMs=1700000000123
|
|
1200
|
+
├─ sync engine: readFile("/app-a/db.json") → data
|
|
1201
|
+
├─ sync engine: writeFileWithMtimeFallback(target, "/app-a/db.json", data, 1700000000123)
|
|
1202
|
+
│ ├─ target has writeFileWithMtime? → YES → target.writeFileWithMtime(path, data, 1700000000123)
|
|
1203
|
+
│ │ → backend.writeFile(path, data, { mtime: 1700000000123 })
|
|
1204
|
+
│ │ → touch(path, { mtimeMs: 1700000000123 })
|
|
1205
|
+
│ └─ target has writeFileWithMtime? → NO → target.writeFile(path, data) [fallback]
|
|
1206
|
+
│
|
|
1207
|
+
└─ Target file: /app-a/db.json (mtime=1700000000123) ← preserved!
|
|
1208
|
+
→ Next sync: source.mtimeMs === target.mtimeMs → skip (no spurious copy)
|
|
1209
|
+
```
|
|
1210
|
+
|
|
1044
1211
|
## 13. Peer Dependencies
|
|
1045
1212
|
|
|
1046
1213
|
| Package | Role | Version | Required |
|
package/dist/index.d.mts
CHANGED
|
@@ -522,6 +522,12 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
522
522
|
readConflictBackup(conflictId: string, fileType: 'source' | 'target' | 'resolved'): Promise<string>;
|
|
523
523
|
dispose(): Promise<void>;
|
|
524
524
|
setupSync(backends: BackendDescriptor[], primaryBackendId: string, pollIntervalMs?: number): Promise<void>;
|
|
525
|
+
/** Write version sidecar for a config file (no-op for .version files). */
|
|
526
|
+
private writeVersionSidecar;
|
|
527
|
+
/** Delete version sidecar on a backend (no-op for .version files). */
|
|
528
|
+
private unlinkVersionSidecar;
|
|
529
|
+
/** Read version sidecar (returns null for .version files). */
|
|
530
|
+
private readVersionSidecar;
|
|
525
531
|
private persistConfig;
|
|
526
532
|
private reloadConfigCache;
|
|
527
533
|
private handleConflict;
|
|
@@ -683,8 +689,11 @@ declare function connect(appId: string, options?: ConnectOptions): Promise<Conne
|
|
|
683
689
|
* /app-a/db.json → /app-a/.db.json.version
|
|
684
690
|
* /shared/flags.json → /shared/.flags.json.version
|
|
685
691
|
* /nodes/s1/env.json → /nodes/s1/.env.json.version
|
|
692
|
+
*
|
|
693
|
+
* Returns null for files that are already version sidecars (.version files),
|
|
694
|
+
* to prevent creating version-of-version files (e.g. ..db.json.version.version).
|
|
686
695
|
*/
|
|
687
|
-
declare function versionPathFor(configFilePath: string): string;
|
|
696
|
+
declare function versionPathFor(configFilePath: string): string | null;
|
|
688
697
|
/**
|
|
689
698
|
* Compute SHA-256 hash of a Uint8Array.
|
|
690
699
|
* Returns "sha256:" prefix + hex digest.
|
package/dist/index.d.ts
CHANGED
|
@@ -522,6 +522,12 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
522
522
|
readConflictBackup(conflictId: string, fileType: 'source' | 'target' | 'resolved'): Promise<string>;
|
|
523
523
|
dispose(): Promise<void>;
|
|
524
524
|
setupSync(backends: BackendDescriptor[], primaryBackendId: string, pollIntervalMs?: number): Promise<void>;
|
|
525
|
+
/** Write version sidecar for a config file (no-op for .version files). */
|
|
526
|
+
private writeVersionSidecar;
|
|
527
|
+
/** Delete version sidecar on a backend (no-op for .version files). */
|
|
528
|
+
private unlinkVersionSidecar;
|
|
529
|
+
/** Read version sidecar (returns null for .version files). */
|
|
530
|
+
private readVersionSidecar;
|
|
525
531
|
private persistConfig;
|
|
526
532
|
private reloadConfigCache;
|
|
527
533
|
private handleConflict;
|
|
@@ -683,8 +689,11 @@ declare function connect(appId: string, options?: ConnectOptions): Promise<Conne
|
|
|
683
689
|
* /app-a/db.json → /app-a/.db.json.version
|
|
684
690
|
* /shared/flags.json → /shared/.flags.json.version
|
|
685
691
|
* /nodes/s1/env.json → /nodes/s1/.env.json.version
|
|
692
|
+
*
|
|
693
|
+
* Returns null for files that are already version sidecars (.version files),
|
|
694
|
+
* to prevent creating version-of-version files (e.g. ..db.json.version.version).
|
|
686
695
|
*/
|
|
687
|
-
declare function versionPathFor(configFilePath: string): string;
|
|
696
|
+
declare function versionPathFor(configFilePath: string): string | null;
|
|
688
697
|
/**
|
|
689
698
|
* Compute SHA-256 hash of a Uint8Array.
|
|
690
699
|
* Returns "sha256:" prefix + hex digest.
|
package/dist/index.js
CHANGED
|
@@ -250,6 +250,9 @@ function backendToSyncableFS(backend, name) {
|
|
|
250
250
|
async writeFile(path, data) {
|
|
251
251
|
return backend.writeFile(path, data);
|
|
252
252
|
},
|
|
253
|
+
async writeFileWithMtime(path, data, mtime) {
|
|
254
|
+
return backend.writeFile(path, data, { mtime });
|
|
255
|
+
},
|
|
253
256
|
async unlink(path) {
|
|
254
257
|
return backend.unlink(path);
|
|
255
258
|
},
|
|
@@ -274,6 +277,12 @@ function backendToSyncableFS(backend, name) {
|
|
|
274
277
|
if (typeof backend.shouldSync === "function") {
|
|
275
278
|
syncable.shouldSync = () => backend.shouldSync();
|
|
276
279
|
}
|
|
280
|
+
if (typeof backend.createSnapshot === "function") {
|
|
281
|
+
syncable.createSnapshot = (root, filter) => backend.createSnapshot(root, filter);
|
|
282
|
+
}
|
|
283
|
+
if (typeof backend.writeFileWithMtime === "function") {
|
|
284
|
+
syncable.writeFileWithMtime = (path, data, mtimeMs) => backend.writeFileWithMtime(path, data, mtimeMs);
|
|
285
|
+
}
|
|
277
286
|
if (typeof backend.checkForUpdates === "function") {
|
|
278
287
|
syncable.checkForUpdates = () => backend.checkForUpdates();
|
|
279
288
|
}
|
|
@@ -367,7 +376,7 @@ async function wrapZenFSFileSystem(config) {
|
|
|
367
376
|
}
|
|
368
377
|
await isolatedFS.write(path, bytes, 0);
|
|
369
378
|
try {
|
|
370
|
-
await isolatedFS.touch(path, { size: bytes.byteLength, mtimeMs: Date.now() });
|
|
379
|
+
await isolatedFS.touch(path, { size: bytes.byteLength, mtimeMs: _options?.mtime ?? Date.now() });
|
|
371
380
|
} catch {
|
|
372
381
|
}
|
|
373
382
|
notifyChange();
|
|
@@ -404,6 +413,12 @@ async function wrapZenFSFileSystem(config) {
|
|
|
404
413
|
backend.onChange = (callback) => {
|
|
405
414
|
changeCallback = callback;
|
|
406
415
|
};
|
|
416
|
+
if (typeof isolatedFS.createSnapshot === "function") {
|
|
417
|
+
backend.createSnapshot = (root, filter) => isolatedFS.createSnapshot(root, filter);
|
|
418
|
+
}
|
|
419
|
+
if (typeof isolatedFS.writeFileWithMtime === "function") {
|
|
420
|
+
backend.writeFileWithMtime = (path, data, mtimeMs) => isolatedFS.writeFileWithMtime(path, data, mtimeMs);
|
|
421
|
+
}
|
|
407
422
|
return backend;
|
|
408
423
|
}
|
|
409
424
|
var inMemoryCounter = 0;
|
|
@@ -442,6 +457,9 @@ function versionPathFor(configFilePath) {
|
|
|
442
457
|
const lastSlash = configFilePath.lastIndexOf("/");
|
|
443
458
|
const dir = lastSlash >= 0 ? configFilePath.slice(0, lastSlash) : "";
|
|
444
459
|
const fileName = lastSlash >= 0 ? configFilePath.slice(lastSlash + 1) : configFilePath;
|
|
460
|
+
if (fileName.endsWith(".version")) {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
445
463
|
const versionFileName = `.${fileName}.version`;
|
|
446
464
|
return dir ? `${dir}/${versionFileName}` : versionFileName;
|
|
447
465
|
}
|
|
@@ -478,7 +496,7 @@ async function writeVersion(fs, versionFilePath, meta) {
|
|
|
478
496
|
}
|
|
479
497
|
async function incrementVersion(fs, configFilePath, newContent, author) {
|
|
480
498
|
const vPath = versionPathFor(configFilePath);
|
|
481
|
-
const prev = await readVersion(fs, vPath);
|
|
499
|
+
const prev = vPath ? await readVersion(fs, vPath) : null;
|
|
482
500
|
const hash = await sha256(newContent);
|
|
483
501
|
return {
|
|
484
502
|
version: (prev?.version ?? 0) + 1,
|
|
@@ -489,6 +507,7 @@ async function incrementVersion(fs, configFilePath, newContent, author) {
|
|
|
489
507
|
}
|
|
490
508
|
async function verifyOrRepairVersion(fs, configFilePath, author) {
|
|
491
509
|
const vPath = versionPathFor(configFilePath);
|
|
510
|
+
if (!vPath) return null;
|
|
492
511
|
const existing = await readVersion(fs, vPath);
|
|
493
512
|
if (!existing) return null;
|
|
494
513
|
try {
|
|
@@ -760,9 +779,11 @@ var ConfigRepo = class {
|
|
|
760
779
|
} catch {
|
|
761
780
|
}
|
|
762
781
|
const versionPath = versionPathFor(normalizedPath);
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
782
|
+
if (versionPath) {
|
|
783
|
+
try {
|
|
784
|
+
await this.cachedFS.unlink(versionPath);
|
|
785
|
+
} catch {
|
|
786
|
+
}
|
|
766
787
|
}
|
|
767
788
|
console.log(`[ConfigRepo] deleteFile: ${normalizedPath} (tombstone at ${tombstonePath})`);
|
|
768
789
|
}
|
|
@@ -796,22 +817,27 @@ var ConfigRepo = class {
|
|
|
796
817
|
if (tombstones.length === 0) return;
|
|
797
818
|
console.log(`[ConfigRepo] processTombstones: ${tombstones.length} tombstone(s)`);
|
|
798
819
|
for (const tombstone of tombstones) {
|
|
820
|
+
const tVersionPath = versionPathFor(tombstone.path);
|
|
799
821
|
try {
|
|
800
822
|
await this.cachedFS.unlink(tombstone.path);
|
|
801
823
|
} catch {
|
|
802
824
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
825
|
+
if (tVersionPath) {
|
|
826
|
+
try {
|
|
827
|
+
await this.cachedFS.unlink(tVersionPath);
|
|
828
|
+
} catch {
|
|
829
|
+
}
|
|
806
830
|
}
|
|
807
831
|
for (const [replicaId, replica] of this.replicaBackends) {
|
|
808
832
|
try {
|
|
809
833
|
await replica.instance.unlink(tombstone.path);
|
|
810
834
|
} catch {
|
|
811
835
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
836
|
+
if (tVersionPath) {
|
|
837
|
+
try {
|
|
838
|
+
await replica.instance.unlink(tVersionPath);
|
|
839
|
+
} catch {
|
|
840
|
+
}
|
|
815
841
|
}
|
|
816
842
|
console.log(`[ConfigRepo] tombstone ${tombstone.path}: deleted on ${replicaId}`);
|
|
817
843
|
}
|
|
@@ -920,7 +946,7 @@ var ConfigRepo = class {
|
|
|
920
946
|
bytes,
|
|
921
947
|
author
|
|
922
948
|
);
|
|
923
|
-
await
|
|
949
|
+
await this.writeVersionSidecar(configPath, version);
|
|
924
950
|
const conflictDir = metaPath.substring(0, metaPath.lastIndexOf("/"));
|
|
925
951
|
const resolvedBackupPath = `${conflictDir}/resolved`;
|
|
926
952
|
const resolvedBytes = typeof mergedContent === "string" ? new TextEncoder().encode(mergedContent) : new TextEncoder().encode(JSON.stringify(mergedContent, null, 2));
|
|
@@ -1021,13 +1047,34 @@ var ConfigRepo = class {
|
|
|
1021
1047
|
// -----------------------------------------------------------------------
|
|
1022
1048
|
// Internal — Persistence
|
|
1023
1049
|
// -----------------------------------------------------------------------
|
|
1050
|
+
/** Write version sidecar for a config file (no-op for .version files). */
|
|
1051
|
+
async writeVersionSidecar(configPath, version) {
|
|
1052
|
+
const vPath = versionPathFor(configPath);
|
|
1053
|
+
if (!vPath) return;
|
|
1054
|
+
await this.ensureDir(vPath);
|
|
1055
|
+
await writeVersion(this.fullFS, vPath, version);
|
|
1056
|
+
}
|
|
1057
|
+
/** Delete version sidecar on a backend (no-op for .version files). */
|
|
1058
|
+
async unlinkVersionSidecar(fs, configPath) {
|
|
1059
|
+
const vPath = versionPathFor(configPath);
|
|
1060
|
+
if (!vPath) return;
|
|
1061
|
+
try {
|
|
1062
|
+
await fs.unlink(vPath);
|
|
1063
|
+
} catch {
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
/** Read version sidecar (returns null for .version files). */
|
|
1067
|
+
async readVersionSidecar(configPath) {
|
|
1068
|
+
const vPath = versionPathFor(configPath);
|
|
1069
|
+
if (!vPath) return null;
|
|
1070
|
+
return readVersion(this.fullFS, vPath);
|
|
1071
|
+
}
|
|
1024
1072
|
async persistConfig(fullPath, bytes) {
|
|
1025
1073
|
await this.ensureDir(fullPath);
|
|
1026
1074
|
await this.cachedFS.writeFile(fullPath, bytes);
|
|
1027
1075
|
const author = `${this.appId}/${this.nodeId}`;
|
|
1028
1076
|
const version = await incrementVersion(this.fullFS, fullPath, bytes, author);
|
|
1029
|
-
await this.
|
|
1030
|
-
await writeVersion(this.fullFS, versionPathFor(fullPath), version);
|
|
1077
|
+
await this.writeVersionSidecar(fullPath, version);
|
|
1031
1078
|
}
|
|
1032
1079
|
async reloadConfigCache() {
|
|
1033
1080
|
const appDir = `/${this.appId}`;
|
|
@@ -1065,7 +1112,7 @@ var ConfigRepo = class {
|
|
|
1065
1112
|
);
|
|
1066
1113
|
let sourceVersion = 0;
|
|
1067
1114
|
try {
|
|
1068
|
-
const srcVer = await
|
|
1115
|
+
const srcVer = await this.readVersionSidecar(conflict.path);
|
|
1069
1116
|
if (srcVer) sourceVersion = srcVer.version;
|
|
1070
1117
|
} catch {
|
|
1071
1118
|
}
|
|
@@ -1150,8 +1197,7 @@ var ConfigRepo = class {
|
|
|
1150
1197
|
await this.cachedFS.writeFile(path, bytes);
|
|
1151
1198
|
const author = `${this.appId}/${this.nodeId}`;
|
|
1152
1199
|
const version = await incrementVersion(this.fullFS, path, bytes, author);
|
|
1153
|
-
await this.
|
|
1154
|
-
await writeVersion(this.fullFS, versionPathFor(path), version);
|
|
1200
|
+
await this.writeVersionSidecar(path, version);
|
|
1155
1201
|
}
|
|
1156
1202
|
async readMetaFile(path) {
|
|
1157
1203
|
try {
|
|
@@ -1209,10 +1255,7 @@ var ConfigRepo = class {
|
|
|
1209
1255
|
await replica.instance.unlink(corruptPath);
|
|
1210
1256
|
} catch {
|
|
1211
1257
|
}
|
|
1212
|
-
|
|
1213
|
-
await replica.instance.unlink(versionPathFor(corruptPath));
|
|
1214
|
-
} catch {
|
|
1215
|
-
}
|
|
1258
|
+
await this.unlinkVersionSidecar(replica.instance, corruptPath);
|
|
1216
1259
|
}
|
|
1217
1260
|
try {
|
|
1218
1261
|
await this.deleteFile(corruptPath);
|
|
@@ -1221,10 +1264,7 @@ var ConfigRepo = class {
|
|
|
1221
1264
|
await this.cachedFS.unlink(corruptPath);
|
|
1222
1265
|
} catch {
|
|
1223
1266
|
}
|
|
1224
|
-
|
|
1225
|
-
await this.cachedFS.unlink(versionPathFor(corruptPath));
|
|
1226
|
-
} catch {
|
|
1227
|
-
}
|
|
1267
|
+
await this.unlinkVersionSidecar(this.cachedFS, corruptPath);
|
|
1228
1268
|
}
|
|
1229
1269
|
}
|
|
1230
1270
|
const seen = /* @__PURE__ */ new Map();
|
|
@@ -1254,10 +1294,7 @@ var ConfigRepo = class {
|
|
|
1254
1294
|
await replica.instance.unlink(descPath);
|
|
1255
1295
|
} catch {
|
|
1256
1296
|
}
|
|
1257
|
-
|
|
1258
|
-
await replica.instance.unlink(versionPathFor(descPath));
|
|
1259
|
-
} catch {
|
|
1260
|
-
}
|
|
1297
|
+
await this.unlinkVersionSidecar(replica.instance, descPath);
|
|
1261
1298
|
}
|
|
1262
1299
|
try {
|
|
1263
1300
|
await this.deleteFile(descPath);
|
|
@@ -1279,8 +1316,7 @@ var ConfigRepo = class {
|
|
|
1279
1316
|
await this.cachedFS.writeFile(path, bytes);
|
|
1280
1317
|
const author = `${this.appId}/${this.nodeId}`;
|
|
1281
1318
|
const version = await incrementVersion(this.fullFS, path, bytes, author);
|
|
1282
|
-
await this.
|
|
1283
|
-
await writeVersion(this.fullFS, versionPathFor(path), version);
|
|
1319
|
+
await this.writeVersionSidecar(path, version);
|
|
1284
1320
|
}
|
|
1285
1321
|
/** Remove a single backend descriptor file + its version sidecar */
|
|
1286
1322
|
async removeBackendDescriptor(id) {
|
|
@@ -1289,10 +1325,7 @@ var ConfigRepo = class {
|
|
|
1289
1325
|
await this.cachedFS.unlink(path);
|
|
1290
1326
|
} catch {
|
|
1291
1327
|
}
|
|
1292
|
-
|
|
1293
|
-
await this.cachedFS.unlink(versionPathFor(path));
|
|
1294
|
-
} catch {
|
|
1295
|
-
}
|
|
1328
|
+
await this.unlinkVersionSidecar(this.cachedFS, path);
|
|
1296
1329
|
}
|
|
1297
1330
|
// -----------------------------------------------------------------------
|
|
1298
1331
|
// IConfigRepo — Meta file access (no chroot)
|
|
@@ -1391,10 +1424,7 @@ var ConfigRepo = class {
|
|
|
1391
1424
|
await replica.instance.unlink(descPath);
|
|
1392
1425
|
} catch {
|
|
1393
1426
|
}
|
|
1394
|
-
|
|
1395
|
-
await replica.instance.unlink(versionPathFor(descPath));
|
|
1396
|
-
} catch {
|
|
1397
|
-
}
|
|
1427
|
+
await this.unlinkVersionSidecar(replica.instance, descPath);
|
|
1398
1428
|
try {
|
|
1399
1429
|
await this.deleteFile(descPath);
|
|
1400
1430
|
} catch {
|
|
@@ -1490,8 +1520,7 @@ var ConfigRepo = class {
|
|
|
1490
1520
|
await this.cachedFS.writeFile(descPath, bytes);
|
|
1491
1521
|
const author = `${this.appId}/${this.nodeId}`;
|
|
1492
1522
|
const version = await incrementVersion(this.fullFS, descPath, bytes, author);
|
|
1493
|
-
await this.
|
|
1494
|
-
await writeVersion(this.fullFS, versionPathFor(descPath), version);
|
|
1523
|
+
await this.writeVersionSidecar(descPath, version);
|
|
1495
1524
|
this.appDataGroups.set(id, group);
|
|
1496
1525
|
console.log(`[ConfigRepo] createAppDataGroup: "${id}" created`);
|
|
1497
1526
|
return group;
|
|
@@ -1551,10 +1580,7 @@ var ConfigRepo = class {
|
|
|
1551
1580
|
await this.cachedFS.unlink(descPath);
|
|
1552
1581
|
} catch {
|
|
1553
1582
|
}
|
|
1554
|
-
|
|
1555
|
-
await this.cachedFS.unlink(versionPathFor(descPath));
|
|
1556
|
-
} catch {
|
|
1557
|
-
}
|
|
1583
|
+
await this.unlinkVersionSidecar(this.cachedFS, descPath);
|
|
1558
1584
|
console.log(`[ConfigRepo] removeAppDataGroup: "${id}" removed`);
|
|
1559
1585
|
}
|
|
1560
1586
|
async listAccountBackends() {
|
|
@@ -1752,9 +1778,14 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
1752
1778
|
await cachedFS.unlink(BACKENDS_FILE);
|
|
1753
1779
|
} catch {
|
|
1754
1780
|
}
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1781
|
+
{
|
|
1782
|
+
const vPath = versionPathFor(BACKENDS_FILE);
|
|
1783
|
+
if (vPath) {
|
|
1784
|
+
try {
|
|
1785
|
+
await cachedFS.unlink(vPath);
|
|
1786
|
+
} catch {
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1758
1789
|
}
|
|
1759
1790
|
console.log(`[createConfigRepo] Migration complete`);
|
|
1760
1791
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -193,6 +193,9 @@ function backendToSyncableFS(backend, name) {
|
|
|
193
193
|
async writeFile(path, data) {
|
|
194
194
|
return backend.writeFile(path, data);
|
|
195
195
|
},
|
|
196
|
+
async writeFileWithMtime(path, data, mtime) {
|
|
197
|
+
return backend.writeFile(path, data, { mtime });
|
|
198
|
+
},
|
|
196
199
|
async unlink(path) {
|
|
197
200
|
return backend.unlink(path);
|
|
198
201
|
},
|
|
@@ -217,6 +220,12 @@ function backendToSyncableFS(backend, name) {
|
|
|
217
220
|
if (typeof backend.shouldSync === "function") {
|
|
218
221
|
syncable.shouldSync = () => backend.shouldSync();
|
|
219
222
|
}
|
|
223
|
+
if (typeof backend.createSnapshot === "function") {
|
|
224
|
+
syncable.createSnapshot = (root, filter) => backend.createSnapshot(root, filter);
|
|
225
|
+
}
|
|
226
|
+
if (typeof backend.writeFileWithMtime === "function") {
|
|
227
|
+
syncable.writeFileWithMtime = (path, data, mtimeMs) => backend.writeFileWithMtime(path, data, mtimeMs);
|
|
228
|
+
}
|
|
220
229
|
if (typeof backend.checkForUpdates === "function") {
|
|
221
230
|
syncable.checkForUpdates = () => backend.checkForUpdates();
|
|
222
231
|
}
|
|
@@ -310,7 +319,7 @@ async function wrapZenFSFileSystem(config) {
|
|
|
310
319
|
}
|
|
311
320
|
await isolatedFS.write(path, bytes, 0);
|
|
312
321
|
try {
|
|
313
|
-
await isolatedFS.touch(path, { size: bytes.byteLength, mtimeMs: Date.now() });
|
|
322
|
+
await isolatedFS.touch(path, { size: bytes.byteLength, mtimeMs: _options?.mtime ?? Date.now() });
|
|
314
323
|
} catch {
|
|
315
324
|
}
|
|
316
325
|
notifyChange();
|
|
@@ -347,6 +356,12 @@ async function wrapZenFSFileSystem(config) {
|
|
|
347
356
|
backend.onChange = (callback) => {
|
|
348
357
|
changeCallback = callback;
|
|
349
358
|
};
|
|
359
|
+
if (typeof isolatedFS.createSnapshot === "function") {
|
|
360
|
+
backend.createSnapshot = (root, filter) => isolatedFS.createSnapshot(root, filter);
|
|
361
|
+
}
|
|
362
|
+
if (typeof isolatedFS.writeFileWithMtime === "function") {
|
|
363
|
+
backend.writeFileWithMtime = (path, data, mtimeMs) => isolatedFS.writeFileWithMtime(path, data, mtimeMs);
|
|
364
|
+
}
|
|
350
365
|
return backend;
|
|
351
366
|
}
|
|
352
367
|
var inMemoryCounter = 0;
|
|
@@ -385,6 +400,9 @@ function versionPathFor(configFilePath) {
|
|
|
385
400
|
const lastSlash = configFilePath.lastIndexOf("/");
|
|
386
401
|
const dir = lastSlash >= 0 ? configFilePath.slice(0, lastSlash) : "";
|
|
387
402
|
const fileName = lastSlash >= 0 ? configFilePath.slice(lastSlash + 1) : configFilePath;
|
|
403
|
+
if (fileName.endsWith(".version")) {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
388
406
|
const versionFileName = `.${fileName}.version`;
|
|
389
407
|
return dir ? `${dir}/${versionFileName}` : versionFileName;
|
|
390
408
|
}
|
|
@@ -421,7 +439,7 @@ async function writeVersion(fs, versionFilePath, meta) {
|
|
|
421
439
|
}
|
|
422
440
|
async function incrementVersion(fs, configFilePath, newContent, author) {
|
|
423
441
|
const vPath = versionPathFor(configFilePath);
|
|
424
|
-
const prev = await readVersion(fs, vPath);
|
|
442
|
+
const prev = vPath ? await readVersion(fs, vPath) : null;
|
|
425
443
|
const hash = await sha256(newContent);
|
|
426
444
|
return {
|
|
427
445
|
version: (prev?.version ?? 0) + 1,
|
|
@@ -432,6 +450,7 @@ async function incrementVersion(fs, configFilePath, newContent, author) {
|
|
|
432
450
|
}
|
|
433
451
|
async function verifyOrRepairVersion(fs, configFilePath, author) {
|
|
434
452
|
const vPath = versionPathFor(configFilePath);
|
|
453
|
+
if (!vPath) return null;
|
|
435
454
|
const existing = await readVersion(fs, vPath);
|
|
436
455
|
if (!existing) return null;
|
|
437
456
|
try {
|
|
@@ -703,9 +722,11 @@ var ConfigRepo = class {
|
|
|
703
722
|
} catch {
|
|
704
723
|
}
|
|
705
724
|
const versionPath = versionPathFor(normalizedPath);
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
725
|
+
if (versionPath) {
|
|
726
|
+
try {
|
|
727
|
+
await this.cachedFS.unlink(versionPath);
|
|
728
|
+
} catch {
|
|
729
|
+
}
|
|
709
730
|
}
|
|
710
731
|
console.log(`[ConfigRepo] deleteFile: ${normalizedPath} (tombstone at ${tombstonePath})`);
|
|
711
732
|
}
|
|
@@ -739,22 +760,27 @@ var ConfigRepo = class {
|
|
|
739
760
|
if (tombstones.length === 0) return;
|
|
740
761
|
console.log(`[ConfigRepo] processTombstones: ${tombstones.length} tombstone(s)`);
|
|
741
762
|
for (const tombstone of tombstones) {
|
|
763
|
+
const tVersionPath = versionPathFor(tombstone.path);
|
|
742
764
|
try {
|
|
743
765
|
await this.cachedFS.unlink(tombstone.path);
|
|
744
766
|
} catch {
|
|
745
767
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
768
|
+
if (tVersionPath) {
|
|
769
|
+
try {
|
|
770
|
+
await this.cachedFS.unlink(tVersionPath);
|
|
771
|
+
} catch {
|
|
772
|
+
}
|
|
749
773
|
}
|
|
750
774
|
for (const [replicaId, replica] of this.replicaBackends) {
|
|
751
775
|
try {
|
|
752
776
|
await replica.instance.unlink(tombstone.path);
|
|
753
777
|
} catch {
|
|
754
778
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
779
|
+
if (tVersionPath) {
|
|
780
|
+
try {
|
|
781
|
+
await replica.instance.unlink(tVersionPath);
|
|
782
|
+
} catch {
|
|
783
|
+
}
|
|
758
784
|
}
|
|
759
785
|
console.log(`[ConfigRepo] tombstone ${tombstone.path}: deleted on ${replicaId}`);
|
|
760
786
|
}
|
|
@@ -863,7 +889,7 @@ var ConfigRepo = class {
|
|
|
863
889
|
bytes,
|
|
864
890
|
author
|
|
865
891
|
);
|
|
866
|
-
await
|
|
892
|
+
await this.writeVersionSidecar(configPath, version);
|
|
867
893
|
const conflictDir = metaPath.substring(0, metaPath.lastIndexOf("/"));
|
|
868
894
|
const resolvedBackupPath = `${conflictDir}/resolved`;
|
|
869
895
|
const resolvedBytes = typeof mergedContent === "string" ? new TextEncoder().encode(mergedContent) : new TextEncoder().encode(JSON.stringify(mergedContent, null, 2));
|
|
@@ -964,13 +990,34 @@ var ConfigRepo = class {
|
|
|
964
990
|
// -----------------------------------------------------------------------
|
|
965
991
|
// Internal — Persistence
|
|
966
992
|
// -----------------------------------------------------------------------
|
|
993
|
+
/** Write version sidecar for a config file (no-op for .version files). */
|
|
994
|
+
async writeVersionSidecar(configPath, version) {
|
|
995
|
+
const vPath = versionPathFor(configPath);
|
|
996
|
+
if (!vPath) return;
|
|
997
|
+
await this.ensureDir(vPath);
|
|
998
|
+
await writeVersion(this.fullFS, vPath, version);
|
|
999
|
+
}
|
|
1000
|
+
/** Delete version sidecar on a backend (no-op for .version files). */
|
|
1001
|
+
async unlinkVersionSidecar(fs, configPath) {
|
|
1002
|
+
const vPath = versionPathFor(configPath);
|
|
1003
|
+
if (!vPath) return;
|
|
1004
|
+
try {
|
|
1005
|
+
await fs.unlink(vPath);
|
|
1006
|
+
} catch {
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
/** Read version sidecar (returns null for .version files). */
|
|
1010
|
+
async readVersionSidecar(configPath) {
|
|
1011
|
+
const vPath = versionPathFor(configPath);
|
|
1012
|
+
if (!vPath) return null;
|
|
1013
|
+
return readVersion(this.fullFS, vPath);
|
|
1014
|
+
}
|
|
967
1015
|
async persistConfig(fullPath, bytes) {
|
|
968
1016
|
await this.ensureDir(fullPath);
|
|
969
1017
|
await this.cachedFS.writeFile(fullPath, bytes);
|
|
970
1018
|
const author = `${this.appId}/${this.nodeId}`;
|
|
971
1019
|
const version = await incrementVersion(this.fullFS, fullPath, bytes, author);
|
|
972
|
-
await this.
|
|
973
|
-
await writeVersion(this.fullFS, versionPathFor(fullPath), version);
|
|
1020
|
+
await this.writeVersionSidecar(fullPath, version);
|
|
974
1021
|
}
|
|
975
1022
|
async reloadConfigCache() {
|
|
976
1023
|
const appDir = `/${this.appId}`;
|
|
@@ -1008,7 +1055,7 @@ var ConfigRepo = class {
|
|
|
1008
1055
|
);
|
|
1009
1056
|
let sourceVersion = 0;
|
|
1010
1057
|
try {
|
|
1011
|
-
const srcVer = await
|
|
1058
|
+
const srcVer = await this.readVersionSidecar(conflict.path);
|
|
1012
1059
|
if (srcVer) sourceVersion = srcVer.version;
|
|
1013
1060
|
} catch {
|
|
1014
1061
|
}
|
|
@@ -1093,8 +1140,7 @@ var ConfigRepo = class {
|
|
|
1093
1140
|
await this.cachedFS.writeFile(path, bytes);
|
|
1094
1141
|
const author = `${this.appId}/${this.nodeId}`;
|
|
1095
1142
|
const version = await incrementVersion(this.fullFS, path, bytes, author);
|
|
1096
|
-
await this.
|
|
1097
|
-
await writeVersion(this.fullFS, versionPathFor(path), version);
|
|
1143
|
+
await this.writeVersionSidecar(path, version);
|
|
1098
1144
|
}
|
|
1099
1145
|
async readMetaFile(path) {
|
|
1100
1146
|
try {
|
|
@@ -1152,10 +1198,7 @@ var ConfigRepo = class {
|
|
|
1152
1198
|
await replica.instance.unlink(corruptPath);
|
|
1153
1199
|
} catch {
|
|
1154
1200
|
}
|
|
1155
|
-
|
|
1156
|
-
await replica.instance.unlink(versionPathFor(corruptPath));
|
|
1157
|
-
} catch {
|
|
1158
|
-
}
|
|
1201
|
+
await this.unlinkVersionSidecar(replica.instance, corruptPath);
|
|
1159
1202
|
}
|
|
1160
1203
|
try {
|
|
1161
1204
|
await this.deleteFile(corruptPath);
|
|
@@ -1164,10 +1207,7 @@ var ConfigRepo = class {
|
|
|
1164
1207
|
await this.cachedFS.unlink(corruptPath);
|
|
1165
1208
|
} catch {
|
|
1166
1209
|
}
|
|
1167
|
-
|
|
1168
|
-
await this.cachedFS.unlink(versionPathFor(corruptPath));
|
|
1169
|
-
} catch {
|
|
1170
|
-
}
|
|
1210
|
+
await this.unlinkVersionSidecar(this.cachedFS, corruptPath);
|
|
1171
1211
|
}
|
|
1172
1212
|
}
|
|
1173
1213
|
const seen = /* @__PURE__ */ new Map();
|
|
@@ -1197,10 +1237,7 @@ var ConfigRepo = class {
|
|
|
1197
1237
|
await replica.instance.unlink(descPath);
|
|
1198
1238
|
} catch {
|
|
1199
1239
|
}
|
|
1200
|
-
|
|
1201
|
-
await replica.instance.unlink(versionPathFor(descPath));
|
|
1202
|
-
} catch {
|
|
1203
|
-
}
|
|
1240
|
+
await this.unlinkVersionSidecar(replica.instance, descPath);
|
|
1204
1241
|
}
|
|
1205
1242
|
try {
|
|
1206
1243
|
await this.deleteFile(descPath);
|
|
@@ -1222,8 +1259,7 @@ var ConfigRepo = class {
|
|
|
1222
1259
|
await this.cachedFS.writeFile(path, bytes);
|
|
1223
1260
|
const author = `${this.appId}/${this.nodeId}`;
|
|
1224
1261
|
const version = await incrementVersion(this.fullFS, path, bytes, author);
|
|
1225
|
-
await this.
|
|
1226
|
-
await writeVersion(this.fullFS, versionPathFor(path), version);
|
|
1262
|
+
await this.writeVersionSidecar(path, version);
|
|
1227
1263
|
}
|
|
1228
1264
|
/** Remove a single backend descriptor file + its version sidecar */
|
|
1229
1265
|
async removeBackendDescriptor(id) {
|
|
@@ -1232,10 +1268,7 @@ var ConfigRepo = class {
|
|
|
1232
1268
|
await this.cachedFS.unlink(path);
|
|
1233
1269
|
} catch {
|
|
1234
1270
|
}
|
|
1235
|
-
|
|
1236
|
-
await this.cachedFS.unlink(versionPathFor(path));
|
|
1237
|
-
} catch {
|
|
1238
|
-
}
|
|
1271
|
+
await this.unlinkVersionSidecar(this.cachedFS, path);
|
|
1239
1272
|
}
|
|
1240
1273
|
// -----------------------------------------------------------------------
|
|
1241
1274
|
// IConfigRepo — Meta file access (no chroot)
|
|
@@ -1334,10 +1367,7 @@ var ConfigRepo = class {
|
|
|
1334
1367
|
await replica.instance.unlink(descPath);
|
|
1335
1368
|
} catch {
|
|
1336
1369
|
}
|
|
1337
|
-
|
|
1338
|
-
await replica.instance.unlink(versionPathFor(descPath));
|
|
1339
|
-
} catch {
|
|
1340
|
-
}
|
|
1370
|
+
await this.unlinkVersionSidecar(replica.instance, descPath);
|
|
1341
1371
|
try {
|
|
1342
1372
|
await this.deleteFile(descPath);
|
|
1343
1373
|
} catch {
|
|
@@ -1433,8 +1463,7 @@ var ConfigRepo = class {
|
|
|
1433
1463
|
await this.cachedFS.writeFile(descPath, bytes);
|
|
1434
1464
|
const author = `${this.appId}/${this.nodeId}`;
|
|
1435
1465
|
const version = await incrementVersion(this.fullFS, descPath, bytes, author);
|
|
1436
|
-
await this.
|
|
1437
|
-
await writeVersion(this.fullFS, versionPathFor(descPath), version);
|
|
1466
|
+
await this.writeVersionSidecar(descPath, version);
|
|
1438
1467
|
this.appDataGroups.set(id, group);
|
|
1439
1468
|
console.log(`[ConfigRepo] createAppDataGroup: "${id}" created`);
|
|
1440
1469
|
return group;
|
|
@@ -1494,10 +1523,7 @@ var ConfigRepo = class {
|
|
|
1494
1523
|
await this.cachedFS.unlink(descPath);
|
|
1495
1524
|
} catch {
|
|
1496
1525
|
}
|
|
1497
|
-
|
|
1498
|
-
await this.cachedFS.unlink(versionPathFor(descPath));
|
|
1499
|
-
} catch {
|
|
1500
|
-
}
|
|
1526
|
+
await this.unlinkVersionSidecar(this.cachedFS, descPath);
|
|
1501
1527
|
console.log(`[ConfigRepo] removeAppDataGroup: "${id}" removed`);
|
|
1502
1528
|
}
|
|
1503
1529
|
async listAccountBackends() {
|
|
@@ -1695,9 +1721,14 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
1695
1721
|
await cachedFS.unlink(BACKENDS_FILE);
|
|
1696
1722
|
} catch {
|
|
1697
1723
|
}
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1724
|
+
{
|
|
1725
|
+
const vPath = versionPathFor(BACKENDS_FILE);
|
|
1726
|
+
if (vPath) {
|
|
1727
|
+
try {
|
|
1728
|
+
await cachedFS.unlink(vPath);
|
|
1729
|
+
} catch {
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1701
1732
|
}
|
|
1702
1733
|
console.log(`[createConfigRepo] Migration complete`);
|
|
1703
1734
|
}
|