zen-fs-config 0.5.1 → 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 {
@@ -1265,7 +1296,12 @@ var ConfigRepo = class {
1265
1296
  const current = await this.readAllBackendDescriptors();
1266
1297
  for (const desc of current) {
1267
1298
  if (!keepIds.has(desc.id)) {
1268
- await this.removeBackendDescriptor(desc.id);
1299
+ const descPath = this.backendFilePath(desc.id);
1300
+ try {
1301
+ await this.deleteFile(descPath);
1302
+ } catch {
1303
+ await this.removeBackendDescriptor(desc.id);
1304
+ }
1269
1305
  }
1270
1306
  }
1271
1307
  }
@@ -1307,9 +1343,9 @@ var ConfigRepo = class {
1307
1343
  this.handleConflict(event);
1308
1344
  };
1309
1345
  this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
1310
- this.syncEngine.watch(pair.pairId);
1311
1346
  console.log(`[ConfigRepo] addBackend: ${id} (${type}) added, sync pair=${pair.pairId}`);
1312
1347
  await this.syncMetaToReplicas();
1348
+ this.syncEngine.watch(pair.pairId);
1313
1349
  }
1314
1350
  async removeBackend(id) {
1315
1351
  this.assertNotDisposed();
@@ -1320,14 +1356,27 @@ var ConfigRepo = class {
1320
1356
  if (!replica) {
1321
1357
  throw new Error(`Backend "${id}" is not a registered replica`);
1322
1358
  }
1359
+ const descPath = this.backendFilePath(id);
1360
+ try {
1361
+ await replica.instance.unlink(descPath);
1362
+ } catch {
1363
+ }
1364
+ try {
1365
+ await replica.instance.unlink(versionPathFor(descPath));
1366
+ } catch {
1367
+ }
1368
+ try {
1369
+ await this.deleteFile(descPath);
1370
+ } catch {
1371
+ await this.removeBackendDescriptor(id);
1372
+ }
1323
1373
  this.syncEngine.removePair(replica.pairId);
1324
1374
  console.log(`[ConfigRepo] removeBackend: sync pair ${replica.pairId} removed`);
1325
1375
  this.replicaBackends.delete(id);
1326
1376
  if (replica.instance?.dispose) {
1327
1377
  await replica.instance.dispose();
1328
1378
  }
1329
- await this.removeBackendDescriptor(id);
1330
- console.log(`[ConfigRepo] removeBackend: ${id} removed`);
1379
+ console.log(`[ConfigRepo] removeBackend: ${id} removed (tombstone written, remote cleaned)`);
1331
1380
  }
1332
1381
  // -----------------------------------------------------------------------
1333
1382
  // IConfigRepo — Group Type
@@ -1541,7 +1590,6 @@ var AppDataGroupImpl = class {
1541
1590
  "/"
1542
1591
  );
1543
1592
  this.dataBackends.set(desc.id, { instance, syncable, pairId: pair.pairId, desc });
1544
- this.syncEngine.watch(pair.pairId);
1545
1593
  console.log(`[AppDataGroup:${this.groupId}] backend ${desc.id} (${desc.type}) connected, pair=${pair.pairId}`);
1546
1594
  } catch (err) {
1547
1595
  console.error(`[AppDataGroup:${this.groupId}] Failed to create backend ${desc.id} (${desc.type}):`, err);
@@ -1552,6 +1600,7 @@ var AppDataGroupImpl = class {
1552
1600
  } catch (err) {
1553
1601
  console.warn(`[AppDataGroup:${this.groupId}] Initial sync failed:`, err);
1554
1602
  }
1603
+ this.syncEngine.watchAll();
1555
1604
  }
1556
1605
  getSyncStatuses() {
1557
1606
  return this.syncEngine.getStatusAll();
@@ -1581,13 +1630,13 @@ var AppDataGroupImpl = class {
1581
1630
  );
1582
1631
  const desc = { id, type, options, description };
1583
1632
  this.dataBackends.set(id, { instance, syncable, pairId: pair.pairId, desc });
1584
- this.syncEngine.watch(pair.pairId);
1585
1633
  console.log(`[AppDataGroup:${this.groupId}] addBackend: ${id} (${type}) connected, pair=${pair.pairId}`);
1586
1634
  try {
1587
1635
  await this.syncEngine.sync(pair.pairId);
1588
1636
  } catch (err) {
1589
1637
  console.warn(`[AppDataGroup:${this.groupId}] addBackend: initial sync failed for ${id}:`, err);
1590
1638
  }
1639
+ this.syncEngine.watch(pair.pairId);
1591
1640
  }
1592
1641
  async removeBackend(id) {
1593
1642
  if (this.disposed) throw new Error("DataSyncGroup has been disposed");
@@ -1721,6 +1770,10 @@ async function createConfigRepo(appId, options = {}) {
1721
1770
  );
1722
1771
  await repo.setupSync(allBackends, LOCAL_IDB_BACKEND_ID, options.syncPollIntervalMs);
1723
1772
  await repo.load();
1773
+ if (repo.replicaCount > 0) {
1774
+ console.log("[createConfigRepo] Initial sync + dedup cycle...");
1775
+ await repo.initialSyncAndDedup();
1776
+ }
1724
1777
  repo.syncMetaToReplicas().catch((err) => {
1725
1778
  console.error("[createConfigRepo] background syncMetaToReplicas failed:", err);
1726
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 {
@@ -1208,7 +1239,12 @@ var ConfigRepo = class {
1208
1239
  const current = await this.readAllBackendDescriptors();
1209
1240
  for (const desc of current) {
1210
1241
  if (!keepIds.has(desc.id)) {
1211
- await this.removeBackendDescriptor(desc.id);
1242
+ const descPath = this.backendFilePath(desc.id);
1243
+ try {
1244
+ await this.deleteFile(descPath);
1245
+ } catch {
1246
+ await this.removeBackendDescriptor(desc.id);
1247
+ }
1212
1248
  }
1213
1249
  }
1214
1250
  }
@@ -1250,9 +1286,9 @@ var ConfigRepo = class {
1250
1286
  this.handleConflict(event);
1251
1287
  };
1252
1288
  this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
1253
- this.syncEngine.watch(pair.pairId);
1254
1289
  console.log(`[ConfigRepo] addBackend: ${id} (${type}) added, sync pair=${pair.pairId}`);
1255
1290
  await this.syncMetaToReplicas();
1291
+ this.syncEngine.watch(pair.pairId);
1256
1292
  }
1257
1293
  async removeBackend(id) {
1258
1294
  this.assertNotDisposed();
@@ -1263,14 +1299,27 @@ var ConfigRepo = class {
1263
1299
  if (!replica) {
1264
1300
  throw new Error(`Backend "${id}" is not a registered replica`);
1265
1301
  }
1302
+ const descPath = this.backendFilePath(id);
1303
+ try {
1304
+ await replica.instance.unlink(descPath);
1305
+ } catch {
1306
+ }
1307
+ try {
1308
+ await replica.instance.unlink(versionPathFor(descPath));
1309
+ } catch {
1310
+ }
1311
+ try {
1312
+ await this.deleteFile(descPath);
1313
+ } catch {
1314
+ await this.removeBackendDescriptor(id);
1315
+ }
1266
1316
  this.syncEngine.removePair(replica.pairId);
1267
1317
  console.log(`[ConfigRepo] removeBackend: sync pair ${replica.pairId} removed`);
1268
1318
  this.replicaBackends.delete(id);
1269
1319
  if (replica.instance?.dispose) {
1270
1320
  await replica.instance.dispose();
1271
1321
  }
1272
- await this.removeBackendDescriptor(id);
1273
- console.log(`[ConfigRepo] removeBackend: ${id} removed`);
1322
+ console.log(`[ConfigRepo] removeBackend: ${id} removed (tombstone written, remote cleaned)`);
1274
1323
  }
1275
1324
  // -----------------------------------------------------------------------
1276
1325
  // IConfigRepo — Group Type
@@ -1484,7 +1533,6 @@ var AppDataGroupImpl = class {
1484
1533
  "/"
1485
1534
  );
1486
1535
  this.dataBackends.set(desc.id, { instance, syncable, pairId: pair.pairId, desc });
1487
- this.syncEngine.watch(pair.pairId);
1488
1536
  console.log(`[AppDataGroup:${this.groupId}] backend ${desc.id} (${desc.type}) connected, pair=${pair.pairId}`);
1489
1537
  } catch (err) {
1490
1538
  console.error(`[AppDataGroup:${this.groupId}] Failed to create backend ${desc.id} (${desc.type}):`, err);
@@ -1495,6 +1543,7 @@ var AppDataGroupImpl = class {
1495
1543
  } catch (err) {
1496
1544
  console.warn(`[AppDataGroup:${this.groupId}] Initial sync failed:`, err);
1497
1545
  }
1546
+ this.syncEngine.watchAll();
1498
1547
  }
1499
1548
  getSyncStatuses() {
1500
1549
  return this.syncEngine.getStatusAll();
@@ -1524,13 +1573,13 @@ var AppDataGroupImpl = class {
1524
1573
  );
1525
1574
  const desc = { id, type, options, description };
1526
1575
  this.dataBackends.set(id, { instance, syncable, pairId: pair.pairId, desc });
1527
- this.syncEngine.watch(pair.pairId);
1528
1576
  console.log(`[AppDataGroup:${this.groupId}] addBackend: ${id} (${type}) connected, pair=${pair.pairId}`);
1529
1577
  try {
1530
1578
  await this.syncEngine.sync(pair.pairId);
1531
1579
  } catch (err) {
1532
1580
  console.warn(`[AppDataGroup:${this.groupId}] addBackend: initial sync failed for ${id}:`, err);
1533
1581
  }
1582
+ this.syncEngine.watch(pair.pairId);
1534
1583
  }
1535
1584
  async removeBackend(id) {
1536
1585
  if (this.disposed) throw new Error("DataSyncGroup has been disposed");
@@ -1664,6 +1713,10 @@ async function createConfigRepo(appId, options = {}) {
1664
1713
  );
1665
1714
  await repo.setupSync(allBackends, LOCAL_IDB_BACKEND_ID, options.syncPollIntervalMs);
1666
1715
  await repo.load();
1716
+ if (repo.replicaCount > 0) {
1717
+ console.log("[createConfigRepo] Initial sync + dedup cycle...");
1718
+ await repo.initialSyncAndDedup();
1719
+ }
1667
1720
  repo.syncMetaToReplicas().catch((err) => {
1668
1721
  console.error("[createConfigRepo] background syncMetaToReplicas failed:", err);
1669
1722
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zen-fs-config",
3
- "version": "0.5.1",
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",