zen-fs-config 0.5.8 → 0.5.10

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.
@@ -0,0 +1,22 @@
1
+ // src/cache-wrapper.ts
2
+ import { CachedFileSystem, IdbCacheStore, MemoryCacheStore } from "zen-fs-cache";
3
+ function wrapWithCache(backend, backendId, options) {
4
+ const storeType = options.storeType ?? "IdbCacheStore";
5
+ const prefix = options.storePrefix ?? `zen-fs-config:${backendId}:`;
6
+ let store;
7
+ if (storeType === "IdbCacheStore") {
8
+ store = new IdbCacheStore(prefix);
9
+ } else {
10
+ store = new MemoryCacheStore();
11
+ }
12
+ const wrapped = new CachedFileSystem(backend, store, {
13
+ ttlMs: options.ttlMs ?? 0
14
+ });
15
+ if (typeof backend.shouldSync === "function") {
16
+ wrapped.shouldSync = (...args) => backend.shouldSync(...args);
17
+ }
18
+ return wrapped;
19
+ }
20
+ export {
21
+ wrapWithCache
22
+ };
package/dist/index.d.mts CHANGED
@@ -89,11 +89,20 @@ interface ConfigSerializer {
89
89
  }
90
90
  /** Cache configuration. */
91
91
  interface CacheOptions {
92
- /** Type of cache store. */
92
+ /**
93
+ * Type of cache store for replica backends.
94
+ * Default: `'IdbCacheStore'` — persists across page reloads so cached
95
+ * content + revision tokens survive. Use `'MemoryCacheStore'` for
96
+ * session-only caching (lost on reload).
97
+ */
93
98
  storeType?: 'MemoryCacheStore' | 'IdbCacheStore';
94
- /** Cache store prefix (for IdbCacheStore). */
99
+ /** Cache store prefix (for IdbCacheStore). Default: `'zen-fs-config:'`. */
95
100
  storePrefix?: string;
96
- /** TTL in milliseconds for cache hits without revalidation. Default: 0 (always revalidate). */
101
+ /**
102
+ * TTL in milliseconds for cache hits without revalidation.
103
+ * Default: 0 (always revalidate via `getRevision` — exact, no stale data).
104
+ * Ignored when the backend implements `getRevision`.
105
+ */
97
106
  ttlMs?: number;
98
107
  }
99
108
  /** Options for creating a ConfigRepo. */
@@ -119,8 +128,19 @@ interface ConfigRepoOptions {
119
128
  idbStoreName?: string;
120
129
  /** Node identifier. Auto-detected if not provided (see DESIGN.md §8.2). */
121
130
  nodeId?: string;
122
- /** Cache configuration. */
123
- cache?: CacheOptions;
131
+ /**
132
+ * Cache configuration for replica backends.
133
+ *
134
+ * - **Default** (omitted): Caching is enabled with `IdbCacheStore`
135
+ * (persists across page reloads). Replica backends (Gitee,
136
+ * RemoteStorage, etc.) are wrapped with `CachedFileSystem` to avoid
137
+ * redundant network reads via `getRevision` revalidation.
138
+ * - Pass a `CacheOptions` object to customize the store type, prefix,
139
+ * or TTL.
140
+ * - Pass `false` to disable caching entirely (all reads go directly
141
+ * to the backend).
142
+ */
143
+ cache?: CacheOptions | false;
124
144
  /** Custom serializer. */
125
145
  serializer?: ConfigSerializer;
126
146
  /** Custom conflict handler. Called before auto-resolution. */
@@ -461,10 +481,11 @@ declare class ConfigRepo implements IConfigRepo {
461
481
  private disposed;
462
482
  private configCache;
463
483
  private readonly primaryBackendId;
484
+ private readonly cacheOptions?;
464
485
  private readonly pollIntervalMs?;
465
486
  /** Tombstone cache — avoids redundant reads within a single flush() cycle. */
466
487
  private tombstoneCache;
467
- constructor(appId: string, nodeId: string, primaryBackendId: string, cachedFS: MinimalAsyncFS, serializer: PathAwareSerializer, onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>, pollIntervalMs?: number);
488
+ constructor(appId: string, nodeId: string, primaryBackendId: string, cachedFS: MinimalAsyncFS, serializer: PathAwareSerializer, onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>, pollIntervalMs?: number, cacheOptions?: CacheOptions);
468
489
  /** Full path to this node's directory on the primary backend. */
469
490
  get nodePath(): string;
470
491
  /** Number of replica backends registered (excludes the local primary). */
@@ -494,8 +515,19 @@ declare class ConfigRepo implements IConfigRepo {
494
515
  /**
495
516
  * Before sync: for each tombstone, delete the actual file on all replicas.
496
517
  * This prevents bi-directional sync from copying the file back.
518
+ *
519
+ * Before calling unlink() on each backend, we check exists() first.
520
+ * This avoids sending wasteful DELETE requests (or GET-then-404) to
521
+ * remote backends when the file was already removed on a previous cycle.
522
+ * Local backends (IndexedDB) are cheap to check, so the guard is
523
+ * effectively free for them.
497
524
  */
498
525
  private processTombstones;
526
+ /**
527
+ * Safe existence check — returns false on any error instead of throwing.
528
+ * Used by processTombstones to avoid unnecessary unlink() calls.
529
+ */
530
+ private safeExists;
499
531
  /** Public wrapper for processTombstones — used by createConfigRepo. */
500
532
  processTombstonesPublic(): Promise<void>;
501
533
  /**
package/dist/index.d.ts CHANGED
@@ -89,11 +89,20 @@ interface ConfigSerializer {
89
89
  }
90
90
  /** Cache configuration. */
91
91
  interface CacheOptions {
92
- /** Type of cache store. */
92
+ /**
93
+ * Type of cache store for replica backends.
94
+ * Default: `'IdbCacheStore'` — persists across page reloads so cached
95
+ * content + revision tokens survive. Use `'MemoryCacheStore'` for
96
+ * session-only caching (lost on reload).
97
+ */
93
98
  storeType?: 'MemoryCacheStore' | 'IdbCacheStore';
94
- /** Cache store prefix (for IdbCacheStore). */
99
+ /** Cache store prefix (for IdbCacheStore). Default: `'zen-fs-config:'`. */
95
100
  storePrefix?: string;
96
- /** TTL in milliseconds for cache hits without revalidation. Default: 0 (always revalidate). */
101
+ /**
102
+ * TTL in milliseconds for cache hits without revalidation.
103
+ * Default: 0 (always revalidate via `getRevision` — exact, no stale data).
104
+ * Ignored when the backend implements `getRevision`.
105
+ */
97
106
  ttlMs?: number;
98
107
  }
99
108
  /** Options for creating a ConfigRepo. */
@@ -119,8 +128,19 @@ interface ConfigRepoOptions {
119
128
  idbStoreName?: string;
120
129
  /** Node identifier. Auto-detected if not provided (see DESIGN.md §8.2). */
121
130
  nodeId?: string;
122
- /** Cache configuration. */
123
- cache?: CacheOptions;
131
+ /**
132
+ * Cache configuration for replica backends.
133
+ *
134
+ * - **Default** (omitted): Caching is enabled with `IdbCacheStore`
135
+ * (persists across page reloads). Replica backends (Gitee,
136
+ * RemoteStorage, etc.) are wrapped with `CachedFileSystem` to avoid
137
+ * redundant network reads via `getRevision` revalidation.
138
+ * - Pass a `CacheOptions` object to customize the store type, prefix,
139
+ * or TTL.
140
+ * - Pass `false` to disable caching entirely (all reads go directly
141
+ * to the backend).
142
+ */
143
+ cache?: CacheOptions | false;
124
144
  /** Custom serializer. */
125
145
  serializer?: ConfigSerializer;
126
146
  /** Custom conflict handler. Called before auto-resolution. */
@@ -461,10 +481,11 @@ declare class ConfigRepo implements IConfigRepo {
461
481
  private disposed;
462
482
  private configCache;
463
483
  private readonly primaryBackendId;
484
+ private readonly cacheOptions?;
464
485
  private readonly pollIntervalMs?;
465
486
  /** Tombstone cache — avoids redundant reads within a single flush() cycle. */
466
487
  private tombstoneCache;
467
- constructor(appId: string, nodeId: string, primaryBackendId: string, cachedFS: MinimalAsyncFS, serializer: PathAwareSerializer, onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>, pollIntervalMs?: number);
488
+ constructor(appId: string, nodeId: string, primaryBackendId: string, cachedFS: MinimalAsyncFS, serializer: PathAwareSerializer, onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>, pollIntervalMs?: number, cacheOptions?: CacheOptions);
468
489
  /** Full path to this node's directory on the primary backend. */
469
490
  get nodePath(): string;
470
491
  /** Number of replica backends registered (excludes the local primary). */
@@ -494,8 +515,19 @@ declare class ConfigRepo implements IConfigRepo {
494
515
  /**
495
516
  * Before sync: for each tombstone, delete the actual file on all replicas.
496
517
  * This prevents bi-directional sync from copying the file back.
518
+ *
519
+ * Before calling unlink() on each backend, we check exists() first.
520
+ * This avoids sending wasteful DELETE requests (or GET-then-404) to
521
+ * remote backends when the file was already removed on a previous cycle.
522
+ * Local backends (IndexedDB) are cheap to check, so the guard is
523
+ * effectively free for them.
497
524
  */
498
525
  private processTombstones;
526
+ /**
527
+ * Safe existence check — returns false on any error instead of throwing.
528
+ * Used by processTombstones to avoid unnecessary unlink() calls.
529
+ */
530
+ private safeExists;
499
531
  /** Public wrapper for processTombstones — used by createConfigRepo. */
500
532
  processTombstonesPublic(): Promise<void>;
501
533
  /**
package/dist/index.js CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,6 +30,36 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
33
+ // src/cache-wrapper.ts
34
+ var cache_wrapper_exports = {};
35
+ __export(cache_wrapper_exports, {
36
+ wrapWithCache: () => wrapWithCache
37
+ });
38
+ function wrapWithCache(backend, backendId, options) {
39
+ const storeType = options.storeType ?? "IdbCacheStore";
40
+ const prefix = options.storePrefix ?? `zen-fs-config:${backendId}:`;
41
+ let store;
42
+ if (storeType === "IdbCacheStore") {
43
+ store = new import_zen_fs_cache.IdbCacheStore(prefix);
44
+ } else {
45
+ store = new import_zen_fs_cache.MemoryCacheStore();
46
+ }
47
+ const wrapped = new import_zen_fs_cache.CachedFileSystem(backend, store, {
48
+ ttlMs: options.ttlMs ?? 0
49
+ });
50
+ if (typeof backend.shouldSync === "function") {
51
+ wrapped.shouldSync = (...args) => backend.shouldSync(...args);
52
+ }
53
+ return wrapped;
54
+ }
55
+ var import_zen_fs_cache;
56
+ var init_cache_wrapper = __esm({
57
+ "src/cache-wrapper.ts"() {
58
+ "use strict";
59
+ import_zen_fs_cache = require("zen-fs-cache");
60
+ }
61
+ });
62
+
30
63
  // src/index.ts
31
64
  var index_exports = {};
32
65
  __export(index_exports, {
@@ -585,10 +618,11 @@ var ConfigRepo = class {
585
618
  disposed = false;
586
619
  configCache = /* @__PURE__ */ new Map();
587
620
  primaryBackendId;
621
+ cacheOptions;
588
622
  pollIntervalMs;
589
623
  /** Tombstone cache — avoids redundant reads within a single flush() cycle. */
590
624
  tombstoneCache = null;
591
- constructor(appId, nodeId, primaryBackendId, cachedFS, serializer, onConflict, pollIntervalMs) {
625
+ constructor(appId, nodeId, primaryBackendId, cachedFS, serializer, onConflict, pollIntervalMs, cacheOptions) {
592
626
  this.appId = appId;
593
627
  this.nodeId = nodeId;
594
628
  this.primaryBackendId = primaryBackendId;
@@ -598,6 +632,7 @@ var ConfigRepo = class {
598
632
  this.replicaBackends = /* @__PURE__ */ new Map();
599
633
  this.onConflictCallback = onConflict;
600
634
  this.pollIntervalMs = pollIntervalMs;
635
+ this.cacheOptions = cacheOptions;
601
636
  this.fullFS = backendToSyncableFS(cachedFS, primaryBackendId);
602
637
  this.fs = createChrootFS(cachedFS, `/${appId}`);
603
638
  this.rootFS = createChrootFS(cachedFS, "/");
@@ -826,37 +861,77 @@ var ConfigRepo = class {
826
861
  /**
827
862
  * Before sync: for each tombstone, delete the actual file on all replicas.
828
863
  * This prevents bi-directional sync from copying the file back.
864
+ *
865
+ * Before calling unlink() on each backend, we check exists() first.
866
+ * This avoids sending wasteful DELETE requests (or GET-then-404) to
867
+ * remote backends when the file was already removed on a previous cycle.
868
+ * Local backends (IndexedDB) are cheap to check, so the guard is
869
+ * effectively free for them.
829
870
  */
830
871
  async processTombstones() {
831
872
  const tombstones = await this.readTombstones();
832
873
  if (tombstones.length === 0) return;
833
- console.log(`[ConfigRepo] processTombstones: ${tombstones.length} tombstone(s)`);
874
+ let processed = 0;
875
+ let alreadyDeleted = 0;
834
876
  for (const tombstone of tombstones) {
835
877
  const tVersionPath = versionPathFor(tombstone.path);
836
- try {
837
- await this.cachedFS.unlink(tombstone.path);
838
- } catch {
878
+ const existedOnPrimary = await this.safeExists(this.cachedFS, tombstone.path);
879
+ console.log(`[ConfigRepo] tombstone check: ${tombstone.path} on primary \u2192 ${existedOnPrimary ? "EXISTS" : "not found"}`);
880
+ if (existedOnPrimary) {
881
+ try {
882
+ await this.cachedFS.unlink(tombstone.path);
883
+ processed++;
884
+ } catch {
885
+ }
839
886
  }
840
- if (tVersionPath) {
887
+ if (tVersionPath && await this.safeExists(this.cachedFS, tVersionPath)) {
841
888
  try {
842
889
  await this.cachedFS.unlink(tVersionPath);
843
890
  } catch {
844
891
  }
845
892
  }
846
893
  for (const [replicaId, replica] of this.replicaBackends) {
847
- try {
848
- await replica.instance.unlink(tombstone.path);
849
- } catch {
894
+ 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
+ if (existed) {
897
+ try {
898
+ await replica.instance.unlink(tombstone.path);
899
+ console.log(`[ConfigRepo] tombstone ${tombstone.path}: deleted on ${replicaId}`);
900
+ processed++;
901
+ } catch {
902
+ alreadyDeleted++;
903
+ }
904
+ } else {
905
+ alreadyDeleted++;
850
906
  }
851
- if (tVersionPath) {
907
+ if (tVersionPath && await this.safeExists(replica.instance, tVersionPath)) {
852
908
  try {
853
909
  await replica.instance.unlink(tVersionPath);
854
910
  } catch {
855
911
  }
856
912
  }
857
- console.log(`[ConfigRepo] tombstone ${tombstone.path}: deleted on ${replicaId}`);
858
913
  }
859
914
  }
915
+ if (processed > 0 || alreadyDeleted > 0) {
916
+ console.log(`[ConfigRepo] processTombstones: ${tombstones.length} tombstone(s), ${processed} deleted, ${alreadyDeleted} already gone`);
917
+ }
918
+ }
919
+ /**
920
+ * Safe existence check — returns false on any error instead of throwing.
921
+ * Used by processTombstones to avoid unnecessary unlink() calls.
922
+ */
923
+ async safeExists(fs, path) {
924
+ try {
925
+ if (typeof fs.exists === "function") {
926
+ const result = await fs.exists(path);
927
+ return result;
928
+ }
929
+ await fs.stat(path);
930
+ return true;
931
+ } catch (err) {
932
+ console.log(`[ConfigRepo] safeExists(${path}): threw ${err?.code ?? err?.status ?? ""} ${err?.message ?? err}`);
933
+ return false;
934
+ }
860
935
  }
861
936
  /** Public wrapper for processTombstones — used by createConfigRepo. */
862
937
  async processTombstonesPublic() {
@@ -1036,7 +1111,13 @@ var ConfigRepo = class {
1036
1111
  console.log(`[ConfigRepo] Creating replica backend: id=${desc.id}, type=${desc.type}`);
1037
1112
  try {
1038
1113
  const instance = await createBackend(desc);
1039
- const syncable = backendToSyncableFS(instance, `${desc.type}(${desc.id})`);
1114
+ let fsInstance = instance;
1115
+ if (this.cacheOptions) {
1116
+ const { wrapWithCache: wrapWithCache2 } = await Promise.resolve().then(() => (init_cache_wrapper(), cache_wrapper_exports));
1117
+ fsInstance = wrapWithCache2(instance, desc.id, this.cacheOptions);
1118
+ console.log(`[ConfigRepo] Replica ${desc.id} wrapped with CachedFileSystem (store=${this.cacheOptions.storeType ?? "IdbCacheStore"})`);
1119
+ }
1120
+ const syncable = backendToSyncableFS(fsInstance, `${desc.type}(${desc.id})`);
1040
1121
  const pair = this.syncEngine.addPair(
1041
1122
  this.fullFS,
1042
1123
  syncable,
@@ -1047,7 +1128,7 @@ var ConfigRepo = class {
1047
1128
  },
1048
1129
  "/"
1049
1130
  );
1050
- this.replicaBackends.set(desc.id, { instance, syncable, pairId: pair.pairId });
1131
+ this.replicaBackends.set(desc.id, { instance: fsInstance, syncable, pairId: pair.pairId });
1051
1132
  const conflictHandler = (event) => {
1052
1133
  this.handleConflict(event);
1053
1134
  };
@@ -1753,6 +1834,7 @@ async function createConfigRepo(appId, options = {}) {
1753
1834
  options: { storeName: idbStoreName }
1754
1835
  });
1755
1836
  const cachedFS = primaryInstance;
1837
+ const cacheOptions = options.cache === false ? void 0 : options.cache ?? {};
1756
1838
  try {
1757
1839
  await primaryInstance.mkdir(META_DIR);
1758
1840
  console.log(`[createConfigRepo] /.meta/ ready`);
@@ -1777,7 +1859,8 @@ async function createConfigRepo(appId, options = {}) {
1777
1859
  cachedFS,
1778
1860
  createSerializerChain(),
1779
1861
  void 0,
1780
- options.syncPollIntervalMs
1862
+ options.syncPollIntervalMs,
1863
+ cacheOptions
1781
1864
  );
1782
1865
  const oldBackendsMeta = await tempRepo.readMetaFile(BACKENDS_FILE);
1783
1866
  if (oldBackendsMeta && oldBackendsMeta.backends?.length > 0) {
@@ -1843,7 +1926,8 @@ async function createConfigRepo(appId, options = {}) {
1843
1926
  cachedFS,
1844
1927
  serializer,
1845
1928
  options.onConflict,
1846
- options.syncPollIntervalMs
1929
+ options.syncPollIntervalMs,
1930
+ cacheOptions
1847
1931
  );
1848
1932
  await repo.setupSync(allBackends, LOCAL_IDB_BACKEND_ID, options.syncPollIntervalMs);
1849
1933
  await repo.load();
package/dist/index.mjs CHANGED
@@ -528,10 +528,11 @@ var ConfigRepo = class {
528
528
  disposed = false;
529
529
  configCache = /* @__PURE__ */ new Map();
530
530
  primaryBackendId;
531
+ cacheOptions;
531
532
  pollIntervalMs;
532
533
  /** Tombstone cache — avoids redundant reads within a single flush() cycle. */
533
534
  tombstoneCache = null;
534
- constructor(appId, nodeId, primaryBackendId, cachedFS, serializer, onConflict, pollIntervalMs) {
535
+ constructor(appId, nodeId, primaryBackendId, cachedFS, serializer, onConflict, pollIntervalMs, cacheOptions) {
535
536
  this.appId = appId;
536
537
  this.nodeId = nodeId;
537
538
  this.primaryBackendId = primaryBackendId;
@@ -541,6 +542,7 @@ var ConfigRepo = class {
541
542
  this.replicaBackends = /* @__PURE__ */ new Map();
542
543
  this.onConflictCallback = onConflict;
543
544
  this.pollIntervalMs = pollIntervalMs;
545
+ this.cacheOptions = cacheOptions;
544
546
  this.fullFS = backendToSyncableFS(cachedFS, primaryBackendId);
545
547
  this.fs = createChrootFS(cachedFS, `/${appId}`);
546
548
  this.rootFS = createChrootFS(cachedFS, "/");
@@ -769,37 +771,77 @@ var ConfigRepo = class {
769
771
  /**
770
772
  * Before sync: for each tombstone, delete the actual file on all replicas.
771
773
  * This prevents bi-directional sync from copying the file back.
774
+ *
775
+ * Before calling unlink() on each backend, we check exists() first.
776
+ * This avoids sending wasteful DELETE requests (or GET-then-404) to
777
+ * remote backends when the file was already removed on a previous cycle.
778
+ * Local backends (IndexedDB) are cheap to check, so the guard is
779
+ * effectively free for them.
772
780
  */
773
781
  async processTombstones() {
774
782
  const tombstones = await this.readTombstones();
775
783
  if (tombstones.length === 0) return;
776
- console.log(`[ConfigRepo] processTombstones: ${tombstones.length} tombstone(s)`);
784
+ let processed = 0;
785
+ let alreadyDeleted = 0;
777
786
  for (const tombstone of tombstones) {
778
787
  const tVersionPath = versionPathFor(tombstone.path);
779
- try {
780
- await this.cachedFS.unlink(tombstone.path);
781
- } catch {
788
+ const existedOnPrimary = await this.safeExists(this.cachedFS, tombstone.path);
789
+ console.log(`[ConfigRepo] tombstone check: ${tombstone.path} on primary \u2192 ${existedOnPrimary ? "EXISTS" : "not found"}`);
790
+ if (existedOnPrimary) {
791
+ try {
792
+ await this.cachedFS.unlink(tombstone.path);
793
+ processed++;
794
+ } catch {
795
+ }
782
796
  }
783
- if (tVersionPath) {
797
+ if (tVersionPath && await this.safeExists(this.cachedFS, tVersionPath)) {
784
798
  try {
785
799
  await this.cachedFS.unlink(tVersionPath);
786
800
  } catch {
787
801
  }
788
802
  }
789
803
  for (const [replicaId, replica] of this.replicaBackends) {
790
- try {
791
- await replica.instance.unlink(tombstone.path);
792
- } catch {
804
+ 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
+ if (existed) {
807
+ try {
808
+ await replica.instance.unlink(tombstone.path);
809
+ console.log(`[ConfigRepo] tombstone ${tombstone.path}: deleted on ${replicaId}`);
810
+ processed++;
811
+ } catch {
812
+ alreadyDeleted++;
813
+ }
814
+ } else {
815
+ alreadyDeleted++;
793
816
  }
794
- if (tVersionPath) {
817
+ if (tVersionPath && await this.safeExists(replica.instance, tVersionPath)) {
795
818
  try {
796
819
  await replica.instance.unlink(tVersionPath);
797
820
  } catch {
798
821
  }
799
822
  }
800
- console.log(`[ConfigRepo] tombstone ${tombstone.path}: deleted on ${replicaId}`);
801
823
  }
802
824
  }
825
+ if (processed > 0 || alreadyDeleted > 0) {
826
+ console.log(`[ConfigRepo] processTombstones: ${tombstones.length} tombstone(s), ${processed} deleted, ${alreadyDeleted} already gone`);
827
+ }
828
+ }
829
+ /**
830
+ * Safe existence check — returns false on any error instead of throwing.
831
+ * Used by processTombstones to avoid unnecessary unlink() calls.
832
+ */
833
+ async safeExists(fs, path) {
834
+ try {
835
+ if (typeof fs.exists === "function") {
836
+ const result = await fs.exists(path);
837
+ return result;
838
+ }
839
+ await fs.stat(path);
840
+ return true;
841
+ } catch (err) {
842
+ console.log(`[ConfigRepo] safeExists(${path}): threw ${err?.code ?? err?.status ?? ""} ${err?.message ?? err}`);
843
+ return false;
844
+ }
803
845
  }
804
846
  /** Public wrapper for processTombstones — used by createConfigRepo. */
805
847
  async processTombstonesPublic() {
@@ -979,7 +1021,13 @@ var ConfigRepo = class {
979
1021
  console.log(`[ConfigRepo] Creating replica backend: id=${desc.id}, type=${desc.type}`);
980
1022
  try {
981
1023
  const instance = await createBackend(desc);
982
- const syncable = backendToSyncableFS(instance, `${desc.type}(${desc.id})`);
1024
+ let fsInstance = instance;
1025
+ if (this.cacheOptions) {
1026
+ const { wrapWithCache } = await import("./cache-wrapper-XQCPZJCD.mjs");
1027
+ fsInstance = wrapWithCache(instance, desc.id, this.cacheOptions);
1028
+ console.log(`[ConfigRepo] Replica ${desc.id} wrapped with CachedFileSystem (store=${this.cacheOptions.storeType ?? "IdbCacheStore"})`);
1029
+ }
1030
+ const syncable = backendToSyncableFS(fsInstance, `${desc.type}(${desc.id})`);
983
1031
  const pair = this.syncEngine.addPair(
984
1032
  this.fullFS,
985
1033
  syncable,
@@ -990,7 +1038,7 @@ var ConfigRepo = class {
990
1038
  },
991
1039
  "/"
992
1040
  );
993
- this.replicaBackends.set(desc.id, { instance, syncable, pairId: pair.pairId });
1041
+ this.replicaBackends.set(desc.id, { instance: fsInstance, syncable, pairId: pair.pairId });
994
1042
  const conflictHandler = (event) => {
995
1043
  this.handleConflict(event);
996
1044
  };
@@ -1696,6 +1744,7 @@ async function createConfigRepo(appId, options = {}) {
1696
1744
  options: { storeName: idbStoreName }
1697
1745
  });
1698
1746
  const cachedFS = primaryInstance;
1747
+ const cacheOptions = options.cache === false ? void 0 : options.cache ?? {};
1699
1748
  try {
1700
1749
  await primaryInstance.mkdir(META_DIR);
1701
1750
  console.log(`[createConfigRepo] /.meta/ ready`);
@@ -1720,7 +1769,8 @@ async function createConfigRepo(appId, options = {}) {
1720
1769
  cachedFS,
1721
1770
  createSerializerChain(),
1722
1771
  void 0,
1723
- options.syncPollIntervalMs
1772
+ options.syncPollIntervalMs,
1773
+ cacheOptions
1724
1774
  );
1725
1775
  const oldBackendsMeta = await tempRepo.readMetaFile(BACKENDS_FILE);
1726
1776
  if (oldBackendsMeta && oldBackendsMeta.backends?.length > 0) {
@@ -1786,7 +1836,8 @@ async function createConfigRepo(appId, options = {}) {
1786
1836
  cachedFS,
1787
1837
  serializer,
1788
1838
  options.onConflict,
1789
- options.syncPollIntervalMs
1839
+ options.syncPollIntervalMs,
1840
+ cacheOptions
1790
1841
  );
1791
1842
  await repo.setupSync(allBackends, LOCAL_IDB_BACKEND_ID, options.syncPollIntervalMs);
1792
1843
  await repo.load();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zen-fs-config",
3
- "version": "0.5.8",
3
+ "version": "0.5.10",
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",