zen-fs-config 0.5.2 → 0.5.3

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
@@ -465,6 +465,8 @@ declare class ConfigRepo implements IConfigRepo {
465
465
  constructor(appId: string, nodeId: string, primaryBackendId: string, cachedFS: MinimalAsyncFS, serializer: PathAwareSerializer, onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>, pollIntervalMs?: number);
466
466
  /** Full path to this node's directory on the primary backend. */
467
467
  get nodePath(): string;
468
+ /** Number of replica backends registered (excludes the local primary). */
469
+ get replicaCount(): number;
468
470
  load(rawConfig?: string): Promise<void>;
469
471
  getConfig<T = unknown>(path: string): T;
470
472
  setConfig(path: string, data: unknown): void;
@@ -489,6 +491,14 @@ declare class ConfigRepo implements IConfigRepo {
489
491
  * This prevents bi-directional sync from copying the file back.
490
492
  */
491
493
  private processTombstones;
494
+ /** Public wrapper for processTombstones — used by createConfigRepo. */
495
+ processTombstonesPublic(): Promise<void>;
496
+ /**
497
+ * Perform a full sync + dedup cycle without the watch snapshot cache.
498
+ * Used by createConfigRepo to pull remote-only files (like duplicate
499
+ * backend descriptors) that watch()'s initial snapshot would skip.
500
+ */
501
+ initialSyncAndDedup(): Promise<void>;
492
502
  /**
493
503
  * After sync: mark each tombstone as confirmed by all replica backends.
494
504
  */
package/dist/index.d.ts CHANGED
@@ -465,6 +465,8 @@ declare class ConfigRepo implements IConfigRepo {
465
465
  constructor(appId: string, nodeId: string, primaryBackendId: string, cachedFS: MinimalAsyncFS, serializer: PathAwareSerializer, onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>, pollIntervalMs?: number);
466
466
  /** Full path to this node's directory on the primary backend. */
467
467
  get nodePath(): string;
468
+ /** Number of replica backends registered (excludes the local primary). */
469
+ get replicaCount(): number;
468
470
  load(rawConfig?: string): Promise<void>;
469
471
  getConfig<T = unknown>(path: string): T;
470
472
  setConfig(path: string, data: unknown): void;
@@ -489,6 +491,14 @@ declare class ConfigRepo implements IConfigRepo {
489
491
  * This prevents bi-directional sync from copying the file back.
490
492
  */
491
493
  private processTombstones;
494
+ /** Public wrapper for processTombstones — used by createConfigRepo. */
495
+ processTombstonesPublic(): Promise<void>;
496
+ /**
497
+ * Perform a full sync + dedup cycle without the watch snapshot cache.
498
+ * Used by createConfigRepo to pull remote-only files (like duplicate
499
+ * backend descriptors) that watch()'s initial snapshot would skip.
500
+ */
501
+ initialSyncAndDedup(): Promise<void>;
492
502
  /**
493
503
  * After sync: mark each tombstone as confirmed by all replica backends.
494
504
  */
package/dist/index.js CHANGED
@@ -585,6 +585,10 @@ var ConfigRepo = class {
585
585
  get nodePath() {
586
586
  return `/nodes/${this.nodeId}`;
587
587
  }
588
+ /** Number of replica backends registered (excludes the local primary). */
589
+ get replicaCount() {
590
+ return this.replicaBackends.size;
591
+ }
588
592
  // -----------------------------------------------------------------------
589
593
  // IConfigRepo — Load
590
594
  // -----------------------------------------------------------------------
@@ -723,6 +727,8 @@ var ConfigRepo = class {
723
727
  this.assertNotDisposed();
724
728
  await this.processTombstones();
725
729
  const resultsMap = await this.syncEngine.syncAll();
730
+ await this.readAllBackendDescriptors();
731
+ await this.processTombstones();
726
732
  await this.updateTombstoneConfirmations();
727
733
  await this.gcTombstones();
728
734
  return Array.from(resultsMap.values());
@@ -811,6 +817,22 @@ var ConfigRepo = class {
811
817
  }
812
818
  }
813
819
  }
820
+ /** Public wrapper for processTombstones — used by createConfigRepo. */
821
+ async processTombstonesPublic() {
822
+ await this.processTombstones();
823
+ }
824
+ /**
825
+ * Perform a full sync + dedup cycle without the watch snapshot cache.
826
+ * Used by createConfigRepo to pull remote-only files (like duplicate
827
+ * backend descriptors) that watch()'s initial snapshot would skip.
828
+ */
829
+ async initialSyncAndDedup() {
830
+ this.syncEngine.unwatchAll();
831
+ await this.syncEngine.syncAll();
832
+ await this.readAllBackendDescriptors();
833
+ await this.processTombstones();
834
+ this.syncEngine.watchAll();
835
+ }
814
836
  /**
815
837
  * After sync: mark each tombstone as confirmed by all replica backends.
816
838
  */
@@ -988,7 +1010,6 @@ var ConfigRepo = class {
988
1010
  this.handleConflict(event);
989
1011
  };
990
1012
  this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
991
- this.syncEngine.watch(pair.pairId);
992
1013
  console.log(`[ConfigRepo] Replica ${desc.id} created, sync pair=${pair.pairId}`);
993
1014
  } catch (err) {
994
1015
  console.error(`[ConfigRepo] Failed to create replica ${desc.id} (${desc.type}):`, err);
@@ -1198,6 +1219,16 @@ var ConfigRepo = class {
1198
1219
  );
1199
1220
  for (const dupId of duplicates) {
1200
1221
  const descPath = this.backendFilePath(dupId);
1222
+ for (const [replicaId, replica] of this.replicaBackends) {
1223
+ try {
1224
+ await replica.instance.unlink(descPath);
1225
+ } catch {
1226
+ }
1227
+ try {
1228
+ await replica.instance.unlink(versionPathFor(descPath));
1229
+ } catch {
1230
+ }
1231
+ }
1201
1232
  try {
1202
1233
  await this.deleteFile(descPath);
1203
1234
  } catch {
@@ -1312,9 +1343,9 @@ var ConfigRepo = class {
1312
1343
  this.handleConflict(event);
1313
1344
  };
1314
1345
  this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
1315
- this.syncEngine.watch(pair.pairId);
1316
1346
  console.log(`[ConfigRepo] addBackend: ${id} (${type}) added, sync pair=${pair.pairId}`);
1317
1347
  await this.syncMetaToReplicas();
1348
+ this.syncEngine.watch(pair.pairId);
1318
1349
  }
1319
1350
  async removeBackend(id) {
1320
1351
  this.assertNotDisposed();
@@ -1559,7 +1590,6 @@ var AppDataGroupImpl = class {
1559
1590
  "/"
1560
1591
  );
1561
1592
  this.dataBackends.set(desc.id, { instance, syncable, pairId: pair.pairId, desc });
1562
- this.syncEngine.watch(pair.pairId);
1563
1593
  console.log(`[AppDataGroup:${this.groupId}] backend ${desc.id} (${desc.type}) connected, pair=${pair.pairId}`);
1564
1594
  } catch (err) {
1565
1595
  console.error(`[AppDataGroup:${this.groupId}] Failed to create backend ${desc.id} (${desc.type}):`, err);
@@ -1570,6 +1600,7 @@ var AppDataGroupImpl = class {
1570
1600
  } catch (err) {
1571
1601
  console.warn(`[AppDataGroup:${this.groupId}] Initial sync failed:`, err);
1572
1602
  }
1603
+ this.syncEngine.watchAll();
1573
1604
  }
1574
1605
  getSyncStatuses() {
1575
1606
  return this.syncEngine.getStatusAll();
@@ -1599,13 +1630,13 @@ var AppDataGroupImpl = class {
1599
1630
  );
1600
1631
  const desc = { id, type, options, description };
1601
1632
  this.dataBackends.set(id, { instance, syncable, pairId: pair.pairId, desc });
1602
- this.syncEngine.watch(pair.pairId);
1603
1633
  console.log(`[AppDataGroup:${this.groupId}] addBackend: ${id} (${type}) connected, pair=${pair.pairId}`);
1604
1634
  try {
1605
1635
  await this.syncEngine.sync(pair.pairId);
1606
1636
  } catch (err) {
1607
1637
  console.warn(`[AppDataGroup:${this.groupId}] addBackend: initial sync failed for ${id}:`, err);
1608
1638
  }
1639
+ this.syncEngine.watch(pair.pairId);
1609
1640
  }
1610
1641
  async removeBackend(id) {
1611
1642
  if (this.disposed) throw new Error("DataSyncGroup has been disposed");
@@ -1739,6 +1770,10 @@ async function createConfigRepo(appId, options = {}) {
1739
1770
  );
1740
1771
  await repo.setupSync(allBackends, LOCAL_IDB_BACKEND_ID, options.syncPollIntervalMs);
1741
1772
  await repo.load();
1773
+ if (repo.replicaCount > 0) {
1774
+ console.log("[createConfigRepo] Initial sync + dedup cycle...");
1775
+ await repo.initialSyncAndDedup();
1776
+ }
1742
1777
  repo.syncMetaToReplicas().catch((err) => {
1743
1778
  console.error("[createConfigRepo] background syncMetaToReplicas failed:", err);
1744
1779
  });
package/dist/index.mjs CHANGED
@@ -528,6 +528,10 @@ var ConfigRepo = class {
528
528
  get nodePath() {
529
529
  return `/nodes/${this.nodeId}`;
530
530
  }
531
+ /** Number of replica backends registered (excludes the local primary). */
532
+ get replicaCount() {
533
+ return this.replicaBackends.size;
534
+ }
531
535
  // -----------------------------------------------------------------------
532
536
  // IConfigRepo — Load
533
537
  // -----------------------------------------------------------------------
@@ -666,6 +670,8 @@ var ConfigRepo = class {
666
670
  this.assertNotDisposed();
667
671
  await this.processTombstones();
668
672
  const resultsMap = await this.syncEngine.syncAll();
673
+ await this.readAllBackendDescriptors();
674
+ await this.processTombstones();
669
675
  await this.updateTombstoneConfirmations();
670
676
  await this.gcTombstones();
671
677
  return Array.from(resultsMap.values());
@@ -754,6 +760,22 @@ var ConfigRepo = class {
754
760
  }
755
761
  }
756
762
  }
763
+ /** Public wrapper for processTombstones — used by createConfigRepo. */
764
+ async processTombstonesPublic() {
765
+ await this.processTombstones();
766
+ }
767
+ /**
768
+ * Perform a full sync + dedup cycle without the watch snapshot cache.
769
+ * Used by createConfigRepo to pull remote-only files (like duplicate
770
+ * backend descriptors) that watch()'s initial snapshot would skip.
771
+ */
772
+ async initialSyncAndDedup() {
773
+ this.syncEngine.unwatchAll();
774
+ await this.syncEngine.syncAll();
775
+ await this.readAllBackendDescriptors();
776
+ await this.processTombstones();
777
+ this.syncEngine.watchAll();
778
+ }
757
779
  /**
758
780
  * After sync: mark each tombstone as confirmed by all replica backends.
759
781
  */
@@ -931,7 +953,6 @@ var ConfigRepo = class {
931
953
  this.handleConflict(event);
932
954
  };
933
955
  this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
934
- this.syncEngine.watch(pair.pairId);
935
956
  console.log(`[ConfigRepo] Replica ${desc.id} created, sync pair=${pair.pairId}`);
936
957
  } catch (err) {
937
958
  console.error(`[ConfigRepo] Failed to create replica ${desc.id} (${desc.type}):`, err);
@@ -1141,6 +1162,16 @@ var ConfigRepo = class {
1141
1162
  );
1142
1163
  for (const dupId of duplicates) {
1143
1164
  const descPath = this.backendFilePath(dupId);
1165
+ for (const [replicaId, replica] of this.replicaBackends) {
1166
+ try {
1167
+ await replica.instance.unlink(descPath);
1168
+ } catch {
1169
+ }
1170
+ try {
1171
+ await replica.instance.unlink(versionPathFor(descPath));
1172
+ } catch {
1173
+ }
1174
+ }
1144
1175
  try {
1145
1176
  await this.deleteFile(descPath);
1146
1177
  } catch {
@@ -1255,9 +1286,9 @@ var ConfigRepo = class {
1255
1286
  this.handleConflict(event);
1256
1287
  };
1257
1288
  this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
1258
- this.syncEngine.watch(pair.pairId);
1259
1289
  console.log(`[ConfigRepo] addBackend: ${id} (${type}) added, sync pair=${pair.pairId}`);
1260
1290
  await this.syncMetaToReplicas();
1291
+ this.syncEngine.watch(pair.pairId);
1261
1292
  }
1262
1293
  async removeBackend(id) {
1263
1294
  this.assertNotDisposed();
@@ -1502,7 +1533,6 @@ var AppDataGroupImpl = class {
1502
1533
  "/"
1503
1534
  );
1504
1535
  this.dataBackends.set(desc.id, { instance, syncable, pairId: pair.pairId, desc });
1505
- this.syncEngine.watch(pair.pairId);
1506
1536
  console.log(`[AppDataGroup:${this.groupId}] backend ${desc.id} (${desc.type}) connected, pair=${pair.pairId}`);
1507
1537
  } catch (err) {
1508
1538
  console.error(`[AppDataGroup:${this.groupId}] Failed to create backend ${desc.id} (${desc.type}):`, err);
@@ -1513,6 +1543,7 @@ var AppDataGroupImpl = class {
1513
1543
  } catch (err) {
1514
1544
  console.warn(`[AppDataGroup:${this.groupId}] Initial sync failed:`, err);
1515
1545
  }
1546
+ this.syncEngine.watchAll();
1516
1547
  }
1517
1548
  getSyncStatuses() {
1518
1549
  return this.syncEngine.getStatusAll();
@@ -1542,13 +1573,13 @@ var AppDataGroupImpl = class {
1542
1573
  );
1543
1574
  const desc = { id, type, options, description };
1544
1575
  this.dataBackends.set(id, { instance, syncable, pairId: pair.pairId, desc });
1545
- this.syncEngine.watch(pair.pairId);
1546
1576
  console.log(`[AppDataGroup:${this.groupId}] addBackend: ${id} (${type}) connected, pair=${pair.pairId}`);
1547
1577
  try {
1548
1578
  await this.syncEngine.sync(pair.pairId);
1549
1579
  } catch (err) {
1550
1580
  console.warn(`[AppDataGroup:${this.groupId}] addBackend: initial sync failed for ${id}:`, err);
1551
1581
  }
1582
+ this.syncEngine.watch(pair.pairId);
1552
1583
  }
1553
1584
  async removeBackend(id) {
1554
1585
  if (this.disposed) throw new Error("DataSyncGroup has been disposed");
@@ -1682,6 +1713,10 @@ async function createConfigRepo(appId, options = {}) {
1682
1713
  );
1683
1714
  await repo.setupSync(allBackends, LOCAL_IDB_BACKEND_ID, options.syncPollIntervalMs);
1684
1715
  await repo.load();
1716
+ if (repo.replicaCount > 0) {
1717
+ console.log("[createConfigRepo] Initial sync + dedup cycle...");
1718
+ await repo.initialSyncAndDedup();
1719
+ }
1685
1720
  repo.syncMetaToReplicas().catch((err) => {
1686
1721
  console.error("[createConfigRepo] background syncMetaToReplicas failed:", err);
1687
1722
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zen-fs-config",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
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",