zen-fs-config 0.5.0 → 0.5.2

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.js CHANGED
@@ -533,6 +533,22 @@ var LOCAL_IDB_BACKEND_ID = "local-idb";
533
533
  function tombstoneFileName(filePath) {
534
534
  return filePath.replace(/^\//, "").replace(/\//g, "__").replace(/\./g, "++") + ".json";
535
535
  }
536
+ function stableOptionsKey(options) {
537
+ if (!options || typeof options !== "object") return "{}";
538
+ return JSON.stringify(sortKeysDeep(options));
539
+ }
540
+ function sortKeysDeep(obj) {
541
+ if (obj === null || typeof obj !== "object") return obj;
542
+ if (Array.isArray(obj)) return obj.map(sortKeysDeep);
543
+ const sorted = {};
544
+ for (const key of Object.keys(obj).sort()) {
545
+ sorted[key] = sortKeysDeep(obj[key]);
546
+ }
547
+ return sorted;
548
+ }
549
+ function backendDedupKey(desc) {
550
+ return `${desc.type}:${stableOptionsKey(desc.options)}`;
551
+ }
536
552
  var ConfigRepo = class {
537
553
  appId;
538
554
  nodeId;
@@ -1163,7 +1179,7 @@ var ConfigRepo = class {
1163
1179
  const seen = /* @__PURE__ */ new Map();
1164
1180
  const duplicates = [];
1165
1181
  for (const item of items) {
1166
- const key = `${item.desc.type}:${JSON.stringify(item.desc.options ?? {})}`;
1182
+ const key = backendDedupKey(item.desc);
1167
1183
  const existing = seen.get(key);
1168
1184
  if (existing) {
1169
1185
  if (item.mtime < existing.mtime) {
@@ -1181,7 +1197,12 @@ var ConfigRepo = class {
1181
1197
  `[ConfigRepo] readAllBackendDescriptors: removing ${duplicates.length} duplicate(s): ${duplicates.join(", ")}`
1182
1198
  );
1183
1199
  for (const dupId of duplicates) {
1184
- await this.removeBackendDescriptor(dupId);
1200
+ const descPath = this.backendFilePath(dupId);
1201
+ try {
1202
+ await this.deleteFile(descPath);
1203
+ } catch {
1204
+ await this.removeBackendDescriptor(dupId);
1205
+ }
1185
1206
  }
1186
1207
  }
1187
1208
  return Array.from(seen.values()).map((i) => i.desc);
@@ -1244,7 +1265,12 @@ var ConfigRepo = class {
1244
1265
  const current = await this.readAllBackendDescriptors();
1245
1266
  for (const desc of current) {
1246
1267
  if (!keepIds.has(desc.id)) {
1247
- await this.removeBackendDescriptor(desc.id);
1268
+ const descPath = this.backendFilePath(desc.id);
1269
+ try {
1270
+ await this.deleteFile(descPath);
1271
+ } catch {
1272
+ await this.removeBackendDescriptor(desc.id);
1273
+ }
1248
1274
  }
1249
1275
  }
1250
1276
  }
@@ -1260,6 +1286,13 @@ var ConfigRepo = class {
1260
1286
  if (existing.some((b) => b.id === id)) {
1261
1287
  throw new Error(`Backend "${id}" already exists. Use removeBackend() first.`);
1262
1288
  }
1289
+ const newKey = backendDedupKey({ id, type, options });
1290
+ const dup = existing.find((b) => backendDedupKey(b) === newKey);
1291
+ if (dup) {
1292
+ throw new Error(
1293
+ `Backend "${id}" has the same configuration as existing backend "${dup.id}" (type=${type}). Use removeBackend("${dup.id}") first, or connect with the existing backend's ID.`
1294
+ );
1295
+ }
1263
1296
  console.log(`[ConfigRepo] addBackend: creating ${id} (${type})...`);
1264
1297
  const instance = await createBackend({ type, options });
1265
1298
  const syncable = backendToSyncableFS(instance, `${type}(${id})`);
@@ -1292,14 +1325,27 @@ var ConfigRepo = class {
1292
1325
  if (!replica) {
1293
1326
  throw new Error(`Backend "${id}" is not a registered replica`);
1294
1327
  }
1328
+ const descPath = this.backendFilePath(id);
1329
+ try {
1330
+ await replica.instance.unlink(descPath);
1331
+ } catch {
1332
+ }
1333
+ try {
1334
+ await replica.instance.unlink(versionPathFor(descPath));
1335
+ } catch {
1336
+ }
1337
+ try {
1338
+ await this.deleteFile(descPath);
1339
+ } catch {
1340
+ await this.removeBackendDescriptor(id);
1341
+ }
1295
1342
  this.syncEngine.removePair(replica.pairId);
1296
1343
  console.log(`[ConfigRepo] removeBackend: sync pair ${replica.pairId} removed`);
1297
1344
  this.replicaBackends.delete(id);
1298
1345
  if (replica.instance?.dispose) {
1299
1346
  await replica.instance.dispose();
1300
1347
  }
1301
- await this.removeBackendDescriptor(id);
1302
- console.log(`[ConfigRepo] removeBackend: ${id} removed`);
1348
+ console.log(`[ConfigRepo] removeBackend: ${id} removed (tombstone written, remote cleaned)`);
1303
1349
  }
1304
1350
  // -----------------------------------------------------------------------
1305
1351
  // IConfigRepo — Group Type
@@ -1655,13 +1701,21 @@ async function createConfigRepo(appId, options = {}) {
1655
1701
  const replicaId = options.primaryBackendId || `${options.backendInfo.type}-replica`;
1656
1702
  const allBackends2 = await tempRepo.readAllBackendDescriptors();
1657
1703
  const hasReplica = allBackends2.some((b) => b.id === replicaId);
1658
- if (!hasReplica) {
1704
+ const newKey = backendDedupKey({
1705
+ id: replicaId,
1706
+ type: options.backendInfo.type,
1707
+ options: options.backendInfo.options
1708
+ });
1709
+ const dupConfig = allBackends2.find((b) => backendDedupKey(b) === newKey);
1710
+ if (!hasReplica && !dupConfig) {
1659
1711
  await tempRepo.writeBackendDescriptor({
1660
1712
  id: replicaId,
1661
1713
  type: options.backendInfo.type,
1662
1714
  options: options.backendInfo.options
1663
1715
  });
1664
1716
  console.log(`[createConfigRepo] Added replica backend: ${replicaId} (${options.backendInfo.type})`);
1717
+ } else if (dupConfig) {
1718
+ console.log(`[createConfigRepo] Replica with same config already registered as "${dupConfig.id}", skipping`);
1665
1719
  } else {
1666
1720
  console.log(`[createConfigRepo] Replica ${replicaId} already registered`);
1667
1721
  }
package/dist/index.mjs CHANGED
@@ -476,6 +476,22 @@ var LOCAL_IDB_BACKEND_ID = "local-idb";
476
476
  function tombstoneFileName(filePath) {
477
477
  return filePath.replace(/^\//, "").replace(/\//g, "__").replace(/\./g, "++") + ".json";
478
478
  }
479
+ function stableOptionsKey(options) {
480
+ if (!options || typeof options !== "object") return "{}";
481
+ return JSON.stringify(sortKeysDeep(options));
482
+ }
483
+ function sortKeysDeep(obj) {
484
+ if (obj === null || typeof obj !== "object") return obj;
485
+ if (Array.isArray(obj)) return obj.map(sortKeysDeep);
486
+ const sorted = {};
487
+ for (const key of Object.keys(obj).sort()) {
488
+ sorted[key] = sortKeysDeep(obj[key]);
489
+ }
490
+ return sorted;
491
+ }
492
+ function backendDedupKey(desc) {
493
+ return `${desc.type}:${stableOptionsKey(desc.options)}`;
494
+ }
479
495
  var ConfigRepo = class {
480
496
  appId;
481
497
  nodeId;
@@ -1106,7 +1122,7 @@ var ConfigRepo = class {
1106
1122
  const seen = /* @__PURE__ */ new Map();
1107
1123
  const duplicates = [];
1108
1124
  for (const item of items) {
1109
- const key = `${item.desc.type}:${JSON.stringify(item.desc.options ?? {})}`;
1125
+ const key = backendDedupKey(item.desc);
1110
1126
  const existing = seen.get(key);
1111
1127
  if (existing) {
1112
1128
  if (item.mtime < existing.mtime) {
@@ -1124,7 +1140,12 @@ var ConfigRepo = class {
1124
1140
  `[ConfigRepo] readAllBackendDescriptors: removing ${duplicates.length} duplicate(s): ${duplicates.join(", ")}`
1125
1141
  );
1126
1142
  for (const dupId of duplicates) {
1127
- await this.removeBackendDescriptor(dupId);
1143
+ const descPath = this.backendFilePath(dupId);
1144
+ try {
1145
+ await this.deleteFile(descPath);
1146
+ } catch {
1147
+ await this.removeBackendDescriptor(dupId);
1148
+ }
1128
1149
  }
1129
1150
  }
1130
1151
  return Array.from(seen.values()).map((i) => i.desc);
@@ -1187,7 +1208,12 @@ var ConfigRepo = class {
1187
1208
  const current = await this.readAllBackendDescriptors();
1188
1209
  for (const desc of current) {
1189
1210
  if (!keepIds.has(desc.id)) {
1190
- await this.removeBackendDescriptor(desc.id);
1211
+ const descPath = this.backendFilePath(desc.id);
1212
+ try {
1213
+ await this.deleteFile(descPath);
1214
+ } catch {
1215
+ await this.removeBackendDescriptor(desc.id);
1216
+ }
1191
1217
  }
1192
1218
  }
1193
1219
  }
@@ -1203,6 +1229,13 @@ var ConfigRepo = class {
1203
1229
  if (existing.some((b) => b.id === id)) {
1204
1230
  throw new Error(`Backend "${id}" already exists. Use removeBackend() first.`);
1205
1231
  }
1232
+ const newKey = backendDedupKey({ id, type, options });
1233
+ const dup = existing.find((b) => backendDedupKey(b) === newKey);
1234
+ if (dup) {
1235
+ throw new Error(
1236
+ `Backend "${id}" has the same configuration as existing backend "${dup.id}" (type=${type}). Use removeBackend("${dup.id}") first, or connect with the existing backend's ID.`
1237
+ );
1238
+ }
1206
1239
  console.log(`[ConfigRepo] addBackend: creating ${id} (${type})...`);
1207
1240
  const instance = await createBackend({ type, options });
1208
1241
  const syncable = backendToSyncableFS(instance, `${type}(${id})`);
@@ -1235,14 +1268,27 @@ var ConfigRepo = class {
1235
1268
  if (!replica) {
1236
1269
  throw new Error(`Backend "${id}" is not a registered replica`);
1237
1270
  }
1271
+ const descPath = this.backendFilePath(id);
1272
+ try {
1273
+ await replica.instance.unlink(descPath);
1274
+ } catch {
1275
+ }
1276
+ try {
1277
+ await replica.instance.unlink(versionPathFor(descPath));
1278
+ } catch {
1279
+ }
1280
+ try {
1281
+ await this.deleteFile(descPath);
1282
+ } catch {
1283
+ await this.removeBackendDescriptor(id);
1284
+ }
1238
1285
  this.syncEngine.removePair(replica.pairId);
1239
1286
  console.log(`[ConfigRepo] removeBackend: sync pair ${replica.pairId} removed`);
1240
1287
  this.replicaBackends.delete(id);
1241
1288
  if (replica.instance?.dispose) {
1242
1289
  await replica.instance.dispose();
1243
1290
  }
1244
- await this.removeBackendDescriptor(id);
1245
- console.log(`[ConfigRepo] removeBackend: ${id} removed`);
1291
+ console.log(`[ConfigRepo] removeBackend: ${id} removed (tombstone written, remote cleaned)`);
1246
1292
  }
1247
1293
  // -----------------------------------------------------------------------
1248
1294
  // IConfigRepo — Group Type
@@ -1598,13 +1644,21 @@ async function createConfigRepo(appId, options = {}) {
1598
1644
  const replicaId = options.primaryBackendId || `${options.backendInfo.type}-replica`;
1599
1645
  const allBackends2 = await tempRepo.readAllBackendDescriptors();
1600
1646
  const hasReplica = allBackends2.some((b) => b.id === replicaId);
1601
- if (!hasReplica) {
1647
+ const newKey = backendDedupKey({
1648
+ id: replicaId,
1649
+ type: options.backendInfo.type,
1650
+ options: options.backendInfo.options
1651
+ });
1652
+ const dupConfig = allBackends2.find((b) => backendDedupKey(b) === newKey);
1653
+ if (!hasReplica && !dupConfig) {
1602
1654
  await tempRepo.writeBackendDescriptor({
1603
1655
  id: replicaId,
1604
1656
  type: options.backendInfo.type,
1605
1657
  options: options.backendInfo.options
1606
1658
  });
1607
1659
  console.log(`[createConfigRepo] Added replica backend: ${replicaId} (${options.backendInfo.type})`);
1660
+ } else if (dupConfig) {
1661
+ console.log(`[createConfigRepo] Replica with same config already registered as "${dupConfig.id}", skipping`);
1608
1662
  } else {
1609
1663
  console.log(`[createConfigRepo] Replica ${replicaId} already registered`);
1610
1664
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zen-fs-config",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
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",