zen-fs-config 0.5.19 → 0.5.21
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/README.md +89 -84
- package/README.zh-CN.md +193 -0
- package/dist/index.js +106 -118
- package/dist/index.mjs +106 -118
- package/package.json +6 -1
package/dist/index.mjs
CHANGED
|
@@ -395,6 +395,9 @@ registerBackend("IndexedDB", async (options) => {
|
|
|
395
395
|
defaultOptions: { storeName: "" }
|
|
396
396
|
});
|
|
397
397
|
|
|
398
|
+
// src/config-repo.ts
|
|
399
|
+
import { createLogger } from "@richard432/localstorage-logger";
|
|
400
|
+
|
|
398
401
|
// src/version.ts
|
|
399
402
|
function versionPathFor(configFilePath) {
|
|
400
403
|
const lastSlash = configFilePath.lastIndexOf("/");
|
|
@@ -483,6 +486,7 @@ async function verifyOrRepairVersion(fs, configFilePath, author) {
|
|
|
483
486
|
}
|
|
484
487
|
|
|
485
488
|
// src/config-repo.ts
|
|
489
|
+
var log = createLogger("zen-fs-config:config-repo");
|
|
486
490
|
var META_DIR = "/.meta";
|
|
487
491
|
var GROUP_TYPE_FILE = `${META_DIR}/group-type`;
|
|
488
492
|
var BACKENDS_FILE = `${META_DIR}/backends.json`;
|
|
@@ -598,7 +602,7 @@ var ConfigRepo = class {
|
|
|
598
602
|
const bytes = this.serializer.serialize(data, fullPath);
|
|
599
603
|
this.configCache.set(fullPath, data);
|
|
600
604
|
this.persistConfig(fullPath, bytes).catch((err) => {
|
|
601
|
-
|
|
605
|
+
log.error(`[zen-fs-config] Failed to persist ${fullPath}:`, err);
|
|
602
606
|
});
|
|
603
607
|
}
|
|
604
608
|
// -----------------------------------------------------------------------
|
|
@@ -744,7 +748,7 @@ var ConfigRepo = class {
|
|
|
744
748
|
} catch {
|
|
745
749
|
}
|
|
746
750
|
}
|
|
747
|
-
|
|
751
|
+
log.log(`[ConfigRepo] deleteFile: ${normalizedPath} (tombstone at ${tombstonePath})`);
|
|
748
752
|
this.invalidateTombstoneCache();
|
|
749
753
|
this.schedulePostDeleteSync();
|
|
750
754
|
}
|
|
@@ -762,7 +766,7 @@ var ConfigRepo = class {
|
|
|
762
766
|
this.postDeleteSyncTimer = void 0;
|
|
763
767
|
if (this.disposed) return;
|
|
764
768
|
this.syncEngine.syncAll().catch((err) => {
|
|
765
|
-
|
|
769
|
+
log.warn("[ConfigRepo] post-delete sync failed:", err);
|
|
766
770
|
});
|
|
767
771
|
}, 500);
|
|
768
772
|
}
|
|
@@ -814,11 +818,11 @@ var ConfigRepo = class {
|
|
|
814
818
|
let alreadyDeleted = 0;
|
|
815
819
|
for (const tombstone of tombstones) {
|
|
816
820
|
const tVersionPath = versionPathFor(tombstone.path);
|
|
817
|
-
|
|
821
|
+
log.log(`[TOMB-TRACE] processing tombstone: path=${tombstone.path} versionPath=${tVersionPath ?? "null"}`);
|
|
818
822
|
const existedOnPrimary = await this.safeExists(this.cachedFS, tombstone.path);
|
|
819
|
-
|
|
823
|
+
log.log(`[TOMB-TRACE] primary safeExists(${tombstone.path}) \u2192 ${existedOnPrimary}`);
|
|
820
824
|
if (existedOnPrimary) {
|
|
821
|
-
|
|
825
|
+
log.log(`[TOMB-TRACE] tombstone check: ${tombstone.path} on primary \u2192 EXISTS`);
|
|
822
826
|
}
|
|
823
827
|
if (existedOnPrimary) {
|
|
824
828
|
try {
|
|
@@ -829,7 +833,7 @@ var ConfigRepo = class {
|
|
|
829
833
|
}
|
|
830
834
|
if (tVersionPath) {
|
|
831
835
|
const vExistedOnPrimary = await this.safeExists(this.cachedFS, tVersionPath);
|
|
832
|
-
|
|
836
|
+
log.log(`[TOMB-TRACE] primary safeExists(${tVersionPath}) \u2192 ${vExistedOnPrimary}`);
|
|
833
837
|
if (vExistedOnPrimary) {
|
|
834
838
|
try {
|
|
835
839
|
await this.cachedFS.unlink(tVersionPath);
|
|
@@ -838,40 +842,40 @@ var ConfigRepo = class {
|
|
|
838
842
|
}
|
|
839
843
|
}
|
|
840
844
|
for (const [replicaId, replica] of this.replicaBackends) {
|
|
841
|
-
|
|
845
|
+
log.log(`[TOMB-TRACE] checking replica: ${replicaId} for ${tombstone.path}`);
|
|
842
846
|
const existed = await this.safeExists(replica.instance, tombstone.path);
|
|
843
|
-
|
|
847
|
+
log.log(`[TOMB-TRACE] replica ${replicaId} safeExists(${tombstone.path}) \u2192 ${existed}`);
|
|
844
848
|
if (existed) {
|
|
845
|
-
|
|
849
|
+
log.log(`[TOMB-TRACE] tombstone check: ${tombstone.path} on ${replicaId} \u2192 EXISTS`);
|
|
846
850
|
try {
|
|
847
|
-
|
|
851
|
+
log.log(`[TOMB-TRACE] calling unlink(${tombstone.path}) on ${replicaId}`);
|
|
848
852
|
await replica.instance.unlink(tombstone.path);
|
|
849
|
-
|
|
853
|
+
log.log(`[TOMB-TRACE] tombstone ${tombstone.path}: deleted on ${replicaId}`);
|
|
850
854
|
processed++;
|
|
851
855
|
} catch (err) {
|
|
852
|
-
|
|
856
|
+
log.log(`[TOMB-TRACE] unlink(${tombstone.path}) on ${replicaId} FAILED: ${err}`);
|
|
853
857
|
alreadyDeleted++;
|
|
854
858
|
}
|
|
855
859
|
} else {
|
|
856
|
-
|
|
860
|
+
log.log(`[TOMB-TRACE] replica ${replicaId}: already gone, skipping unlink`);
|
|
857
861
|
alreadyDeleted++;
|
|
858
862
|
}
|
|
859
863
|
if (tVersionPath) {
|
|
860
864
|
const vExisted = await this.safeExists(replica.instance, tVersionPath);
|
|
861
|
-
|
|
865
|
+
log.log(`[TOMB-TRACE] replica ${replicaId} safeExists(${tVersionPath}) \u2192 ${vExisted}`);
|
|
862
866
|
if (vExisted) {
|
|
863
867
|
try {
|
|
864
|
-
|
|
868
|
+
log.log(`[TOMB-TRACE] calling unlink(${tVersionPath}) on ${replicaId}`);
|
|
865
869
|
await replica.instance.unlink(tVersionPath);
|
|
866
870
|
} catch (err) {
|
|
867
|
-
|
|
871
|
+
log.log(`[TOMB-TRACE] unlink(${tVersionPath}) on ${replicaId} FAILED: ${err}`);
|
|
868
872
|
}
|
|
869
873
|
}
|
|
870
874
|
}
|
|
871
875
|
}
|
|
872
876
|
}
|
|
873
877
|
if (processed > 0 || alreadyDeleted > 0) {
|
|
874
|
-
|
|
878
|
+
log.log(`[TOMB-TRACE] processTombstones: ${tombstones.length} tombstone(s), ${processed} deleted, ${alreadyDeleted} already gone`);
|
|
875
879
|
}
|
|
876
880
|
}
|
|
877
881
|
/**
|
|
@@ -887,21 +891,21 @@ var ConfigRepo = class {
|
|
|
887
891
|
const innerBackendName = fs?.inner?.backendName;
|
|
888
892
|
const directBackendName = fs?.backendName;
|
|
889
893
|
const backendLabel = innerBackendName ?? directBackendName ?? innerBackend ?? ctorName;
|
|
890
|
-
|
|
894
|
+
log.log(`[TOMB-TRACE] safeExists(${path}): fs type=${ctorName} backend=${backendLabel} hasExists=${hasExists} hasStat=${hasStat} inner=${innerBackend ?? "N/A"}`);
|
|
891
895
|
if (hasExists) {
|
|
892
|
-
|
|
896
|
+
log.log(`[TOMB-TRACE] safeExists(${path}): \u2192 calling fs.exists() [backend=${backendLabel}]`);
|
|
893
897
|
const result = await fs.exists(path);
|
|
894
|
-
|
|
898
|
+
log.log(`[TOMB-TRACE] safeExists(${path}): \u2190 fs.exists() [backend=${backendLabel}] \u2192 ${result}`);
|
|
895
899
|
return result;
|
|
896
900
|
}
|
|
897
|
-
|
|
901
|
+
log.log(`[TOMB-TRACE] safeExists(${path}): no exists(), calling fs.stat() [backend=${backendLabel}]`);
|
|
898
902
|
await fs.stat(path);
|
|
899
|
-
|
|
903
|
+
log.log(`[TOMB-TRACE] safeExists(${path}): \u2190 fs.stat() [backend=${backendLabel}] \u2192 OK (exists)`);
|
|
900
904
|
return true;
|
|
901
905
|
} catch (err) {
|
|
902
906
|
const errCode = err?.code ?? err?.status ?? "";
|
|
903
907
|
const errName = err?.constructor?.name ?? "";
|
|
904
|
-
|
|
908
|
+
log.log(`[TOMB-TRACE] safeExists(${path}): \u2190 ERROR ${errName} ${errCode} ${err?.message ?? err}`);
|
|
905
909
|
return false;
|
|
906
910
|
}
|
|
907
911
|
}
|
|
@@ -928,9 +932,9 @@ var ConfigRepo = class {
|
|
|
928
932
|
*/
|
|
929
933
|
startBackgroundSync() {
|
|
930
934
|
this.initialSyncPromise = this.initialSyncAndDedup().then(() => {
|
|
931
|
-
|
|
935
|
+
log.log("[ConfigRepo] Background initial sync complete");
|
|
932
936
|
}).catch((err) => {
|
|
933
|
-
|
|
937
|
+
log.error("[ConfigRepo] Background initial sync failed:", err);
|
|
934
938
|
}).finally(() => {
|
|
935
939
|
this.initialSyncPromise = null;
|
|
936
940
|
});
|
|
@@ -958,7 +962,7 @@ var ConfigRepo = class {
|
|
|
958
962
|
} catch {
|
|
959
963
|
}
|
|
960
964
|
}
|
|
961
|
-
|
|
965
|
+
log.log(`[ConfigRepo] updateTombstoneConfirmations: ${tombstones.length} tombstone(s) updated`);
|
|
962
966
|
this.invalidateTombstoneCache();
|
|
963
967
|
}
|
|
964
968
|
/**
|
|
@@ -990,7 +994,7 @@ var ConfigRepo = class {
|
|
|
990
994
|
}
|
|
991
995
|
}
|
|
992
996
|
}
|
|
993
|
-
|
|
997
|
+
log.log(`[ConfigRepo] gcTombstones: removed ${tombstonePath} (all ${allBackendIds.length} backends confirmed)`);
|
|
994
998
|
}
|
|
995
999
|
}
|
|
996
1000
|
}
|
|
@@ -1006,7 +1010,7 @@ var ConfigRepo = class {
|
|
|
1006
1010
|
this.assertNotDisposed();
|
|
1007
1011
|
const results = await this.flush();
|
|
1008
1012
|
for (const result of results) {
|
|
1009
|
-
|
|
1013
|
+
log.log(
|
|
1010
1014
|
`[ConfigRepo] syncMetaToReplicas: ${result.pairId} +${result.filesCreated}/~${result.filesUpdated}/-${result.filesDeleted} skip:${result.filesSkipped} ${result.durationMs}ms`
|
|
1011
1015
|
);
|
|
1012
1016
|
}
|
|
@@ -1108,21 +1112,21 @@ var ConfigRepo = class {
|
|
|
1108
1112
|
// Internal — Setup
|
|
1109
1113
|
// -----------------------------------------------------------------------
|
|
1110
1114
|
async setupSync(backends, primaryBackendId, pollIntervalMs) {
|
|
1111
|
-
|
|
1115
|
+
log.log(`[ConfigRepo] setupSync: ${backends.length} backends, primary=${primaryBackendId} pollInterval=${pollIntervalMs ?? "default"}ms`);
|
|
1112
1116
|
for (const desc of backends) {
|
|
1113
1117
|
if (desc.id === primaryBackendId) continue;
|
|
1114
1118
|
if (desc.enabled === false) {
|
|
1115
|
-
|
|
1119
|
+
log.log(`[ConfigRepo] Skipping disabled replica: ${desc.id}`);
|
|
1116
1120
|
continue;
|
|
1117
1121
|
}
|
|
1118
|
-
|
|
1122
|
+
log.log(`[ConfigRepo] Creating replica backend: id=${desc.id}, type=${desc.type}`);
|
|
1119
1123
|
try {
|
|
1120
1124
|
const instance = await createBackend(desc);
|
|
1121
1125
|
let fsInstance = instance;
|
|
1122
1126
|
if (this.cacheOptions) {
|
|
1123
1127
|
const { wrapWithCache } = await import("./cache-wrapper-XQCPZJCD.mjs");
|
|
1124
1128
|
fsInstance = wrapWithCache(instance, desc.id, this.cacheOptions);
|
|
1125
|
-
|
|
1129
|
+
log.log(`[ConfigRepo] Replica ${desc.id} wrapped with CachedFileSystem (store=${this.cacheOptions.storeType ?? "IdbCacheStore"})`);
|
|
1126
1130
|
}
|
|
1127
1131
|
const syncable = backendToSyncableFS(fsInstance, `${desc.type}(${desc.id})`);
|
|
1128
1132
|
const pair = this.syncEngine.addPair(
|
|
@@ -1137,7 +1141,7 @@ var ConfigRepo = class {
|
|
|
1137
1141
|
this.invalidateTombstoneCache();
|
|
1138
1142
|
await this.processTombstones();
|
|
1139
1143
|
} catch (err) {
|
|
1140
|
-
|
|
1144
|
+
log.warn("[ConfigRepo] preSyncHook processTombstones failed:", err);
|
|
1141
1145
|
}
|
|
1142
1146
|
},
|
|
1143
1147
|
postSyncHook: async () => {
|
|
@@ -1146,7 +1150,7 @@ var ConfigRepo = class {
|
|
|
1146
1150
|
await this.processTombstones();
|
|
1147
1151
|
await this.updateTombstoneConfirmations();
|
|
1148
1152
|
} catch (err) {
|
|
1149
|
-
|
|
1153
|
+
log.warn("[ConfigRepo] postSyncHook tombstone processing failed:", err);
|
|
1150
1154
|
}
|
|
1151
1155
|
}
|
|
1152
1156
|
},
|
|
@@ -1157,13 +1161,13 @@ var ConfigRepo = class {
|
|
|
1157
1161
|
this.handleConflict(event);
|
|
1158
1162
|
};
|
|
1159
1163
|
this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
|
|
1160
|
-
|
|
1164
|
+
log.log(`[ConfigRepo] Replica ${desc.id} created, sync pair=${pair.pairId}`);
|
|
1161
1165
|
} catch (err) {
|
|
1162
|
-
|
|
1166
|
+
log.error(`[ConfigRepo] Failed to create replica ${desc.id} (${desc.type}):`, err);
|
|
1163
1167
|
}
|
|
1164
1168
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1169
|
+
log.log(`[ConfigRepo] setupSync complete. Replicas:`, Array.from(this.replicaBackends.keys()));
|
|
1170
|
+
log.log(`[ConfigRepo] Sync statuses:`, this.getSyncStatuses());
|
|
1167
1171
|
}
|
|
1168
1172
|
// -----------------------------------------------------------------------
|
|
1169
1173
|
// Internal — Persistence
|
|
@@ -1275,7 +1279,7 @@ var ConfigRepo = class {
|
|
|
1275
1279
|
await this.resolveConflict(`${conflictId}/meta.json`, customMerge);
|
|
1276
1280
|
}
|
|
1277
1281
|
} catch (err) {
|
|
1278
|
-
|
|
1282
|
+
log.error("[zen-fs-config] Conflict handler error:", err);
|
|
1279
1283
|
}
|
|
1280
1284
|
}
|
|
1281
1285
|
}
|
|
@@ -1374,11 +1378,11 @@ var ConfigRepo = class {
|
|
|
1374
1378
|
}
|
|
1375
1379
|
return { kind: "ok", desc, mtime };
|
|
1376
1380
|
} else {
|
|
1377
|
-
|
|
1381
|
+
log.warn(`[ConfigRepo] Backend descriptor ${entry} is missing id/type fields, marking for cleanup`);
|
|
1378
1382
|
return { kind: "corrupt", filePath };
|
|
1379
1383
|
}
|
|
1380
1384
|
} catch (parseErr) {
|
|
1381
|
-
|
|
1385
|
+
log.warn(`[ConfigRepo] Backend descriptor ${entry} has corrupted JSON: ${parseErr}. Marking for cleanup.`);
|
|
1382
1386
|
return { kind: "corrupt", filePath };
|
|
1383
1387
|
}
|
|
1384
1388
|
})
|
|
@@ -1427,7 +1431,7 @@ var ConfigRepo = class {
|
|
|
1427
1431
|
}
|
|
1428
1432
|
}
|
|
1429
1433
|
if (duplicates.length > 0) {
|
|
1430
|
-
|
|
1434
|
+
log.log(
|
|
1431
1435
|
`[ConfigRepo] readAllBackendDescriptors: removing ${duplicates.length} duplicate(s): ${duplicates.join(", ")}`
|
|
1432
1436
|
);
|
|
1433
1437
|
for (const dupId of duplicates) {
|
|
@@ -1530,7 +1534,7 @@ var ConfigRepo = class {
|
|
|
1530
1534
|
`Backend "${id}" has the same configuration as existing backend "${dup.id}" (type=${type}). Use removeBackend("${dup.id}") first, or connect with the existing backend's ID.`
|
|
1531
1535
|
);
|
|
1532
1536
|
}
|
|
1533
|
-
|
|
1537
|
+
log.log(`[ConfigRepo] addBackend: creating ${id} (${type})...`);
|
|
1534
1538
|
const instance = await createBackend({ type, options });
|
|
1535
1539
|
const syncable = backendToSyncableFS(instance, `${type}(${id})`);
|
|
1536
1540
|
const desc = { id, type, options, description };
|
|
@@ -1546,7 +1550,7 @@ var ConfigRepo = class {
|
|
|
1546
1550
|
this.invalidateTombstoneCache();
|
|
1547
1551
|
await this.processTombstones();
|
|
1548
1552
|
} catch (err) {
|
|
1549
|
-
|
|
1553
|
+
log.warn("[ConfigRepo] preSyncHook processTombstones failed:", err);
|
|
1550
1554
|
}
|
|
1551
1555
|
},
|
|
1552
1556
|
postSyncHook: async () => {
|
|
@@ -1555,7 +1559,7 @@ var ConfigRepo = class {
|
|
|
1555
1559
|
await this.processTombstones();
|
|
1556
1560
|
await this.updateTombstoneConfirmations();
|
|
1557
1561
|
} catch (err) {
|
|
1558
|
-
|
|
1562
|
+
log.warn("[ConfigRepo] postSyncHook tombstone processing failed:", err);
|
|
1559
1563
|
}
|
|
1560
1564
|
}
|
|
1561
1565
|
},
|
|
@@ -1566,7 +1570,7 @@ var ConfigRepo = class {
|
|
|
1566
1570
|
this.handleConflict(event);
|
|
1567
1571
|
};
|
|
1568
1572
|
this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
|
|
1569
|
-
|
|
1573
|
+
log.log(`[ConfigRepo] addBackend: ${id} (${type}) added, sync pair=${pair.pairId}`);
|
|
1570
1574
|
await this.syncMetaToReplicas();
|
|
1571
1575
|
this.syncEngine.watch(pair.pairId);
|
|
1572
1576
|
}
|
|
@@ -1589,13 +1593,13 @@ var ConfigRepo = class {
|
|
|
1589
1593
|
await this.removeBackendDescriptor(id);
|
|
1590
1594
|
}
|
|
1591
1595
|
this.syncEngine.removePair(replica.pairId);
|
|
1592
|
-
|
|
1596
|
+
log.log(`[ConfigRepo] removeBackend: sync pair ${replica.pairId} removed`);
|
|
1593
1597
|
this.replicaBackends.delete(id);
|
|
1594
1598
|
if (replica.instance?.dispose) {
|
|
1595
1599
|
await replica.instance.dispose();
|
|
1596
1600
|
}
|
|
1597
1601
|
} else {
|
|
1598
|
-
|
|
1602
|
+
log.log(`[ConfigRepo] removeBackend: "${id}" not in replicaBackends, cleaning up descriptor only`);
|
|
1599
1603
|
try {
|
|
1600
1604
|
await this.deleteFile(descPath);
|
|
1601
1605
|
} catch {
|
|
@@ -1603,7 +1607,7 @@ var ConfigRepo = class {
|
|
|
1603
1607
|
}
|
|
1604
1608
|
}
|
|
1605
1609
|
this.schedulePostDeleteSync();
|
|
1606
|
-
|
|
1610
|
+
log.log(`[ConfigRepo] removeBackend: ${id} removed (tombstone written, remote cleaned)`);
|
|
1607
1611
|
}
|
|
1608
1612
|
// -----------------------------------------------------------------------
|
|
1609
1613
|
// IConfigRepo — Group Type
|
|
@@ -1615,14 +1619,14 @@ var ConfigRepo = class {
|
|
|
1615
1619
|
const existing = await this.cachedFS.readFile(GROUP_TYPE_FILE, "utf-8");
|
|
1616
1620
|
const current = existing.trim();
|
|
1617
1621
|
if (current && current !== type) {
|
|
1618
|
-
|
|
1622
|
+
log.warn(`[ConfigRepo] group-type already set to "${current}", ignoring request to set "${type}"`);
|
|
1619
1623
|
return;
|
|
1620
1624
|
}
|
|
1621
1625
|
} catch {
|
|
1622
1626
|
}
|
|
1623
1627
|
await this.ensureDir(GROUP_TYPE_FILE);
|
|
1624
1628
|
await this.cachedFS.writeFile(GROUP_TYPE_FILE, new TextEncoder().encode(type));
|
|
1625
|
-
|
|
1629
|
+
log.log(`[ConfigRepo] group-type set to "${type}"`);
|
|
1626
1630
|
}
|
|
1627
1631
|
/** Read the group-type marker. Returns null if not set. */
|
|
1628
1632
|
async getGroupType() {
|
|
@@ -1663,7 +1667,7 @@ var ConfigRepo = class {
|
|
|
1663
1667
|
if (this.appDataGroups.has(id)) {
|
|
1664
1668
|
throw new Error(`App data group "${id}" already exists. Use removeAppDataGroup() first.`);
|
|
1665
1669
|
}
|
|
1666
|
-
|
|
1670
|
+
log.log(`[ConfigRepo] createAppDataGroup: creating "${id}" with ${backends.length} backend(s)`);
|
|
1667
1671
|
const resolvedBackends = [];
|
|
1668
1672
|
for (const desc of backends) {
|
|
1669
1673
|
const mergedOptions = await this.resolveAppDataBackendOptions(desc);
|
|
@@ -1689,7 +1693,7 @@ var ConfigRepo = class {
|
|
|
1689
1693
|
const version = await incrementVersion(this.fullFS, descPath, bytes, author);
|
|
1690
1694
|
await this.writeVersionSidecar(descPath, version);
|
|
1691
1695
|
this.appDataGroups.set(id, group);
|
|
1692
|
-
|
|
1696
|
+
log.log(`[ConfigRepo] createAppDataGroup: "${id}" created`);
|
|
1693
1697
|
return group;
|
|
1694
1698
|
}
|
|
1695
1699
|
async getAppDataGroup(id) {
|
|
@@ -1748,7 +1752,7 @@ var ConfigRepo = class {
|
|
|
1748
1752
|
} catch {
|
|
1749
1753
|
}
|
|
1750
1754
|
await this.unlinkVersionSidecar(this.cachedFS, descPath);
|
|
1751
|
-
|
|
1755
|
+
log.log(`[ConfigRepo] removeAppDataGroup: "${id}" removed`);
|
|
1752
1756
|
}
|
|
1753
1757
|
async listAccountBackends() {
|
|
1754
1758
|
this.assertNotDisposed();
|
|
@@ -1813,15 +1817,15 @@ var AppDataGroupImpl = class {
|
|
|
1813
1817
|
"/"
|
|
1814
1818
|
);
|
|
1815
1819
|
this.dataBackends.set(desc.id, { instance, syncable, pairId: pair.pairId, desc });
|
|
1816
|
-
|
|
1820
|
+
log.log(`[AppDataGroup:${this.groupId}] backend ${desc.id} (${desc.type}) connected, pair=${pair.pairId}`);
|
|
1817
1821
|
} catch (err) {
|
|
1818
|
-
|
|
1822
|
+
log.error(`[AppDataGroup:${this.groupId}] Failed to create backend ${desc.id} (${desc.type}):`, err);
|
|
1819
1823
|
}
|
|
1820
1824
|
}
|
|
1821
1825
|
try {
|
|
1822
1826
|
await this.syncEngine.syncAll();
|
|
1823
1827
|
} catch (err) {
|
|
1824
|
-
|
|
1828
|
+
log.warn(`[AppDataGroup:${this.groupId}] Initial sync failed:`, err);
|
|
1825
1829
|
}
|
|
1826
1830
|
this.syncEngine.watchAll();
|
|
1827
1831
|
}
|
|
@@ -1837,7 +1841,7 @@ var AppDataGroupImpl = class {
|
|
|
1837
1841
|
if (this.dataBackends.has(id)) {
|
|
1838
1842
|
throw new Error(`Backend "${id}" already exists in data group "${this.groupId}"`);
|
|
1839
1843
|
}
|
|
1840
|
-
|
|
1844
|
+
log.log(`[AppDataGroup:${this.groupId}] addBackend: creating ${id} (${type})...`);
|
|
1841
1845
|
const instance = await createBackend({ type, options });
|
|
1842
1846
|
const syncable = backendToSyncableFS(instance, `${type}(${id})`);
|
|
1843
1847
|
const localSyncable = backendToSyncableFS(this.localFS, `local(${this.groupId})`);
|
|
@@ -1853,11 +1857,11 @@ var AppDataGroupImpl = class {
|
|
|
1853
1857
|
);
|
|
1854
1858
|
const desc = { id, type, options, description };
|
|
1855
1859
|
this.dataBackends.set(id, { instance, syncable, pairId: pair.pairId, desc });
|
|
1856
|
-
|
|
1860
|
+
log.log(`[AppDataGroup:${this.groupId}] addBackend: ${id} (${type}) connected, pair=${pair.pairId}`);
|
|
1857
1861
|
try {
|
|
1858
1862
|
await this.syncEngine.sync(pair.pairId);
|
|
1859
1863
|
} catch (err) {
|
|
1860
|
-
|
|
1864
|
+
log.warn(`[AppDataGroup:${this.groupId}] addBackend: initial sync failed for ${id}:`, err);
|
|
1861
1865
|
}
|
|
1862
1866
|
this.syncEngine.watch(pair.pairId);
|
|
1863
1867
|
}
|
|
@@ -1872,7 +1876,7 @@ var AppDataGroupImpl = class {
|
|
|
1872
1876
|
if (backend.instance?.dispose) {
|
|
1873
1877
|
await backend.instance.dispose();
|
|
1874
1878
|
}
|
|
1875
|
-
|
|
1879
|
+
log.log(`[AppDataGroup:${this.groupId}] removeBackend: ${id} removed`);
|
|
1876
1880
|
}
|
|
1877
1881
|
listBackends() {
|
|
1878
1882
|
return Array.from(this.dataBackends.values()).map((b) => b.desc);
|
|
@@ -1898,7 +1902,7 @@ function toUint8Array(raw) {
|
|
|
1898
1902
|
}
|
|
1899
1903
|
async function createConfigRepo(appId, options = {}) {
|
|
1900
1904
|
const idbStoreName = options.idbStoreName || `zen-fs-config-${appId}`;
|
|
1901
|
-
|
|
1905
|
+
log.log(`[createConfigRepo] Creating IndexedDB primary (store: ${idbStoreName})...`);
|
|
1902
1906
|
const primaryInstance = await createBackend({
|
|
1903
1907
|
type: "IndexedDB",
|
|
1904
1908
|
options: { storeName: idbStoreName }
|
|
@@ -1907,12 +1911,12 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
1907
1911
|
const cacheOptions = options.cache === false ? void 0 : options.cache ?? {};
|
|
1908
1912
|
try {
|
|
1909
1913
|
await primaryInstance.mkdir(META_DIR);
|
|
1910
|
-
|
|
1914
|
+
log.log(`[createConfigRepo] /.meta/ ready`);
|
|
1911
1915
|
} catch (err) {
|
|
1912
1916
|
const msg = err.message || "";
|
|
1913
1917
|
if (msg.includes("File exists") || msg.includes("EEXIST") || err.code === "EEXIST") {
|
|
1914
1918
|
} else {
|
|
1915
|
-
|
|
1919
|
+
log.error(`[createConfigRepo] Failed to ensure /.meta/:`, err.message);
|
|
1916
1920
|
}
|
|
1917
1921
|
}
|
|
1918
1922
|
try {
|
|
@@ -1921,10 +1925,10 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
1921
1925
|
await primaryInstance.readFile(`${META_DIR}/group-type`);
|
|
1922
1926
|
} catch {
|
|
1923
1927
|
await primaryInstance.writeFile(`${META_DIR}/group-type`, groupTypeBytes);
|
|
1924
|
-
|
|
1928
|
+
log.log(`[createConfigRepo] group-type set to "config-sync"`);
|
|
1925
1929
|
}
|
|
1926
1930
|
} catch (err) {
|
|
1927
|
-
|
|
1931
|
+
log.warn(`[createConfigRepo] Failed to write group-type:`, err.message);
|
|
1928
1932
|
}
|
|
1929
1933
|
const tempRepo = new ConfigRepo(
|
|
1930
1934
|
appId,
|
|
@@ -1938,11 +1942,11 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
1938
1942
|
);
|
|
1939
1943
|
const oldBackendsMeta = await tempRepo.readMetaFile(BACKENDS_FILE);
|
|
1940
1944
|
if (oldBackendsMeta && oldBackendsMeta.backends?.length > 0) {
|
|
1941
|
-
|
|
1945
|
+
log.log(`[createConfigRepo] Migrating ${oldBackendsMeta.backends.length} backend(s) from backends.json to individual files...`);
|
|
1942
1946
|
await tempRepo.ensureDir(`${BACKENDS_DIR}/.keep`);
|
|
1943
1947
|
for (const desc of oldBackendsMeta.backends) {
|
|
1944
1948
|
if (desc.id === LOCAL_IDB_BACKEND_ID || desc.type === "IndexedDB") {
|
|
1945
|
-
|
|
1949
|
+
log.log(`[createConfigRepo] Skipping local backend ${desc.id} during migration`);
|
|
1946
1950
|
continue;
|
|
1947
1951
|
}
|
|
1948
1952
|
await tempRepo.writeBackendDescriptor(desc);
|
|
@@ -1960,7 +1964,7 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
1960
1964
|
}
|
|
1961
1965
|
}
|
|
1962
1966
|
}
|
|
1963
|
-
|
|
1967
|
+
log.log(`[createConfigRepo] Migration complete`);
|
|
1964
1968
|
}
|
|
1965
1969
|
let allBackends = await tempRepo.readAllBackendDescriptors();
|
|
1966
1970
|
if (options.backendInfo) {
|
|
@@ -1978,19 +1982,19 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
1978
1982
|
type: options.backendInfo.type,
|
|
1979
1983
|
options: options.backendInfo.options
|
|
1980
1984
|
});
|
|
1981
|
-
|
|
1985
|
+
log.log(`[createConfigRepo] Added replica backend: ${replicaId} (${options.backendInfo.type})`);
|
|
1982
1986
|
allBackends = [...allBackends, { id: replicaId, type: options.backendInfo.type, options: options.backendInfo.options }];
|
|
1983
1987
|
} else if (dupConfig) {
|
|
1984
|
-
|
|
1988
|
+
log.log(`[createConfigRepo] Replica with same config already registered as "${dupConfig.id}", skipping`);
|
|
1985
1989
|
} else {
|
|
1986
|
-
|
|
1990
|
+
log.log(`[createConfigRepo] Replica ${replicaId} already registered`);
|
|
1987
1991
|
}
|
|
1988
1992
|
}
|
|
1989
|
-
|
|
1993
|
+
log.log(`[createConfigRepo] Replica backends: ${allBackends.map((b) => b.id).join(", ") || "(none)"}`);
|
|
1990
1994
|
let nodeId = options.nodeId;
|
|
1991
1995
|
if (!nodeId) {
|
|
1992
1996
|
nodeId = `node-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1993
|
-
|
|
1997
|
+
log.log(`[createConfigRepo] Generated nodeId: ${nodeId}`);
|
|
1994
1998
|
}
|
|
1995
1999
|
const serializer = createSerializerChain(options.serializer);
|
|
1996
2000
|
const repo = new ConfigRepo(
|
|
@@ -2006,11 +2010,11 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
2006
2010
|
await repo.setupSync(allBackends, LOCAL_IDB_BACKEND_ID, options.syncPollIntervalMs);
|
|
2007
2011
|
await repo.load();
|
|
2008
2012
|
if (repo.replicaCount > 0) {
|
|
2009
|
-
|
|
2013
|
+
log.log("[createConfigRepo] Starting background initial sync + dedup...");
|
|
2010
2014
|
repo.startBackgroundSync();
|
|
2011
2015
|
}
|
|
2012
2016
|
repo.syncMetaToReplicas().catch((err) => {
|
|
2013
|
-
|
|
2017
|
+
log.error("[createConfigRepo] background syncMetaToReplicas failed:", err);
|
|
2014
2018
|
});
|
|
2015
2019
|
return repo;
|
|
2016
2020
|
}
|
|
@@ -2022,31 +2026,15 @@ import {
|
|
|
2022
2026
|
} from "zen-fs-sync";
|
|
2023
2027
|
|
|
2024
2028
|
// src/logger.ts
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
try {
|
|
2031
|
-
return localStorage.getItem("ZEN_FS_CONFIG_DEBUG") === "1";
|
|
2032
|
-
} catch {
|
|
2033
|
-
return false;
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
return false;
|
|
2037
|
-
})();
|
|
2038
|
-
function createLogger(prefix) {
|
|
2039
|
-
if (!isDebug) {
|
|
2040
|
-
return () => {
|
|
2041
|
-
};
|
|
2042
|
-
}
|
|
2043
|
-
return (...args) => {
|
|
2044
|
-
console.log(`[${prefix}]`, ...args);
|
|
2045
|
-
};
|
|
2029
|
+
import { createLogger as createLoggerBase } from "@richard432/localstorage-logger";
|
|
2030
|
+
var MODULE_PREFIX = "zen-fs-config";
|
|
2031
|
+
function createLogger2(prefix) {
|
|
2032
|
+
const logger = createLoggerBase(`${MODULE_PREFIX}:${prefix}`);
|
|
2033
|
+
return (...args) => logger.log(...args);
|
|
2046
2034
|
}
|
|
2047
2035
|
|
|
2048
2036
|
// src/data-sync-group.ts
|
|
2049
|
-
var
|
|
2037
|
+
var log2 = createLogger2("data-sync-group");
|
|
2050
2038
|
var META_DIR2 = "/.meta";
|
|
2051
2039
|
var GROUP_TYPE_FILE2 = `${META_DIR2}/group-type`;
|
|
2052
2040
|
var BACKENDS_DIR2 = `${META_DIR2}/backends`;
|
|
@@ -2073,7 +2061,7 @@ var DataSyncGroup = class {
|
|
|
2073
2061
|
if (this.dataBackends.has(id)) {
|
|
2074
2062
|
throw new Error(`Backend "${id}" already exists in data group "${this.groupId}"`);
|
|
2075
2063
|
}
|
|
2076
|
-
|
|
2064
|
+
log2(`addBackend: creating ${id} (${type})...`);
|
|
2077
2065
|
const instance = await createBackend({ type, options });
|
|
2078
2066
|
const syncable = backendToSyncableFS(instance, `${type}(${id})`);
|
|
2079
2067
|
const localSyncable = backendToSyncableFS(this.localFS, `local(${this.groupId})`);
|
|
@@ -2088,12 +2076,12 @@ var DataSyncGroup = class {
|
|
|
2088
2076
|
);
|
|
2089
2077
|
this.dataBackends.set(id, { instance, syncable, pairId: pair.pairId, desc: { id, type, options, description } });
|
|
2090
2078
|
this.syncEngine.watch(pair.pairId);
|
|
2091
|
-
|
|
2079
|
+
log2(`addBackend: ${id} (${type}) connected, pair=${pair.pairId}`);
|
|
2092
2080
|
await this.saveBackendDescriptor(id, type, options, description);
|
|
2093
2081
|
try {
|
|
2094
2082
|
await this.syncEngine.sync(pair.pairId);
|
|
2095
2083
|
} catch (err) {
|
|
2096
|
-
|
|
2084
|
+
log2(`addBackend: initial sync failed for ${id}:`, err);
|
|
2097
2085
|
}
|
|
2098
2086
|
}
|
|
2099
2087
|
/**
|
|
@@ -2114,7 +2102,7 @@ var DataSyncGroup = class {
|
|
|
2114
2102
|
await this.localFS.unlink(`${BACKENDS_DIR2}/${id}.json`);
|
|
2115
2103
|
} catch {
|
|
2116
2104
|
}
|
|
2117
|
-
|
|
2105
|
+
log2(`removeBackend: ${id} removed`);
|
|
2118
2106
|
}
|
|
2119
2107
|
getSyncStatuses() {
|
|
2120
2108
|
return this.syncEngine.getStatusAll();
|
|
@@ -2136,7 +2124,7 @@ var DataSyncGroup = class {
|
|
|
2136
2124
|
}
|
|
2137
2125
|
}
|
|
2138
2126
|
this.dataBackends.clear();
|
|
2139
|
-
|
|
2127
|
+
log2(`disposed`);
|
|
2140
2128
|
}
|
|
2141
2129
|
// -----------------------------------------------------------------------
|
|
2142
2130
|
// Internal — used by the factory function
|
|
@@ -2194,7 +2182,7 @@ var DataSyncGroup = class {
|
|
|
2194
2182
|
};
|
|
2195
2183
|
async function createDataSyncGroup(appId, options = {}) {
|
|
2196
2184
|
const groupId = `data-${appId}-${Date.now().toString(36)}`;
|
|
2197
|
-
|
|
2185
|
+
log2(`createDataSyncGroup: appId=${appId} groupId=${groupId}`);
|
|
2198
2186
|
const localFS = await createBackend({
|
|
2199
2187
|
type: "InMemory",
|
|
2200
2188
|
options: { label: `data-sync-${appId}-${Date.now()}` }
|
|
@@ -2224,7 +2212,7 @@ async function createDataSyncGroup(appId, options = {}) {
|
|
|
2224
2212
|
new TextEncoder().encode("data-sync")
|
|
2225
2213
|
);
|
|
2226
2214
|
} catch (writeErr) {
|
|
2227
|
-
|
|
2215
|
+
log2(`Could not write group-type on remote: ${writeErr.message}`);
|
|
2228
2216
|
}
|
|
2229
2217
|
}
|
|
2230
2218
|
let remoteBackendDescs = [];
|
|
@@ -2269,15 +2257,15 @@ async function createDataSyncGroup(appId, options = {}) {
|
|
|
2269
2257
|
const syncable = backendToSyncableFS(instance, `${rdesc.type}(${rdesc.id})`);
|
|
2270
2258
|
group2._registerBackend(rdesc.id, instance, syncable, options.pollIntervalMs);
|
|
2271
2259
|
} catch (err) {
|
|
2272
|
-
|
|
2260
|
+
log2(`Failed to add remote backend ${rdesc.id}:`, err);
|
|
2273
2261
|
}
|
|
2274
2262
|
}
|
|
2275
2263
|
try {
|
|
2276
2264
|
await group2._syncAll();
|
|
2277
2265
|
} catch (err) {
|
|
2278
|
-
|
|
2266
|
+
log2(`Initial sync failed:`, err);
|
|
2279
2267
|
}
|
|
2280
|
-
|
|
2268
|
+
log2(`createDataSyncGroup: ready with ${group2._backendCount} backend(s)`);
|
|
2281
2269
|
return group2;
|
|
2282
2270
|
}
|
|
2283
2271
|
const group = new DataSyncGroup(appId, groupId, localFS);
|
|
@@ -2285,28 +2273,28 @@ async function createDataSyncGroup(appId, options = {}) {
|
|
|
2285
2273
|
await localFS.writeFile(GROUP_TYPE_FILE2, new TextEncoder().encode("data-sync"));
|
|
2286
2274
|
} catch {
|
|
2287
2275
|
}
|
|
2288
|
-
|
|
2276
|
+
log2(`createDataSyncGroup: local-only group created`);
|
|
2289
2277
|
return group;
|
|
2290
2278
|
}
|
|
2291
2279
|
|
|
2292
2280
|
// src/connect.ts
|
|
2293
|
-
var
|
|
2281
|
+
var log3 = createLogger2("connect");
|
|
2294
2282
|
var META_DIR3 = "/.meta";
|
|
2295
2283
|
var GROUP_TYPE_FILE3 = `${META_DIR3}/group-type`;
|
|
2296
2284
|
async function detectGroupType(type, options) {
|
|
2297
|
-
|
|
2285
|
+
log3(`detectGroupType: connecting to ${type}...`);
|
|
2298
2286
|
const tempBackend = await createBackend({ type, options });
|
|
2299
2287
|
try {
|
|
2300
2288
|
const raw = await tempBackend.readFile(GROUP_TYPE_FILE3, "utf-8");
|
|
2301
2289
|
const groupType = raw.trim();
|
|
2302
2290
|
if (groupType === "config-sync" || groupType === "data-sync") {
|
|
2303
|
-
|
|
2291
|
+
log3(`detectGroupType: detected "${groupType}"`);
|
|
2304
2292
|
return groupType;
|
|
2305
2293
|
}
|
|
2306
|
-
|
|
2294
|
+
log3(`detectGroupType: unknown group-type value "${groupType}", treating as null`);
|
|
2307
2295
|
return null;
|
|
2308
2296
|
} catch {
|
|
2309
|
-
|
|
2297
|
+
log3(`detectGroupType: no group-type file found (new backend)`);
|
|
2310
2298
|
return null;
|
|
2311
2299
|
} finally {
|
|
2312
2300
|
if (tempBackend?.dispose) {
|
|
@@ -2315,10 +2303,10 @@ async function detectGroupType(type, options) {
|
|
|
2315
2303
|
}
|
|
2316
2304
|
}
|
|
2317
2305
|
async function connect(appId, options = {}) {
|
|
2318
|
-
|
|
2306
|
+
log3(`connect: appId=${appId}`);
|
|
2319
2307
|
if (!options.backendInfo) {
|
|
2320
2308
|
const groupType2 = options.groupType ?? "config-sync";
|
|
2321
|
-
|
|
2309
|
+
log3(`connect: no backendInfo, using groupType="${groupType2}"`);
|
|
2322
2310
|
if (groupType2 === "data-sync") {
|
|
2323
2311
|
const dataGroup = await createDataSyncGroup(appId, {});
|
|
2324
2312
|
return { groupType: "data-sync", dataGroup };
|
|
@@ -2342,7 +2330,7 @@ async function connect(appId, options = {}) {
|
|
|
2342
2330
|
groupType = detectedType;
|
|
2343
2331
|
} else {
|
|
2344
2332
|
groupType = options.groupType ?? "config-sync";
|
|
2345
|
-
|
|
2333
|
+
log3(`connect: new backend, using groupType="${groupType}"`);
|
|
2346
2334
|
}
|
|
2347
2335
|
if (groupType === "data-sync") {
|
|
2348
2336
|
const dataGroup = await createDataSyncGroup(appId, {
|