zen-fs-config 0.5.12 → 0.5.14
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/dist/index.d.mts +19 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +181 -66
- package/dist/index.mjs +181 -66
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -485,6 +485,12 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
485
485
|
private readonly pollIntervalMs?;
|
|
486
486
|
/** Tombstone cache — avoids redundant reads within a single flush() cycle. */
|
|
487
487
|
private tombstoneCache;
|
|
488
|
+
/**
|
|
489
|
+
* Tracks the background initial sync started by createConfigRepo().
|
|
490
|
+
* `flush()` and `dispose()` will await this if it hasn't completed yet,
|
|
491
|
+
* preventing concurrent syncEngine.syncAll() calls.
|
|
492
|
+
*/
|
|
493
|
+
private initialSyncPromise;
|
|
488
494
|
constructor(appId: string, nodeId: string, primaryBackendId: string, cachedFS: MinimalAsyncFS, serializer: PathAwareSerializer, onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>, pollIntervalMs?: number, cacheOptions?: CacheOptions);
|
|
489
495
|
/** Full path to this node's directory on the primary backend. */
|
|
490
496
|
get nodePath(): string;
|
|
@@ -505,6 +511,13 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
505
511
|
* to all backends instead of being treated as "missing file → re-create".
|
|
506
512
|
*/
|
|
507
513
|
deleteFile(path: string): Promise<void>;
|
|
514
|
+
/**
|
|
515
|
+
* Background sync scheduled after a deleteFile() call.
|
|
516
|
+
* Uses a short debounce to coalesce multiple rapid deletions.
|
|
517
|
+
* If a sync is already in progress, the next poll will pick up the tombstone.
|
|
518
|
+
*/
|
|
519
|
+
private postDeleteSyncTimer?;
|
|
520
|
+
private schedulePostDeleteSync;
|
|
508
521
|
/**
|
|
509
522
|
* Read all tombstones from the primary backend.
|
|
510
523
|
* Results are cached within a flush() cycle to avoid redundant reads.
|
|
@@ -536,6 +549,12 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
536
549
|
* backend descriptors) that watch()'s initial snapshot would skip.
|
|
537
550
|
*/
|
|
538
551
|
initialSyncAndDedup(): Promise<void>;
|
|
552
|
+
/**
|
|
553
|
+
* Start the initial sync + dedup cycle in the background.
|
|
554
|
+
* `flush()` and `dispose()` will await this promise if it hasn't
|
|
555
|
+
* completed yet, preventing concurrent syncEngine operations.
|
|
556
|
+
*/
|
|
557
|
+
startBackgroundSync(): void;
|
|
539
558
|
/**
|
|
540
559
|
* After sync: mark each tombstone as confirmed by all replica backends.
|
|
541
560
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -485,6 +485,12 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
485
485
|
private readonly pollIntervalMs?;
|
|
486
486
|
/** Tombstone cache — avoids redundant reads within a single flush() cycle. */
|
|
487
487
|
private tombstoneCache;
|
|
488
|
+
/**
|
|
489
|
+
* Tracks the background initial sync started by createConfigRepo().
|
|
490
|
+
* `flush()` and `dispose()` will await this if it hasn't completed yet,
|
|
491
|
+
* preventing concurrent syncEngine.syncAll() calls.
|
|
492
|
+
*/
|
|
493
|
+
private initialSyncPromise;
|
|
488
494
|
constructor(appId: string, nodeId: string, primaryBackendId: string, cachedFS: MinimalAsyncFS, serializer: PathAwareSerializer, onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>, pollIntervalMs?: number, cacheOptions?: CacheOptions);
|
|
489
495
|
/** Full path to this node's directory on the primary backend. */
|
|
490
496
|
get nodePath(): string;
|
|
@@ -505,6 +511,13 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
505
511
|
* to all backends instead of being treated as "missing file → re-create".
|
|
506
512
|
*/
|
|
507
513
|
deleteFile(path: string): Promise<void>;
|
|
514
|
+
/**
|
|
515
|
+
* Background sync scheduled after a deleteFile() call.
|
|
516
|
+
* Uses a short debounce to coalesce multiple rapid deletions.
|
|
517
|
+
* If a sync is already in progress, the next poll will pick up the tombstone.
|
|
518
|
+
*/
|
|
519
|
+
private postDeleteSyncTimer?;
|
|
520
|
+
private schedulePostDeleteSync;
|
|
508
521
|
/**
|
|
509
522
|
* Read all tombstones from the primary backend.
|
|
510
523
|
* Results are cached within a flush() cycle to avoid redundant reads.
|
|
@@ -536,6 +549,12 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
536
549
|
* backend descriptors) that watch()'s initial snapshot would skip.
|
|
537
550
|
*/
|
|
538
551
|
initialSyncAndDedup(): Promise<void>;
|
|
552
|
+
/**
|
|
553
|
+
* Start the initial sync + dedup cycle in the background.
|
|
554
|
+
* `flush()` and `dispose()` will await this promise if it hasn't
|
|
555
|
+
* completed yet, preventing concurrent syncEngine operations.
|
|
556
|
+
*/
|
|
557
|
+
startBackgroundSync(): void;
|
|
539
558
|
/**
|
|
540
559
|
* After sync: mark each tombstone as confirmed by all replica backends.
|
|
541
560
|
*/
|
package/dist/index.js
CHANGED
|
@@ -622,6 +622,12 @@ var ConfigRepo = class {
|
|
|
622
622
|
pollIntervalMs;
|
|
623
623
|
/** Tombstone cache — avoids redundant reads within a single flush() cycle. */
|
|
624
624
|
tombstoneCache = null;
|
|
625
|
+
/**
|
|
626
|
+
* Tracks the background initial sync started by createConfigRepo().
|
|
627
|
+
* `flush()` and `dispose()` will await this if it hasn't completed yet,
|
|
628
|
+
* preventing concurrent syncEngine.syncAll() calls.
|
|
629
|
+
*/
|
|
630
|
+
initialSyncPromise = null;
|
|
625
631
|
constructor(appId, nodeId, primaryBackendId, cachedFS, serializer, onConflict, pollIntervalMs, cacheOptions) {
|
|
626
632
|
this.appId = appId;
|
|
627
633
|
this.nodeId = nodeId;
|
|
@@ -781,6 +787,10 @@ var ConfigRepo = class {
|
|
|
781
787
|
// -----------------------------------------------------------------------
|
|
782
788
|
async flush() {
|
|
783
789
|
this.assertNotDisposed();
|
|
790
|
+
if (this.initialSyncPromise) {
|
|
791
|
+
await this.initialSyncPromise;
|
|
792
|
+
this.initialSyncPromise = null;
|
|
793
|
+
}
|
|
784
794
|
await this.processTombstones();
|
|
785
795
|
const resultsMap = await this.syncEngine.syncAll();
|
|
786
796
|
this.invalidateTombstoneCache();
|
|
@@ -826,6 +836,25 @@ var ConfigRepo = class {
|
|
|
826
836
|
}
|
|
827
837
|
console.log(`[ConfigRepo] deleteFile: ${normalizedPath} (tombstone at ${tombstonePath})`);
|
|
828
838
|
this.invalidateTombstoneCache();
|
|
839
|
+
this.schedulePostDeleteSync();
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Background sync scheduled after a deleteFile() call.
|
|
843
|
+
* Uses a short debounce to coalesce multiple rapid deletions.
|
|
844
|
+
* If a sync is already in progress, the next poll will pick up the tombstone.
|
|
845
|
+
*/
|
|
846
|
+
postDeleteSyncTimer;
|
|
847
|
+
schedulePostDeleteSync() {
|
|
848
|
+
if (this.postDeleteSyncTimer) {
|
|
849
|
+
clearTimeout(this.postDeleteSyncTimer);
|
|
850
|
+
}
|
|
851
|
+
this.postDeleteSyncTimer = setTimeout(() => {
|
|
852
|
+
this.postDeleteSyncTimer = void 0;
|
|
853
|
+
if (this.disposed) return;
|
|
854
|
+
this.syncEngine.syncAll().catch((err) => {
|
|
855
|
+
console.warn("[ConfigRepo] post-delete sync failed:", err);
|
|
856
|
+
});
|
|
857
|
+
}, 500);
|
|
829
858
|
}
|
|
830
859
|
/**
|
|
831
860
|
* Read all tombstones from the primary backend.
|
|
@@ -876,7 +905,9 @@ var ConfigRepo = class {
|
|
|
876
905
|
for (const tombstone of tombstones) {
|
|
877
906
|
const tVersionPath = versionPathFor(tombstone.path);
|
|
878
907
|
const existedOnPrimary = await this.safeExists(this.cachedFS, tombstone.path);
|
|
879
|
-
|
|
908
|
+
if (existedOnPrimary) {
|
|
909
|
+
console.log(`[ConfigRepo] tombstone check: ${tombstone.path} on primary \u2192 EXISTS`);
|
|
910
|
+
}
|
|
880
911
|
if (existedOnPrimary) {
|
|
881
912
|
try {
|
|
882
913
|
await this.cachedFS.unlink(tombstone.path);
|
|
@@ -892,8 +923,8 @@ var ConfigRepo = class {
|
|
|
892
923
|
}
|
|
893
924
|
for (const [replicaId, replica] of this.replicaBackends) {
|
|
894
925
|
const existed = await this.safeExists(replica.instance, tombstone.path);
|
|
895
|
-
console.log(`[ConfigRepo] tombstone check: ${tombstone.path} on ${replicaId} \u2192 ${existed ? "EXISTS" : "not found"}`);
|
|
896
926
|
if (existed) {
|
|
927
|
+
console.log(`[ConfigRepo] tombstone check: ${tombstone.path} on ${replicaId} \u2192 EXISTS`);
|
|
897
928
|
try {
|
|
898
929
|
await replica.instance.unlink(tombstone.path);
|
|
899
930
|
console.log(`[ConfigRepo] tombstone ${tombstone.path}: deleted on ${replicaId}`);
|
|
@@ -949,6 +980,20 @@ var ConfigRepo = class {
|
|
|
949
980
|
await this.processTombstones();
|
|
950
981
|
this.syncEngine.watchAll();
|
|
951
982
|
}
|
|
983
|
+
/**
|
|
984
|
+
* Start the initial sync + dedup cycle in the background.
|
|
985
|
+
* `flush()` and `dispose()` will await this promise if it hasn't
|
|
986
|
+
* completed yet, preventing concurrent syncEngine operations.
|
|
987
|
+
*/
|
|
988
|
+
startBackgroundSync() {
|
|
989
|
+
this.initialSyncPromise = this.initialSyncAndDedup().then(() => {
|
|
990
|
+
console.log("[ConfigRepo] Background initial sync complete");
|
|
991
|
+
}).catch((err) => {
|
|
992
|
+
console.error("[ConfigRepo] Background initial sync failed:", err);
|
|
993
|
+
}).finally(() => {
|
|
994
|
+
this.initialSyncPromise = null;
|
|
995
|
+
});
|
|
996
|
+
}
|
|
952
997
|
/**
|
|
953
998
|
* After sync: mark each tombstone as confirmed by all replica backends.
|
|
954
999
|
*/
|
|
@@ -1096,7 +1141,15 @@ var ConfigRepo = class {
|
|
|
1096
1141
|
// -----------------------------------------------------------------------
|
|
1097
1142
|
async dispose() {
|
|
1098
1143
|
if (this.disposed) return;
|
|
1144
|
+
if (this.initialSyncPromise) {
|
|
1145
|
+
await this.initialSyncPromise;
|
|
1146
|
+
this.initialSyncPromise = null;
|
|
1147
|
+
}
|
|
1099
1148
|
this.disposed = true;
|
|
1149
|
+
if (this.postDeleteSyncTimer) {
|
|
1150
|
+
clearTimeout(this.postDeleteSyncTimer);
|
|
1151
|
+
this.postDeleteSyncTimer = void 0;
|
|
1152
|
+
}
|
|
1100
1153
|
this.syncEngine.dispose();
|
|
1101
1154
|
for (const [_id, replica] of this.replicaBackends) {
|
|
1102
1155
|
if (replica.instance?.dispose) {
|
|
@@ -1137,7 +1190,24 @@ var ConfigRepo = class {
|
|
|
1137
1190
|
{
|
|
1138
1191
|
direction: import_zen_fs_sync.SyncDirection.BiDirectional,
|
|
1139
1192
|
conflictStrategy: "source-wins",
|
|
1140
|
-
pollIntervalMs
|
|
1193
|
+
pollIntervalMs,
|
|
1194
|
+
preSyncHook: async () => {
|
|
1195
|
+
try {
|
|
1196
|
+
this.invalidateTombstoneCache();
|
|
1197
|
+
await this.processTombstones();
|
|
1198
|
+
} catch (err) {
|
|
1199
|
+
console.warn("[ConfigRepo] preSyncHook processTombstones failed:", err);
|
|
1200
|
+
}
|
|
1201
|
+
},
|
|
1202
|
+
postSyncHook: async () => {
|
|
1203
|
+
try {
|
|
1204
|
+
this.invalidateTombstoneCache();
|
|
1205
|
+
await this.processTombstones();
|
|
1206
|
+
await this.updateTombstoneConfirmations();
|
|
1207
|
+
} catch (err) {
|
|
1208
|
+
console.warn("[ConfigRepo] postSyncHook tombstone processing failed:", err);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1141
1211
|
},
|
|
1142
1212
|
"/"
|
|
1143
1213
|
);
|
|
@@ -1190,13 +1260,19 @@ var ConfigRepo = class {
|
|
|
1190
1260
|
const appDir = `/${this.appId}`;
|
|
1191
1261
|
try {
|
|
1192
1262
|
const files = await this.walkDir(appDir);
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1263
|
+
const readResults = await Promise.all(
|
|
1264
|
+
files.map(async (filePath) => {
|
|
1265
|
+
try {
|
|
1266
|
+
const raw = await this.cachedFS.readFile(filePath);
|
|
1267
|
+
const data = this.serializer.deserialize(toUint8Array(raw), filePath);
|
|
1268
|
+
return { filePath, data };
|
|
1269
|
+
} catch {
|
|
1270
|
+
return null;
|
|
1271
|
+
}
|
|
1272
|
+
})
|
|
1273
|
+
);
|
|
1274
|
+
for (const item of readResults) {
|
|
1275
|
+
if (item) this.configCache.set(item.filePath, item.data);
|
|
1200
1276
|
}
|
|
1201
1277
|
} catch {
|
|
1202
1278
|
}
|
|
@@ -1283,17 +1359,23 @@ var ConfigRepo = class {
|
|
|
1283
1359
|
const current = stack.pop();
|
|
1284
1360
|
try {
|
|
1285
1361
|
const entries = await this.cachedFS.readdir(current);
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
results.push(fullPath);
|
|
1362
|
+
const statResults = await Promise.all(
|
|
1363
|
+
entries.filter((entry) => !entry.startsWith(".")).map(async (entry) => {
|
|
1364
|
+
const fullPath = current === "/" ? `/${entry}` : `${current}/${entry}`;
|
|
1365
|
+
try {
|
|
1366
|
+
const stat = await this.cachedFS.stat(fullPath);
|
|
1367
|
+
return { fullPath, stat };
|
|
1368
|
+
} catch {
|
|
1369
|
+
return null;
|
|
1295
1370
|
}
|
|
1296
|
-
}
|
|
1371
|
+
})
|
|
1372
|
+
);
|
|
1373
|
+
for (const item of statResults) {
|
|
1374
|
+
if (!item) continue;
|
|
1375
|
+
if (item.stat.mode !== void 0 && (item.stat.mode & 16384) === 16384) {
|
|
1376
|
+
stack.push(item.fullPath);
|
|
1377
|
+
} else {
|
|
1378
|
+
results.push(item.fullPath);
|
|
1297
1379
|
}
|
|
1298
1380
|
}
|
|
1299
1381
|
} catch {
|
|
@@ -1334,29 +1416,38 @@ var ConfigRepo = class {
|
|
|
1334
1416
|
async readAllBackendDescriptors() {
|
|
1335
1417
|
try {
|
|
1336
1418
|
const entries = await this.cachedFS.readdir(BACKENDS_DIR);
|
|
1337
|
-
const
|
|
1338
|
-
const
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1419
|
+
const jsonEntries = entries.filter((e) => e.endsWith(".json"));
|
|
1420
|
+
const readResults = await Promise.all(
|
|
1421
|
+
jsonEntries.map(async (entry) => {
|
|
1422
|
+
const filePath = `${BACKENDS_DIR}/${entry}`;
|
|
1423
|
+
try {
|
|
1424
|
+
const raw = await this.cachedFS.readFile(filePath);
|
|
1425
|
+
const desc = JSON.parse(new TextDecoder().decode(toUint8Array(raw)));
|
|
1426
|
+
if (desc.id && desc.type) {
|
|
1427
|
+
let mtime = 0;
|
|
1428
|
+
try {
|
|
1429
|
+
const stat = await this.cachedFS.stat(filePath);
|
|
1430
|
+
mtime = stat.mtimeMs ?? 0;
|
|
1431
|
+
} catch {
|
|
1432
|
+
}
|
|
1433
|
+
return { kind: "ok", desc, mtime };
|
|
1434
|
+
} else {
|
|
1435
|
+
console.warn(`[ConfigRepo] Backend descriptor ${entry} is missing id/type fields, marking for cleanup`);
|
|
1436
|
+
return { kind: "corrupt", filePath };
|
|
1351
1437
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
corruptFiles.push(filePath);
|
|
1438
|
+
} catch (parseErr) {
|
|
1439
|
+
console.warn(`[ConfigRepo] Backend descriptor ${entry} has corrupted JSON: ${parseErr}. Marking for cleanup.`);
|
|
1440
|
+
return { kind: "corrupt", filePath };
|
|
1356
1441
|
}
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1442
|
+
})
|
|
1443
|
+
);
|
|
1444
|
+
const items = [];
|
|
1445
|
+
const corruptFiles = [];
|
|
1446
|
+
for (const result of readResults) {
|
|
1447
|
+
if (result.kind === "ok") {
|
|
1448
|
+
items.push({ desc: result.desc, mtime: result.mtime });
|
|
1449
|
+
} else {
|
|
1450
|
+
corruptFiles.push(result.filePath);
|
|
1360
1451
|
}
|
|
1361
1452
|
}
|
|
1362
1453
|
for (const corruptPath of corruptFiles) {
|
|
@@ -1507,7 +1598,24 @@ var ConfigRepo = class {
|
|
|
1507
1598
|
syncable,
|
|
1508
1599
|
{
|
|
1509
1600
|
direction: import_zen_fs_sync.SyncDirection.BiDirectional,
|
|
1510
|
-
conflictStrategy: "source-wins"
|
|
1601
|
+
conflictStrategy: "source-wins",
|
|
1602
|
+
preSyncHook: async () => {
|
|
1603
|
+
try {
|
|
1604
|
+
this.invalidateTombstoneCache();
|
|
1605
|
+
await this.processTombstones();
|
|
1606
|
+
} catch (err) {
|
|
1607
|
+
console.warn("[ConfigRepo] preSyncHook processTombstones failed:", err);
|
|
1608
|
+
}
|
|
1609
|
+
},
|
|
1610
|
+
postSyncHook: async () => {
|
|
1611
|
+
try {
|
|
1612
|
+
this.invalidateTombstoneCache();
|
|
1613
|
+
await this.processTombstones();
|
|
1614
|
+
await this.updateTombstoneConfirmations();
|
|
1615
|
+
} catch (err) {
|
|
1616
|
+
console.warn("[ConfigRepo] postSyncHook tombstone processing failed:", err);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1511
1619
|
},
|
|
1512
1620
|
"/"
|
|
1513
1621
|
);
|
|
@@ -1526,26 +1634,33 @@ var ConfigRepo = class {
|
|
|
1526
1634
|
throw new Error("Cannot remove the local IndexedDB primary backend");
|
|
1527
1635
|
}
|
|
1528
1636
|
const replica = this.replicaBackends.get(id);
|
|
1529
|
-
if (!replica) {
|
|
1530
|
-
throw new Error(`Backend "${id}" is not a registered replica`);
|
|
1531
|
-
}
|
|
1532
1637
|
const descPath = this.backendFilePath(id);
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1638
|
+
if (replica) {
|
|
1639
|
+
try {
|
|
1640
|
+
await replica.instance.unlink(descPath);
|
|
1641
|
+
} catch {
|
|
1642
|
+
}
|
|
1643
|
+
await this.unlinkVersionSidecar(replica.instance, descPath);
|
|
1644
|
+
try {
|
|
1645
|
+
await this.deleteFile(descPath);
|
|
1646
|
+
} catch {
|
|
1647
|
+
await this.removeBackendDescriptor(id);
|
|
1648
|
+
}
|
|
1649
|
+
this.syncEngine.removePair(replica.pairId);
|
|
1650
|
+
console.log(`[ConfigRepo] removeBackend: sync pair ${replica.pairId} removed`);
|
|
1651
|
+
this.replicaBackends.delete(id);
|
|
1652
|
+
if (replica.instance?.dispose) {
|
|
1653
|
+
await replica.instance.dispose();
|
|
1654
|
+
}
|
|
1655
|
+
} else {
|
|
1656
|
+
console.log(`[ConfigRepo] removeBackend: "${id}" not in replicaBackends, cleaning up descriptor only`);
|
|
1657
|
+
try {
|
|
1658
|
+
await this.deleteFile(descPath);
|
|
1659
|
+
} catch {
|
|
1660
|
+
await this.removeBackendDescriptor(id);
|
|
1661
|
+
}
|
|
1548
1662
|
}
|
|
1663
|
+
this.schedulePostDeleteSync();
|
|
1549
1664
|
console.log(`[ConfigRepo] removeBackend: ${id} removed (tombstone written, remote cleaned)`);
|
|
1550
1665
|
}
|
|
1551
1666
|
// -----------------------------------------------------------------------
|
|
@@ -1905,16 +2020,16 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
1905
2020
|
}
|
|
1906
2021
|
console.log(`[createConfigRepo] Migration complete`);
|
|
1907
2022
|
}
|
|
2023
|
+
let allBackends = await tempRepo.readAllBackendDescriptors();
|
|
1908
2024
|
if (options.backendInfo) {
|
|
1909
2025
|
const replicaId = options.primaryBackendId || `${options.backendInfo.type}-replica`;
|
|
1910
|
-
const
|
|
1911
|
-
const hasReplica = allBackends2.some((b) => b.id === replicaId);
|
|
2026
|
+
const hasReplica = allBackends.some((b) => b.id === replicaId);
|
|
1912
2027
|
const newKey = backendDedupKey({
|
|
1913
2028
|
id: replicaId,
|
|
1914
2029
|
type: options.backendInfo.type,
|
|
1915
2030
|
options: options.backendInfo.options
|
|
1916
2031
|
});
|
|
1917
|
-
const dupConfig =
|
|
2032
|
+
const dupConfig = allBackends.find((b) => backendDedupKey(b) === newKey);
|
|
1918
2033
|
if (!hasReplica && !dupConfig) {
|
|
1919
2034
|
await tempRepo.writeBackendDescriptor({
|
|
1920
2035
|
id: replicaId,
|
|
@@ -1922,13 +2037,13 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
1922
2037
|
options: options.backendInfo.options
|
|
1923
2038
|
});
|
|
1924
2039
|
console.log(`[createConfigRepo] Added replica backend: ${replicaId} (${options.backendInfo.type})`);
|
|
2040
|
+
allBackends = [...allBackends, { id: replicaId, type: options.backendInfo.type, options: options.backendInfo.options }];
|
|
1925
2041
|
} else if (dupConfig) {
|
|
1926
2042
|
console.log(`[createConfigRepo] Replica with same config already registered as "${dupConfig.id}", skipping`);
|
|
1927
2043
|
} else {
|
|
1928
2044
|
console.log(`[createConfigRepo] Replica ${replicaId} already registered`);
|
|
1929
2045
|
}
|
|
1930
2046
|
}
|
|
1931
|
-
const allBackends = await tempRepo.readAllBackendDescriptors();
|
|
1932
2047
|
console.log(`[createConfigRepo] Replica backends: ${allBackends.map((b) => b.id).join(", ") || "(none)"}`);
|
|
1933
2048
|
let nodeId = options.nodeId;
|
|
1934
2049
|
if (!nodeId) {
|
|
@@ -1949,8 +2064,8 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
1949
2064
|
await repo.setupSync(allBackends, LOCAL_IDB_BACKEND_ID, options.syncPollIntervalMs);
|
|
1950
2065
|
await repo.load();
|
|
1951
2066
|
if (repo.replicaCount > 0) {
|
|
1952
|
-
console.log("[createConfigRepo]
|
|
1953
|
-
|
|
2067
|
+
console.log("[createConfigRepo] Starting background initial sync + dedup...");
|
|
2068
|
+
repo.startBackgroundSync();
|
|
1954
2069
|
}
|
|
1955
2070
|
repo.syncMetaToReplicas().catch((err) => {
|
|
1956
2071
|
console.error("[createConfigRepo] background syncMetaToReplicas failed:", err);
|
package/dist/index.mjs
CHANGED
|
@@ -532,6 +532,12 @@ var ConfigRepo = class {
|
|
|
532
532
|
pollIntervalMs;
|
|
533
533
|
/** Tombstone cache — avoids redundant reads within a single flush() cycle. */
|
|
534
534
|
tombstoneCache = null;
|
|
535
|
+
/**
|
|
536
|
+
* Tracks the background initial sync started by createConfigRepo().
|
|
537
|
+
* `flush()` and `dispose()` will await this if it hasn't completed yet,
|
|
538
|
+
* preventing concurrent syncEngine.syncAll() calls.
|
|
539
|
+
*/
|
|
540
|
+
initialSyncPromise = null;
|
|
535
541
|
constructor(appId, nodeId, primaryBackendId, cachedFS, serializer, onConflict, pollIntervalMs, cacheOptions) {
|
|
536
542
|
this.appId = appId;
|
|
537
543
|
this.nodeId = nodeId;
|
|
@@ -691,6 +697,10 @@ var ConfigRepo = class {
|
|
|
691
697
|
// -----------------------------------------------------------------------
|
|
692
698
|
async flush() {
|
|
693
699
|
this.assertNotDisposed();
|
|
700
|
+
if (this.initialSyncPromise) {
|
|
701
|
+
await this.initialSyncPromise;
|
|
702
|
+
this.initialSyncPromise = null;
|
|
703
|
+
}
|
|
694
704
|
await this.processTombstones();
|
|
695
705
|
const resultsMap = await this.syncEngine.syncAll();
|
|
696
706
|
this.invalidateTombstoneCache();
|
|
@@ -736,6 +746,25 @@ var ConfigRepo = class {
|
|
|
736
746
|
}
|
|
737
747
|
console.log(`[ConfigRepo] deleteFile: ${normalizedPath} (tombstone at ${tombstonePath})`);
|
|
738
748
|
this.invalidateTombstoneCache();
|
|
749
|
+
this.schedulePostDeleteSync();
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Background sync scheduled after a deleteFile() call.
|
|
753
|
+
* Uses a short debounce to coalesce multiple rapid deletions.
|
|
754
|
+
* If a sync is already in progress, the next poll will pick up the tombstone.
|
|
755
|
+
*/
|
|
756
|
+
postDeleteSyncTimer;
|
|
757
|
+
schedulePostDeleteSync() {
|
|
758
|
+
if (this.postDeleteSyncTimer) {
|
|
759
|
+
clearTimeout(this.postDeleteSyncTimer);
|
|
760
|
+
}
|
|
761
|
+
this.postDeleteSyncTimer = setTimeout(() => {
|
|
762
|
+
this.postDeleteSyncTimer = void 0;
|
|
763
|
+
if (this.disposed) return;
|
|
764
|
+
this.syncEngine.syncAll().catch((err) => {
|
|
765
|
+
console.warn("[ConfigRepo] post-delete sync failed:", err);
|
|
766
|
+
});
|
|
767
|
+
}, 500);
|
|
739
768
|
}
|
|
740
769
|
/**
|
|
741
770
|
* Read all tombstones from the primary backend.
|
|
@@ -786,7 +815,9 @@ var ConfigRepo = class {
|
|
|
786
815
|
for (const tombstone of tombstones) {
|
|
787
816
|
const tVersionPath = versionPathFor(tombstone.path);
|
|
788
817
|
const existedOnPrimary = await this.safeExists(this.cachedFS, tombstone.path);
|
|
789
|
-
|
|
818
|
+
if (existedOnPrimary) {
|
|
819
|
+
console.log(`[ConfigRepo] tombstone check: ${tombstone.path} on primary \u2192 EXISTS`);
|
|
820
|
+
}
|
|
790
821
|
if (existedOnPrimary) {
|
|
791
822
|
try {
|
|
792
823
|
await this.cachedFS.unlink(tombstone.path);
|
|
@@ -802,8 +833,8 @@ var ConfigRepo = class {
|
|
|
802
833
|
}
|
|
803
834
|
for (const [replicaId, replica] of this.replicaBackends) {
|
|
804
835
|
const existed = await this.safeExists(replica.instance, tombstone.path);
|
|
805
|
-
console.log(`[ConfigRepo] tombstone check: ${tombstone.path} on ${replicaId} \u2192 ${existed ? "EXISTS" : "not found"}`);
|
|
806
836
|
if (existed) {
|
|
837
|
+
console.log(`[ConfigRepo] tombstone check: ${tombstone.path} on ${replicaId} \u2192 EXISTS`);
|
|
807
838
|
try {
|
|
808
839
|
await replica.instance.unlink(tombstone.path);
|
|
809
840
|
console.log(`[ConfigRepo] tombstone ${tombstone.path}: deleted on ${replicaId}`);
|
|
@@ -859,6 +890,20 @@ var ConfigRepo = class {
|
|
|
859
890
|
await this.processTombstones();
|
|
860
891
|
this.syncEngine.watchAll();
|
|
861
892
|
}
|
|
893
|
+
/**
|
|
894
|
+
* Start the initial sync + dedup cycle in the background.
|
|
895
|
+
* `flush()` and `dispose()` will await this promise if it hasn't
|
|
896
|
+
* completed yet, preventing concurrent syncEngine operations.
|
|
897
|
+
*/
|
|
898
|
+
startBackgroundSync() {
|
|
899
|
+
this.initialSyncPromise = this.initialSyncAndDedup().then(() => {
|
|
900
|
+
console.log("[ConfigRepo] Background initial sync complete");
|
|
901
|
+
}).catch((err) => {
|
|
902
|
+
console.error("[ConfigRepo] Background initial sync failed:", err);
|
|
903
|
+
}).finally(() => {
|
|
904
|
+
this.initialSyncPromise = null;
|
|
905
|
+
});
|
|
906
|
+
}
|
|
862
907
|
/**
|
|
863
908
|
* After sync: mark each tombstone as confirmed by all replica backends.
|
|
864
909
|
*/
|
|
@@ -1006,7 +1051,15 @@ var ConfigRepo = class {
|
|
|
1006
1051
|
// -----------------------------------------------------------------------
|
|
1007
1052
|
async dispose() {
|
|
1008
1053
|
if (this.disposed) return;
|
|
1054
|
+
if (this.initialSyncPromise) {
|
|
1055
|
+
await this.initialSyncPromise;
|
|
1056
|
+
this.initialSyncPromise = null;
|
|
1057
|
+
}
|
|
1009
1058
|
this.disposed = true;
|
|
1059
|
+
if (this.postDeleteSyncTimer) {
|
|
1060
|
+
clearTimeout(this.postDeleteSyncTimer);
|
|
1061
|
+
this.postDeleteSyncTimer = void 0;
|
|
1062
|
+
}
|
|
1010
1063
|
this.syncEngine.dispose();
|
|
1011
1064
|
for (const [_id, replica] of this.replicaBackends) {
|
|
1012
1065
|
if (replica.instance?.dispose) {
|
|
@@ -1047,7 +1100,24 @@ var ConfigRepo = class {
|
|
|
1047
1100
|
{
|
|
1048
1101
|
direction: SyncDirection.BiDirectional,
|
|
1049
1102
|
conflictStrategy: "source-wins",
|
|
1050
|
-
pollIntervalMs
|
|
1103
|
+
pollIntervalMs,
|
|
1104
|
+
preSyncHook: async () => {
|
|
1105
|
+
try {
|
|
1106
|
+
this.invalidateTombstoneCache();
|
|
1107
|
+
await this.processTombstones();
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
console.warn("[ConfigRepo] preSyncHook processTombstones failed:", err);
|
|
1110
|
+
}
|
|
1111
|
+
},
|
|
1112
|
+
postSyncHook: async () => {
|
|
1113
|
+
try {
|
|
1114
|
+
this.invalidateTombstoneCache();
|
|
1115
|
+
await this.processTombstones();
|
|
1116
|
+
await this.updateTombstoneConfirmations();
|
|
1117
|
+
} catch (err) {
|
|
1118
|
+
console.warn("[ConfigRepo] postSyncHook tombstone processing failed:", err);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1051
1121
|
},
|
|
1052
1122
|
"/"
|
|
1053
1123
|
);
|
|
@@ -1100,13 +1170,19 @@ var ConfigRepo = class {
|
|
|
1100
1170
|
const appDir = `/${this.appId}`;
|
|
1101
1171
|
try {
|
|
1102
1172
|
const files = await this.walkDir(appDir);
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1173
|
+
const readResults = await Promise.all(
|
|
1174
|
+
files.map(async (filePath) => {
|
|
1175
|
+
try {
|
|
1176
|
+
const raw = await this.cachedFS.readFile(filePath);
|
|
1177
|
+
const data = this.serializer.deserialize(toUint8Array(raw), filePath);
|
|
1178
|
+
return { filePath, data };
|
|
1179
|
+
} catch {
|
|
1180
|
+
return null;
|
|
1181
|
+
}
|
|
1182
|
+
})
|
|
1183
|
+
);
|
|
1184
|
+
for (const item of readResults) {
|
|
1185
|
+
if (item) this.configCache.set(item.filePath, item.data);
|
|
1110
1186
|
}
|
|
1111
1187
|
} catch {
|
|
1112
1188
|
}
|
|
@@ -1193,17 +1269,23 @@ var ConfigRepo = class {
|
|
|
1193
1269
|
const current = stack.pop();
|
|
1194
1270
|
try {
|
|
1195
1271
|
const entries = await this.cachedFS.readdir(current);
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
results.push(fullPath);
|
|
1272
|
+
const statResults = await Promise.all(
|
|
1273
|
+
entries.filter((entry) => !entry.startsWith(".")).map(async (entry) => {
|
|
1274
|
+
const fullPath = current === "/" ? `/${entry}` : `${current}/${entry}`;
|
|
1275
|
+
try {
|
|
1276
|
+
const stat = await this.cachedFS.stat(fullPath);
|
|
1277
|
+
return { fullPath, stat };
|
|
1278
|
+
} catch {
|
|
1279
|
+
return null;
|
|
1205
1280
|
}
|
|
1206
|
-
}
|
|
1281
|
+
})
|
|
1282
|
+
);
|
|
1283
|
+
for (const item of statResults) {
|
|
1284
|
+
if (!item) continue;
|
|
1285
|
+
if (item.stat.mode !== void 0 && (item.stat.mode & 16384) === 16384) {
|
|
1286
|
+
stack.push(item.fullPath);
|
|
1287
|
+
} else {
|
|
1288
|
+
results.push(item.fullPath);
|
|
1207
1289
|
}
|
|
1208
1290
|
}
|
|
1209
1291
|
} catch {
|
|
@@ -1244,29 +1326,38 @@ var ConfigRepo = class {
|
|
|
1244
1326
|
async readAllBackendDescriptors() {
|
|
1245
1327
|
try {
|
|
1246
1328
|
const entries = await this.cachedFS.readdir(BACKENDS_DIR);
|
|
1247
|
-
const
|
|
1248
|
-
const
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1329
|
+
const jsonEntries = entries.filter((e) => e.endsWith(".json"));
|
|
1330
|
+
const readResults = await Promise.all(
|
|
1331
|
+
jsonEntries.map(async (entry) => {
|
|
1332
|
+
const filePath = `${BACKENDS_DIR}/${entry}`;
|
|
1333
|
+
try {
|
|
1334
|
+
const raw = await this.cachedFS.readFile(filePath);
|
|
1335
|
+
const desc = JSON.parse(new TextDecoder().decode(toUint8Array(raw)));
|
|
1336
|
+
if (desc.id && desc.type) {
|
|
1337
|
+
let mtime = 0;
|
|
1338
|
+
try {
|
|
1339
|
+
const stat = await this.cachedFS.stat(filePath);
|
|
1340
|
+
mtime = stat.mtimeMs ?? 0;
|
|
1341
|
+
} catch {
|
|
1342
|
+
}
|
|
1343
|
+
return { kind: "ok", desc, mtime };
|
|
1344
|
+
} else {
|
|
1345
|
+
console.warn(`[ConfigRepo] Backend descriptor ${entry} is missing id/type fields, marking for cleanup`);
|
|
1346
|
+
return { kind: "corrupt", filePath };
|
|
1261
1347
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
corruptFiles.push(filePath);
|
|
1348
|
+
} catch (parseErr) {
|
|
1349
|
+
console.warn(`[ConfigRepo] Backend descriptor ${entry} has corrupted JSON: ${parseErr}. Marking for cleanup.`);
|
|
1350
|
+
return { kind: "corrupt", filePath };
|
|
1266
1351
|
}
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1352
|
+
})
|
|
1353
|
+
);
|
|
1354
|
+
const items = [];
|
|
1355
|
+
const corruptFiles = [];
|
|
1356
|
+
for (const result of readResults) {
|
|
1357
|
+
if (result.kind === "ok") {
|
|
1358
|
+
items.push({ desc: result.desc, mtime: result.mtime });
|
|
1359
|
+
} else {
|
|
1360
|
+
corruptFiles.push(result.filePath);
|
|
1270
1361
|
}
|
|
1271
1362
|
}
|
|
1272
1363
|
for (const corruptPath of corruptFiles) {
|
|
@@ -1417,7 +1508,24 @@ var ConfigRepo = class {
|
|
|
1417
1508
|
syncable,
|
|
1418
1509
|
{
|
|
1419
1510
|
direction: SyncDirection.BiDirectional,
|
|
1420
|
-
conflictStrategy: "source-wins"
|
|
1511
|
+
conflictStrategy: "source-wins",
|
|
1512
|
+
preSyncHook: async () => {
|
|
1513
|
+
try {
|
|
1514
|
+
this.invalidateTombstoneCache();
|
|
1515
|
+
await this.processTombstones();
|
|
1516
|
+
} catch (err) {
|
|
1517
|
+
console.warn("[ConfigRepo] preSyncHook processTombstones failed:", err);
|
|
1518
|
+
}
|
|
1519
|
+
},
|
|
1520
|
+
postSyncHook: async () => {
|
|
1521
|
+
try {
|
|
1522
|
+
this.invalidateTombstoneCache();
|
|
1523
|
+
await this.processTombstones();
|
|
1524
|
+
await this.updateTombstoneConfirmations();
|
|
1525
|
+
} catch (err) {
|
|
1526
|
+
console.warn("[ConfigRepo] postSyncHook tombstone processing failed:", err);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1421
1529
|
},
|
|
1422
1530
|
"/"
|
|
1423
1531
|
);
|
|
@@ -1436,26 +1544,33 @@ var ConfigRepo = class {
|
|
|
1436
1544
|
throw new Error("Cannot remove the local IndexedDB primary backend");
|
|
1437
1545
|
}
|
|
1438
1546
|
const replica = this.replicaBackends.get(id);
|
|
1439
|
-
if (!replica) {
|
|
1440
|
-
throw new Error(`Backend "${id}" is not a registered replica`);
|
|
1441
|
-
}
|
|
1442
1547
|
const descPath = this.backendFilePath(id);
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1548
|
+
if (replica) {
|
|
1549
|
+
try {
|
|
1550
|
+
await replica.instance.unlink(descPath);
|
|
1551
|
+
} catch {
|
|
1552
|
+
}
|
|
1553
|
+
await this.unlinkVersionSidecar(replica.instance, descPath);
|
|
1554
|
+
try {
|
|
1555
|
+
await this.deleteFile(descPath);
|
|
1556
|
+
} catch {
|
|
1557
|
+
await this.removeBackendDescriptor(id);
|
|
1558
|
+
}
|
|
1559
|
+
this.syncEngine.removePair(replica.pairId);
|
|
1560
|
+
console.log(`[ConfigRepo] removeBackend: sync pair ${replica.pairId} removed`);
|
|
1561
|
+
this.replicaBackends.delete(id);
|
|
1562
|
+
if (replica.instance?.dispose) {
|
|
1563
|
+
await replica.instance.dispose();
|
|
1564
|
+
}
|
|
1565
|
+
} else {
|
|
1566
|
+
console.log(`[ConfigRepo] removeBackend: "${id}" not in replicaBackends, cleaning up descriptor only`);
|
|
1567
|
+
try {
|
|
1568
|
+
await this.deleteFile(descPath);
|
|
1569
|
+
} catch {
|
|
1570
|
+
await this.removeBackendDescriptor(id);
|
|
1571
|
+
}
|
|
1458
1572
|
}
|
|
1573
|
+
this.schedulePostDeleteSync();
|
|
1459
1574
|
console.log(`[ConfigRepo] removeBackend: ${id} removed (tombstone written, remote cleaned)`);
|
|
1460
1575
|
}
|
|
1461
1576
|
// -----------------------------------------------------------------------
|
|
@@ -1815,16 +1930,16 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
1815
1930
|
}
|
|
1816
1931
|
console.log(`[createConfigRepo] Migration complete`);
|
|
1817
1932
|
}
|
|
1933
|
+
let allBackends = await tempRepo.readAllBackendDescriptors();
|
|
1818
1934
|
if (options.backendInfo) {
|
|
1819
1935
|
const replicaId = options.primaryBackendId || `${options.backendInfo.type}-replica`;
|
|
1820
|
-
const
|
|
1821
|
-
const hasReplica = allBackends2.some((b) => b.id === replicaId);
|
|
1936
|
+
const hasReplica = allBackends.some((b) => b.id === replicaId);
|
|
1822
1937
|
const newKey = backendDedupKey({
|
|
1823
1938
|
id: replicaId,
|
|
1824
1939
|
type: options.backendInfo.type,
|
|
1825
1940
|
options: options.backendInfo.options
|
|
1826
1941
|
});
|
|
1827
|
-
const dupConfig =
|
|
1942
|
+
const dupConfig = allBackends.find((b) => backendDedupKey(b) === newKey);
|
|
1828
1943
|
if (!hasReplica && !dupConfig) {
|
|
1829
1944
|
await tempRepo.writeBackendDescriptor({
|
|
1830
1945
|
id: replicaId,
|
|
@@ -1832,13 +1947,13 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
1832
1947
|
options: options.backendInfo.options
|
|
1833
1948
|
});
|
|
1834
1949
|
console.log(`[createConfigRepo] Added replica backend: ${replicaId} (${options.backendInfo.type})`);
|
|
1950
|
+
allBackends = [...allBackends, { id: replicaId, type: options.backendInfo.type, options: options.backendInfo.options }];
|
|
1835
1951
|
} else if (dupConfig) {
|
|
1836
1952
|
console.log(`[createConfigRepo] Replica with same config already registered as "${dupConfig.id}", skipping`);
|
|
1837
1953
|
} else {
|
|
1838
1954
|
console.log(`[createConfigRepo] Replica ${replicaId} already registered`);
|
|
1839
1955
|
}
|
|
1840
1956
|
}
|
|
1841
|
-
const allBackends = await tempRepo.readAllBackendDescriptors();
|
|
1842
1957
|
console.log(`[createConfigRepo] Replica backends: ${allBackends.map((b) => b.id).join(", ") || "(none)"}`);
|
|
1843
1958
|
let nodeId = options.nodeId;
|
|
1844
1959
|
if (!nodeId) {
|
|
@@ -1859,8 +1974,8 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
1859
1974
|
await repo.setupSync(allBackends, LOCAL_IDB_BACKEND_ID, options.syncPollIntervalMs);
|
|
1860
1975
|
await repo.load();
|
|
1861
1976
|
if (repo.replicaCount > 0) {
|
|
1862
|
-
console.log("[createConfigRepo]
|
|
1863
|
-
|
|
1977
|
+
console.log("[createConfigRepo] Starting background initial sync + dedup...");
|
|
1978
|
+
repo.startBackgroundSync();
|
|
1864
1979
|
}
|
|
1865
1980
|
repo.syncMetaToReplicas().catch((err) => {
|
|
1866
1981
|
console.error("[createConfigRepo] background syncMetaToReplicas failed:", err);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zen-fs-config",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.14",
|
|
4
4
|
"description": "Distributed config management library built on ZenFS, zen-fs-cache, and zen-fs-sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@zenfs/core": ">=2.3.0",
|
|
45
45
|
"@zenfs/dom": ">=1.0.0",
|
|
46
46
|
"zen-fs-cache": ">=1.0.0",
|
|
47
|
-
"zen-fs-sync": ">=0.
|
|
47
|
+
"zen-fs-sync": ">=0.4.6"
|
|
48
48
|
},
|
|
49
49
|
"peerDependenciesMeta": {
|
|
50
50
|
"@zenfs/dom": {
|
|
@@ -61,6 +61,6 @@
|
|
|
61
61
|
"typescript": "^5.9.3",
|
|
62
62
|
"vitest": "^1.6.1",
|
|
63
63
|
"zen-fs-cache": "^1.0.1",
|
|
64
|
-
"zen-fs-sync": "^0.4.
|
|
64
|
+
"zen-fs-sync": "^0.4.6"
|
|
65
65
|
}
|
|
66
66
|
}
|