zen-fs-config 0.5.9 → 0.5.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -515,8 +515,19 @@ declare class ConfigRepo implements IConfigRepo {
515
515
  /**
516
516
  * Before sync: for each tombstone, delete the actual file on all replicas.
517
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.
518
524
  */
519
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;
520
531
  /** Public wrapper for processTombstones — used by createConfigRepo. */
521
532
  processTombstonesPublic(): Promise<void>;
522
533
  /**
@@ -531,6 +542,11 @@ declare class ConfigRepo implements IConfigRepo {
531
542
  private updateTombstoneConfirmations;
532
543
  /**
533
544
  * GC: remove tombstones where all backends in backends.json have confirmed.
545
+ *
546
+ * Tombstones are deleted from ALL backends (local + replicas), not just
547
+ * the local IndexedDB. If we only deleted locally, the sync engine would
548
+ * see them as "created on target" and copy them back every cycle — causing
549
+ * an infinite loop of copy → GC → copy → GC.
534
550
  */
535
551
  private gcTombstones;
536
552
  /**
package/dist/index.d.ts CHANGED
@@ -515,8 +515,19 @@ declare class ConfigRepo implements IConfigRepo {
515
515
  /**
516
516
  * Before sync: for each tombstone, delete the actual file on all replicas.
517
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.
518
524
  */
519
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;
520
531
  /** Public wrapper for processTombstones — used by createConfigRepo. */
521
532
  processTombstonesPublic(): Promise<void>;
522
533
  /**
@@ -531,6 +542,11 @@ declare class ConfigRepo implements IConfigRepo {
531
542
  private updateTombstoneConfirmations;
532
543
  /**
533
544
  * GC: remove tombstones where all backends in backends.json have confirmed.
545
+ *
546
+ * Tombstones are deleted from ALL backends (local + replicas), not just
547
+ * the local IndexedDB. If we only deleted locally, the sync engine would
548
+ * see them as "created on target" and copy them back every cycle — causing
549
+ * an infinite loop of copy → GC → copy → GC.
534
550
  */
535
551
  private gcTombstones;
536
552
  /**
package/dist/index.js CHANGED
@@ -861,37 +861,77 @@ var ConfigRepo = class {
861
861
  /**
862
862
  * Before sync: for each tombstone, delete the actual file on all replicas.
863
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.
864
870
  */
865
871
  async processTombstones() {
866
872
  const tombstones = await this.readTombstones();
867
873
  if (tombstones.length === 0) return;
868
- console.log(`[ConfigRepo] processTombstones: ${tombstones.length} tombstone(s)`);
874
+ let processed = 0;
875
+ let alreadyDeleted = 0;
869
876
  for (const tombstone of tombstones) {
870
877
  const tVersionPath = versionPathFor(tombstone.path);
871
- try {
872
- await this.cachedFS.unlink(tombstone.path);
873
- } 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
+ }
874
886
  }
875
- if (tVersionPath) {
887
+ if (tVersionPath && await this.safeExists(this.cachedFS, tVersionPath)) {
876
888
  try {
877
889
  await this.cachedFS.unlink(tVersionPath);
878
890
  } catch {
879
891
  }
880
892
  }
881
893
  for (const [replicaId, replica] of this.replicaBackends) {
882
- try {
883
- await replica.instance.unlink(tombstone.path);
884
- } 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++;
885
906
  }
886
- if (tVersionPath) {
907
+ if (tVersionPath && await this.safeExists(replica.instance, tVersionPath)) {
887
908
  try {
888
909
  await replica.instance.unlink(tVersionPath);
889
910
  } catch {
890
911
  }
891
912
  }
892
- console.log(`[ConfigRepo] tombstone ${tombstone.path}: deleted on ${replicaId}`);
893
913
  }
894
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
+ }
895
935
  }
896
936
  /** Public wrapper for processTombstones — used by createConfigRepo. */
897
937
  async processTombstonesPublic() {
@@ -937,6 +977,11 @@ var ConfigRepo = class {
937
977
  }
938
978
  /**
939
979
  * GC: remove tombstones where all backends in backends.json have confirmed.
980
+ *
981
+ * Tombstones are deleted from ALL backends (local + replicas), not just
982
+ * the local IndexedDB. If we only deleted locally, the sync engine would
983
+ * see them as "created on target" and copy them back every cycle — causing
984
+ * an infinite loop of copy → GC → copy → GC.
940
985
  */
941
986
  async gcTombstones() {
942
987
  const tombstones = await this.readTombstones();
@@ -949,9 +994,17 @@ var ConfigRepo = class {
949
994
  const tombstonePath = `${DELETIONS_DIR}/${tombstoneFileName(tombstone.path)}`;
950
995
  try {
951
996
  await this.cachedFS.unlink(tombstonePath);
952
- console.log(`[ConfigRepo] gcTombstones: removed ${tombstonePath} (all ${allBackendIds.length} backends confirmed)`);
953
997
  } catch {
954
998
  }
999
+ for (const [replicaId, replica] of this.replicaBackends) {
1000
+ if (await this.safeExists(replica.instance, tombstonePath)) {
1001
+ try {
1002
+ await replica.instance.unlink(tombstonePath);
1003
+ } catch {
1004
+ }
1005
+ }
1006
+ }
1007
+ console.log(`[ConfigRepo] gcTombstones: removed ${tombstonePath} (all ${allBackendIds.length} backends confirmed)`);
955
1008
  }
956
1009
  }
957
1010
  }
package/dist/index.mjs CHANGED
@@ -771,37 +771,77 @@ var ConfigRepo = class {
771
771
  /**
772
772
  * Before sync: for each tombstone, delete the actual file on all replicas.
773
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.
774
780
  */
775
781
  async processTombstones() {
776
782
  const tombstones = await this.readTombstones();
777
783
  if (tombstones.length === 0) return;
778
- console.log(`[ConfigRepo] processTombstones: ${tombstones.length} tombstone(s)`);
784
+ let processed = 0;
785
+ let alreadyDeleted = 0;
779
786
  for (const tombstone of tombstones) {
780
787
  const tVersionPath = versionPathFor(tombstone.path);
781
- try {
782
- await this.cachedFS.unlink(tombstone.path);
783
- } 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
+ }
784
796
  }
785
- if (tVersionPath) {
797
+ if (tVersionPath && await this.safeExists(this.cachedFS, tVersionPath)) {
786
798
  try {
787
799
  await this.cachedFS.unlink(tVersionPath);
788
800
  } catch {
789
801
  }
790
802
  }
791
803
  for (const [replicaId, replica] of this.replicaBackends) {
792
- try {
793
- await replica.instance.unlink(tombstone.path);
794
- } 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++;
795
816
  }
796
- if (tVersionPath) {
817
+ if (tVersionPath && await this.safeExists(replica.instance, tVersionPath)) {
797
818
  try {
798
819
  await replica.instance.unlink(tVersionPath);
799
820
  } catch {
800
821
  }
801
822
  }
802
- console.log(`[ConfigRepo] tombstone ${tombstone.path}: deleted on ${replicaId}`);
803
823
  }
804
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
+ }
805
845
  }
806
846
  /** Public wrapper for processTombstones — used by createConfigRepo. */
807
847
  async processTombstonesPublic() {
@@ -847,6 +887,11 @@ var ConfigRepo = class {
847
887
  }
848
888
  /**
849
889
  * GC: remove tombstones where all backends in backends.json have confirmed.
890
+ *
891
+ * Tombstones are deleted from ALL backends (local + replicas), not just
892
+ * the local IndexedDB. If we only deleted locally, the sync engine would
893
+ * see them as "created on target" and copy them back every cycle — causing
894
+ * an infinite loop of copy → GC → copy → GC.
850
895
  */
851
896
  async gcTombstones() {
852
897
  const tombstones = await this.readTombstones();
@@ -859,9 +904,17 @@ var ConfigRepo = class {
859
904
  const tombstonePath = `${DELETIONS_DIR}/${tombstoneFileName(tombstone.path)}`;
860
905
  try {
861
906
  await this.cachedFS.unlink(tombstonePath);
862
- console.log(`[ConfigRepo] gcTombstones: removed ${tombstonePath} (all ${allBackendIds.length} backends confirmed)`);
863
907
  } catch {
864
908
  }
909
+ for (const [replicaId, replica] of this.replicaBackends) {
910
+ if (await this.safeExists(replica.instance, tombstonePath)) {
911
+ try {
912
+ await replica.instance.unlink(tombstonePath);
913
+ } catch {
914
+ }
915
+ }
916
+ }
917
+ console.log(`[ConfigRepo] gcTombstones: removed ${tombstonePath} (all ${allBackendIds.length} backends confirmed)`);
865
918
  }
866
919
  }
867
920
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zen-fs-config",
3
- "version": "0.5.9",
3
+ "version": "0.5.11",
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",