zen-fs-config 0.5.5 → 0.5.7
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 +61 -0
- package/dist/index.d.mts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +83 -48
- package/dist/index.mjs +83 -48
- package/package.json +1 -1
package/DESIGN.md
CHANGED
|
@@ -1147,6 +1147,67 @@ Application
|
|
|
1147
1147
|
→ Dispose temporary SyncPair
|
|
1148
1148
|
```
|
|
1149
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
|
+
|
|
1150
1211
|
## 13. Peer Dependencies
|
|
1151
1212
|
|
|
1152
1213
|
| Package | Role | Version | Required |
|
package/dist/index.d.mts
CHANGED
|
@@ -462,6 +462,8 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
462
462
|
private configCache;
|
|
463
463
|
private readonly primaryBackendId;
|
|
464
464
|
private readonly pollIntervalMs?;
|
|
465
|
+
/** Tombstone cache — avoids redundant reads within a single flush() cycle. */
|
|
466
|
+
private tombstoneCache;
|
|
465
467
|
constructor(appId: string, nodeId: string, primaryBackendId: string, cachedFS: MinimalAsyncFS, serializer: PathAwareSerializer, onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>, pollIntervalMs?: number);
|
|
466
468
|
/** Full path to this node's directory on the primary backend. */
|
|
467
469
|
get nodePath(): string;
|
|
@@ -484,8 +486,11 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
484
486
|
deleteFile(path: string): Promise<void>;
|
|
485
487
|
/**
|
|
486
488
|
* Read all tombstones from the primary backend.
|
|
489
|
+
* Results are cached within a flush() cycle to avoid redundant reads.
|
|
487
490
|
*/
|
|
488
491
|
private readTombstones;
|
|
492
|
+
/** Invalidate the tombstone cache — call after tombstones are modified. */
|
|
493
|
+
private invalidateTombstoneCache;
|
|
489
494
|
/**
|
|
490
495
|
* Before sync: for each tombstone, delete the actual file on all replicas.
|
|
491
496
|
* This prevents bi-directional sync from copying the file back.
|
|
@@ -522,6 +527,12 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
522
527
|
readConflictBackup(conflictId: string, fileType: 'source' | 'target' | 'resolved'): Promise<string>;
|
|
523
528
|
dispose(): Promise<void>;
|
|
524
529
|
setupSync(backends: BackendDescriptor[], primaryBackendId: string, pollIntervalMs?: number): Promise<void>;
|
|
530
|
+
/** Write version sidecar for a config file (no-op for .version files). */
|
|
531
|
+
private writeVersionSidecar;
|
|
532
|
+
/** Delete version sidecar on a backend (no-op for .version files). */
|
|
533
|
+
private unlinkVersionSidecar;
|
|
534
|
+
/** Read version sidecar (returns null for .version files). */
|
|
535
|
+
private readVersionSidecar;
|
|
525
536
|
private persistConfig;
|
|
526
537
|
private reloadConfigCache;
|
|
527
538
|
private handleConflict;
|
|
@@ -683,8 +694,11 @@ declare function connect(appId: string, options?: ConnectOptions): Promise<Conne
|
|
|
683
694
|
* /app-a/db.json → /app-a/.db.json.version
|
|
684
695
|
* /shared/flags.json → /shared/.flags.json.version
|
|
685
696
|
* /nodes/s1/env.json → /nodes/s1/.env.json.version
|
|
697
|
+
*
|
|
698
|
+
* Returns null for files that are already version sidecars (.version files),
|
|
699
|
+
* to prevent creating version-of-version files (e.g. ..db.json.version.version).
|
|
686
700
|
*/
|
|
687
|
-
declare function versionPathFor(configFilePath: string): string;
|
|
701
|
+
declare function versionPathFor(configFilePath: string): string | null;
|
|
688
702
|
/**
|
|
689
703
|
* Compute SHA-256 hash of a Uint8Array.
|
|
690
704
|
* Returns "sha256:" prefix + hex digest.
|
package/dist/index.d.ts
CHANGED
|
@@ -462,6 +462,8 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
462
462
|
private configCache;
|
|
463
463
|
private readonly primaryBackendId;
|
|
464
464
|
private readonly pollIntervalMs?;
|
|
465
|
+
/** Tombstone cache — avoids redundant reads within a single flush() cycle. */
|
|
466
|
+
private tombstoneCache;
|
|
465
467
|
constructor(appId: string, nodeId: string, primaryBackendId: string, cachedFS: MinimalAsyncFS, serializer: PathAwareSerializer, onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>, pollIntervalMs?: number);
|
|
466
468
|
/** Full path to this node's directory on the primary backend. */
|
|
467
469
|
get nodePath(): string;
|
|
@@ -484,8 +486,11 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
484
486
|
deleteFile(path: string): Promise<void>;
|
|
485
487
|
/**
|
|
486
488
|
* Read all tombstones from the primary backend.
|
|
489
|
+
* Results are cached within a flush() cycle to avoid redundant reads.
|
|
487
490
|
*/
|
|
488
491
|
private readTombstones;
|
|
492
|
+
/** Invalidate the tombstone cache — call after tombstones are modified. */
|
|
493
|
+
private invalidateTombstoneCache;
|
|
489
494
|
/**
|
|
490
495
|
* Before sync: for each tombstone, delete the actual file on all replicas.
|
|
491
496
|
* This prevents bi-directional sync from copying the file back.
|
|
@@ -522,6 +527,12 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
522
527
|
readConflictBackup(conflictId: string, fileType: 'source' | 'target' | 'resolved'): Promise<string>;
|
|
523
528
|
dispose(): Promise<void>;
|
|
524
529
|
setupSync(backends: BackendDescriptor[], primaryBackendId: string, pollIntervalMs?: number): Promise<void>;
|
|
530
|
+
/** Write version sidecar for a config file (no-op for .version files). */
|
|
531
|
+
private writeVersionSidecar;
|
|
532
|
+
/** Delete version sidecar on a backend (no-op for .version files). */
|
|
533
|
+
private unlinkVersionSidecar;
|
|
534
|
+
/** Read version sidecar (returns null for .version files). */
|
|
535
|
+
private readVersionSidecar;
|
|
525
536
|
private persistConfig;
|
|
526
537
|
private reloadConfigCache;
|
|
527
538
|
private handleConflict;
|
|
@@ -683,8 +694,11 @@ declare function connect(appId: string, options?: ConnectOptions): Promise<Conne
|
|
|
683
694
|
* /app-a/db.json → /app-a/.db.json.version
|
|
684
695
|
* /shared/flags.json → /shared/.flags.json.version
|
|
685
696
|
* /nodes/s1/env.json → /nodes/s1/.env.json.version
|
|
697
|
+
*
|
|
698
|
+
* Returns null for files that are already version sidecars (.version files),
|
|
699
|
+
* to prevent creating version-of-version files (e.g. ..db.json.version.version).
|
|
686
700
|
*/
|
|
687
|
-
declare function versionPathFor(configFilePath: string): string;
|
|
701
|
+
declare function versionPathFor(configFilePath: string): string | null;
|
|
688
702
|
/**
|
|
689
703
|
* Compute SHA-256 hash of a Uint8Array.
|
|
690
704
|
* 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
|
},
|
|
@@ -373,7 +376,7 @@ async function wrapZenFSFileSystem(config) {
|
|
|
373
376
|
}
|
|
374
377
|
await isolatedFS.write(path, bytes, 0);
|
|
375
378
|
try {
|
|
376
|
-
await isolatedFS.touch(path, { size: bytes.byteLength, mtimeMs: Date.now() });
|
|
379
|
+
await isolatedFS.touch(path, { size: bytes.byteLength, mtimeMs: _options?.mtime ?? Date.now() });
|
|
377
380
|
} catch {
|
|
378
381
|
}
|
|
379
382
|
notifyChange();
|
|
@@ -454,6 +457,9 @@ function versionPathFor(configFilePath) {
|
|
|
454
457
|
const lastSlash = configFilePath.lastIndexOf("/");
|
|
455
458
|
const dir = lastSlash >= 0 ? configFilePath.slice(0, lastSlash) : "";
|
|
456
459
|
const fileName = lastSlash >= 0 ? configFilePath.slice(lastSlash + 1) : configFilePath;
|
|
460
|
+
if (fileName.endsWith(".version")) {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
457
463
|
const versionFileName = `.${fileName}.version`;
|
|
458
464
|
return dir ? `${dir}/${versionFileName}` : versionFileName;
|
|
459
465
|
}
|
|
@@ -490,7 +496,7 @@ async function writeVersion(fs, versionFilePath, meta) {
|
|
|
490
496
|
}
|
|
491
497
|
async function incrementVersion(fs, configFilePath, newContent, author) {
|
|
492
498
|
const vPath = versionPathFor(configFilePath);
|
|
493
|
-
const prev = await readVersion(fs, vPath);
|
|
499
|
+
const prev = vPath ? await readVersion(fs, vPath) : null;
|
|
494
500
|
const hash = await sha256(newContent);
|
|
495
501
|
return {
|
|
496
502
|
version: (prev?.version ?? 0) + 1,
|
|
@@ -501,6 +507,7 @@ async function incrementVersion(fs, configFilePath, newContent, author) {
|
|
|
501
507
|
}
|
|
502
508
|
async function verifyOrRepairVersion(fs, configFilePath, author) {
|
|
503
509
|
const vPath = versionPathFor(configFilePath);
|
|
510
|
+
if (!vPath) return null;
|
|
504
511
|
const existing = await readVersion(fs, vPath);
|
|
505
512
|
if (!existing) return null;
|
|
506
513
|
try {
|
|
@@ -579,6 +586,8 @@ var ConfigRepo = class {
|
|
|
579
586
|
configCache = /* @__PURE__ */ new Map();
|
|
580
587
|
primaryBackendId;
|
|
581
588
|
pollIntervalMs;
|
|
589
|
+
/** Tombstone cache — avoids redundant reads within a single flush() cycle. */
|
|
590
|
+
tombstoneCache = null;
|
|
582
591
|
constructor(appId, nodeId, primaryBackendId, cachedFS, serializer, onConflict, pollIntervalMs) {
|
|
583
592
|
this.appId = appId;
|
|
584
593
|
this.nodeId = nodeId;
|
|
@@ -739,10 +748,12 @@ var ConfigRepo = class {
|
|
|
739
748
|
this.assertNotDisposed();
|
|
740
749
|
await this.processTombstones();
|
|
741
750
|
const resultsMap = await this.syncEngine.syncAll();
|
|
751
|
+
this.invalidateTombstoneCache();
|
|
742
752
|
await this.readAllBackendDescriptors();
|
|
743
753
|
await this.processTombstones();
|
|
744
754
|
await this.updateTombstoneConfirmations();
|
|
745
755
|
await this.gcTombstones();
|
|
756
|
+
this.invalidateTombstoneCache();
|
|
746
757
|
return Array.from(resultsMap.values());
|
|
747
758
|
}
|
|
748
759
|
// -----------------------------------------------------------------------
|
|
@@ -772,16 +783,23 @@ var ConfigRepo = class {
|
|
|
772
783
|
} catch {
|
|
773
784
|
}
|
|
774
785
|
const versionPath = versionPathFor(normalizedPath);
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
786
|
+
if (versionPath) {
|
|
787
|
+
try {
|
|
788
|
+
await this.cachedFS.unlink(versionPath);
|
|
789
|
+
} catch {
|
|
790
|
+
}
|
|
778
791
|
}
|
|
779
792
|
console.log(`[ConfigRepo] deleteFile: ${normalizedPath} (tombstone at ${tombstonePath})`);
|
|
793
|
+
this.invalidateTombstoneCache();
|
|
780
794
|
}
|
|
781
795
|
/**
|
|
782
796
|
* Read all tombstones from the primary backend.
|
|
797
|
+
* Results are cached within a flush() cycle to avoid redundant reads.
|
|
783
798
|
*/
|
|
784
799
|
async readTombstones() {
|
|
800
|
+
if (this.tombstoneCache !== null) {
|
|
801
|
+
return this.tombstoneCache;
|
|
802
|
+
}
|
|
785
803
|
try {
|
|
786
804
|
const entries = await this.cachedFS.readdir(DELETIONS_DIR);
|
|
787
805
|
const tombstones = [];
|
|
@@ -794,11 +812,17 @@ var ConfigRepo = class {
|
|
|
794
812
|
} catch {
|
|
795
813
|
}
|
|
796
814
|
}
|
|
815
|
+
this.tombstoneCache = tombstones;
|
|
797
816
|
return tombstones;
|
|
798
817
|
} catch {
|
|
818
|
+
this.tombstoneCache = [];
|
|
799
819
|
return [];
|
|
800
820
|
}
|
|
801
821
|
}
|
|
822
|
+
/** Invalidate the tombstone cache — call after tombstones are modified. */
|
|
823
|
+
invalidateTombstoneCache() {
|
|
824
|
+
this.tombstoneCache = null;
|
|
825
|
+
}
|
|
802
826
|
/**
|
|
803
827
|
* Before sync: for each tombstone, delete the actual file on all replicas.
|
|
804
828
|
* This prevents bi-directional sync from copying the file back.
|
|
@@ -808,22 +832,27 @@ var ConfigRepo = class {
|
|
|
808
832
|
if (tombstones.length === 0) return;
|
|
809
833
|
console.log(`[ConfigRepo] processTombstones: ${tombstones.length} tombstone(s)`);
|
|
810
834
|
for (const tombstone of tombstones) {
|
|
835
|
+
const tVersionPath = versionPathFor(tombstone.path);
|
|
811
836
|
try {
|
|
812
837
|
await this.cachedFS.unlink(tombstone.path);
|
|
813
838
|
} catch {
|
|
814
839
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
840
|
+
if (tVersionPath) {
|
|
841
|
+
try {
|
|
842
|
+
await this.cachedFS.unlink(tVersionPath);
|
|
843
|
+
} catch {
|
|
844
|
+
}
|
|
818
845
|
}
|
|
819
846
|
for (const [replicaId, replica] of this.replicaBackends) {
|
|
820
847
|
try {
|
|
821
848
|
await replica.instance.unlink(tombstone.path);
|
|
822
849
|
} catch {
|
|
823
850
|
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
851
|
+
if (tVersionPath) {
|
|
852
|
+
try {
|
|
853
|
+
await replica.instance.unlink(tVersionPath);
|
|
854
|
+
} catch {
|
|
855
|
+
}
|
|
827
856
|
}
|
|
828
857
|
console.log(`[ConfigRepo] tombstone ${tombstone.path}: deleted on ${replicaId}`);
|
|
829
858
|
}
|
|
@@ -869,6 +898,7 @@ var ConfigRepo = class {
|
|
|
869
898
|
}
|
|
870
899
|
}
|
|
871
900
|
console.log(`[ConfigRepo] updateTombstoneConfirmations: ${tombstones.length} tombstone(s) updated`);
|
|
901
|
+
this.invalidateTombstoneCache();
|
|
872
902
|
}
|
|
873
903
|
/**
|
|
874
904
|
* GC: remove tombstones where all backends in backends.json have confirmed.
|
|
@@ -932,7 +962,7 @@ var ConfigRepo = class {
|
|
|
932
962
|
bytes,
|
|
933
963
|
author
|
|
934
964
|
);
|
|
935
|
-
await
|
|
965
|
+
await this.writeVersionSidecar(configPath, version);
|
|
936
966
|
const conflictDir = metaPath.substring(0, metaPath.lastIndexOf("/"));
|
|
937
967
|
const resolvedBackupPath = `${conflictDir}/resolved`;
|
|
938
968
|
const resolvedBytes = typeof mergedContent === "string" ? new TextEncoder().encode(mergedContent) : new TextEncoder().encode(JSON.stringify(mergedContent, null, 2));
|
|
@@ -1033,13 +1063,34 @@ var ConfigRepo = class {
|
|
|
1033
1063
|
// -----------------------------------------------------------------------
|
|
1034
1064
|
// Internal — Persistence
|
|
1035
1065
|
// -----------------------------------------------------------------------
|
|
1066
|
+
/** Write version sidecar for a config file (no-op for .version files). */
|
|
1067
|
+
async writeVersionSidecar(configPath, version) {
|
|
1068
|
+
const vPath = versionPathFor(configPath);
|
|
1069
|
+
if (!vPath) return;
|
|
1070
|
+
await this.ensureDir(vPath);
|
|
1071
|
+
await writeVersion(this.fullFS, vPath, version);
|
|
1072
|
+
}
|
|
1073
|
+
/** Delete version sidecar on a backend (no-op for .version files). */
|
|
1074
|
+
async unlinkVersionSidecar(fs, configPath) {
|
|
1075
|
+
const vPath = versionPathFor(configPath);
|
|
1076
|
+
if (!vPath) return;
|
|
1077
|
+
try {
|
|
1078
|
+
await fs.unlink(vPath);
|
|
1079
|
+
} catch {
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
/** Read version sidecar (returns null for .version files). */
|
|
1083
|
+
async readVersionSidecar(configPath) {
|
|
1084
|
+
const vPath = versionPathFor(configPath);
|
|
1085
|
+
if (!vPath) return null;
|
|
1086
|
+
return readVersion(this.fullFS, vPath);
|
|
1087
|
+
}
|
|
1036
1088
|
async persistConfig(fullPath, bytes) {
|
|
1037
1089
|
await this.ensureDir(fullPath);
|
|
1038
1090
|
await this.cachedFS.writeFile(fullPath, bytes);
|
|
1039
1091
|
const author = `${this.appId}/${this.nodeId}`;
|
|
1040
1092
|
const version = await incrementVersion(this.fullFS, fullPath, bytes, author);
|
|
1041
|
-
await this.
|
|
1042
|
-
await writeVersion(this.fullFS, versionPathFor(fullPath), version);
|
|
1093
|
+
await this.writeVersionSidecar(fullPath, version);
|
|
1043
1094
|
}
|
|
1044
1095
|
async reloadConfigCache() {
|
|
1045
1096
|
const appDir = `/${this.appId}`;
|
|
@@ -1077,7 +1128,7 @@ var ConfigRepo = class {
|
|
|
1077
1128
|
);
|
|
1078
1129
|
let sourceVersion = 0;
|
|
1079
1130
|
try {
|
|
1080
|
-
const srcVer = await
|
|
1131
|
+
const srcVer = await this.readVersionSidecar(conflict.path);
|
|
1081
1132
|
if (srcVer) sourceVersion = srcVer.version;
|
|
1082
1133
|
} catch {
|
|
1083
1134
|
}
|
|
@@ -1162,8 +1213,7 @@ var ConfigRepo = class {
|
|
|
1162
1213
|
await this.cachedFS.writeFile(path, bytes);
|
|
1163
1214
|
const author = `${this.appId}/${this.nodeId}`;
|
|
1164
1215
|
const version = await incrementVersion(this.fullFS, path, bytes, author);
|
|
1165
|
-
await this.
|
|
1166
|
-
await writeVersion(this.fullFS, versionPathFor(path), version);
|
|
1216
|
+
await this.writeVersionSidecar(path, version);
|
|
1167
1217
|
}
|
|
1168
1218
|
async readMetaFile(path) {
|
|
1169
1219
|
try {
|
|
@@ -1221,10 +1271,7 @@ var ConfigRepo = class {
|
|
|
1221
1271
|
await replica.instance.unlink(corruptPath);
|
|
1222
1272
|
} catch {
|
|
1223
1273
|
}
|
|
1224
|
-
|
|
1225
|
-
await replica.instance.unlink(versionPathFor(corruptPath));
|
|
1226
|
-
} catch {
|
|
1227
|
-
}
|
|
1274
|
+
await this.unlinkVersionSidecar(replica.instance, corruptPath);
|
|
1228
1275
|
}
|
|
1229
1276
|
try {
|
|
1230
1277
|
await this.deleteFile(corruptPath);
|
|
@@ -1233,10 +1280,7 @@ var ConfigRepo = class {
|
|
|
1233
1280
|
await this.cachedFS.unlink(corruptPath);
|
|
1234
1281
|
} catch {
|
|
1235
1282
|
}
|
|
1236
|
-
|
|
1237
|
-
await this.cachedFS.unlink(versionPathFor(corruptPath));
|
|
1238
|
-
} catch {
|
|
1239
|
-
}
|
|
1283
|
+
await this.unlinkVersionSidecar(this.cachedFS, corruptPath);
|
|
1240
1284
|
}
|
|
1241
1285
|
}
|
|
1242
1286
|
const seen = /* @__PURE__ */ new Map();
|
|
@@ -1266,10 +1310,7 @@ var ConfigRepo = class {
|
|
|
1266
1310
|
await replica.instance.unlink(descPath);
|
|
1267
1311
|
} catch {
|
|
1268
1312
|
}
|
|
1269
|
-
|
|
1270
|
-
await replica.instance.unlink(versionPathFor(descPath));
|
|
1271
|
-
} catch {
|
|
1272
|
-
}
|
|
1313
|
+
await this.unlinkVersionSidecar(replica.instance, descPath);
|
|
1273
1314
|
}
|
|
1274
1315
|
try {
|
|
1275
1316
|
await this.deleteFile(descPath);
|
|
@@ -1291,8 +1332,7 @@ var ConfigRepo = class {
|
|
|
1291
1332
|
await this.cachedFS.writeFile(path, bytes);
|
|
1292
1333
|
const author = `${this.appId}/${this.nodeId}`;
|
|
1293
1334
|
const version = await incrementVersion(this.fullFS, path, bytes, author);
|
|
1294
|
-
await this.
|
|
1295
|
-
await writeVersion(this.fullFS, versionPathFor(path), version);
|
|
1335
|
+
await this.writeVersionSidecar(path, version);
|
|
1296
1336
|
}
|
|
1297
1337
|
/** Remove a single backend descriptor file + its version sidecar */
|
|
1298
1338
|
async removeBackendDescriptor(id) {
|
|
@@ -1301,10 +1341,7 @@ var ConfigRepo = class {
|
|
|
1301
1341
|
await this.cachedFS.unlink(path);
|
|
1302
1342
|
} catch {
|
|
1303
1343
|
}
|
|
1304
|
-
|
|
1305
|
-
await this.cachedFS.unlink(versionPathFor(path));
|
|
1306
|
-
} catch {
|
|
1307
|
-
}
|
|
1344
|
+
await this.unlinkVersionSidecar(this.cachedFS, path);
|
|
1308
1345
|
}
|
|
1309
1346
|
// -----------------------------------------------------------------------
|
|
1310
1347
|
// IConfigRepo — Meta file access (no chroot)
|
|
@@ -1403,10 +1440,7 @@ var ConfigRepo = class {
|
|
|
1403
1440
|
await replica.instance.unlink(descPath);
|
|
1404
1441
|
} catch {
|
|
1405
1442
|
}
|
|
1406
|
-
|
|
1407
|
-
await replica.instance.unlink(versionPathFor(descPath));
|
|
1408
|
-
} catch {
|
|
1409
|
-
}
|
|
1443
|
+
await this.unlinkVersionSidecar(replica.instance, descPath);
|
|
1410
1444
|
try {
|
|
1411
1445
|
await this.deleteFile(descPath);
|
|
1412
1446
|
} catch {
|
|
@@ -1502,8 +1536,7 @@ var ConfigRepo = class {
|
|
|
1502
1536
|
await this.cachedFS.writeFile(descPath, bytes);
|
|
1503
1537
|
const author = `${this.appId}/${this.nodeId}`;
|
|
1504
1538
|
const version = await incrementVersion(this.fullFS, descPath, bytes, author);
|
|
1505
|
-
await this.
|
|
1506
|
-
await writeVersion(this.fullFS, versionPathFor(descPath), version);
|
|
1539
|
+
await this.writeVersionSidecar(descPath, version);
|
|
1507
1540
|
this.appDataGroups.set(id, group);
|
|
1508
1541
|
console.log(`[ConfigRepo] createAppDataGroup: "${id}" created`);
|
|
1509
1542
|
return group;
|
|
@@ -1563,10 +1596,7 @@ var ConfigRepo = class {
|
|
|
1563
1596
|
await this.cachedFS.unlink(descPath);
|
|
1564
1597
|
} catch {
|
|
1565
1598
|
}
|
|
1566
|
-
|
|
1567
|
-
await this.cachedFS.unlink(versionPathFor(descPath));
|
|
1568
|
-
} catch {
|
|
1569
|
-
}
|
|
1599
|
+
await this.unlinkVersionSidecar(this.cachedFS, descPath);
|
|
1570
1600
|
console.log(`[ConfigRepo] removeAppDataGroup: "${id}" removed`);
|
|
1571
1601
|
}
|
|
1572
1602
|
async listAccountBackends() {
|
|
@@ -1764,9 +1794,14 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
1764
1794
|
await cachedFS.unlink(BACKENDS_FILE);
|
|
1765
1795
|
} catch {
|
|
1766
1796
|
}
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1797
|
+
{
|
|
1798
|
+
const vPath = versionPathFor(BACKENDS_FILE);
|
|
1799
|
+
if (vPath) {
|
|
1800
|
+
try {
|
|
1801
|
+
await cachedFS.unlink(vPath);
|
|
1802
|
+
} catch {
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1770
1805
|
}
|
|
1771
1806
|
console.log(`[createConfigRepo] Migration complete`);
|
|
1772
1807
|
}
|
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
|
},
|
|
@@ -316,7 +319,7 @@ async function wrapZenFSFileSystem(config) {
|
|
|
316
319
|
}
|
|
317
320
|
await isolatedFS.write(path, bytes, 0);
|
|
318
321
|
try {
|
|
319
|
-
await isolatedFS.touch(path, { size: bytes.byteLength, mtimeMs: Date.now() });
|
|
322
|
+
await isolatedFS.touch(path, { size: bytes.byteLength, mtimeMs: _options?.mtime ?? Date.now() });
|
|
320
323
|
} catch {
|
|
321
324
|
}
|
|
322
325
|
notifyChange();
|
|
@@ -397,6 +400,9 @@ function versionPathFor(configFilePath) {
|
|
|
397
400
|
const lastSlash = configFilePath.lastIndexOf("/");
|
|
398
401
|
const dir = lastSlash >= 0 ? configFilePath.slice(0, lastSlash) : "";
|
|
399
402
|
const fileName = lastSlash >= 0 ? configFilePath.slice(lastSlash + 1) : configFilePath;
|
|
403
|
+
if (fileName.endsWith(".version")) {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
400
406
|
const versionFileName = `.${fileName}.version`;
|
|
401
407
|
return dir ? `${dir}/${versionFileName}` : versionFileName;
|
|
402
408
|
}
|
|
@@ -433,7 +439,7 @@ async function writeVersion(fs, versionFilePath, meta) {
|
|
|
433
439
|
}
|
|
434
440
|
async function incrementVersion(fs, configFilePath, newContent, author) {
|
|
435
441
|
const vPath = versionPathFor(configFilePath);
|
|
436
|
-
const prev = await readVersion(fs, vPath);
|
|
442
|
+
const prev = vPath ? await readVersion(fs, vPath) : null;
|
|
437
443
|
const hash = await sha256(newContent);
|
|
438
444
|
return {
|
|
439
445
|
version: (prev?.version ?? 0) + 1,
|
|
@@ -444,6 +450,7 @@ async function incrementVersion(fs, configFilePath, newContent, author) {
|
|
|
444
450
|
}
|
|
445
451
|
async function verifyOrRepairVersion(fs, configFilePath, author) {
|
|
446
452
|
const vPath = versionPathFor(configFilePath);
|
|
453
|
+
if (!vPath) return null;
|
|
447
454
|
const existing = await readVersion(fs, vPath);
|
|
448
455
|
if (!existing) return null;
|
|
449
456
|
try {
|
|
@@ -522,6 +529,8 @@ var ConfigRepo = class {
|
|
|
522
529
|
configCache = /* @__PURE__ */ new Map();
|
|
523
530
|
primaryBackendId;
|
|
524
531
|
pollIntervalMs;
|
|
532
|
+
/** Tombstone cache — avoids redundant reads within a single flush() cycle. */
|
|
533
|
+
tombstoneCache = null;
|
|
525
534
|
constructor(appId, nodeId, primaryBackendId, cachedFS, serializer, onConflict, pollIntervalMs) {
|
|
526
535
|
this.appId = appId;
|
|
527
536
|
this.nodeId = nodeId;
|
|
@@ -682,10 +691,12 @@ var ConfigRepo = class {
|
|
|
682
691
|
this.assertNotDisposed();
|
|
683
692
|
await this.processTombstones();
|
|
684
693
|
const resultsMap = await this.syncEngine.syncAll();
|
|
694
|
+
this.invalidateTombstoneCache();
|
|
685
695
|
await this.readAllBackendDescriptors();
|
|
686
696
|
await this.processTombstones();
|
|
687
697
|
await this.updateTombstoneConfirmations();
|
|
688
698
|
await this.gcTombstones();
|
|
699
|
+
this.invalidateTombstoneCache();
|
|
689
700
|
return Array.from(resultsMap.values());
|
|
690
701
|
}
|
|
691
702
|
// -----------------------------------------------------------------------
|
|
@@ -715,16 +726,23 @@ var ConfigRepo = class {
|
|
|
715
726
|
} catch {
|
|
716
727
|
}
|
|
717
728
|
const versionPath = versionPathFor(normalizedPath);
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
729
|
+
if (versionPath) {
|
|
730
|
+
try {
|
|
731
|
+
await this.cachedFS.unlink(versionPath);
|
|
732
|
+
} catch {
|
|
733
|
+
}
|
|
721
734
|
}
|
|
722
735
|
console.log(`[ConfigRepo] deleteFile: ${normalizedPath} (tombstone at ${tombstonePath})`);
|
|
736
|
+
this.invalidateTombstoneCache();
|
|
723
737
|
}
|
|
724
738
|
/**
|
|
725
739
|
* Read all tombstones from the primary backend.
|
|
740
|
+
* Results are cached within a flush() cycle to avoid redundant reads.
|
|
726
741
|
*/
|
|
727
742
|
async readTombstones() {
|
|
743
|
+
if (this.tombstoneCache !== null) {
|
|
744
|
+
return this.tombstoneCache;
|
|
745
|
+
}
|
|
728
746
|
try {
|
|
729
747
|
const entries = await this.cachedFS.readdir(DELETIONS_DIR);
|
|
730
748
|
const tombstones = [];
|
|
@@ -737,11 +755,17 @@ var ConfigRepo = class {
|
|
|
737
755
|
} catch {
|
|
738
756
|
}
|
|
739
757
|
}
|
|
758
|
+
this.tombstoneCache = tombstones;
|
|
740
759
|
return tombstones;
|
|
741
760
|
} catch {
|
|
761
|
+
this.tombstoneCache = [];
|
|
742
762
|
return [];
|
|
743
763
|
}
|
|
744
764
|
}
|
|
765
|
+
/** Invalidate the tombstone cache — call after tombstones are modified. */
|
|
766
|
+
invalidateTombstoneCache() {
|
|
767
|
+
this.tombstoneCache = null;
|
|
768
|
+
}
|
|
745
769
|
/**
|
|
746
770
|
* Before sync: for each tombstone, delete the actual file on all replicas.
|
|
747
771
|
* This prevents bi-directional sync from copying the file back.
|
|
@@ -751,22 +775,27 @@ var ConfigRepo = class {
|
|
|
751
775
|
if (tombstones.length === 0) return;
|
|
752
776
|
console.log(`[ConfigRepo] processTombstones: ${tombstones.length} tombstone(s)`);
|
|
753
777
|
for (const tombstone of tombstones) {
|
|
778
|
+
const tVersionPath = versionPathFor(tombstone.path);
|
|
754
779
|
try {
|
|
755
780
|
await this.cachedFS.unlink(tombstone.path);
|
|
756
781
|
} catch {
|
|
757
782
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
783
|
+
if (tVersionPath) {
|
|
784
|
+
try {
|
|
785
|
+
await this.cachedFS.unlink(tVersionPath);
|
|
786
|
+
} catch {
|
|
787
|
+
}
|
|
761
788
|
}
|
|
762
789
|
for (const [replicaId, replica] of this.replicaBackends) {
|
|
763
790
|
try {
|
|
764
791
|
await replica.instance.unlink(tombstone.path);
|
|
765
792
|
} catch {
|
|
766
793
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
794
|
+
if (tVersionPath) {
|
|
795
|
+
try {
|
|
796
|
+
await replica.instance.unlink(tVersionPath);
|
|
797
|
+
} catch {
|
|
798
|
+
}
|
|
770
799
|
}
|
|
771
800
|
console.log(`[ConfigRepo] tombstone ${tombstone.path}: deleted on ${replicaId}`);
|
|
772
801
|
}
|
|
@@ -812,6 +841,7 @@ var ConfigRepo = class {
|
|
|
812
841
|
}
|
|
813
842
|
}
|
|
814
843
|
console.log(`[ConfigRepo] updateTombstoneConfirmations: ${tombstones.length} tombstone(s) updated`);
|
|
844
|
+
this.invalidateTombstoneCache();
|
|
815
845
|
}
|
|
816
846
|
/**
|
|
817
847
|
* GC: remove tombstones where all backends in backends.json have confirmed.
|
|
@@ -875,7 +905,7 @@ var ConfigRepo = class {
|
|
|
875
905
|
bytes,
|
|
876
906
|
author
|
|
877
907
|
);
|
|
878
|
-
await
|
|
908
|
+
await this.writeVersionSidecar(configPath, version);
|
|
879
909
|
const conflictDir = metaPath.substring(0, metaPath.lastIndexOf("/"));
|
|
880
910
|
const resolvedBackupPath = `${conflictDir}/resolved`;
|
|
881
911
|
const resolvedBytes = typeof mergedContent === "string" ? new TextEncoder().encode(mergedContent) : new TextEncoder().encode(JSON.stringify(mergedContent, null, 2));
|
|
@@ -976,13 +1006,34 @@ var ConfigRepo = class {
|
|
|
976
1006
|
// -----------------------------------------------------------------------
|
|
977
1007
|
// Internal — Persistence
|
|
978
1008
|
// -----------------------------------------------------------------------
|
|
1009
|
+
/** Write version sidecar for a config file (no-op for .version files). */
|
|
1010
|
+
async writeVersionSidecar(configPath, version) {
|
|
1011
|
+
const vPath = versionPathFor(configPath);
|
|
1012
|
+
if (!vPath) return;
|
|
1013
|
+
await this.ensureDir(vPath);
|
|
1014
|
+
await writeVersion(this.fullFS, vPath, version);
|
|
1015
|
+
}
|
|
1016
|
+
/** Delete version sidecar on a backend (no-op for .version files). */
|
|
1017
|
+
async unlinkVersionSidecar(fs, configPath) {
|
|
1018
|
+
const vPath = versionPathFor(configPath);
|
|
1019
|
+
if (!vPath) return;
|
|
1020
|
+
try {
|
|
1021
|
+
await fs.unlink(vPath);
|
|
1022
|
+
} catch {
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
/** Read version sidecar (returns null for .version files). */
|
|
1026
|
+
async readVersionSidecar(configPath) {
|
|
1027
|
+
const vPath = versionPathFor(configPath);
|
|
1028
|
+
if (!vPath) return null;
|
|
1029
|
+
return readVersion(this.fullFS, vPath);
|
|
1030
|
+
}
|
|
979
1031
|
async persistConfig(fullPath, bytes) {
|
|
980
1032
|
await this.ensureDir(fullPath);
|
|
981
1033
|
await this.cachedFS.writeFile(fullPath, bytes);
|
|
982
1034
|
const author = `${this.appId}/${this.nodeId}`;
|
|
983
1035
|
const version = await incrementVersion(this.fullFS, fullPath, bytes, author);
|
|
984
|
-
await this.
|
|
985
|
-
await writeVersion(this.fullFS, versionPathFor(fullPath), version);
|
|
1036
|
+
await this.writeVersionSidecar(fullPath, version);
|
|
986
1037
|
}
|
|
987
1038
|
async reloadConfigCache() {
|
|
988
1039
|
const appDir = `/${this.appId}`;
|
|
@@ -1020,7 +1071,7 @@ var ConfigRepo = class {
|
|
|
1020
1071
|
);
|
|
1021
1072
|
let sourceVersion = 0;
|
|
1022
1073
|
try {
|
|
1023
|
-
const srcVer = await
|
|
1074
|
+
const srcVer = await this.readVersionSidecar(conflict.path);
|
|
1024
1075
|
if (srcVer) sourceVersion = srcVer.version;
|
|
1025
1076
|
} catch {
|
|
1026
1077
|
}
|
|
@@ -1105,8 +1156,7 @@ var ConfigRepo = class {
|
|
|
1105
1156
|
await this.cachedFS.writeFile(path, bytes);
|
|
1106
1157
|
const author = `${this.appId}/${this.nodeId}`;
|
|
1107
1158
|
const version = await incrementVersion(this.fullFS, path, bytes, author);
|
|
1108
|
-
await this.
|
|
1109
|
-
await writeVersion(this.fullFS, versionPathFor(path), version);
|
|
1159
|
+
await this.writeVersionSidecar(path, version);
|
|
1110
1160
|
}
|
|
1111
1161
|
async readMetaFile(path) {
|
|
1112
1162
|
try {
|
|
@@ -1164,10 +1214,7 @@ var ConfigRepo = class {
|
|
|
1164
1214
|
await replica.instance.unlink(corruptPath);
|
|
1165
1215
|
} catch {
|
|
1166
1216
|
}
|
|
1167
|
-
|
|
1168
|
-
await replica.instance.unlink(versionPathFor(corruptPath));
|
|
1169
|
-
} catch {
|
|
1170
|
-
}
|
|
1217
|
+
await this.unlinkVersionSidecar(replica.instance, corruptPath);
|
|
1171
1218
|
}
|
|
1172
1219
|
try {
|
|
1173
1220
|
await this.deleteFile(corruptPath);
|
|
@@ -1176,10 +1223,7 @@ var ConfigRepo = class {
|
|
|
1176
1223
|
await this.cachedFS.unlink(corruptPath);
|
|
1177
1224
|
} catch {
|
|
1178
1225
|
}
|
|
1179
|
-
|
|
1180
|
-
await this.cachedFS.unlink(versionPathFor(corruptPath));
|
|
1181
|
-
} catch {
|
|
1182
|
-
}
|
|
1226
|
+
await this.unlinkVersionSidecar(this.cachedFS, corruptPath);
|
|
1183
1227
|
}
|
|
1184
1228
|
}
|
|
1185
1229
|
const seen = /* @__PURE__ */ new Map();
|
|
@@ -1209,10 +1253,7 @@ var ConfigRepo = class {
|
|
|
1209
1253
|
await replica.instance.unlink(descPath);
|
|
1210
1254
|
} catch {
|
|
1211
1255
|
}
|
|
1212
|
-
|
|
1213
|
-
await replica.instance.unlink(versionPathFor(descPath));
|
|
1214
|
-
} catch {
|
|
1215
|
-
}
|
|
1256
|
+
await this.unlinkVersionSidecar(replica.instance, descPath);
|
|
1216
1257
|
}
|
|
1217
1258
|
try {
|
|
1218
1259
|
await this.deleteFile(descPath);
|
|
@@ -1234,8 +1275,7 @@ var ConfigRepo = class {
|
|
|
1234
1275
|
await this.cachedFS.writeFile(path, bytes);
|
|
1235
1276
|
const author = `${this.appId}/${this.nodeId}`;
|
|
1236
1277
|
const version = await incrementVersion(this.fullFS, path, bytes, author);
|
|
1237
|
-
await this.
|
|
1238
|
-
await writeVersion(this.fullFS, versionPathFor(path), version);
|
|
1278
|
+
await this.writeVersionSidecar(path, version);
|
|
1239
1279
|
}
|
|
1240
1280
|
/** Remove a single backend descriptor file + its version sidecar */
|
|
1241
1281
|
async removeBackendDescriptor(id) {
|
|
@@ -1244,10 +1284,7 @@ var ConfigRepo = class {
|
|
|
1244
1284
|
await this.cachedFS.unlink(path);
|
|
1245
1285
|
} catch {
|
|
1246
1286
|
}
|
|
1247
|
-
|
|
1248
|
-
await this.cachedFS.unlink(versionPathFor(path));
|
|
1249
|
-
} catch {
|
|
1250
|
-
}
|
|
1287
|
+
await this.unlinkVersionSidecar(this.cachedFS, path);
|
|
1251
1288
|
}
|
|
1252
1289
|
// -----------------------------------------------------------------------
|
|
1253
1290
|
// IConfigRepo — Meta file access (no chroot)
|
|
@@ -1346,10 +1383,7 @@ var ConfigRepo = class {
|
|
|
1346
1383
|
await replica.instance.unlink(descPath);
|
|
1347
1384
|
} catch {
|
|
1348
1385
|
}
|
|
1349
|
-
|
|
1350
|
-
await replica.instance.unlink(versionPathFor(descPath));
|
|
1351
|
-
} catch {
|
|
1352
|
-
}
|
|
1386
|
+
await this.unlinkVersionSidecar(replica.instance, descPath);
|
|
1353
1387
|
try {
|
|
1354
1388
|
await this.deleteFile(descPath);
|
|
1355
1389
|
} catch {
|
|
@@ -1445,8 +1479,7 @@ var ConfigRepo = class {
|
|
|
1445
1479
|
await this.cachedFS.writeFile(descPath, bytes);
|
|
1446
1480
|
const author = `${this.appId}/${this.nodeId}`;
|
|
1447
1481
|
const version = await incrementVersion(this.fullFS, descPath, bytes, author);
|
|
1448
|
-
await this.
|
|
1449
|
-
await writeVersion(this.fullFS, versionPathFor(descPath), version);
|
|
1482
|
+
await this.writeVersionSidecar(descPath, version);
|
|
1450
1483
|
this.appDataGroups.set(id, group);
|
|
1451
1484
|
console.log(`[ConfigRepo] createAppDataGroup: "${id}" created`);
|
|
1452
1485
|
return group;
|
|
@@ -1506,10 +1539,7 @@ var ConfigRepo = class {
|
|
|
1506
1539
|
await this.cachedFS.unlink(descPath);
|
|
1507
1540
|
} catch {
|
|
1508
1541
|
}
|
|
1509
|
-
|
|
1510
|
-
await this.cachedFS.unlink(versionPathFor(descPath));
|
|
1511
|
-
} catch {
|
|
1512
|
-
}
|
|
1542
|
+
await this.unlinkVersionSidecar(this.cachedFS, descPath);
|
|
1513
1543
|
console.log(`[ConfigRepo] removeAppDataGroup: "${id}" removed`);
|
|
1514
1544
|
}
|
|
1515
1545
|
async listAccountBackends() {
|
|
@@ -1707,9 +1737,14 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
1707
1737
|
await cachedFS.unlink(BACKENDS_FILE);
|
|
1708
1738
|
} catch {
|
|
1709
1739
|
}
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1740
|
+
{
|
|
1741
|
+
const vPath = versionPathFor(BACKENDS_FILE);
|
|
1742
|
+
if (vPath) {
|
|
1743
|
+
try {
|
|
1744
|
+
await cachedFS.unlink(vPath);
|
|
1745
|
+
} catch {
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1713
1748
|
}
|
|
1714
1749
|
console.log(`[createConfigRepo] Migration complete`);
|
|
1715
1750
|
}
|