zen-fs-config 0.5.44 → 0.5.46

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
@@ -345,8 +345,30 @@ interface AppDataBackendDescriptor {
345
345
  interface AppDataGroupDescriptor {
346
346
  id: string;
347
347
  groupType: 'data-sync';
348
+ /** User-editable display name. `id` is the stable identifier. */
349
+ name?: string;
348
350
  backends: AppDataBackendDescriptor[];
349
351
  }
352
+ /** Descriptor for the config-sync group itself (display metadata only). */
353
+ interface ConfigSyncGroupDescriptor {
354
+ groupType: 'config-sync';
355
+ /** User-editable display name. The group has no separate id. */
356
+ name?: string;
357
+ }
358
+ /** Per-backend sync state of a data group. */
359
+ interface BackendSyncState {
360
+ backendId: string;
361
+ /** Pair id when the backend instance was created and paired successfully. */
362
+ pairId?: string;
363
+ /**
364
+ * Present when this descriptor could not be connected (e.g. a required
365
+ * option is missing), so no sync pair exists for it.
366
+ */
367
+ error?: {
368
+ message: string;
369
+ timestamp: number;
370
+ };
371
+ }
350
372
  /** Handle to a data-sync group, providing direct fs access. */
351
373
  interface AppDataGroup {
352
374
  readonly groupId: string;
@@ -355,6 +377,11 @@ interface AppDataGroup {
355
377
  readonly fs: typeof node_fs;
356
378
  /** Get sync status for this data group's sync pairs. */
357
379
  getSyncStatuses(): Map<string, SyncPairStatus>;
380
+ /**
381
+ * Per-backend sync state, including descriptors that failed to connect and
382
+ * therefore have no pair (they do not appear in getSyncStatuses()).
383
+ */
384
+ getBackendSyncStates(): Map<string, BackendSyncState>;
358
385
  /** Manually flush pending sync. */
359
386
  flush(): Promise<SyncResult[]>;
360
387
  /** Stop sync and release resources. */
@@ -536,6 +563,20 @@ interface IConfigRepo {
536
563
  getAppDataGroup(id: string): Promise<AppDataGroup>;
537
564
  /** List all data-sync group descriptors for this app. */
538
565
  listAppDataGroups(): Promise<AppDataGroupDescriptor[]>;
566
+ /**
567
+ * Update a data-sync group's display name (descriptor-only; id, backends,
568
+ * local and remote data are untouched). Empty/oversized names are rejected.
569
+ */
570
+ updateAppDataGroupName(id: string, name: string): Promise<void>;
571
+ /** Read the config-sync group's display descriptor (may be absent). */
572
+ getConfigSyncGroupDescriptor(): Promise<ConfigSyncGroupDescriptor | null>;
573
+ /**
574
+ * Update the config-sync group's display name (descriptor-only; backends,
575
+ * local store and remote data are untouched).
576
+ */
577
+ updateConfigSyncGroupDescriptor(patch: {
578
+ name: string;
579
+ }): Promise<ConfigSyncGroupDescriptor>;
539
580
  /** Remove a data-sync group (stops sync, removes descriptor). */
540
581
  removeAppDataGroup(id: string): Promise<void>;
541
582
  /**
@@ -986,6 +1027,20 @@ declare class ConfigRepo implements IConfigRepo {
986
1027
  * authoritative source of the data group's backend topology (FR2.3).
987
1028
  */
988
1029
  updateAppDataGroupDescriptor(id: string, backends: AppDataBackendDescriptor[]): Promise<void>;
1030
+ /**
1031
+ * Change a data-sync group's display name only. `id`, backends, local stores
1032
+ * and remote data are untouched, so existing sync pairs keep working.
1033
+ */
1034
+ updateAppDataGroupName(id: string, name: string): Promise<void>;
1035
+ getConfigSyncGroupDescriptor(): Promise<ConfigSyncGroupDescriptor | null>;
1036
+ /**
1037
+ * Change the config-sync group's display name only. Backends, the local
1038
+ * store and remote data are untouched.
1039
+ */
1040
+ updateConfigSyncGroupDescriptor(patch: {
1041
+ name: string;
1042
+ }): Promise<ConfigSyncGroupDescriptor>;
1043
+ private readAppDataGroupDescriptor;
989
1044
  getAppDataGroup(id: string): Promise<AppDataGroup>;
990
1045
  listAppDataGroups(): Promise<AppDataGroupDescriptor[]>;
991
1046
  removeAppDataGroup(id: string): Promise<void>;
@@ -1141,4 +1196,4 @@ declare function incrementVersion(fs: SyncableFS, configFilePath: string, newCon
1141
1196
  */
1142
1197
  declare function verifyOrRepairVersion(fs: SyncableFS, configFilePath: string, author: string): Promise<VersionMeta | null>;
1143
1198
 
1144
- export { type AppDataBackendDescriptor, type AppDataGroup, type AppDataGroupDescriptor, type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendMetadata, type BackendParamDef, type BackendsMeta, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type ConnectOptions, type ConnectResult, FolderStore, type IConfigRepo, type KeepPurgeResult, LOCAL_IDB_BACKEND_ID, type MigrateLocalFS, type MigrateReplicaFS, type MtimePurgeResult, type PurgeKeepOptions, type PurgeMtimeOptions, type PurgeableFS$1 as PurgeableFS, type SyncGroupType, type TombstoneMeta, type VersionMeta, type VersionMigrationOptions, type VersionMigrationResult, configKeyToFilePath, connect, createBackend, createConfigRepo, createSerializerChain, getAccountFields, getBackendMetadata, getExtension, hasBackend, incrementVersion, isBrowserEnv, isKeepFile, isMtimeSidecar, legacyVersionPathFor, listBackendMetadata, listBackends, localPrimaryType, mergeAccountFields, migrateVersionSidecars, purgeKeepFiles, purgeMtimeSidecars, readVersion, registerBackend, registerFolderBackend, resolveLocalPrimary, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
1199
+ export { type AppDataBackendDescriptor, type AppDataGroup, type AppDataGroupDescriptor, type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendMetadata, type BackendParamDef, type BackendSyncState, type BackendsMeta, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConfigSyncGroupDescriptor, type ConflictArchive, type ConflictInfo, type ConnectOptions, type ConnectResult, FolderStore, type IConfigRepo, type KeepPurgeResult, LOCAL_IDB_BACKEND_ID, type MigrateLocalFS, type MigrateReplicaFS, type MtimePurgeResult, type PurgeKeepOptions, type PurgeMtimeOptions, type PurgeableFS$1 as PurgeableFS, type SyncGroupType, type TombstoneMeta, type VersionMeta, type VersionMigrationOptions, type VersionMigrationResult, configKeyToFilePath, connect, createBackend, createConfigRepo, createSerializerChain, getAccountFields, getBackendMetadata, getExtension, hasBackend, incrementVersion, isBrowserEnv, isKeepFile, isMtimeSidecar, legacyVersionPathFor, listBackendMetadata, listBackends, localPrimaryType, mergeAccountFields, migrateVersionSidecars, purgeKeepFiles, purgeMtimeSidecars, readVersion, registerBackend, registerFolderBackend, resolveLocalPrimary, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
package/dist/index.d.ts CHANGED
@@ -345,8 +345,30 @@ interface AppDataBackendDescriptor {
345
345
  interface AppDataGroupDescriptor {
346
346
  id: string;
347
347
  groupType: 'data-sync';
348
+ /** User-editable display name. `id` is the stable identifier. */
349
+ name?: string;
348
350
  backends: AppDataBackendDescriptor[];
349
351
  }
352
+ /** Descriptor for the config-sync group itself (display metadata only). */
353
+ interface ConfigSyncGroupDescriptor {
354
+ groupType: 'config-sync';
355
+ /** User-editable display name. The group has no separate id. */
356
+ name?: string;
357
+ }
358
+ /** Per-backend sync state of a data group. */
359
+ interface BackendSyncState {
360
+ backendId: string;
361
+ /** Pair id when the backend instance was created and paired successfully. */
362
+ pairId?: string;
363
+ /**
364
+ * Present when this descriptor could not be connected (e.g. a required
365
+ * option is missing), so no sync pair exists for it.
366
+ */
367
+ error?: {
368
+ message: string;
369
+ timestamp: number;
370
+ };
371
+ }
350
372
  /** Handle to a data-sync group, providing direct fs access. */
351
373
  interface AppDataGroup {
352
374
  readonly groupId: string;
@@ -355,6 +377,11 @@ interface AppDataGroup {
355
377
  readonly fs: typeof node_fs;
356
378
  /** Get sync status for this data group's sync pairs. */
357
379
  getSyncStatuses(): Map<string, SyncPairStatus>;
380
+ /**
381
+ * Per-backend sync state, including descriptors that failed to connect and
382
+ * therefore have no pair (they do not appear in getSyncStatuses()).
383
+ */
384
+ getBackendSyncStates(): Map<string, BackendSyncState>;
358
385
  /** Manually flush pending sync. */
359
386
  flush(): Promise<SyncResult[]>;
360
387
  /** Stop sync and release resources. */
@@ -536,6 +563,20 @@ interface IConfigRepo {
536
563
  getAppDataGroup(id: string): Promise<AppDataGroup>;
537
564
  /** List all data-sync group descriptors for this app. */
538
565
  listAppDataGroups(): Promise<AppDataGroupDescriptor[]>;
566
+ /**
567
+ * Update a data-sync group's display name (descriptor-only; id, backends,
568
+ * local and remote data are untouched). Empty/oversized names are rejected.
569
+ */
570
+ updateAppDataGroupName(id: string, name: string): Promise<void>;
571
+ /** Read the config-sync group's display descriptor (may be absent). */
572
+ getConfigSyncGroupDescriptor(): Promise<ConfigSyncGroupDescriptor | null>;
573
+ /**
574
+ * Update the config-sync group's display name (descriptor-only; backends,
575
+ * local store and remote data are untouched).
576
+ */
577
+ updateConfigSyncGroupDescriptor(patch: {
578
+ name: string;
579
+ }): Promise<ConfigSyncGroupDescriptor>;
539
580
  /** Remove a data-sync group (stops sync, removes descriptor). */
540
581
  removeAppDataGroup(id: string): Promise<void>;
541
582
  /**
@@ -986,6 +1027,20 @@ declare class ConfigRepo implements IConfigRepo {
986
1027
  * authoritative source of the data group's backend topology (FR2.3).
987
1028
  */
988
1029
  updateAppDataGroupDescriptor(id: string, backends: AppDataBackendDescriptor[]): Promise<void>;
1030
+ /**
1031
+ * Change a data-sync group's display name only. `id`, backends, local stores
1032
+ * and remote data are untouched, so existing sync pairs keep working.
1033
+ */
1034
+ updateAppDataGroupName(id: string, name: string): Promise<void>;
1035
+ getConfigSyncGroupDescriptor(): Promise<ConfigSyncGroupDescriptor | null>;
1036
+ /**
1037
+ * Change the config-sync group's display name only. Backends, the local
1038
+ * store and remote data are untouched.
1039
+ */
1040
+ updateConfigSyncGroupDescriptor(patch: {
1041
+ name: string;
1042
+ }): Promise<ConfigSyncGroupDescriptor>;
1043
+ private readAppDataGroupDescriptor;
989
1044
  getAppDataGroup(id: string): Promise<AppDataGroup>;
990
1045
  listAppDataGroups(): Promise<AppDataGroupDescriptor[]>;
991
1046
  removeAppDataGroup(id: string): Promise<void>;
@@ -1141,4 +1196,4 @@ declare function incrementVersion(fs: SyncableFS, configFilePath: string, newCon
1141
1196
  */
1142
1197
  declare function verifyOrRepairVersion(fs: SyncableFS, configFilePath: string, author: string): Promise<VersionMeta | null>;
1143
1198
 
1144
- export { type AppDataBackendDescriptor, type AppDataGroup, type AppDataGroupDescriptor, type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendMetadata, type BackendParamDef, type BackendsMeta, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type ConnectOptions, type ConnectResult, FolderStore, type IConfigRepo, type KeepPurgeResult, LOCAL_IDB_BACKEND_ID, type MigrateLocalFS, type MigrateReplicaFS, type MtimePurgeResult, type PurgeKeepOptions, type PurgeMtimeOptions, type PurgeableFS$1 as PurgeableFS, type SyncGroupType, type TombstoneMeta, type VersionMeta, type VersionMigrationOptions, type VersionMigrationResult, configKeyToFilePath, connect, createBackend, createConfigRepo, createSerializerChain, getAccountFields, getBackendMetadata, getExtension, hasBackend, incrementVersion, isBrowserEnv, isKeepFile, isMtimeSidecar, legacyVersionPathFor, listBackendMetadata, listBackends, localPrimaryType, mergeAccountFields, migrateVersionSidecars, purgeKeepFiles, purgeMtimeSidecars, readVersion, registerBackend, registerFolderBackend, resolveLocalPrimary, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
1199
+ export { type AppDataBackendDescriptor, type AppDataGroup, type AppDataGroupDescriptor, type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendMetadata, type BackendParamDef, type BackendSyncState, type BackendsMeta, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConfigSyncGroupDescriptor, type ConflictArchive, type ConflictInfo, type ConnectOptions, type ConnectResult, FolderStore, type IConfigRepo, type KeepPurgeResult, LOCAL_IDB_BACKEND_ID, type MigrateLocalFS, type MigrateReplicaFS, type MtimePurgeResult, type PurgeKeepOptions, type PurgeMtimeOptions, type PurgeableFS$1 as PurgeableFS, type SyncGroupType, type TombstoneMeta, type VersionMeta, type VersionMigrationOptions, type VersionMigrationResult, configKeyToFilePath, connect, createBackend, createConfigRepo, createSerializerChain, getAccountFields, getBackendMetadata, getExtension, hasBackend, incrementVersion, isBrowserEnv, isKeepFile, isMtimeSidecar, legacyVersionPathFor, listBackendMetadata, listBackends, localPrimaryType, mergeAccountFields, migrateVersionSidecars, purgeKeepFiles, purgeMtimeSidecars, readVersion, registerBackend, registerFolderBackend, resolveLocalPrimary, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
package/dist/index.js CHANGED
@@ -1424,10 +1424,25 @@ var GROUP_TYPE_FILE = `${META_DIR}/group-type`;
1424
1424
  var BACKENDS_FILE = `${META_DIR}/backends.json`;
1425
1425
  var BACKENDS_DIR = `${META_DIR}/backends`;
1426
1426
  var APP_DATA_GROUPS_DIR = `${META_DIR}/app-data-groups`;
1427
+ var CONFIG_SYNC_GROUP_FILE = `${META_DIR}/config-sync-group.json`;
1428
+ var GROUP_NAME_MAX_LENGTH = 40;
1427
1429
  var CONFLICTS_DIR = `${META_DIR}/.conflicts`;
1428
1430
  var DELETIONS_DIR = `${META_DIR}/.deleted`;
1429
1431
  var NODES_DIR = "/nodes";
1430
1432
  var SHARED_DIR = "/shared";
1433
+ function normalizeGroupName(value) {
1434
+ if (typeof value !== "string") {
1435
+ throw new Error("Group name must be a string");
1436
+ }
1437
+ const name = value.trim();
1438
+ if (!name) {
1439
+ throw new Error("Group name must not be empty");
1440
+ }
1441
+ if (name.length > GROUP_NAME_MAX_LENGTH) {
1442
+ throw new Error(`Group name must be at most ${GROUP_NAME_MAX_LENGTH} characters`);
1443
+ }
1444
+ return name;
1445
+ }
1431
1446
  var CONFIG_ONLY_PREFIXES = [META_DIR, NODES_DIR, SHARED_DIR];
1432
1447
  var LOCAL_IDB_BACKEND_ID = "local-idb";
1433
1448
  function tombstoneFileName(filePath) {
@@ -2783,9 +2798,11 @@ var ConfigRepo = class {
2783
2798
  */
2784
2799
  async updateAppDataGroupDescriptor(id, backends) {
2785
2800
  this.assertNotDisposed();
2801
+ const existing = await this.readAppDataGroupDescriptor(id);
2786
2802
  const descriptor = {
2787
2803
  id,
2788
2804
  groupType: "data-sync",
2805
+ ...existing?.name ? { name: existing.name } : {},
2789
2806
  backends
2790
2807
  };
2791
2808
  const descPath = this.appDataGroupFilePath(id);
@@ -2796,6 +2813,73 @@ var ConfigRepo = class {
2796
2813
  const version = await incrementVersion(this.fullFS, descPath, bytes, author);
2797
2814
  await this.writeVersionSidecar(descPath, version);
2798
2815
  }
2816
+ /**
2817
+ * Change a data-sync group's display name only. `id`, backends, local stores
2818
+ * and remote data are untouched, so existing sync pairs keep working.
2819
+ */
2820
+ async updateAppDataGroupName(id, name) {
2821
+ this.assertNotDisposed();
2822
+ const normalized = normalizeGroupName(name);
2823
+ const existing = await this.readAppDataGroupDescriptor(id);
2824
+ if (!existing) {
2825
+ throw new Error(`App data group "${id}" not found`);
2826
+ }
2827
+ const descriptor = {
2828
+ ...existing,
2829
+ id,
2830
+ groupType: "data-sync",
2831
+ name: normalized
2832
+ };
2833
+ const descPath = this.appDataGroupFilePath(id);
2834
+ await this.ensureDir(descPath);
2835
+ const bytes = new TextEncoder().encode(JSON.stringify(descriptor, null, 2));
2836
+ await this.cachedFS.writeFile(descPath, bytes);
2837
+ const author = `${this.appId}/${this.nodeId}`;
2838
+ const version = await incrementVersion(this.fullFS, descPath, bytes, author);
2839
+ await this.writeVersionSidecar(descPath, version);
2840
+ }
2841
+ async getConfigSyncGroupDescriptor() {
2842
+ this.assertNotDisposed();
2843
+ try {
2844
+ const raw = await this.cachedFS.readFile(CONFIG_SYNC_GROUP_FILE);
2845
+ const parsed = JSON.parse(new TextDecoder().decode(toUint8Array2(raw)));
2846
+ if (parsed && parsed.groupType === "config-sync") {
2847
+ return parsed;
2848
+ }
2849
+ } catch {
2850
+ }
2851
+ return null;
2852
+ }
2853
+ /**
2854
+ * Change the config-sync group's display name only. Backends, the local
2855
+ * store and remote data are untouched.
2856
+ */
2857
+ async updateConfigSyncGroupDescriptor(patch) {
2858
+ this.assertNotDisposed();
2859
+ const normalized = normalizeGroupName(patch?.name);
2860
+ const descriptor = {
2861
+ groupType: "config-sync",
2862
+ name: normalized
2863
+ };
2864
+ await this.ensureDir(CONFIG_SYNC_GROUP_FILE);
2865
+ const bytes = new TextEncoder().encode(JSON.stringify(descriptor, null, 2));
2866
+ await this.cachedFS.writeFile(CONFIG_SYNC_GROUP_FILE, bytes);
2867
+ const author = `${this.appId}/${this.nodeId}`;
2868
+ const version = await incrementVersion(this.fullFS, CONFIG_SYNC_GROUP_FILE, bytes, author);
2869
+ await this.writeVersionSidecar(CONFIG_SYNC_GROUP_FILE, version);
2870
+ return descriptor;
2871
+ }
2872
+ async readAppDataGroupDescriptor(id) {
2873
+ try {
2874
+ const raw = await this.cachedFS.readFile(this.appDataGroupFilePath(id));
2875
+ const parsed = JSON.parse(new TextDecoder().decode(toUint8Array2(raw)));
2876
+ if (parsed && parsed.id === id && parsed.groupType === "data-sync") {
2877
+ return parsed;
2878
+ }
2879
+ } catch {
2880
+ }
2881
+ return null;
2882
+ }
2799
2883
  async getAppDataGroup(id) {
2800
2884
  this.assertNotDisposed();
2801
2885
  const cached = this.appDataGroups.get(id);
@@ -2885,6 +2969,7 @@ var AppDataGroupImpl = class {
2885
2969
  syncEngine;
2886
2970
  localFS;
2887
2971
  dataBackends = /* @__PURE__ */ new Map();
2972
+ backendStates = /* @__PURE__ */ new Map();
2888
2973
  disposed = false;
2889
2974
  constructor(groupId, appId, backends, pollIntervalMs, parent) {
2890
2975
  this.groupId = groupId;
@@ -2957,8 +3042,14 @@ var AppDataGroupImpl = class {
2957
3042
  "/"
2958
3043
  );
2959
3044
  this.dataBackends.set(desc.id, { instance, syncable, pairId: pair.pairId, desc });
3045
+ this.backendStates.set(desc.id, { backendId: desc.id, pairId: pair.pairId });
2960
3046
  log2.log(`[AppDataGroup:${this.groupId}] backend ${desc.id} (${desc.type}) connected, pair=${pair.pairId}`);
2961
3047
  } catch (err) {
3048
+ const message = err instanceof Error ? err.message : String(err);
3049
+ this.backendStates.set(desc.id, {
3050
+ backendId: desc.id,
3051
+ error: { message, timestamp: Date.now() }
3052
+ });
2962
3053
  log2.error(`[AppDataGroup:${this.groupId}] Failed to create backend ${desc.id} (${desc.type}):`, err);
2963
3054
  }
2964
3055
  }
@@ -2983,6 +3074,9 @@ var AppDataGroupImpl = class {
2983
3074
  const results = await this.syncEngine.syncAll();
2984
3075
  return Array.from(results.values());
2985
3076
  }
3077
+ getBackendSyncStates() {
3078
+ return new Map(this.backendStates);
3079
+ }
2986
3080
  async addBackend(id, type, options, description) {
2987
3081
  if (this.disposed) throw new Error("DataSyncGroup has been disposed");
2988
3082
  if (this.dataBackends.has(id)) {
@@ -3005,6 +3099,8 @@ var AppDataGroupImpl = class {
3005
3099
  );
3006
3100
  const desc = { id, type, options, description, createdAt: Date.now() };
3007
3101
  this.dataBackends.set(id, { instance, syncable, pairId: pair.pairId, desc });
3102
+ this.backendStates.set(id, { backendId: id, pairId: pair.pairId });
3103
+ this._backends = [...this._backends.filter((b) => b.id !== id), desc];
3008
3104
  log2.log(`[AppDataGroup:${this.groupId}] addBackend: ${id} (${type}) connected, pair=${pair.pairId}`);
3009
3105
  try {
3010
3106
  await this.syncEngine.sync(pair.pairId);
@@ -3012,18 +3108,23 @@ var AppDataGroupImpl = class {
3012
3108
  log2.warn(`[AppDataGroup:${this.groupId}] addBackend: initial sync failed for ${id}:`, err);
3013
3109
  }
3014
3110
  this.syncEngine.watch(pair.pairId);
3015
- await this.parent.updateAppDataGroupDescriptor(this.groupId, this.listBackends());
3111
+ await this.parent.updateAppDataGroupDescriptor(this.groupId, this._backends);
3016
3112
  }
3017
3113
  async removeBackend(id) {
3018
3114
  if (this.disposed) throw new Error("DataSyncGroup has been disposed");
3019
3115
  const backend = this.dataBackends.get(id);
3020
- if (!backend) {
3116
+ const desc = this._backends.find((b) => b.id === id);
3117
+ if (!backend && !desc) {
3021
3118
  throw new Error(`Backend "${id}" not found in data group "${this.groupId}"`);
3022
3119
  }
3023
- this.syncEngine.removePair(backend.pairId);
3024
- this.dataBackends.delete(id);
3025
- await this.parent.updateAppDataGroupDescriptor(this.groupId, this.listBackends());
3026
- if (backend.instance?.dispose) {
3120
+ if (backend) {
3121
+ this.syncEngine.removePair(backend.pairId);
3122
+ this.dataBackends.delete(id);
3123
+ }
3124
+ this.backendStates.delete(id);
3125
+ this._backends = this._backends.filter((b) => b.id !== id);
3126
+ await this.parent.updateAppDataGroupDescriptor(this.groupId, this._backends);
3127
+ if (backend?.instance?.dispose) {
3027
3128
  await backend.instance.dispose();
3028
3129
  }
3029
3130
  log2.log(`[AppDataGroup:${this.groupId}] removeBackend: ${id} removed`);
package/dist/index.mjs CHANGED
@@ -884,10 +884,25 @@ var GROUP_TYPE_FILE = `${META_DIR}/group-type`;
884
884
  var BACKENDS_FILE = `${META_DIR}/backends.json`;
885
885
  var BACKENDS_DIR = `${META_DIR}/backends`;
886
886
  var APP_DATA_GROUPS_DIR = `${META_DIR}/app-data-groups`;
887
+ var CONFIG_SYNC_GROUP_FILE = `${META_DIR}/config-sync-group.json`;
888
+ var GROUP_NAME_MAX_LENGTH = 40;
887
889
  var CONFLICTS_DIR = `${META_DIR}/.conflicts`;
888
890
  var DELETIONS_DIR = `${META_DIR}/.deleted`;
889
891
  var NODES_DIR = "/nodes";
890
892
  var SHARED_DIR = "/shared";
893
+ function normalizeGroupName(value) {
894
+ if (typeof value !== "string") {
895
+ throw new Error("Group name must be a string");
896
+ }
897
+ const name = value.trim();
898
+ if (!name) {
899
+ throw new Error("Group name must not be empty");
900
+ }
901
+ if (name.length > GROUP_NAME_MAX_LENGTH) {
902
+ throw new Error(`Group name must be at most ${GROUP_NAME_MAX_LENGTH} characters`);
903
+ }
904
+ return name;
905
+ }
891
906
  var CONFIG_ONLY_PREFIXES = [META_DIR, NODES_DIR, SHARED_DIR];
892
907
  var LOCAL_IDB_BACKEND_ID = "local-idb";
893
908
  function tombstoneFileName(filePath) {
@@ -2243,9 +2258,11 @@ var ConfigRepo = class {
2243
2258
  */
2244
2259
  async updateAppDataGroupDescriptor(id, backends) {
2245
2260
  this.assertNotDisposed();
2261
+ const existing = await this.readAppDataGroupDescriptor(id);
2246
2262
  const descriptor = {
2247
2263
  id,
2248
2264
  groupType: "data-sync",
2265
+ ...existing?.name ? { name: existing.name } : {},
2249
2266
  backends
2250
2267
  };
2251
2268
  const descPath = this.appDataGroupFilePath(id);
@@ -2256,6 +2273,73 @@ var ConfigRepo = class {
2256
2273
  const version = await incrementVersion(this.fullFS, descPath, bytes, author);
2257
2274
  await this.writeVersionSidecar(descPath, version);
2258
2275
  }
2276
+ /**
2277
+ * Change a data-sync group's display name only. `id`, backends, local stores
2278
+ * and remote data are untouched, so existing sync pairs keep working.
2279
+ */
2280
+ async updateAppDataGroupName(id, name) {
2281
+ this.assertNotDisposed();
2282
+ const normalized = normalizeGroupName(name);
2283
+ const existing = await this.readAppDataGroupDescriptor(id);
2284
+ if (!existing) {
2285
+ throw new Error(`App data group "${id}" not found`);
2286
+ }
2287
+ const descriptor = {
2288
+ ...existing,
2289
+ id,
2290
+ groupType: "data-sync",
2291
+ name: normalized
2292
+ };
2293
+ const descPath = this.appDataGroupFilePath(id);
2294
+ await this.ensureDir(descPath);
2295
+ const bytes = new TextEncoder().encode(JSON.stringify(descriptor, null, 2));
2296
+ await this.cachedFS.writeFile(descPath, bytes);
2297
+ const author = `${this.appId}/${this.nodeId}`;
2298
+ const version = await incrementVersion(this.fullFS, descPath, bytes, author);
2299
+ await this.writeVersionSidecar(descPath, version);
2300
+ }
2301
+ async getConfigSyncGroupDescriptor() {
2302
+ this.assertNotDisposed();
2303
+ try {
2304
+ const raw = await this.cachedFS.readFile(CONFIG_SYNC_GROUP_FILE);
2305
+ const parsed = JSON.parse(new TextDecoder().decode(toUint8Array2(raw)));
2306
+ if (parsed && parsed.groupType === "config-sync") {
2307
+ return parsed;
2308
+ }
2309
+ } catch {
2310
+ }
2311
+ return null;
2312
+ }
2313
+ /**
2314
+ * Change the config-sync group's display name only. Backends, the local
2315
+ * store and remote data are untouched.
2316
+ */
2317
+ async updateConfigSyncGroupDescriptor(patch) {
2318
+ this.assertNotDisposed();
2319
+ const normalized = normalizeGroupName(patch?.name);
2320
+ const descriptor = {
2321
+ groupType: "config-sync",
2322
+ name: normalized
2323
+ };
2324
+ await this.ensureDir(CONFIG_SYNC_GROUP_FILE);
2325
+ const bytes = new TextEncoder().encode(JSON.stringify(descriptor, null, 2));
2326
+ await this.cachedFS.writeFile(CONFIG_SYNC_GROUP_FILE, bytes);
2327
+ const author = `${this.appId}/${this.nodeId}`;
2328
+ const version = await incrementVersion(this.fullFS, CONFIG_SYNC_GROUP_FILE, bytes, author);
2329
+ await this.writeVersionSidecar(CONFIG_SYNC_GROUP_FILE, version);
2330
+ return descriptor;
2331
+ }
2332
+ async readAppDataGroupDescriptor(id) {
2333
+ try {
2334
+ const raw = await this.cachedFS.readFile(this.appDataGroupFilePath(id));
2335
+ const parsed = JSON.parse(new TextDecoder().decode(toUint8Array2(raw)));
2336
+ if (parsed && parsed.id === id && parsed.groupType === "data-sync") {
2337
+ return parsed;
2338
+ }
2339
+ } catch {
2340
+ }
2341
+ return null;
2342
+ }
2259
2343
  async getAppDataGroup(id) {
2260
2344
  this.assertNotDisposed();
2261
2345
  const cached = this.appDataGroups.get(id);
@@ -2345,6 +2429,7 @@ var AppDataGroupImpl = class {
2345
2429
  syncEngine;
2346
2430
  localFS;
2347
2431
  dataBackends = /* @__PURE__ */ new Map();
2432
+ backendStates = /* @__PURE__ */ new Map();
2348
2433
  disposed = false;
2349
2434
  constructor(groupId, appId, backends, pollIntervalMs, parent) {
2350
2435
  this.groupId = groupId;
@@ -2417,8 +2502,14 @@ var AppDataGroupImpl = class {
2417
2502
  "/"
2418
2503
  );
2419
2504
  this.dataBackends.set(desc.id, { instance, syncable, pairId: pair.pairId, desc });
2505
+ this.backendStates.set(desc.id, { backendId: desc.id, pairId: pair.pairId });
2420
2506
  log2.log(`[AppDataGroup:${this.groupId}] backend ${desc.id} (${desc.type}) connected, pair=${pair.pairId}`);
2421
2507
  } catch (err) {
2508
+ const message = err instanceof Error ? err.message : String(err);
2509
+ this.backendStates.set(desc.id, {
2510
+ backendId: desc.id,
2511
+ error: { message, timestamp: Date.now() }
2512
+ });
2422
2513
  log2.error(`[AppDataGroup:${this.groupId}] Failed to create backend ${desc.id} (${desc.type}):`, err);
2423
2514
  }
2424
2515
  }
@@ -2443,6 +2534,9 @@ var AppDataGroupImpl = class {
2443
2534
  const results = await this.syncEngine.syncAll();
2444
2535
  return Array.from(results.values());
2445
2536
  }
2537
+ getBackendSyncStates() {
2538
+ return new Map(this.backendStates);
2539
+ }
2446
2540
  async addBackend(id, type, options, description) {
2447
2541
  if (this.disposed) throw new Error("DataSyncGroup has been disposed");
2448
2542
  if (this.dataBackends.has(id)) {
@@ -2465,6 +2559,8 @@ var AppDataGroupImpl = class {
2465
2559
  );
2466
2560
  const desc = { id, type, options, description, createdAt: Date.now() };
2467
2561
  this.dataBackends.set(id, { instance, syncable, pairId: pair.pairId, desc });
2562
+ this.backendStates.set(id, { backendId: id, pairId: pair.pairId });
2563
+ this._backends = [...this._backends.filter((b) => b.id !== id), desc];
2468
2564
  log2.log(`[AppDataGroup:${this.groupId}] addBackend: ${id} (${type}) connected, pair=${pair.pairId}`);
2469
2565
  try {
2470
2566
  await this.syncEngine.sync(pair.pairId);
@@ -2472,18 +2568,23 @@ var AppDataGroupImpl = class {
2472
2568
  log2.warn(`[AppDataGroup:${this.groupId}] addBackend: initial sync failed for ${id}:`, err);
2473
2569
  }
2474
2570
  this.syncEngine.watch(pair.pairId);
2475
- await this.parent.updateAppDataGroupDescriptor(this.groupId, this.listBackends());
2571
+ await this.parent.updateAppDataGroupDescriptor(this.groupId, this._backends);
2476
2572
  }
2477
2573
  async removeBackend(id) {
2478
2574
  if (this.disposed) throw new Error("DataSyncGroup has been disposed");
2479
2575
  const backend = this.dataBackends.get(id);
2480
- if (!backend) {
2576
+ const desc = this._backends.find((b) => b.id === id);
2577
+ if (!backend && !desc) {
2481
2578
  throw new Error(`Backend "${id}" not found in data group "${this.groupId}"`);
2482
2579
  }
2483
- this.syncEngine.removePair(backend.pairId);
2484
- this.dataBackends.delete(id);
2485
- await this.parent.updateAppDataGroupDescriptor(this.groupId, this.listBackends());
2486
- if (backend.instance?.dispose) {
2580
+ if (backend) {
2581
+ this.syncEngine.removePair(backend.pairId);
2582
+ this.dataBackends.delete(id);
2583
+ }
2584
+ this.backendStates.delete(id);
2585
+ this._backends = this._backends.filter((b) => b.id !== id);
2586
+ await this.parent.updateAppDataGroupDescriptor(this.groupId, this._backends);
2587
+ if (backend?.instance?.dispose) {
2487
2588
  await backend.instance.dispose();
2488
2589
  }
2489
2590
  log2.log(`[AppDataGroup:${this.groupId}] removeBackend: ${id} removed`);
@@ -27449,10 +27449,25 @@ var ZenFSConfig = (() => {
27449
27449
  var BACKENDS_FILE = `${META_DIR}/backends.json`;
27450
27450
  var BACKENDS_DIR = `${META_DIR}/backends`;
27451
27451
  var APP_DATA_GROUPS_DIR = `${META_DIR}/app-data-groups`;
27452
+ var CONFIG_SYNC_GROUP_FILE = `${META_DIR}/config-sync-group.json`;
27453
+ var GROUP_NAME_MAX_LENGTH = 40;
27452
27454
  var CONFLICTS_DIR = `${META_DIR}/.conflicts`;
27453
27455
  var DELETIONS_DIR = `${META_DIR}/.deleted`;
27454
27456
  var NODES_DIR = "/nodes";
27455
27457
  var SHARED_DIR = "/shared";
27458
+ function normalizeGroupName(value) {
27459
+ if (typeof value !== "string") {
27460
+ throw new Error("Group name must be a string");
27461
+ }
27462
+ const name = value.trim();
27463
+ if (!name) {
27464
+ throw new Error("Group name must not be empty");
27465
+ }
27466
+ if (name.length > GROUP_NAME_MAX_LENGTH) {
27467
+ throw new Error(`Group name must be at most ${GROUP_NAME_MAX_LENGTH} characters`);
27468
+ }
27469
+ return name;
27470
+ }
27456
27471
  var CONFIG_ONLY_PREFIXES = [META_DIR, NODES_DIR, SHARED_DIR];
27457
27472
  var LOCAL_IDB_BACKEND_ID = "local-idb";
27458
27473
  function tombstoneFileName(filePath) {
@@ -28808,9 +28823,11 @@ var ZenFSConfig = (() => {
28808
28823
  */
28809
28824
  async updateAppDataGroupDescriptor(id, backends) {
28810
28825
  this.assertNotDisposed();
28826
+ const existing = await this.readAppDataGroupDescriptor(id);
28811
28827
  const descriptor = {
28812
28828
  id,
28813
28829
  groupType: "data-sync",
28830
+ ...existing?.name ? { name: existing.name } : {},
28814
28831
  backends
28815
28832
  };
28816
28833
  const descPath = this.appDataGroupFilePath(id);
@@ -28821,6 +28838,73 @@ var ZenFSConfig = (() => {
28821
28838
  const version2 = await incrementVersion(this.fullFS, descPath, bytes, author);
28822
28839
  await this.writeVersionSidecar(descPath, version2);
28823
28840
  }
28841
+ /**
28842
+ * Change a data-sync group's display name only. `id`, backends, local stores
28843
+ * and remote data are untouched, so existing sync pairs keep working.
28844
+ */
28845
+ async updateAppDataGroupName(id, name) {
28846
+ this.assertNotDisposed();
28847
+ const normalized = normalizeGroupName(name);
28848
+ const existing = await this.readAppDataGroupDescriptor(id);
28849
+ if (!existing) {
28850
+ throw new Error(`App data group "${id}" not found`);
28851
+ }
28852
+ const descriptor = {
28853
+ ...existing,
28854
+ id,
28855
+ groupType: "data-sync",
28856
+ name: normalized
28857
+ };
28858
+ const descPath = this.appDataGroupFilePath(id);
28859
+ await this.ensureDir(descPath);
28860
+ const bytes = new TextEncoder().encode(JSON.stringify(descriptor, null, 2));
28861
+ await this.cachedFS.writeFile(descPath, bytes);
28862
+ const author = `${this.appId}/${this.nodeId}`;
28863
+ const version2 = await incrementVersion(this.fullFS, descPath, bytes, author);
28864
+ await this.writeVersionSidecar(descPath, version2);
28865
+ }
28866
+ async getConfigSyncGroupDescriptor() {
28867
+ this.assertNotDisposed();
28868
+ try {
28869
+ const raw = await this.cachedFS.readFile(CONFIG_SYNC_GROUP_FILE);
28870
+ const parsed = JSON.parse(new TextDecoder().decode(toUint8Array2(raw)));
28871
+ if (parsed && parsed.groupType === "config-sync") {
28872
+ return parsed;
28873
+ }
28874
+ } catch {
28875
+ }
28876
+ return null;
28877
+ }
28878
+ /**
28879
+ * Change the config-sync group's display name only. Backends, the local
28880
+ * store and remote data are untouched.
28881
+ */
28882
+ async updateConfigSyncGroupDescriptor(patch) {
28883
+ this.assertNotDisposed();
28884
+ const normalized = normalizeGroupName(patch?.name);
28885
+ const descriptor = {
28886
+ groupType: "config-sync",
28887
+ name: normalized
28888
+ };
28889
+ await this.ensureDir(CONFIG_SYNC_GROUP_FILE);
28890
+ const bytes = new TextEncoder().encode(JSON.stringify(descriptor, null, 2));
28891
+ await this.cachedFS.writeFile(CONFIG_SYNC_GROUP_FILE, bytes);
28892
+ const author = `${this.appId}/${this.nodeId}`;
28893
+ const version2 = await incrementVersion(this.fullFS, CONFIG_SYNC_GROUP_FILE, bytes, author);
28894
+ await this.writeVersionSidecar(CONFIG_SYNC_GROUP_FILE, version2);
28895
+ return descriptor;
28896
+ }
28897
+ async readAppDataGroupDescriptor(id) {
28898
+ try {
28899
+ const raw = await this.cachedFS.readFile(this.appDataGroupFilePath(id));
28900
+ const parsed = JSON.parse(new TextDecoder().decode(toUint8Array2(raw)));
28901
+ if (parsed && parsed.id === id && parsed.groupType === "data-sync") {
28902
+ return parsed;
28903
+ }
28904
+ } catch {
28905
+ }
28906
+ return null;
28907
+ }
28824
28908
  async getAppDataGroup(id) {
28825
28909
  this.assertNotDisposed();
28826
28910
  const cached = this.appDataGroups.get(id);
@@ -28911,6 +28995,7 @@ var ZenFSConfig = (() => {
28911
28995
  __publicField(this, "syncEngine");
28912
28996
  __publicField(this, "localFS");
28913
28997
  __publicField(this, "dataBackends", /* @__PURE__ */ new Map());
28998
+ __publicField(this, "backendStates", /* @__PURE__ */ new Map());
28914
28999
  __publicField(this, "disposed", false);
28915
29000
  __publicField(this, "parent");
28916
29001
  __publicField(this, "_backends");
@@ -28982,8 +29067,14 @@ var ZenFSConfig = (() => {
28982
29067
  "/"
28983
29068
  );
28984
29069
  this.dataBackends.set(desc.id, { instance, syncable, pairId: pair.pairId, desc });
29070
+ this.backendStates.set(desc.id, { backendId: desc.id, pairId: pair.pairId });
28985
29071
  log6.log(`[AppDataGroup:${this.groupId}] backend ${desc.id} (${desc.type}) connected, pair=${pair.pairId}`);
28986
29072
  } catch (err2) {
29073
+ const message = err2 instanceof Error ? err2.message : String(err2);
29074
+ this.backendStates.set(desc.id, {
29075
+ backendId: desc.id,
29076
+ error: { message, timestamp: Date.now() }
29077
+ });
28987
29078
  log6.error(`[AppDataGroup:${this.groupId}] Failed to create backend ${desc.id} (${desc.type}):`, err2);
28988
29079
  }
28989
29080
  }
@@ -29008,6 +29099,9 @@ var ZenFSConfig = (() => {
29008
29099
  const results = await this.syncEngine.syncAll();
29009
29100
  return Array.from(results.values());
29010
29101
  }
29102
+ getBackendSyncStates() {
29103
+ return new Map(this.backendStates);
29104
+ }
29011
29105
  async addBackend(id, type, options, description) {
29012
29106
  if (this.disposed) throw new Error("DataSyncGroup has been disposed");
29013
29107
  if (this.dataBackends.has(id)) {
@@ -29030,6 +29124,8 @@ var ZenFSConfig = (() => {
29030
29124
  );
29031
29125
  const desc = { id, type, options, description, createdAt: Date.now() };
29032
29126
  this.dataBackends.set(id, { instance, syncable, pairId: pair.pairId, desc });
29127
+ this.backendStates.set(id, { backendId: id, pairId: pair.pairId });
29128
+ this._backends = [...this._backends.filter((b) => b.id !== id), desc];
29033
29129
  log6.log(`[AppDataGroup:${this.groupId}] addBackend: ${id} (${type}) connected, pair=${pair.pairId}`);
29034
29130
  try {
29035
29131
  await this.syncEngine.sync(pair.pairId);
@@ -29037,18 +29133,23 @@ var ZenFSConfig = (() => {
29037
29133
  log6.warn(`[AppDataGroup:${this.groupId}] addBackend: initial sync failed for ${id}:`, err2);
29038
29134
  }
29039
29135
  this.syncEngine.watch(pair.pairId);
29040
- await this.parent.updateAppDataGroupDescriptor(this.groupId, this.listBackends());
29136
+ await this.parent.updateAppDataGroupDescriptor(this.groupId, this._backends);
29041
29137
  }
29042
29138
  async removeBackend(id) {
29043
29139
  if (this.disposed) throw new Error("DataSyncGroup has been disposed");
29044
29140
  const backend = this.dataBackends.get(id);
29045
- if (!backend) {
29141
+ const desc = this._backends.find((b) => b.id === id);
29142
+ if (!backend && !desc) {
29046
29143
  throw new Error(`Backend "${id}" not found in data group "${this.groupId}"`);
29047
29144
  }
29048
- this.syncEngine.removePair(backend.pairId);
29049
- this.dataBackends.delete(id);
29050
- await this.parent.updateAppDataGroupDescriptor(this.groupId, this.listBackends());
29051
- if (backend.instance?.dispose) {
29145
+ if (backend) {
29146
+ this.syncEngine.removePair(backend.pairId);
29147
+ this.dataBackends.delete(id);
29148
+ }
29149
+ this.backendStates.delete(id);
29150
+ this._backends = this._backends.filter((b) => b.id !== id);
29151
+ await this.parent.updateAppDataGroupDescriptor(this.groupId, this._backends);
29152
+ if (backend?.instance?.dispose) {
29052
29153
  await backend.instance.dispose();
29053
29154
  }
29054
29155
  log6.log(`[AppDataGroup:${this.groupId}] removeBackend: ${id} removed`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zen-fs-config",
3
- "version": "0.5.44",
3
+ "version": "0.5.46",
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",