zen-fs-config 0.5.36 → 0.5.38
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/{backend-registry-7QGFWLM7.mjs → backend-registry-LELIKPQ4.mjs} +1 -1
- package/dist/{chunk-UDEOYFWS.mjs → chunk-4RP2AXAB.mjs} +18 -5
- package/dist/index.d.mts +23 -5
- package/dist/index.d.ts +23 -5
- package/dist/index.js +45 -9
- package/dist/index.mjs +29 -6
- package/dist/zen-fs-config.js +45 -9
- package/package.json +1 -1
|
@@ -137,7 +137,9 @@ registerBackend("InMemory", async (options) => {
|
|
|
137
137
|
{ key: "maxSize", label: "Max Size (bytes)", type: "text", placeholder: "104857600" },
|
|
138
138
|
{ key: "label", label: "Label", type: "text", placeholder: "zen-fs-config-1" }
|
|
139
139
|
],
|
|
140
|
-
defaultOptions: { maxSize: "", label: "" }
|
|
140
|
+
defaultOptions: { maxSize: "", label: "" },
|
|
141
|
+
// identity = store label (maxSize is just client-side capacity tuning)
|
|
142
|
+
identityFields: ["label"]
|
|
141
143
|
});
|
|
142
144
|
registerBackend("IndexedDB", async (options) => {
|
|
143
145
|
const { IndexedDB } = await import("@zenfs/dom");
|
|
@@ -151,7 +153,9 @@ registerBackend("IndexedDB", async (options) => {
|
|
|
151
153
|
fields: [
|
|
152
154
|
{ key: "storeName", label: "Store Name", type: "text", placeholder: "zen-fs-config" }
|
|
153
155
|
],
|
|
154
|
-
defaultOptions: { storeName: "" }
|
|
156
|
+
defaultOptions: { storeName: "" },
|
|
157
|
+
// identity = IndexedDB store name
|
|
158
|
+
identityFields: ["storeName"]
|
|
155
159
|
});
|
|
156
160
|
var _memCache = /* @__PURE__ */ new Map();
|
|
157
161
|
function _cacheGet(key) {
|
|
@@ -260,7 +264,10 @@ registerBackend("GitHub", async (options) => {
|
|
|
260
264
|
{ key: "baseUrl", label: "API URL", type: "text", placeholder: "https://api.github.com" }
|
|
261
265
|
],
|
|
262
266
|
defaultOptions: { owner: "", repo: "", branch: "main", token: "", baseUrl: "" },
|
|
263
|
-
accountFields: ["token", "owner", "baseUrl"]
|
|
267
|
+
accountFields: ["token", "owner", "baseUrl"],
|
|
268
|
+
// identity = the Git repo location (token is auth, but included so that
|
|
269
|
+
// same-repo different-credential backends are still treated as distinct)
|
|
270
|
+
identityFields: ["owner", "repo", "branch", "baseUrl"]
|
|
264
271
|
});
|
|
265
272
|
registerBackend("Gitee", async (options) => {
|
|
266
273
|
const _gt = await importOptionalBackend("zen-fs-gitee");
|
|
@@ -290,7 +297,9 @@ registerBackend("Gitee", async (options) => {
|
|
|
290
297
|
{ key: "baseUrl", label: "API URL", type: "text", placeholder: "https://gitee.com/api/v5" }
|
|
291
298
|
],
|
|
292
299
|
defaultOptions: { owner: "", repo: "", branch: "master", token: "", baseUrl: "" },
|
|
293
|
-
accountFields: ["token", "owner", "baseUrl"]
|
|
300
|
+
accountFields: ["token", "owner", "baseUrl"],
|
|
301
|
+
// identity = the Git repo location
|
|
302
|
+
identityFields: ["owner", "repo", "branch", "baseUrl"]
|
|
294
303
|
});
|
|
295
304
|
registerBackend("RemoteStorage", async (options) => {
|
|
296
305
|
const _rs = await importOptionalBackend("zen-fs-remotestoragejs");
|
|
@@ -317,7 +326,11 @@ registerBackend("RemoteStorage", async (options) => {
|
|
|
317
326
|
{ key: "basePath", label: "Base Path", type: "text", placeholder: "/zen-fs-config/" }
|
|
318
327
|
],
|
|
319
328
|
defaultOptions: { href: "", token: "", basePath: "/" },
|
|
320
|
-
accountFields: ["href", "token"]
|
|
329
|
+
accountFields: ["href", "token"],
|
|
330
|
+
// identity = the RemoteStorage account + subtree. Client tuning (persistCache,
|
|
331
|
+
// cacheFile, preciseMtime, headers, timeout) is intentionally excluded so two
|
|
332
|
+
// connections to the same endpoint with different tuning are deduplicated.
|
|
333
|
+
identityFields: ["href", "token", "basePath"]
|
|
321
334
|
});
|
|
322
335
|
|
|
323
336
|
export {
|
package/dist/index.d.mts
CHANGED
|
@@ -81,8 +81,10 @@ declare function purgeMtimeSidecars(fs: PurgeableFS$1, options?: PurgeMtimeOptio
|
|
|
81
81
|
* metadata that keep real (empty) directories alive, and deleting them would
|
|
82
82
|
* lose directory structure.
|
|
83
83
|
*
|
|
84
|
-
* The intentional `.keep`
|
|
85
|
-
* alive) is protected and never deleted — see
|
|
84
|
+
* The intentional `.keep` placeholder at `/.meta/backends/.keep` (used to keep
|
|
85
|
+
* the backends directory alive) is protected and never deleted — see
|
|
86
|
+
* `protectedDirs`. Any other `.keep` (including `/.meta/.keep`, which some older
|
|
87
|
+
* builds leaked into the local primary) is treated as a leak and purged.
|
|
86
88
|
*/
|
|
87
89
|
/** Minimal async FS surface needed for the purge. */
|
|
88
90
|
interface PurgeableFS {
|
|
@@ -98,9 +100,10 @@ interface PurgeKeepOptions {
|
|
|
98
100
|
/** Report what would be deleted without unlinking anything. */
|
|
99
101
|
dryRun?: boolean;
|
|
100
102
|
/**
|
|
101
|
-
*
|
|
102
|
-
* Defaults to `['/.meta']` to protect the
|
|
103
|
-
* that keeps `/.meta/backends` alive.
|
|
103
|
+
* Exact paths (or directories and their subtrees) whose `.keep` files are
|
|
104
|
+
* NEVER deleted. Defaults to `['/.meta/backends/.keep']` to protect the
|
|
105
|
+
* single intentional placeholder that keeps `/.meta/backends` alive. A leaked
|
|
106
|
+
* `.keep` elsewhere (e.g. `/.meta/.keep`, `/docs/.keep`) is still purged.
|
|
104
107
|
*/
|
|
105
108
|
protectedDirs?: string[];
|
|
106
109
|
}
|
|
@@ -664,6 +667,21 @@ interface BackendMetadata {
|
|
|
664
667
|
* Fields not listed here are considered "storage location" fields.
|
|
665
668
|
*/
|
|
666
669
|
accountFields?: string[];
|
|
670
|
+
/**
|
|
671
|
+
* Option keys that uniquely identify the backend's STORAGE ENDPOINT.
|
|
672
|
+
* Two descriptors of the same `type` whose `identityFields` values match are
|
|
673
|
+
* considered the SAME replica (e.g. same RemoteStorage `href`+`token`, or
|
|
674
|
+
* same Gitee `owner`+`repo`+`branch`) and are deduplicated — even if they
|
|
675
|
+
* differ in client-side tuning options such as `basePath`, `persistCache`,
|
|
676
|
+
* `maxSize`, etc.
|
|
677
|
+
*
|
|
678
|
+
* When omitted, dedup falls back to the FULL options blob (legacy behavior),
|
|
679
|
+
* so only types that declare `identityFields` gain the connection-aware
|
|
680
|
+
* dedup. At minimum include the endpoint-identifying fields; auth/credential
|
|
681
|
+
* fields may be included or omitted depending on whether same-location
|
|
682
|
+
* different-credential backends should be treated as distinct.
|
|
683
|
+
*/
|
|
684
|
+
identityFields?: string[];
|
|
667
685
|
}
|
|
668
686
|
declare function registerBackend(type: string, factory: BackendFactory, metadata?: BackendMetadata): void;
|
|
669
687
|
declare function unregisterBackend(type: string): boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -81,8 +81,10 @@ declare function purgeMtimeSidecars(fs: PurgeableFS$1, options?: PurgeMtimeOptio
|
|
|
81
81
|
* metadata that keep real (empty) directories alive, and deleting them would
|
|
82
82
|
* lose directory structure.
|
|
83
83
|
*
|
|
84
|
-
* The intentional `.keep`
|
|
85
|
-
* alive) is protected and never deleted — see
|
|
84
|
+
* The intentional `.keep` placeholder at `/.meta/backends/.keep` (used to keep
|
|
85
|
+
* the backends directory alive) is protected and never deleted — see
|
|
86
|
+
* `protectedDirs`. Any other `.keep` (including `/.meta/.keep`, which some older
|
|
87
|
+
* builds leaked into the local primary) is treated as a leak and purged.
|
|
86
88
|
*/
|
|
87
89
|
/** Minimal async FS surface needed for the purge. */
|
|
88
90
|
interface PurgeableFS {
|
|
@@ -98,9 +100,10 @@ interface PurgeKeepOptions {
|
|
|
98
100
|
/** Report what would be deleted without unlinking anything. */
|
|
99
101
|
dryRun?: boolean;
|
|
100
102
|
/**
|
|
101
|
-
*
|
|
102
|
-
* Defaults to `['/.meta']` to protect the
|
|
103
|
-
* that keeps `/.meta/backends` alive.
|
|
103
|
+
* Exact paths (or directories and their subtrees) whose `.keep` files are
|
|
104
|
+
* NEVER deleted. Defaults to `['/.meta/backends/.keep']` to protect the
|
|
105
|
+
* single intentional placeholder that keeps `/.meta/backends` alive. A leaked
|
|
106
|
+
* `.keep` elsewhere (e.g. `/.meta/.keep`, `/docs/.keep`) is still purged.
|
|
104
107
|
*/
|
|
105
108
|
protectedDirs?: string[];
|
|
106
109
|
}
|
|
@@ -664,6 +667,21 @@ interface BackendMetadata {
|
|
|
664
667
|
* Fields not listed here are considered "storage location" fields.
|
|
665
668
|
*/
|
|
666
669
|
accountFields?: string[];
|
|
670
|
+
/**
|
|
671
|
+
* Option keys that uniquely identify the backend's STORAGE ENDPOINT.
|
|
672
|
+
* Two descriptors of the same `type` whose `identityFields` values match are
|
|
673
|
+
* considered the SAME replica (e.g. same RemoteStorage `href`+`token`, or
|
|
674
|
+
* same Gitee `owner`+`repo`+`branch`) and are deduplicated — even if they
|
|
675
|
+
* differ in client-side tuning options such as `basePath`, `persistCache`,
|
|
676
|
+
* `maxSize`, etc.
|
|
677
|
+
*
|
|
678
|
+
* When omitted, dedup falls back to the FULL options blob (legacy behavior),
|
|
679
|
+
* so only types that declare `identityFields` gain the connection-aware
|
|
680
|
+
* dedup. At minimum include the endpoint-identifying fields; auth/credential
|
|
681
|
+
* fields may be included or omitted depending on whether same-location
|
|
682
|
+
* different-credential backends should be treated as distinct.
|
|
683
|
+
*/
|
|
684
|
+
identityFields?: string[];
|
|
667
685
|
}
|
|
668
686
|
declare function registerBackend(type: string, factory: BackendFactory, metadata?: BackendMetadata): void;
|
|
669
687
|
declare function unregisterBackend(type: string): boolean;
|
package/dist/index.js
CHANGED
|
@@ -231,7 +231,9 @@ var init_backend_registry = __esm({
|
|
|
231
231
|
{ key: "maxSize", label: "Max Size (bytes)", type: "text", placeholder: "104857600" },
|
|
232
232
|
{ key: "label", label: "Label", type: "text", placeholder: "zen-fs-config-1" }
|
|
233
233
|
],
|
|
234
|
-
defaultOptions: { maxSize: "", label: "" }
|
|
234
|
+
defaultOptions: { maxSize: "", label: "" },
|
|
235
|
+
// identity = store label (maxSize is just client-side capacity tuning)
|
|
236
|
+
identityFields: ["label"]
|
|
235
237
|
});
|
|
236
238
|
registerBackend("IndexedDB", async (options) => {
|
|
237
239
|
const { IndexedDB } = await import("@zenfs/dom");
|
|
@@ -245,7 +247,9 @@ var init_backend_registry = __esm({
|
|
|
245
247
|
fields: [
|
|
246
248
|
{ key: "storeName", label: "Store Name", type: "text", placeholder: "zen-fs-config" }
|
|
247
249
|
],
|
|
248
|
-
defaultOptions: { storeName: "" }
|
|
250
|
+
defaultOptions: { storeName: "" },
|
|
251
|
+
// identity = IndexedDB store name
|
|
252
|
+
identityFields: ["storeName"]
|
|
249
253
|
});
|
|
250
254
|
_memCache = /* @__PURE__ */ new Map();
|
|
251
255
|
OPTIONAL_BACKEND_GLOBALS = {
|
|
@@ -309,7 +313,10 @@ var init_backend_registry = __esm({
|
|
|
309
313
|
{ key: "baseUrl", label: "API URL", type: "text", placeholder: "https://api.github.com" }
|
|
310
314
|
],
|
|
311
315
|
defaultOptions: { owner: "", repo: "", branch: "main", token: "", baseUrl: "" },
|
|
312
|
-
accountFields: ["token", "owner", "baseUrl"]
|
|
316
|
+
accountFields: ["token", "owner", "baseUrl"],
|
|
317
|
+
// identity = the Git repo location (token is auth, but included so that
|
|
318
|
+
// same-repo different-credential backends are still treated as distinct)
|
|
319
|
+
identityFields: ["owner", "repo", "branch", "baseUrl"]
|
|
313
320
|
});
|
|
314
321
|
registerBackend("Gitee", async (options) => {
|
|
315
322
|
const _gt = await importOptionalBackend("zen-fs-gitee");
|
|
@@ -339,7 +346,9 @@ var init_backend_registry = __esm({
|
|
|
339
346
|
{ key: "baseUrl", label: "API URL", type: "text", placeholder: "https://gitee.com/api/v5" }
|
|
340
347
|
],
|
|
341
348
|
defaultOptions: { owner: "", repo: "", branch: "master", token: "", baseUrl: "" },
|
|
342
|
-
accountFields: ["token", "owner", "baseUrl"]
|
|
349
|
+
accountFields: ["token", "owner", "baseUrl"],
|
|
350
|
+
// identity = the Git repo location
|
|
351
|
+
identityFields: ["owner", "repo", "branch", "baseUrl"]
|
|
343
352
|
});
|
|
344
353
|
registerBackend("RemoteStorage", async (options) => {
|
|
345
354
|
const _rs = await importOptionalBackend("zen-fs-remotestoragejs");
|
|
@@ -366,7 +375,11 @@ var init_backend_registry = __esm({
|
|
|
366
375
|
{ key: "basePath", label: "Base Path", type: "text", placeholder: "/zen-fs-config/" }
|
|
367
376
|
],
|
|
368
377
|
defaultOptions: { href: "", token: "", basePath: "/" },
|
|
369
|
-
accountFields: ["href", "token"]
|
|
378
|
+
accountFields: ["href", "token"],
|
|
379
|
+
// identity = the RemoteStorage account + subtree. Client tuning (persistCache,
|
|
380
|
+
// cacheFile, preciseMtime, headers, timeout) is intentionally excluded so two
|
|
381
|
+
// connections to the same endpoint with different tuning are deduplicated.
|
|
382
|
+
identityFields: ["href", "token", "basePath"]
|
|
370
383
|
});
|
|
371
384
|
}
|
|
372
385
|
});
|
|
@@ -1135,7 +1148,7 @@ function isKeepFile(fileName) {
|
|
|
1135
1148
|
return fileName === ".keep";
|
|
1136
1149
|
}
|
|
1137
1150
|
var S_IFDIR2 = 16384;
|
|
1138
|
-
var DEFAULT_PROTECTED_DIRS = ["/.meta"];
|
|
1151
|
+
var DEFAULT_PROTECTED_DIRS = ["/.meta/backends/.keep"];
|
|
1139
1152
|
function isDirectory2(stat) {
|
|
1140
1153
|
return typeof stat.mode === "number" && (stat.mode & S_IFDIR2) === S_IFDIR2;
|
|
1141
1154
|
}
|
|
@@ -1346,8 +1359,22 @@ function sortKeysDeep(obj) {
|
|
|
1346
1359
|
}
|
|
1347
1360
|
return sorted;
|
|
1348
1361
|
}
|
|
1362
|
+
function identityOptions(type, options) {
|
|
1363
|
+
const meta = getBackendMetadata(type);
|
|
1364
|
+
const fields = meta?.identityFields;
|
|
1365
|
+
if (!fields || fields.length === 0) {
|
|
1366
|
+
log2.warn(`[DEDUP-DIAG] identityOptions(${type}): NO identityFields -> falling back to FULL options (dedup will be key-sensitive to tuning fields)`);
|
|
1367
|
+
return options ?? {};
|
|
1368
|
+
}
|
|
1369
|
+
const subset = {};
|
|
1370
|
+
for (const f of fields) {
|
|
1371
|
+
if (options && options[f] !== void 0) subset[f] = options[f];
|
|
1372
|
+
}
|
|
1373
|
+
log2.log(`[DEDUP-DIAG] identityOptions(${type}): identityFields=${JSON.stringify(fields)} identitySubset=${JSON.stringify(subset)}`);
|
|
1374
|
+
return subset;
|
|
1375
|
+
}
|
|
1349
1376
|
function backendDedupKey(desc) {
|
|
1350
|
-
return `${desc.type}:${stableOptionsKey(desc.options)}`;
|
|
1377
|
+
return `${desc.type}:${stableOptionsKey(identityOptions(desc.type, desc.options))}`;
|
|
1351
1378
|
}
|
|
1352
1379
|
function backendAgeRank(b) {
|
|
1353
1380
|
if (typeof b.createdAt === "number") return b.createdAt;
|
|
@@ -2382,18 +2409,22 @@ var ConfigRepo = class {
|
|
|
2382
2409
|
const duplicates = [];
|
|
2383
2410
|
for (const item of items) {
|
|
2384
2411
|
const key = backendDedupKey(item.desc);
|
|
2412
|
+
log2.log(`[DEDUP-DIAG] item id=${item.desc.id} type=${item.desc.type} dedupKey=${key} mtime=${item.mtime} rawOptions=${JSON.stringify(item.desc.options)}`);
|
|
2385
2413
|
const existing = seen.get(key);
|
|
2386
2414
|
if (existing) {
|
|
2387
2415
|
if (item.mtime < existing.mtime) {
|
|
2416
|
+
log2.log(`[DEDUP-DIAG] DUPLICATE: new id=${item.desc.id} (older) keeps key, dropping existing=${existing.desc.id}`);
|
|
2388
2417
|
duplicates.push(existing.desc.id);
|
|
2389
2418
|
seen.set(key, item);
|
|
2390
2419
|
} else {
|
|
2420
|
+
log2.log(`[DEDUP-DIAG] DUPLICATE: new id=${item.desc.id} dropped, kept=${existing.desc.id}`);
|
|
2391
2421
|
duplicates.push(item.desc.id);
|
|
2392
2422
|
}
|
|
2393
2423
|
} else {
|
|
2394
2424
|
seen.set(key, item);
|
|
2395
2425
|
}
|
|
2396
2426
|
}
|
|
2427
|
+
log2.log(`[DEDUP-DIAG] dedup summary: keptKeys=${[...seen.keys()].length} duplicates=${JSON.stringify(duplicates)}`);
|
|
2397
2428
|
if (duplicates.length > 0) {
|
|
2398
2429
|
log2.log(
|
|
2399
2430
|
`[ConfigRepo] readAllBackendDescriptors: removing ${duplicates.length} duplicate(s): ${duplicates.join(", ")}`
|
|
@@ -2403,10 +2434,13 @@ var ConfigRepo = class {
|
|
|
2403
2434
|
for (const [replicaId, replica] of this.replicaBackends) {
|
|
2404
2435
|
try {
|
|
2405
2436
|
await replica.instance.unlink(descPath);
|
|
2406
|
-
|
|
2437
|
+
log2.log(`[DEDUP-DIAG] removed dup ${dupId} from replica ${replicaId}`);
|
|
2438
|
+
} catch (e) {
|
|
2439
|
+
log2.log(`[DEDUP-DIAG] remove dup ${dupId} from replica ${replicaId} failed/skipped: ${String(e)}`);
|
|
2407
2440
|
}
|
|
2408
2441
|
await this.unlinkVersionSidecar(replica.instance, descPath);
|
|
2409
2442
|
}
|
|
2443
|
+
log2.log(`[DEDUP-DIAG] replicaBackends available during removal: ${[...this.replicaBackends.keys()].join(",") || "(none)"}`);
|
|
2410
2444
|
try {
|
|
2411
2445
|
await this.deleteFile(descPath);
|
|
2412
2446
|
} catch {
|
|
@@ -2415,7 +2449,8 @@ var ConfigRepo = class {
|
|
|
2415
2449
|
}
|
|
2416
2450
|
}
|
|
2417
2451
|
return Array.from(seen.values()).map((i) => i.desc);
|
|
2418
|
-
} catch {
|
|
2452
|
+
} catch (e) {
|
|
2453
|
+
log2.error(`[DEDUP-DIAG] readAllBackendDescriptors threw, returning []:`, e);
|
|
2419
2454
|
return [];
|
|
2420
2455
|
}
|
|
2421
2456
|
}
|
|
@@ -3019,6 +3054,7 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
3019
3054
|
log2.log(`[createConfigRepo] Migration complete`);
|
|
3020
3055
|
}
|
|
3021
3056
|
let allBackends = await tempRepo.readAllBackendDescriptors();
|
|
3057
|
+
log2.log(`[DEDUP-DIAG] createConfigRepo after readAllBackendDescriptors: allBackends=${allBackends.map((b) => b.id).join(",")}`);
|
|
3022
3058
|
if (options.backendInfo) {
|
|
3023
3059
|
const replicaId = options.primaryBackendId || `${options.backendInfo.type}-replica`;
|
|
3024
3060
|
const hasReplica = allBackends.some((b) => b.id === replicaId);
|
package/dist/index.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
registerBackend,
|
|
10
10
|
unregisterBackend,
|
|
11
11
|
wrapZenFSFileSystem
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-4RP2AXAB.mjs";
|
|
13
13
|
|
|
14
14
|
// src/config-repo.ts
|
|
15
15
|
import {
|
|
@@ -351,7 +351,7 @@ function registerFolderBackend() {
|
|
|
351
351
|
const pathMod = await import("path");
|
|
352
352
|
const dir = options.path;
|
|
353
353
|
const store = new FolderStore({ path: dir, label: options.label, fs, pathMod });
|
|
354
|
-
return (await import("./backend-registry-
|
|
354
|
+
return (await import("./backend-registry-LELIKPQ4.mjs")).wrapZenFSFileSystem({ backend: FolderBackend, path: dir, label: options.label });
|
|
355
355
|
},
|
|
356
356
|
{
|
|
357
357
|
type: "Folder",
|
|
@@ -693,7 +693,7 @@ function isKeepFile(fileName) {
|
|
|
693
693
|
return fileName === ".keep";
|
|
694
694
|
}
|
|
695
695
|
var S_IFDIR2 = 16384;
|
|
696
|
-
var DEFAULT_PROTECTED_DIRS = ["/.meta"];
|
|
696
|
+
var DEFAULT_PROTECTED_DIRS = ["/.meta/backends/.keep"];
|
|
697
697
|
function isDirectory2(stat) {
|
|
698
698
|
return typeof stat.mode === "number" && (stat.mode & S_IFDIR2) === S_IFDIR2;
|
|
699
699
|
}
|
|
@@ -904,8 +904,22 @@ function sortKeysDeep(obj) {
|
|
|
904
904
|
}
|
|
905
905
|
return sorted;
|
|
906
906
|
}
|
|
907
|
+
function identityOptions(type, options) {
|
|
908
|
+
const meta = getBackendMetadata(type);
|
|
909
|
+
const fields = meta?.identityFields;
|
|
910
|
+
if (!fields || fields.length === 0) {
|
|
911
|
+
log2.warn(`[DEDUP-DIAG] identityOptions(${type}): NO identityFields -> falling back to FULL options (dedup will be key-sensitive to tuning fields)`);
|
|
912
|
+
return options ?? {};
|
|
913
|
+
}
|
|
914
|
+
const subset = {};
|
|
915
|
+
for (const f of fields) {
|
|
916
|
+
if (options && options[f] !== void 0) subset[f] = options[f];
|
|
917
|
+
}
|
|
918
|
+
log2.log(`[DEDUP-DIAG] identityOptions(${type}): identityFields=${JSON.stringify(fields)} identitySubset=${JSON.stringify(subset)}`);
|
|
919
|
+
return subset;
|
|
920
|
+
}
|
|
907
921
|
function backendDedupKey(desc) {
|
|
908
|
-
return `${desc.type}:${stableOptionsKey(desc.options)}`;
|
|
922
|
+
return `${desc.type}:${stableOptionsKey(identityOptions(desc.type, desc.options))}`;
|
|
909
923
|
}
|
|
910
924
|
function backendAgeRank(b) {
|
|
911
925
|
if (typeof b.createdAt === "number") return b.createdAt;
|
|
@@ -1940,18 +1954,22 @@ var ConfigRepo = class {
|
|
|
1940
1954
|
const duplicates = [];
|
|
1941
1955
|
for (const item of items) {
|
|
1942
1956
|
const key = backendDedupKey(item.desc);
|
|
1957
|
+
log2.log(`[DEDUP-DIAG] item id=${item.desc.id} type=${item.desc.type} dedupKey=${key} mtime=${item.mtime} rawOptions=${JSON.stringify(item.desc.options)}`);
|
|
1943
1958
|
const existing = seen.get(key);
|
|
1944
1959
|
if (existing) {
|
|
1945
1960
|
if (item.mtime < existing.mtime) {
|
|
1961
|
+
log2.log(`[DEDUP-DIAG] DUPLICATE: new id=${item.desc.id} (older) keeps key, dropping existing=${existing.desc.id}`);
|
|
1946
1962
|
duplicates.push(existing.desc.id);
|
|
1947
1963
|
seen.set(key, item);
|
|
1948
1964
|
} else {
|
|
1965
|
+
log2.log(`[DEDUP-DIAG] DUPLICATE: new id=${item.desc.id} dropped, kept=${existing.desc.id}`);
|
|
1949
1966
|
duplicates.push(item.desc.id);
|
|
1950
1967
|
}
|
|
1951
1968
|
} else {
|
|
1952
1969
|
seen.set(key, item);
|
|
1953
1970
|
}
|
|
1954
1971
|
}
|
|
1972
|
+
log2.log(`[DEDUP-DIAG] dedup summary: keptKeys=${[...seen.keys()].length} duplicates=${JSON.stringify(duplicates)}`);
|
|
1955
1973
|
if (duplicates.length > 0) {
|
|
1956
1974
|
log2.log(
|
|
1957
1975
|
`[ConfigRepo] readAllBackendDescriptors: removing ${duplicates.length} duplicate(s): ${duplicates.join(", ")}`
|
|
@@ -1961,10 +1979,13 @@ var ConfigRepo = class {
|
|
|
1961
1979
|
for (const [replicaId, replica] of this.replicaBackends) {
|
|
1962
1980
|
try {
|
|
1963
1981
|
await replica.instance.unlink(descPath);
|
|
1964
|
-
|
|
1982
|
+
log2.log(`[DEDUP-DIAG] removed dup ${dupId} from replica ${replicaId}`);
|
|
1983
|
+
} catch (e) {
|
|
1984
|
+
log2.log(`[DEDUP-DIAG] remove dup ${dupId} from replica ${replicaId} failed/skipped: ${String(e)}`);
|
|
1965
1985
|
}
|
|
1966
1986
|
await this.unlinkVersionSidecar(replica.instance, descPath);
|
|
1967
1987
|
}
|
|
1988
|
+
log2.log(`[DEDUP-DIAG] replicaBackends available during removal: ${[...this.replicaBackends.keys()].join(",") || "(none)"}`);
|
|
1968
1989
|
try {
|
|
1969
1990
|
await this.deleteFile(descPath);
|
|
1970
1991
|
} catch {
|
|
@@ -1973,7 +1994,8 @@ var ConfigRepo = class {
|
|
|
1973
1994
|
}
|
|
1974
1995
|
}
|
|
1975
1996
|
return Array.from(seen.values()).map((i) => i.desc);
|
|
1976
|
-
} catch {
|
|
1997
|
+
} catch (e) {
|
|
1998
|
+
log2.error(`[DEDUP-DIAG] readAllBackendDescriptors threw, returning []:`, e);
|
|
1977
1999
|
return [];
|
|
1978
2000
|
}
|
|
1979
2001
|
}
|
|
@@ -2577,6 +2599,7 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
2577
2599
|
log2.log(`[createConfigRepo] Migration complete`);
|
|
2578
2600
|
}
|
|
2579
2601
|
let allBackends = await tempRepo.readAllBackendDescriptors();
|
|
2602
|
+
log2.log(`[DEDUP-DIAG] createConfigRepo after readAllBackendDescriptors: allBackends=${allBackends.map((b) => b.id).join(",")}`);
|
|
2580
2603
|
if (options.backendInfo) {
|
|
2581
2604
|
const replicaId = options.primaryBackendId || `${options.backendInfo.type}-replica`;
|
|
2582
2605
|
const hasReplica = allBackends.some((b) => b.id === replicaId);
|
package/dist/zen-fs-config.js
CHANGED
|
@@ -24544,7 +24544,9 @@ var ZenFSConfig = (() => {
|
|
|
24544
24544
|
{ key: "maxSize", label: "Max Size (bytes)", type: "text", placeholder: "104857600" },
|
|
24545
24545
|
{ key: "label", label: "Label", type: "text", placeholder: "zen-fs-config-1" }
|
|
24546
24546
|
],
|
|
24547
|
-
defaultOptions: { maxSize: "", label: "" }
|
|
24547
|
+
defaultOptions: { maxSize: "", label: "" },
|
|
24548
|
+
// identity = store label (maxSize is just client-side capacity tuning)
|
|
24549
|
+
identityFields: ["label"]
|
|
24548
24550
|
});
|
|
24549
24551
|
registerBackend("IndexedDB", async (options) => {
|
|
24550
24552
|
const { IndexedDB: IndexedDB2 } = await Promise.resolve().then(() => (init_dist5(), dist_exports2));
|
|
@@ -24558,7 +24560,9 @@ var ZenFSConfig = (() => {
|
|
|
24558
24560
|
fields: [
|
|
24559
24561
|
{ key: "storeName", label: "Store Name", type: "text", placeholder: "zen-fs-config" }
|
|
24560
24562
|
],
|
|
24561
|
-
defaultOptions: { storeName: "" }
|
|
24563
|
+
defaultOptions: { storeName: "" },
|
|
24564
|
+
// identity = IndexedDB store name
|
|
24565
|
+
identityFields: ["storeName"]
|
|
24562
24566
|
});
|
|
24563
24567
|
_memCache = /* @__PURE__ */ new Map();
|
|
24564
24568
|
OPTIONAL_BACKEND_GLOBALS = {
|
|
@@ -24622,7 +24626,10 @@ var ZenFSConfig = (() => {
|
|
|
24622
24626
|
{ key: "baseUrl", label: "API URL", type: "text", placeholder: "https://api.github.com" }
|
|
24623
24627
|
],
|
|
24624
24628
|
defaultOptions: { owner: "", repo: "", branch: "main", token: "", baseUrl: "" },
|
|
24625
|
-
accountFields: ["token", "owner", "baseUrl"]
|
|
24629
|
+
accountFields: ["token", "owner", "baseUrl"],
|
|
24630
|
+
// identity = the Git repo location (token is auth, but included so that
|
|
24631
|
+
// same-repo different-credential backends are still treated as distinct)
|
|
24632
|
+
identityFields: ["owner", "repo", "branch", "baseUrl"]
|
|
24626
24633
|
});
|
|
24627
24634
|
registerBackend("Gitee", async (options) => {
|
|
24628
24635
|
const _gt = await importOptionalBackend("zen-fs-gitee");
|
|
@@ -24652,7 +24659,9 @@ var ZenFSConfig = (() => {
|
|
|
24652
24659
|
{ key: "baseUrl", label: "API URL", type: "text", placeholder: "https://gitee.com/api/v5" }
|
|
24653
24660
|
],
|
|
24654
24661
|
defaultOptions: { owner: "", repo: "", branch: "master", token: "", baseUrl: "" },
|
|
24655
|
-
accountFields: ["token", "owner", "baseUrl"]
|
|
24662
|
+
accountFields: ["token", "owner", "baseUrl"],
|
|
24663
|
+
// identity = the Git repo location
|
|
24664
|
+
identityFields: ["owner", "repo", "branch", "baseUrl"]
|
|
24656
24665
|
});
|
|
24657
24666
|
registerBackend("RemoteStorage", async (options) => {
|
|
24658
24667
|
const _rs = await importOptionalBackend("zen-fs-remotestoragejs");
|
|
@@ -24679,7 +24688,11 @@ var ZenFSConfig = (() => {
|
|
|
24679
24688
|
{ key: "basePath", label: "Base Path", type: "text", placeholder: "/zen-fs-config/" }
|
|
24680
24689
|
],
|
|
24681
24690
|
defaultOptions: { href: "", token: "", basePath: "/" },
|
|
24682
|
-
accountFields: ["href", "token"]
|
|
24691
|
+
accountFields: ["href", "token"],
|
|
24692
|
+
// identity = the RemoteStorage account + subtree. Client tuning (persistCache,
|
|
24693
|
+
// cacheFile, preciseMtime, headers, timeout) is intentionally excluded so two
|
|
24694
|
+
// connections to the same endpoint with different tuning are deduplicated.
|
|
24695
|
+
identityFields: ["href", "token", "basePath"]
|
|
24683
24696
|
});
|
|
24684
24697
|
}
|
|
24685
24698
|
});
|
|
@@ -27068,7 +27081,7 @@ var ZenFSConfig = (() => {
|
|
|
27068
27081
|
return fileName === ".keep";
|
|
27069
27082
|
}
|
|
27070
27083
|
var S_IFDIR3 = 16384;
|
|
27071
|
-
var DEFAULT_PROTECTED_DIRS = ["/.meta"];
|
|
27084
|
+
var DEFAULT_PROTECTED_DIRS = ["/.meta/backends/.keep"];
|
|
27072
27085
|
function isDirectory4(stat5) {
|
|
27073
27086
|
return typeof stat5.mode === "number" && (stat5.mode & S_IFDIR3) === S_IFDIR3;
|
|
27074
27087
|
}
|
|
@@ -27279,8 +27292,22 @@ var ZenFSConfig = (() => {
|
|
|
27279
27292
|
}
|
|
27280
27293
|
return sorted;
|
|
27281
27294
|
}
|
|
27295
|
+
function identityOptions(type, options) {
|
|
27296
|
+
const meta = getBackendMetadata(type);
|
|
27297
|
+
const fields = meta?.identityFields;
|
|
27298
|
+
if (!fields || fields.length === 0) {
|
|
27299
|
+
log6.warn(`[DEDUP-DIAG] identityOptions(${type}): NO identityFields -> falling back to FULL options (dedup will be key-sensitive to tuning fields)`);
|
|
27300
|
+
return options ?? {};
|
|
27301
|
+
}
|
|
27302
|
+
const subset = {};
|
|
27303
|
+
for (const f of fields) {
|
|
27304
|
+
if (options && options[f] !== void 0) subset[f] = options[f];
|
|
27305
|
+
}
|
|
27306
|
+
log6.log(`[DEDUP-DIAG] identityOptions(${type}): identityFields=${JSON.stringify(fields)} identitySubset=${JSON.stringify(subset)}`);
|
|
27307
|
+
return subset;
|
|
27308
|
+
}
|
|
27282
27309
|
function backendDedupKey(desc) {
|
|
27283
|
-
return `${desc.type}:${stableOptionsKey(desc.options)}`;
|
|
27310
|
+
return `${desc.type}:${stableOptionsKey(identityOptions(desc.type, desc.options))}`;
|
|
27284
27311
|
}
|
|
27285
27312
|
function backendAgeRank(b) {
|
|
27286
27313
|
if (typeof b.createdAt === "number") return b.createdAt;
|
|
@@ -28315,18 +28342,22 @@ var ZenFSConfig = (() => {
|
|
|
28315
28342
|
const duplicates = [];
|
|
28316
28343
|
for (const item of items) {
|
|
28317
28344
|
const key = backendDedupKey(item.desc);
|
|
28345
|
+
log6.log(`[DEDUP-DIAG] item id=${item.desc.id} type=${item.desc.type} dedupKey=${key} mtime=${item.mtime} rawOptions=${JSON.stringify(item.desc.options)}`);
|
|
28318
28346
|
const existing = seen.get(key);
|
|
28319
28347
|
if (existing) {
|
|
28320
28348
|
if (item.mtime < existing.mtime) {
|
|
28349
|
+
log6.log(`[DEDUP-DIAG] DUPLICATE: new id=${item.desc.id} (older) keeps key, dropping existing=${existing.desc.id}`);
|
|
28321
28350
|
duplicates.push(existing.desc.id);
|
|
28322
28351
|
seen.set(key, item);
|
|
28323
28352
|
} else {
|
|
28353
|
+
log6.log(`[DEDUP-DIAG] DUPLICATE: new id=${item.desc.id} dropped, kept=${existing.desc.id}`);
|
|
28324
28354
|
duplicates.push(item.desc.id);
|
|
28325
28355
|
}
|
|
28326
28356
|
} else {
|
|
28327
28357
|
seen.set(key, item);
|
|
28328
28358
|
}
|
|
28329
28359
|
}
|
|
28360
|
+
log6.log(`[DEDUP-DIAG] dedup summary: keptKeys=${[...seen.keys()].length} duplicates=${JSON.stringify(duplicates)}`);
|
|
28330
28361
|
if (duplicates.length > 0) {
|
|
28331
28362
|
log6.log(
|
|
28332
28363
|
`[ConfigRepo] readAllBackendDescriptors: removing ${duplicates.length} duplicate(s): ${duplicates.join(", ")}`
|
|
@@ -28336,10 +28367,13 @@ var ZenFSConfig = (() => {
|
|
|
28336
28367
|
for (const [replicaId, replica] of this.replicaBackends) {
|
|
28337
28368
|
try {
|
|
28338
28369
|
await replica.instance.unlink(descPath);
|
|
28339
|
-
|
|
28370
|
+
log6.log(`[DEDUP-DIAG] removed dup ${dupId} from replica ${replicaId}`);
|
|
28371
|
+
} catch (e) {
|
|
28372
|
+
log6.log(`[DEDUP-DIAG] remove dup ${dupId} from replica ${replicaId} failed/skipped: ${String(e)}`);
|
|
28340
28373
|
}
|
|
28341
28374
|
await this.unlinkVersionSidecar(replica.instance, descPath);
|
|
28342
28375
|
}
|
|
28376
|
+
log6.log(`[DEDUP-DIAG] replicaBackends available during removal: ${[...this.replicaBackends.keys()].join(",") || "(none)"}`);
|
|
28343
28377
|
try {
|
|
28344
28378
|
await this.deleteFile(descPath);
|
|
28345
28379
|
} catch {
|
|
@@ -28348,7 +28382,8 @@ var ZenFSConfig = (() => {
|
|
|
28348
28382
|
}
|
|
28349
28383
|
}
|
|
28350
28384
|
return Array.from(seen.values()).map((i) => i.desc);
|
|
28351
|
-
} catch {
|
|
28385
|
+
} catch (e) {
|
|
28386
|
+
log6.error(`[DEDUP-DIAG] readAllBackendDescriptors threw, returning []:`, e);
|
|
28352
28387
|
return [];
|
|
28353
28388
|
}
|
|
28354
28389
|
}
|
|
@@ -28952,6 +28987,7 @@ var ZenFSConfig = (() => {
|
|
|
28952
28987
|
log6.log(`[createConfigRepo] Migration complete`);
|
|
28953
28988
|
}
|
|
28954
28989
|
let allBackends = await tempRepo.readAllBackendDescriptors();
|
|
28990
|
+
log6.log(`[DEDUP-DIAG] createConfigRepo after readAllBackendDescriptors: allBackends=${allBackends.map((b) => b.id).join(",")}`);
|
|
28955
28991
|
if (options.backendInfo) {
|
|
28956
28992
|
const replicaId = options.primaryBackendId || `${options.backendInfo.type}-replica`;
|
|
28957
28993
|
const hasReplica = allBackends.some((b) => b.id === replicaId);
|