zen-fs-config 0.4.3 → 0.4.5

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
@@ -125,6 +125,12 @@ interface ConfigRepoOptions {
125
125
  serializer?: ConfigSerializer;
126
126
  /** Custom conflict handler. Called before auto-resolution. */
127
127
  onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>;
128
+ /**
129
+ * Sync polling interval in milliseconds.
130
+ * Controls how often the sync engine checks for remote changes.
131
+ * Default: 1800000 (30 minutes).
132
+ */
133
+ syncPollIntervalMs?: number;
128
134
  }
129
135
  /** The main configuration repository interface. */
130
136
  interface IConfigRepo {
@@ -389,7 +395,7 @@ declare class ConfigRepo implements IConfigRepo {
389
395
  listConflicts(): Promise<ConflictArchive[]>;
390
396
  readConflictBackup(conflictId: string, fileType: 'source' | 'target' | 'resolved'): Promise<string>;
391
397
  dispose(): Promise<void>;
392
- setupSync(backends: BackendDescriptor[], primaryBackendId: string): Promise<void>;
398
+ setupSync(backends: BackendDescriptor[], primaryBackendId: string, pollIntervalMs?: number): Promise<void>;
393
399
  private persistConfig;
394
400
  private reloadConfigCache;
395
401
  private handleConflict;
package/dist/index.d.ts CHANGED
@@ -125,6 +125,12 @@ interface ConfigRepoOptions {
125
125
  serializer?: ConfigSerializer;
126
126
  /** Custom conflict handler. Called before auto-resolution. */
127
127
  onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>;
128
+ /**
129
+ * Sync polling interval in milliseconds.
130
+ * Controls how often the sync engine checks for remote changes.
131
+ * Default: 1800000 (30 minutes).
132
+ */
133
+ syncPollIntervalMs?: number;
128
134
  }
129
135
  /** The main configuration repository interface. */
130
136
  interface IConfigRepo {
@@ -389,7 +395,7 @@ declare class ConfigRepo implements IConfigRepo {
389
395
  listConflicts(): Promise<ConflictArchive[]>;
390
396
  readConflictBackup(conflictId: string, fileType: 'source' | 'target' | 'resolved'): Promise<string>;
391
397
  dispose(): Promise<void>;
392
- setupSync(backends: BackendDescriptor[], primaryBackendId: string): Promise<void>;
398
+ setupSync(backends: BackendDescriptor[], primaryBackendId: string, pollIntervalMs?: number): Promise<void>;
393
399
  private persistConfig;
394
400
  private reloadConfigCache;
395
401
  private handleConflict;
package/dist/index.js CHANGED
@@ -263,6 +263,9 @@ function backendToSyncableFS(backend, name) {
263
263
  return backend.exists(path);
264
264
  }
265
265
  };
266
+ if (typeof backend.checkForUpdates === "function") {
267
+ syncable.checkForUpdates = () => backend.checkForUpdates();
268
+ }
266
269
  if (name) {
267
270
  syncable.backendName = name;
268
271
  } else if (backend.backendName) {
@@ -894,8 +897,8 @@ var ConfigRepo = class {
894
897
  // -----------------------------------------------------------------------
895
898
  // Internal — Setup
896
899
  // -----------------------------------------------------------------------
897
- async setupSync(backends, primaryBackendId) {
898
- console.log(`[ConfigRepo] setupSync: ${backends.length} backends, primary=${primaryBackendId}`);
900
+ async setupSync(backends, primaryBackendId, pollIntervalMs) {
901
+ console.log(`[ConfigRepo] setupSync: ${backends.length} backends, primary=${primaryBackendId} pollInterval=${pollIntervalMs ?? "default"}ms`);
899
902
  for (const desc of backends) {
900
903
  if (desc.id === primaryBackendId) continue;
901
904
  if (desc.enabled === false) {
@@ -911,7 +914,8 @@ var ConfigRepo = class {
911
914
  syncable,
912
915
  {
913
916
  direction: import_zen_fs_sync.SyncDirection.BiDirectional,
914
- conflictStrategy: "source-wins"
917
+ conflictStrategy: "source-wins",
918
+ pollIntervalMs
915
919
  },
916
920
  "/"
917
921
  );
@@ -1089,27 +1093,39 @@ var ConfigRepo = class {
1089
1093
  async readAllBackendDescriptors() {
1090
1094
  try {
1091
1095
  const entries = await this.cachedFS.readdir(BACKENDS_DIR);
1092
- const descriptors = [];
1096
+ const items = [];
1093
1097
  for (const entry of entries) {
1094
1098
  if (!entry.endsWith(".json")) continue;
1099
+ const filePath = `${BACKENDS_DIR}/${entry}`;
1095
1100
  try {
1096
- const raw = await this.cachedFS.readFile(`${BACKENDS_DIR}/${entry}`);
1101
+ const raw = await this.cachedFS.readFile(filePath);
1097
1102
  const desc = JSON.parse(new TextDecoder().decode(toUint8Array(raw)));
1098
1103
  if (desc.id && desc.type) {
1099
- descriptors.push(desc);
1104
+ let mtime = 0;
1105
+ try {
1106
+ const stat = await this.cachedFS.stat(filePath);
1107
+ mtime = stat.mtimeMs ?? 0;
1108
+ } catch {
1109
+ }
1110
+ items.push({ desc, mtime });
1100
1111
  }
1101
1112
  } catch {
1102
1113
  }
1103
1114
  }
1104
1115
  const seen = /* @__PURE__ */ new Map();
1105
1116
  const duplicates = [];
1106
- descriptors.sort((a, b) => a.id.localeCompare(b.id));
1107
- for (const desc of descriptors) {
1108
- const key = `${desc.type}:${JSON.stringify(desc.options ?? {})}`;
1109
- if (seen.has(key)) {
1110
- duplicates.push(desc.id);
1117
+ for (const item of items) {
1118
+ const key = `${item.desc.type}:${JSON.stringify(item.desc.options ?? {})}`;
1119
+ const existing = seen.get(key);
1120
+ if (existing) {
1121
+ if (item.mtime < existing.mtime) {
1122
+ duplicates.push(existing.desc.id);
1123
+ seen.set(key, item);
1124
+ } else {
1125
+ duplicates.push(item.desc.id);
1126
+ }
1111
1127
  } else {
1112
- seen.set(key, desc.id);
1128
+ seen.set(key, item);
1113
1129
  }
1114
1130
  }
1115
1131
  if (duplicates.length > 0) {
@@ -1120,7 +1136,7 @@ var ConfigRepo = class {
1120
1136
  await this.removeBackendDescriptor(dupId);
1121
1137
  }
1122
1138
  }
1123
- return descriptors.filter((d) => !duplicates.includes(d.id));
1139
+ return Array.from(seen.values()).map((i) => i.desc);
1124
1140
  } catch {
1125
1141
  return [];
1126
1142
  }
@@ -1331,9 +1347,11 @@ async function createConfigRepo(appId, options = {}) {
1331
1347
  serializer,
1332
1348
  options.onConflict
1333
1349
  );
1334
- await repo.setupSync(allBackends, LOCAL_IDB_BACKEND_ID);
1335
- await repo.syncMetaToReplicas();
1350
+ await repo.setupSync(allBackends, LOCAL_IDB_BACKEND_ID, options.syncPollIntervalMs);
1336
1351
  await repo.load();
1352
+ repo.syncMetaToReplicas().catch((err) => {
1353
+ console.error("[createConfigRepo] background syncMetaToReplicas failed:", err);
1354
+ });
1337
1355
  return repo;
1338
1356
  }
1339
1357
  // Annotate the CommonJS export names for ESM import in node:
package/dist/index.mjs CHANGED
@@ -211,6 +211,9 @@ function backendToSyncableFS(backend, name) {
211
211
  return backend.exists(path);
212
212
  }
213
213
  };
214
+ if (typeof backend.checkForUpdates === "function") {
215
+ syncable.checkForUpdates = () => backend.checkForUpdates();
216
+ }
214
217
  if (name) {
215
218
  syncable.backendName = name;
216
219
  } else if (backend.backendName) {
@@ -842,8 +845,8 @@ var ConfigRepo = class {
842
845
  // -----------------------------------------------------------------------
843
846
  // Internal — Setup
844
847
  // -----------------------------------------------------------------------
845
- async setupSync(backends, primaryBackendId) {
846
- console.log(`[ConfigRepo] setupSync: ${backends.length} backends, primary=${primaryBackendId}`);
848
+ async setupSync(backends, primaryBackendId, pollIntervalMs) {
849
+ console.log(`[ConfigRepo] setupSync: ${backends.length} backends, primary=${primaryBackendId} pollInterval=${pollIntervalMs ?? "default"}ms`);
847
850
  for (const desc of backends) {
848
851
  if (desc.id === primaryBackendId) continue;
849
852
  if (desc.enabled === false) {
@@ -859,7 +862,8 @@ var ConfigRepo = class {
859
862
  syncable,
860
863
  {
861
864
  direction: SyncDirection.BiDirectional,
862
- conflictStrategy: "source-wins"
865
+ conflictStrategy: "source-wins",
866
+ pollIntervalMs
863
867
  },
864
868
  "/"
865
869
  );
@@ -1037,27 +1041,39 @@ var ConfigRepo = class {
1037
1041
  async readAllBackendDescriptors() {
1038
1042
  try {
1039
1043
  const entries = await this.cachedFS.readdir(BACKENDS_DIR);
1040
- const descriptors = [];
1044
+ const items = [];
1041
1045
  for (const entry of entries) {
1042
1046
  if (!entry.endsWith(".json")) continue;
1047
+ const filePath = `${BACKENDS_DIR}/${entry}`;
1043
1048
  try {
1044
- const raw = await this.cachedFS.readFile(`${BACKENDS_DIR}/${entry}`);
1049
+ const raw = await this.cachedFS.readFile(filePath);
1045
1050
  const desc = JSON.parse(new TextDecoder().decode(toUint8Array(raw)));
1046
1051
  if (desc.id && desc.type) {
1047
- descriptors.push(desc);
1052
+ let mtime = 0;
1053
+ try {
1054
+ const stat = await this.cachedFS.stat(filePath);
1055
+ mtime = stat.mtimeMs ?? 0;
1056
+ } catch {
1057
+ }
1058
+ items.push({ desc, mtime });
1048
1059
  }
1049
1060
  } catch {
1050
1061
  }
1051
1062
  }
1052
1063
  const seen = /* @__PURE__ */ new Map();
1053
1064
  const duplicates = [];
1054
- descriptors.sort((a, b) => a.id.localeCompare(b.id));
1055
- for (const desc of descriptors) {
1056
- const key = `${desc.type}:${JSON.stringify(desc.options ?? {})}`;
1057
- if (seen.has(key)) {
1058
- duplicates.push(desc.id);
1065
+ for (const item of items) {
1066
+ const key = `${item.desc.type}:${JSON.stringify(item.desc.options ?? {})}`;
1067
+ const existing = seen.get(key);
1068
+ if (existing) {
1069
+ if (item.mtime < existing.mtime) {
1070
+ duplicates.push(existing.desc.id);
1071
+ seen.set(key, item);
1072
+ } else {
1073
+ duplicates.push(item.desc.id);
1074
+ }
1059
1075
  } else {
1060
- seen.set(key, desc.id);
1076
+ seen.set(key, item);
1061
1077
  }
1062
1078
  }
1063
1079
  if (duplicates.length > 0) {
@@ -1068,7 +1084,7 @@ var ConfigRepo = class {
1068
1084
  await this.removeBackendDescriptor(dupId);
1069
1085
  }
1070
1086
  }
1071
- return descriptors.filter((d) => !duplicates.includes(d.id));
1087
+ return Array.from(seen.values()).map((i) => i.desc);
1072
1088
  } catch {
1073
1089
  return [];
1074
1090
  }
@@ -1279,9 +1295,11 @@ async function createConfigRepo(appId, options = {}) {
1279
1295
  serializer,
1280
1296
  options.onConflict
1281
1297
  );
1282
- await repo.setupSync(allBackends, LOCAL_IDB_BACKEND_ID);
1283
- await repo.syncMetaToReplicas();
1298
+ await repo.setupSync(allBackends, LOCAL_IDB_BACKEND_ID, options.syncPollIntervalMs);
1284
1299
  await repo.load();
1300
+ repo.syncMetaToReplicas().catch((err) => {
1301
+ console.error("[createConfigRepo] background syncMetaToReplicas failed:", err);
1302
+ });
1285
1303
  return repo;
1286
1304
  }
1287
1305
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zen-fs-config",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
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",
@@ -61,6 +61,6 @@
61
61
  "typescript": "^5.9.3",
62
62
  "vitest": "^1.6.1",
63
63
  "zen-fs-cache": "^1.0.1",
64
- "zen-fs-sync": "^0.4.1"
64
+ "zen-fs-sync": "^0.4.3"
65
65
  }
66
66
  }