zen-fs-config 0.5.36 → 0.5.37

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.
@@ -9,7 +9,7 @@ import {
9
9
  registerBackend,
10
10
  unregisterBackend,
11
11
  wrapZenFSFileSystem
12
- } from "./chunk-UDEOYFWS.mjs";
12
+ } from "./chunk-4RP2AXAB.mjs";
13
13
  export {
14
14
  createBackend,
15
15
  getAccountFields,
@@ -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` under `/.meta/` (used to keep the backends directory
85
- * alive) is protected and never deleted — see `protectedDirs`.
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
- * Directories (and their subtrees) whose `.keep` files are NEVER deleted.
102
- * Defaults to `['/.meta']` to protect the intentional backends placeholder
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` under `/.meta/` (used to keep the backends directory
85
- * alive) is protected and never deleted — see `protectedDirs`.
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
- * Directories (and their subtrees) whose `.keep` files are NEVER deleted.
102
- * Defaults to `['/.meta']` to protect the intentional backends placeholder
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,17 @@ function sortKeysDeep(obj) {
1346
1359
  }
1347
1360
  return sorted;
1348
1361
  }
1362
+ function identityOptions(type, options) {
1363
+ const fields = getBackendMetadata(type)?.identityFields;
1364
+ if (!fields || fields.length === 0) return options ?? {};
1365
+ const subset = {};
1366
+ for (const f of fields) {
1367
+ if (options && options[f] !== void 0) subset[f] = options[f];
1368
+ }
1369
+ return subset;
1370
+ }
1349
1371
  function backendDedupKey(desc) {
1350
- return `${desc.type}:${stableOptionsKey(desc.options)}`;
1372
+ return `${desc.type}:${stableOptionsKey(identityOptions(desc.type, desc.options))}`;
1351
1373
  }
1352
1374
  function backendAgeRank(b) {
1353
1375
  if (typeof b.createdAt === "number") return b.createdAt;
package/dist/index.mjs CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  registerBackend,
10
10
  unregisterBackend,
11
11
  wrapZenFSFileSystem
12
- } from "./chunk-UDEOYFWS.mjs";
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-7QGFWLM7.mjs")).wrapZenFSFileSystem({ backend: FolderBackend, path: dir, label: options.label });
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,17 @@ function sortKeysDeep(obj) {
904
904
  }
905
905
  return sorted;
906
906
  }
907
+ function identityOptions(type, options) {
908
+ const fields = getBackendMetadata(type)?.identityFields;
909
+ if (!fields || fields.length === 0) return options ?? {};
910
+ const subset = {};
911
+ for (const f of fields) {
912
+ if (options && options[f] !== void 0) subset[f] = options[f];
913
+ }
914
+ return subset;
915
+ }
907
916
  function backendDedupKey(desc) {
908
- return `${desc.type}:${stableOptionsKey(desc.options)}`;
917
+ return `${desc.type}:${stableOptionsKey(identityOptions(desc.type, desc.options))}`;
909
918
  }
910
919
  function backendAgeRank(b) {
911
920
  if (typeof b.createdAt === "number") return b.createdAt;
@@ -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,17 @@ var ZenFSConfig = (() => {
27279
27292
  }
27280
27293
  return sorted;
27281
27294
  }
27295
+ function identityOptions(type, options) {
27296
+ const fields = getBackendMetadata(type)?.identityFields;
27297
+ if (!fields || fields.length === 0) return options ?? {};
27298
+ const subset = {};
27299
+ for (const f of fields) {
27300
+ if (options && options[f] !== void 0) subset[f] = options[f];
27301
+ }
27302
+ return subset;
27303
+ }
27282
27304
  function backendDedupKey(desc) {
27283
- return `${desc.type}:${stableOptionsKey(desc.options)}`;
27305
+ return `${desc.type}:${stableOptionsKey(identityOptions(desc.type, desc.options))}`;
27284
27306
  }
27285
27307
  function backendAgeRank(b) {
27286
27308
  if (typeof b.createdAt === "number") return b.createdAt;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zen-fs-config",
3
- "version": "0.5.36",
3
+ "version": "0.5.37",
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",