zen-fs-config 0.3.21 → 0.3.22

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
@@ -1,4 +1,4 @@
1
- import { ConflictStrategy, SyncResult, SyncPairStatus, SyncableFS } from 'zen-fs-sync';
1
+ import { SyncResult, SyncPairStatus, ConflictStrategy, SyncableFS } from 'zen-fs-sync';
2
2
  export { SyncPairStatus, SyncResult } from 'zen-fs-sync';
3
3
  import * as node_fs from 'node:fs';
4
4
 
@@ -18,24 +18,6 @@ interface BackendsMeta {
18
18
  version: 1;
19
19
  backends: BackendDescriptor[];
20
20
  }
21
- /** Sync direction for a path prefix. */
22
- type SyncDirection = 'one-way' | 'bi-directional' | 'none';
23
- /** A single sync rule. */
24
- interface SyncRule {
25
- /** Path prefix this rule applies to (e.g., "/app-a/"). */
26
- prefix: string;
27
- /** Sync direction. */
28
- direction: SyncDirection;
29
- /** Conflict resolution strategy (only relevant for bi-directional). */
30
- conflictStrategy?: ConflictStrategy;
31
- /** IDs of replica backends to sync with (from .meta/backends.json). */
32
- replicas?: string[];
33
- }
34
- /** Content of `.meta/sync-rules.json`. */
35
- interface SyncRulesMeta {
36
- version: 1;
37
- rules: SyncRule[];
38
- }
39
21
  /** Content of a sidecar `.version` file. */
40
22
  interface VersionMeta {
41
23
  /** Monotonically increasing version number. */
@@ -57,18 +39,18 @@ interface ConflictArchive {
57
39
  sourceAuthor: string;
58
40
  /** Author of the target side. */
59
41
  targetAuthor: string;
60
- /** Source side content. */
61
- sourceContent: unknown;
62
- /** Target side content. */
63
- targetContent: unknown;
64
42
  /** Source side version. */
65
43
  sourceVersion: number;
66
44
  /** Target side version. */
67
45
  targetVersion: number;
68
46
  /** Strategy that was used to auto-resolve (if any). */
69
47
  resolvedStrategy?: ConflictStrategy;
70
- /** The content that was written as the resolved result (if auto-resolved). */
71
- resolvedContent?: unknown;
48
+ /** Path to the source-side backup file. */
49
+ sourceBackupPath: string;
50
+ /** Path to the target-side backup file. */
51
+ targetBackupPath: string;
52
+ /** Path to the resolved file (present after resolution). */
53
+ resolvedBackupPath?: string;
72
54
  }
73
55
  /** Information passed to conflict event handlers. */
74
56
  interface ConflictInfo {
@@ -103,11 +85,6 @@ interface CacheOptions {
103
85
  /** TTL in milliseconds for cache hits without revalidation. Default: 0 (always revalidate). */
104
86
  ttlMs?: number;
105
87
  }
106
- /** Bootstrap data, written to .meta/ only on first initialization. */
107
- interface BootstrapData {
108
- backends: Omit<BackendDescriptor, 'description'>[];
109
- syncRules: SyncRule[];
110
- }
111
88
  /** Options for creating a ConfigRepo. */
112
89
  interface ConfigRepoOptions {
113
90
  /** The backend ID (from .meta/backends.json) to use as this instance's primary. */
@@ -121,8 +98,6 @@ interface ConfigRepoOptions {
121
98
  nodeId?: string;
122
99
  /** Cache configuration. */
123
100
  cache?: CacheOptions;
124
- /** Bootstrap data (only used when .meta/backends.json doesn't exist). */
125
- bootstrap?: BootstrapData;
126
101
  /** Custom serializer. */
127
102
  serializer?: ConfigSerializer;
128
103
  /** Custom conflict handler. Called before auto-resolution. */
@@ -162,16 +137,17 @@ interface IConfigRepo {
162
137
  resolveConflict(conflictId: string, mergedContent: unknown): Promise<void>;
163
138
  /** List all conflict archives. */
164
139
  listConflicts(): Promise<ConflictArchive[]>;
140
+ /** Read the raw content of a conflict backup file (source/target/resolved).
141
+ * @param conflictId The meta.json path (e.g., "12345_path.conflict/meta.json")
142
+ * @param fileType One of "source", "target", or "resolved"
143
+ */
144
+ readConflictBackup(conflictId: string, fileType: 'source' | 'target' | 'resolved'): Promise<string>;
165
145
  /** Read .meta/backends.json. */
166
146
  getBackends(): Promise<BackendsMeta | null>;
167
147
  /** Write .meta/backends.json. */
168
148
  updateBackends(meta: BackendsMeta): Promise<void>;
169
- /** Read .meta/sync-rules.json. */
170
- getSyncRules(): Promise<SyncRulesMeta | null>;
171
- /** Write .meta/sync-rules.json. */
172
- updateSyncRules(meta: SyncRulesMeta): Promise<void>;
173
149
  /**
174
- * Sync .meta/ files (backends.json, sync-rules.json) to all replica backends.
150
+ * Sync .meta/ files (backends.json) to all replica backends.
175
151
  * Called automatically by createConfigRepo() after setupSync().
176
152
  */
177
153
  syncMetaToReplicas(): Promise<void>;
@@ -260,18 +236,18 @@ declare class ConfigRepo implements IConfigRepo {
260
236
  peekNodeConfig<T = unknown>(nodeId: string, path: string): Promise<T>;
261
237
  flush(): Promise<SyncResult[]>;
262
238
  /**
263
- * Sync .meta/ files (backends.json, sync-rules.json) to all replica backends.
239
+ * Sync .meta/ files (backends.json) to all replica backends.
264
240
  *
265
241
  * This ensures the backend topology is available on every replica, enabling
266
242
  * any program that connects to any backend to discover the full topology.
267
243
  *
268
244
  * Called automatically by createConfigRepo() after setupSync().
269
- * Can also be called manually after updateBackends() / updateSyncRules().
270
245
  */
271
246
  syncMetaToReplicas(): Promise<void>;
272
247
  getSyncStatuses(): Map<string, SyncPairStatus>;
273
248
  resolveConflict(conflictId: string, mergedContent: unknown): Promise<void>;
274
249
  listConflicts(): Promise<ConflictArchive[]>;
250
+ readConflictBackup(conflictId: string, fileType: 'source' | 'target' | 'resolved'): Promise<string>;
275
251
  dispose(): Promise<void>;
276
252
  setupSync(backends: BackendDescriptor[], primaryBackendId: string): Promise<void>;
277
253
  private persistConfig;
@@ -279,12 +255,10 @@ declare class ConfigRepo implements IConfigRepo {
279
255
  private handleConflict;
280
256
  private ensureDir;
281
257
  private walkDir;
282
- writeMetaFile(path: string, data: BackendsMeta | SyncRulesMeta): Promise<void>;
258
+ writeMetaFile(path: string, data: BackendsMeta): Promise<void>;
283
259
  readMetaFile<T>(path: string): Promise<T | null>;
284
260
  getBackends(): Promise<BackendsMeta | null>;
285
261
  updateBackends(meta: BackendsMeta): Promise<void>;
286
- getSyncRules(): Promise<SyncRulesMeta | null>;
287
- updateSyncRules(meta: SyncRulesMeta): Promise<void>;
288
262
  private tryParse;
289
263
  private assertNotDisposed;
290
264
  }
@@ -374,4 +348,4 @@ declare function incrementVersion(fs: SyncableFS, configFilePath: string, newCon
374
348
  */
375
349
  declare function verifyOrRepairVersion(fs: SyncableFS, configFilePath: string, author: string): Promise<VersionMeta | null>;
376
350
 
377
- export { type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendsMeta, type BootstrapData, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type IConfigRepo, type SyncRule, type SyncRulesMeta, type VersionMeta, configKeyToFilePath, createBackend, createConfigRepo, createSerializerChain, getExtension, hasBackend, incrementVersion, listBackends, readVersion, registerBackend, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
351
+ export { type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendsMeta, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type IConfigRepo, type VersionMeta, configKeyToFilePath, createBackend, createConfigRepo, createSerializerChain, getExtension, hasBackend, incrementVersion, listBackends, readVersion, registerBackend, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ConflictStrategy, SyncResult, SyncPairStatus, SyncableFS } from 'zen-fs-sync';
1
+ import { SyncResult, SyncPairStatus, ConflictStrategy, SyncableFS } from 'zen-fs-sync';
2
2
  export { SyncPairStatus, SyncResult } from 'zen-fs-sync';
3
3
  import * as node_fs from 'node:fs';
4
4
 
@@ -18,24 +18,6 @@ interface BackendsMeta {
18
18
  version: 1;
19
19
  backends: BackendDescriptor[];
20
20
  }
21
- /** Sync direction for a path prefix. */
22
- type SyncDirection = 'one-way' | 'bi-directional' | 'none';
23
- /** A single sync rule. */
24
- interface SyncRule {
25
- /** Path prefix this rule applies to (e.g., "/app-a/"). */
26
- prefix: string;
27
- /** Sync direction. */
28
- direction: SyncDirection;
29
- /** Conflict resolution strategy (only relevant for bi-directional). */
30
- conflictStrategy?: ConflictStrategy;
31
- /** IDs of replica backends to sync with (from .meta/backends.json). */
32
- replicas?: string[];
33
- }
34
- /** Content of `.meta/sync-rules.json`. */
35
- interface SyncRulesMeta {
36
- version: 1;
37
- rules: SyncRule[];
38
- }
39
21
  /** Content of a sidecar `.version` file. */
40
22
  interface VersionMeta {
41
23
  /** Monotonically increasing version number. */
@@ -57,18 +39,18 @@ interface ConflictArchive {
57
39
  sourceAuthor: string;
58
40
  /** Author of the target side. */
59
41
  targetAuthor: string;
60
- /** Source side content. */
61
- sourceContent: unknown;
62
- /** Target side content. */
63
- targetContent: unknown;
64
42
  /** Source side version. */
65
43
  sourceVersion: number;
66
44
  /** Target side version. */
67
45
  targetVersion: number;
68
46
  /** Strategy that was used to auto-resolve (if any). */
69
47
  resolvedStrategy?: ConflictStrategy;
70
- /** The content that was written as the resolved result (if auto-resolved). */
71
- resolvedContent?: unknown;
48
+ /** Path to the source-side backup file. */
49
+ sourceBackupPath: string;
50
+ /** Path to the target-side backup file. */
51
+ targetBackupPath: string;
52
+ /** Path to the resolved file (present after resolution). */
53
+ resolvedBackupPath?: string;
72
54
  }
73
55
  /** Information passed to conflict event handlers. */
74
56
  interface ConflictInfo {
@@ -103,11 +85,6 @@ interface CacheOptions {
103
85
  /** TTL in milliseconds for cache hits without revalidation. Default: 0 (always revalidate). */
104
86
  ttlMs?: number;
105
87
  }
106
- /** Bootstrap data, written to .meta/ only on first initialization. */
107
- interface BootstrapData {
108
- backends: Omit<BackendDescriptor, 'description'>[];
109
- syncRules: SyncRule[];
110
- }
111
88
  /** Options for creating a ConfigRepo. */
112
89
  interface ConfigRepoOptions {
113
90
  /** The backend ID (from .meta/backends.json) to use as this instance's primary. */
@@ -121,8 +98,6 @@ interface ConfigRepoOptions {
121
98
  nodeId?: string;
122
99
  /** Cache configuration. */
123
100
  cache?: CacheOptions;
124
- /** Bootstrap data (only used when .meta/backends.json doesn't exist). */
125
- bootstrap?: BootstrapData;
126
101
  /** Custom serializer. */
127
102
  serializer?: ConfigSerializer;
128
103
  /** Custom conflict handler. Called before auto-resolution. */
@@ -162,16 +137,17 @@ interface IConfigRepo {
162
137
  resolveConflict(conflictId: string, mergedContent: unknown): Promise<void>;
163
138
  /** List all conflict archives. */
164
139
  listConflicts(): Promise<ConflictArchive[]>;
140
+ /** Read the raw content of a conflict backup file (source/target/resolved).
141
+ * @param conflictId The meta.json path (e.g., "12345_path.conflict/meta.json")
142
+ * @param fileType One of "source", "target", or "resolved"
143
+ */
144
+ readConflictBackup(conflictId: string, fileType: 'source' | 'target' | 'resolved'): Promise<string>;
165
145
  /** Read .meta/backends.json. */
166
146
  getBackends(): Promise<BackendsMeta | null>;
167
147
  /** Write .meta/backends.json. */
168
148
  updateBackends(meta: BackendsMeta): Promise<void>;
169
- /** Read .meta/sync-rules.json. */
170
- getSyncRules(): Promise<SyncRulesMeta | null>;
171
- /** Write .meta/sync-rules.json. */
172
- updateSyncRules(meta: SyncRulesMeta): Promise<void>;
173
149
  /**
174
- * Sync .meta/ files (backends.json, sync-rules.json) to all replica backends.
150
+ * Sync .meta/ files (backends.json) to all replica backends.
175
151
  * Called automatically by createConfigRepo() after setupSync().
176
152
  */
177
153
  syncMetaToReplicas(): Promise<void>;
@@ -260,18 +236,18 @@ declare class ConfigRepo implements IConfigRepo {
260
236
  peekNodeConfig<T = unknown>(nodeId: string, path: string): Promise<T>;
261
237
  flush(): Promise<SyncResult[]>;
262
238
  /**
263
- * Sync .meta/ files (backends.json, sync-rules.json) to all replica backends.
239
+ * Sync .meta/ files (backends.json) to all replica backends.
264
240
  *
265
241
  * This ensures the backend topology is available on every replica, enabling
266
242
  * any program that connects to any backend to discover the full topology.
267
243
  *
268
244
  * Called automatically by createConfigRepo() after setupSync().
269
- * Can also be called manually after updateBackends() / updateSyncRules().
270
245
  */
271
246
  syncMetaToReplicas(): Promise<void>;
272
247
  getSyncStatuses(): Map<string, SyncPairStatus>;
273
248
  resolveConflict(conflictId: string, mergedContent: unknown): Promise<void>;
274
249
  listConflicts(): Promise<ConflictArchive[]>;
250
+ readConflictBackup(conflictId: string, fileType: 'source' | 'target' | 'resolved'): Promise<string>;
275
251
  dispose(): Promise<void>;
276
252
  setupSync(backends: BackendDescriptor[], primaryBackendId: string): Promise<void>;
277
253
  private persistConfig;
@@ -279,12 +255,10 @@ declare class ConfigRepo implements IConfigRepo {
279
255
  private handleConflict;
280
256
  private ensureDir;
281
257
  private walkDir;
282
- writeMetaFile(path: string, data: BackendsMeta | SyncRulesMeta): Promise<void>;
258
+ writeMetaFile(path: string, data: BackendsMeta): Promise<void>;
283
259
  readMetaFile<T>(path: string): Promise<T | null>;
284
260
  getBackends(): Promise<BackendsMeta | null>;
285
261
  updateBackends(meta: BackendsMeta): Promise<void>;
286
- getSyncRules(): Promise<SyncRulesMeta | null>;
287
- updateSyncRules(meta: SyncRulesMeta): Promise<void>;
288
262
  private tryParse;
289
263
  private assertNotDisposed;
290
264
  }
@@ -374,4 +348,4 @@ declare function incrementVersion(fs: SyncableFS, configFilePath: string, newCon
374
348
  */
375
349
  declare function verifyOrRepairVersion(fs: SyncableFS, configFilePath: string, author: string): Promise<VersionMeta | null>;
376
350
 
377
- export { type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendsMeta, type BootstrapData, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type IConfigRepo, type SyncRule, type SyncRulesMeta, type VersionMeta, configKeyToFilePath, createBackend, createConfigRepo, createSerializerChain, getExtension, hasBackend, incrementVersion, listBackends, readVersion, registerBackend, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
351
+ export { type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendsMeta, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type IConfigRepo, type VersionMeta, configKeyToFilePath, createBackend, createConfigRepo, createSerializerChain, getExtension, hasBackend, incrementVersion, listBackends, readVersion, registerBackend, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
package/dist/index.js CHANGED
@@ -484,10 +484,8 @@ async function verifyOrRepairVersion(fs, configFilePath, author) {
484
484
  // src/config-repo.ts
485
485
  var META_DIR = "/.meta";
486
486
  var BACKENDS_FILE = `${META_DIR}/backends.json`;
487
- var SYNC_RULES_FILE = `${META_DIR}/sync-rules.json`;
488
487
  var CONFLICTS_DIR = `${META_DIR}/.conflicts`;
489
488
  var NODES_DIR = "/nodes";
490
- var SHARED_DIR = "/shared";
491
489
  var NODE_ID_FILE = `${NODES_DIR}/.node-id`;
492
490
  var ConfigRepo = class {
493
491
  appId;
@@ -533,12 +531,6 @@ var ConfigRepo = class {
533
531
  backends: data.backends
534
532
  });
535
533
  }
536
- if (data.syncRules) {
537
- await this.writeMetaFile(SYNC_RULES_FILE, {
538
- version: 1,
539
- rules: data.syncRules
540
- });
541
- }
542
534
  }
543
535
  await this.reloadConfigCache();
544
536
  }
@@ -666,32 +658,29 @@ var ConfigRepo = class {
666
658
  return Array.from(resultsMap.values());
667
659
  }
668
660
  /**
669
- * Sync .meta/ files (backends.json, sync-rules.json) to all replica backends.
661
+ * Sync .meta/ files (backends.json) to all replica backends.
670
662
  *
671
663
  * This ensures the backend topology is available on every replica, enabling
672
664
  * any program that connects to any backend to discover the full topology.
673
665
  *
674
666
  * Called automatically by createConfigRepo() after setupSync().
675
- * Can also be called manually after updateBackends() / updateSyncRules().
676
667
  */
677
668
  async syncMetaToReplicas() {
678
669
  this.assertNotDisposed();
679
- for (const metaFile of [BACKENDS_FILE, SYNC_RULES_FILE]) {
680
- try {
681
- const content = await this.cachedFS.readFile(metaFile);
682
- const vPath = versionPathFor(metaFile);
683
- const vContent = await this.cachedFS.readFile(vPath);
684
- for (const [id, replica] of this.replicaBackends) {
685
- try {
686
- await replica.syncable.writeFile(metaFile, content);
687
- await replica.syncable.writeFile(vPath, vContent);
688
- console.log(`[ConfigRepo] Synced ${metaFile} + .version to replica ${id}`);
689
- } catch (err) {
690
- console.error(`[ConfigRepo] Failed to sync ${metaFile} to ${id}:`, err.message);
691
- }
670
+ try {
671
+ const content = await this.cachedFS.readFile(BACKENDS_FILE);
672
+ const vPath = versionPathFor(BACKENDS_FILE);
673
+ const vContent = await this.cachedFS.readFile(vPath);
674
+ for (const [id, replica] of this.replicaBackends) {
675
+ try {
676
+ await replica.syncable.writeFile(BACKENDS_FILE, content);
677
+ await replica.syncable.writeFile(vPath, vContent);
678
+ console.log(`[ConfigRepo] Synced ${BACKENDS_FILE} + .version to replica ${id}`);
679
+ } catch (err) {
680
+ console.error(`[ConfigRepo] Failed to sync ${BACKENDS_FILE} to ${id}:`, err.message);
692
681
  }
693
- } catch {
694
682
  }
683
+ } catch {
695
684
  }
696
685
  }
697
686
  getSyncStatuses() {
@@ -703,9 +692,9 @@ var ConfigRepo = class {
703
692
  // -----------------------------------------------------------------------
704
693
  async resolveConflict(conflictId, mergedContent) {
705
694
  this.assertNotDisposed();
706
- const archivePath = `${CONFLICTS_DIR}/${conflictId}`;
695
+ const metaPath = `${CONFLICTS_DIR}/${conflictId}`;
707
696
  try {
708
- const raw = await this.cachedFS.readFile(archivePath);
697
+ const raw = await this.cachedFS.readFile(metaPath);
709
698
  const archive = JSON.parse(
710
699
  new TextDecoder().decode(toUint8Array(raw))
711
700
  );
@@ -720,9 +709,13 @@ var ConfigRepo = class {
720
709
  author
721
710
  );
722
711
  await writeVersion(this.fullFS, versionPathFor(configPath), version);
723
- archive.resolvedContent = mergedContent;
712
+ const conflictDir = metaPath.substring(0, metaPath.lastIndexOf("/"));
713
+ const resolvedBackupPath = `${conflictDir}/resolved`;
714
+ const resolvedBytes = typeof mergedContent === "string" ? new TextEncoder().encode(mergedContent) : new TextEncoder().encode(JSON.stringify(mergedContent, null, 2));
715
+ await this.cachedFS.writeFile(resolvedBackupPath, resolvedBytes);
716
+ archive.resolvedBackupPath = `./resolved`;
724
717
  await this.cachedFS.writeFile(
725
- archivePath,
718
+ metaPath,
726
719
  new TextEncoder().encode(JSON.stringify(archive, null, 2))
727
720
  );
728
721
  } catch (err) {
@@ -735,9 +728,9 @@ var ConfigRepo = class {
735
728
  try {
736
729
  const entries = await this.cachedFS.readdir(CONFLICTS_DIR);
737
730
  for (const entry of entries) {
738
- if (!entry.endsWith(".json")) continue;
731
+ const metaPath = `${CONFLICTS_DIR}/${entry}/meta.json`;
739
732
  try {
740
- const raw = await this.cachedFS.readFile(`${CONFLICTS_DIR}/${entry}`);
733
+ const raw = await this.cachedFS.readFile(metaPath);
741
734
  const archive = JSON.parse(
742
735
  new TextDecoder().decode(toUint8Array(raw))
743
736
  );
@@ -749,6 +742,13 @@ var ConfigRepo = class {
749
742
  }
750
743
  return archives.sort((a, b) => a.timestamp - b.timestamp);
751
744
  }
745
+ async readConflictBackup(conflictId, fileType) {
746
+ this.assertNotDisposed();
747
+ const conflictDir = `${CONFLICTS_DIR}/${conflictId}`.replace(/\/meta\.json$/, "");
748
+ const filePath = `${conflictDir}/${fileType}`;
749
+ const raw = await this.cachedFS.readFile(filePath);
750
+ return new TextDecoder().decode(toUint8Array(raw));
751
+ }
752
752
  // -----------------------------------------------------------------------
753
753
  // IConfigRepo — Lifecycle
754
754
  // -----------------------------------------------------------------------
@@ -799,7 +799,7 @@ var ConfigRepo = class {
799
799
  );
800
800
  console.log(`[ConfigRepo] Sync pair added: pairId=${pair.pairId}, replica=${replicaId}, root=/`);
801
801
  const conflictHandler = (event) => {
802
- this.handleConflict(event, { prefix: "/", direction: "one-way" });
802
+ this.handleConflict(event);
803
803
  };
804
804
  this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
805
805
  this.syncEngine.watch(pair.pairId);
@@ -835,45 +835,57 @@ var ConfigRepo = class {
835
835
  // -----------------------------------------------------------------------
836
836
  // Internal — Conflict Handling
837
837
  // -----------------------------------------------------------------------
838
- async handleConflict(event, _rule) {
838
+ async handleConflict(event) {
839
839
  const conflict = event.conflict;
840
840
  if (!conflict) return;
841
+ const conflictId = `${event.timestamp}_${conflict.path.replace(/\//g, "_")}`;
842
+ const conflictDir = `${CONFLICTS_DIR}/${conflictId}`;
843
+ const sourceBackupPath = `${conflictDir}/source`;
844
+ const targetBackupPath = `${conflictDir}/target`;
845
+ await this.ensureDir(conflictDir);
846
+ await this.cachedFS.writeFile(
847
+ sourceBackupPath,
848
+ new TextEncoder().encode(conflict.sourceContent)
849
+ );
850
+ await this.cachedFS.writeFile(
851
+ targetBackupPath,
852
+ new TextEncoder().encode(conflict.targetContent)
853
+ );
854
+ let sourceVersion = 0;
855
+ try {
856
+ const srcVer = await readVersion(this.fullFS, versionPathFor(conflict.path));
857
+ if (srcVer) sourceVersion = srcVer.version;
858
+ } catch {
859
+ }
841
860
  const archive = {
842
861
  conflictPath: conflict.path,
843
862
  timestamp: event.timestamp,
844
863
  sourceAuthor: `${this.appId}/${this.nodeId}`,
845
864
  targetAuthor: "unknown",
846
- sourceContent: this.tryParse(conflict.sourceContent),
847
- targetContent: this.tryParse(conflict.targetContent),
848
- sourceVersion: 0,
865
+ sourceVersion,
849
866
  targetVersion: 0,
850
- resolvedStrategy: conflict.resolvedWith
867
+ resolvedStrategy: conflict.resolvedWith,
868
+ sourceBackupPath: `./source`,
869
+ targetBackupPath: `./target`
851
870
  };
852
- try {
853
- const srcVer = await readVersion(this.fullFS, versionPathFor(conflict.path));
854
- if (srcVer) archive.sourceVersion = srcVer.version;
855
- } catch {
856
- }
857
- const archiveFileName = `${event.timestamp}_${conflict.path.replace(/\//g, "_")}.conflict.json`;
858
- const archivePath = `${CONFLICTS_DIR}/${archiveFileName}`;
859
- await this.ensureDir(archivePath);
871
+ const metaPath = `${conflictDir}/meta.json`;
860
872
  await this.cachedFS.writeFile(
861
- archivePath,
873
+ metaPath,
862
874
  new TextEncoder().encode(JSON.stringify(archive, null, 2))
863
875
  );
864
876
  if (this.onConflictCallback) {
865
877
  const info = {
866
- conflictId: archiveFileName,
878
+ conflictId: `${conflictId}/meta.json`,
867
879
  path: conflict.path,
868
880
  sourceAuthor: archive.sourceAuthor,
869
881
  targetAuthor: archive.targetAuthor,
870
- sourceContent: archive.sourceContent,
871
- targetContent: archive.targetContent
882
+ sourceContent: this.tryParse(conflict.sourceContent),
883
+ targetContent: this.tryParse(conflict.targetContent)
872
884
  };
873
885
  try {
874
886
  const customMerge = await this.onConflictCallback(info);
875
887
  if (customMerge !== null && customMerge !== void 0) {
876
- await this.resolveConflict(archiveFileName, customMerge);
888
+ await this.resolveConflict(`${conflictId}/meta.json`, customMerge);
877
889
  }
878
890
  } catch (err) {
879
891
  console.error("[zen-fs-config] Conflict handler error:", err);
@@ -958,14 +970,6 @@ var ConfigRepo = class {
958
970
  this.assertNotDisposed();
959
971
  await this.writeMetaFile(BACKENDS_FILE, meta);
960
972
  }
961
- async getSyncRules() {
962
- this.assertNotDisposed();
963
- return this.readMetaFile(SYNC_RULES_FILE);
964
- }
965
- async updateSyncRules(meta) {
966
- this.assertNotDisposed();
967
- await this.writeMetaFile(SYNC_RULES_FILE, meta);
968
- }
969
973
  tryParse(content) {
970
974
  try {
971
975
  return JSON.parse(content);
@@ -1026,24 +1030,17 @@ async function createConfigRepo(appId, options) {
1026
1030
  );
1027
1031
  let backendsMeta = await tempRepo.readMetaFile(BACKENDS_FILE);
1028
1032
  if (!backendsMeta) {
1029
- if (options.bootstrap) {
1030
- backendsMeta = {
1031
- version: 1,
1032
- backends: options.bootstrap.backends
1033
- };
1034
- console.log(`[createConfigRepo] First init: using bootstrap backends: ${backendsMeta.backends.map((b) => b.id).join(", ")}`);
1035
- } else {
1036
- backendsMeta = {
1037
- version: 1,
1038
- backends: [
1039
- {
1040
- id: options.primaryBackendId,
1041
- type: options.backendInfo.type,
1042
- options: options.backendInfo.options
1043
- }
1044
- ]
1045
- };
1046
- }
1033
+ backendsMeta = {
1034
+ version: 1,
1035
+ backends: [
1036
+ {
1037
+ id: options.primaryBackendId,
1038
+ type: options.backendInfo.type,
1039
+ options: options.backendInfo.options
1040
+ }
1041
+ ]
1042
+ };
1043
+ console.log(`[createConfigRepo] First init: using bootstrap backends: ${backendsMeta.backends.map((b) => b.id).join(", ")}`);
1047
1044
  } else {
1048
1045
  console.log(`[createConfigRepo] Reconnect: using stored backends: ${backendsMeta.backends.map((b) => b.id).join(", ")}`);
1049
1046
  }
@@ -1058,45 +1055,6 @@ async function createConfigRepo(appId, options) {
1058
1055
  });
1059
1056
  }
1060
1057
  await tempRepo.writeMetaFile(BACKENDS_FILE, backendsMeta);
1061
- let syncRulesMeta = await tempRepo.readMetaFile(SYNC_RULES_FILE);
1062
- if (!syncRulesMeta) {
1063
- if (options.bootstrap) {
1064
- syncRulesMeta = {
1065
- version: 1,
1066
- rules: options.bootstrap.syncRules
1067
- };
1068
- console.log(`[createConfigRepo] First init: using bootstrap syncRules: ${syncRulesMeta.rules.length} rules`);
1069
- } else {
1070
- syncRulesMeta = {
1071
- version: 1,
1072
- rules: [
1073
- {
1074
- prefix: `/${appId}/`,
1075
- direction: "one-way",
1076
- conflictStrategy: "source-wins",
1077
- replicas: backendsMeta.backends.map((b) => b.id)
1078
- },
1079
- {
1080
- prefix: `${SHARED_DIR}/`,
1081
- direction: "bi-directional",
1082
- conflictStrategy: "merge",
1083
- replicas: backendsMeta.backends.map((b) => b.id)
1084
- },
1085
- { prefix: `${NODES_DIR}/`, direction: "none" },
1086
- {
1087
- prefix: `${META_DIR}/`,
1088
- direction: "one-way",
1089
- conflictStrategy: "source-wins",
1090
- replicas: backendsMeta.backends.map((b) => b.id)
1091
- }
1092
- ]
1093
- };
1094
- }
1095
- }
1096
- if (syncRulesMeta) {
1097
- console.log(`[createConfigRepo] Reconnect: using stored syncRules: ${syncRulesMeta.rules.length} rules`);
1098
- }
1099
- await tempRepo.writeMetaFile(SYNC_RULES_FILE, syncRulesMeta);
1100
1058
  let nodeId = options.nodeId;
1101
1059
  if (!nodeId) {
1102
1060
  nodeId = process.env.NODE_ID;
package/dist/index.mjs CHANGED
@@ -435,10 +435,8 @@ async function verifyOrRepairVersion(fs, configFilePath, author) {
435
435
  // src/config-repo.ts
436
436
  var META_DIR = "/.meta";
437
437
  var BACKENDS_FILE = `${META_DIR}/backends.json`;
438
- var SYNC_RULES_FILE = `${META_DIR}/sync-rules.json`;
439
438
  var CONFLICTS_DIR = `${META_DIR}/.conflicts`;
440
439
  var NODES_DIR = "/nodes";
441
- var SHARED_DIR = "/shared";
442
440
  var NODE_ID_FILE = `${NODES_DIR}/.node-id`;
443
441
  var ConfigRepo = class {
444
442
  appId;
@@ -484,12 +482,6 @@ var ConfigRepo = class {
484
482
  backends: data.backends
485
483
  });
486
484
  }
487
- if (data.syncRules) {
488
- await this.writeMetaFile(SYNC_RULES_FILE, {
489
- version: 1,
490
- rules: data.syncRules
491
- });
492
- }
493
485
  }
494
486
  await this.reloadConfigCache();
495
487
  }
@@ -617,32 +609,29 @@ var ConfigRepo = class {
617
609
  return Array.from(resultsMap.values());
618
610
  }
619
611
  /**
620
- * Sync .meta/ files (backends.json, sync-rules.json) to all replica backends.
612
+ * Sync .meta/ files (backends.json) to all replica backends.
621
613
  *
622
614
  * This ensures the backend topology is available on every replica, enabling
623
615
  * any program that connects to any backend to discover the full topology.
624
616
  *
625
617
  * Called automatically by createConfigRepo() after setupSync().
626
- * Can also be called manually after updateBackends() / updateSyncRules().
627
618
  */
628
619
  async syncMetaToReplicas() {
629
620
  this.assertNotDisposed();
630
- for (const metaFile of [BACKENDS_FILE, SYNC_RULES_FILE]) {
631
- try {
632
- const content = await this.cachedFS.readFile(metaFile);
633
- const vPath = versionPathFor(metaFile);
634
- const vContent = await this.cachedFS.readFile(vPath);
635
- for (const [id, replica] of this.replicaBackends) {
636
- try {
637
- await replica.syncable.writeFile(metaFile, content);
638
- await replica.syncable.writeFile(vPath, vContent);
639
- console.log(`[ConfigRepo] Synced ${metaFile} + .version to replica ${id}`);
640
- } catch (err) {
641
- console.error(`[ConfigRepo] Failed to sync ${metaFile} to ${id}:`, err.message);
642
- }
621
+ try {
622
+ const content = await this.cachedFS.readFile(BACKENDS_FILE);
623
+ const vPath = versionPathFor(BACKENDS_FILE);
624
+ const vContent = await this.cachedFS.readFile(vPath);
625
+ for (const [id, replica] of this.replicaBackends) {
626
+ try {
627
+ await replica.syncable.writeFile(BACKENDS_FILE, content);
628
+ await replica.syncable.writeFile(vPath, vContent);
629
+ console.log(`[ConfigRepo] Synced ${BACKENDS_FILE} + .version to replica ${id}`);
630
+ } catch (err) {
631
+ console.error(`[ConfigRepo] Failed to sync ${BACKENDS_FILE} to ${id}:`, err.message);
643
632
  }
644
- } catch {
645
633
  }
634
+ } catch {
646
635
  }
647
636
  }
648
637
  getSyncStatuses() {
@@ -654,9 +643,9 @@ var ConfigRepo = class {
654
643
  // -----------------------------------------------------------------------
655
644
  async resolveConflict(conflictId, mergedContent) {
656
645
  this.assertNotDisposed();
657
- const archivePath = `${CONFLICTS_DIR}/${conflictId}`;
646
+ const metaPath = `${CONFLICTS_DIR}/${conflictId}`;
658
647
  try {
659
- const raw = await this.cachedFS.readFile(archivePath);
648
+ const raw = await this.cachedFS.readFile(metaPath);
660
649
  const archive = JSON.parse(
661
650
  new TextDecoder().decode(toUint8Array(raw))
662
651
  );
@@ -671,9 +660,13 @@ var ConfigRepo = class {
671
660
  author
672
661
  );
673
662
  await writeVersion(this.fullFS, versionPathFor(configPath), version);
674
- archive.resolvedContent = mergedContent;
663
+ const conflictDir = metaPath.substring(0, metaPath.lastIndexOf("/"));
664
+ const resolvedBackupPath = `${conflictDir}/resolved`;
665
+ const resolvedBytes = typeof mergedContent === "string" ? new TextEncoder().encode(mergedContent) : new TextEncoder().encode(JSON.stringify(mergedContent, null, 2));
666
+ await this.cachedFS.writeFile(resolvedBackupPath, resolvedBytes);
667
+ archive.resolvedBackupPath = `./resolved`;
675
668
  await this.cachedFS.writeFile(
676
- archivePath,
669
+ metaPath,
677
670
  new TextEncoder().encode(JSON.stringify(archive, null, 2))
678
671
  );
679
672
  } catch (err) {
@@ -686,9 +679,9 @@ var ConfigRepo = class {
686
679
  try {
687
680
  const entries = await this.cachedFS.readdir(CONFLICTS_DIR);
688
681
  for (const entry of entries) {
689
- if (!entry.endsWith(".json")) continue;
682
+ const metaPath = `${CONFLICTS_DIR}/${entry}/meta.json`;
690
683
  try {
691
- const raw = await this.cachedFS.readFile(`${CONFLICTS_DIR}/${entry}`);
684
+ const raw = await this.cachedFS.readFile(metaPath);
692
685
  const archive = JSON.parse(
693
686
  new TextDecoder().decode(toUint8Array(raw))
694
687
  );
@@ -700,6 +693,13 @@ var ConfigRepo = class {
700
693
  }
701
694
  return archives.sort((a, b) => a.timestamp - b.timestamp);
702
695
  }
696
+ async readConflictBackup(conflictId, fileType) {
697
+ this.assertNotDisposed();
698
+ const conflictDir = `${CONFLICTS_DIR}/${conflictId}`.replace(/\/meta\.json$/, "");
699
+ const filePath = `${conflictDir}/${fileType}`;
700
+ const raw = await this.cachedFS.readFile(filePath);
701
+ return new TextDecoder().decode(toUint8Array(raw));
702
+ }
703
703
  // -----------------------------------------------------------------------
704
704
  // IConfigRepo — Lifecycle
705
705
  // -----------------------------------------------------------------------
@@ -750,7 +750,7 @@ var ConfigRepo = class {
750
750
  );
751
751
  console.log(`[ConfigRepo] Sync pair added: pairId=${pair.pairId}, replica=${replicaId}, root=/`);
752
752
  const conflictHandler = (event) => {
753
- this.handleConflict(event, { prefix: "/", direction: "one-way" });
753
+ this.handleConflict(event);
754
754
  };
755
755
  this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
756
756
  this.syncEngine.watch(pair.pairId);
@@ -786,45 +786,57 @@ var ConfigRepo = class {
786
786
  // -----------------------------------------------------------------------
787
787
  // Internal — Conflict Handling
788
788
  // -----------------------------------------------------------------------
789
- async handleConflict(event, _rule) {
789
+ async handleConflict(event) {
790
790
  const conflict = event.conflict;
791
791
  if (!conflict) return;
792
+ const conflictId = `${event.timestamp}_${conflict.path.replace(/\//g, "_")}`;
793
+ const conflictDir = `${CONFLICTS_DIR}/${conflictId}`;
794
+ const sourceBackupPath = `${conflictDir}/source`;
795
+ const targetBackupPath = `${conflictDir}/target`;
796
+ await this.ensureDir(conflictDir);
797
+ await this.cachedFS.writeFile(
798
+ sourceBackupPath,
799
+ new TextEncoder().encode(conflict.sourceContent)
800
+ );
801
+ await this.cachedFS.writeFile(
802
+ targetBackupPath,
803
+ new TextEncoder().encode(conflict.targetContent)
804
+ );
805
+ let sourceVersion = 0;
806
+ try {
807
+ const srcVer = await readVersion(this.fullFS, versionPathFor(conflict.path));
808
+ if (srcVer) sourceVersion = srcVer.version;
809
+ } catch {
810
+ }
792
811
  const archive = {
793
812
  conflictPath: conflict.path,
794
813
  timestamp: event.timestamp,
795
814
  sourceAuthor: `${this.appId}/${this.nodeId}`,
796
815
  targetAuthor: "unknown",
797
- sourceContent: this.tryParse(conflict.sourceContent),
798
- targetContent: this.tryParse(conflict.targetContent),
799
- sourceVersion: 0,
816
+ sourceVersion,
800
817
  targetVersion: 0,
801
- resolvedStrategy: conflict.resolvedWith
818
+ resolvedStrategy: conflict.resolvedWith,
819
+ sourceBackupPath: `./source`,
820
+ targetBackupPath: `./target`
802
821
  };
803
- try {
804
- const srcVer = await readVersion(this.fullFS, versionPathFor(conflict.path));
805
- if (srcVer) archive.sourceVersion = srcVer.version;
806
- } catch {
807
- }
808
- const archiveFileName = `${event.timestamp}_${conflict.path.replace(/\//g, "_")}.conflict.json`;
809
- const archivePath = `${CONFLICTS_DIR}/${archiveFileName}`;
810
- await this.ensureDir(archivePath);
822
+ const metaPath = `${conflictDir}/meta.json`;
811
823
  await this.cachedFS.writeFile(
812
- archivePath,
824
+ metaPath,
813
825
  new TextEncoder().encode(JSON.stringify(archive, null, 2))
814
826
  );
815
827
  if (this.onConflictCallback) {
816
828
  const info = {
817
- conflictId: archiveFileName,
829
+ conflictId: `${conflictId}/meta.json`,
818
830
  path: conflict.path,
819
831
  sourceAuthor: archive.sourceAuthor,
820
832
  targetAuthor: archive.targetAuthor,
821
- sourceContent: archive.sourceContent,
822
- targetContent: archive.targetContent
833
+ sourceContent: this.tryParse(conflict.sourceContent),
834
+ targetContent: this.tryParse(conflict.targetContent)
823
835
  };
824
836
  try {
825
837
  const customMerge = await this.onConflictCallback(info);
826
838
  if (customMerge !== null && customMerge !== void 0) {
827
- await this.resolveConflict(archiveFileName, customMerge);
839
+ await this.resolveConflict(`${conflictId}/meta.json`, customMerge);
828
840
  }
829
841
  } catch (err) {
830
842
  console.error("[zen-fs-config] Conflict handler error:", err);
@@ -909,14 +921,6 @@ var ConfigRepo = class {
909
921
  this.assertNotDisposed();
910
922
  await this.writeMetaFile(BACKENDS_FILE, meta);
911
923
  }
912
- async getSyncRules() {
913
- this.assertNotDisposed();
914
- return this.readMetaFile(SYNC_RULES_FILE);
915
- }
916
- async updateSyncRules(meta) {
917
- this.assertNotDisposed();
918
- await this.writeMetaFile(SYNC_RULES_FILE, meta);
919
- }
920
924
  tryParse(content) {
921
925
  try {
922
926
  return JSON.parse(content);
@@ -977,24 +981,17 @@ async function createConfigRepo(appId, options) {
977
981
  );
978
982
  let backendsMeta = await tempRepo.readMetaFile(BACKENDS_FILE);
979
983
  if (!backendsMeta) {
980
- if (options.bootstrap) {
981
- backendsMeta = {
982
- version: 1,
983
- backends: options.bootstrap.backends
984
- };
985
- console.log(`[createConfigRepo] First init: using bootstrap backends: ${backendsMeta.backends.map((b) => b.id).join(", ")}`);
986
- } else {
987
- backendsMeta = {
988
- version: 1,
989
- backends: [
990
- {
991
- id: options.primaryBackendId,
992
- type: options.backendInfo.type,
993
- options: options.backendInfo.options
994
- }
995
- ]
996
- };
997
- }
984
+ backendsMeta = {
985
+ version: 1,
986
+ backends: [
987
+ {
988
+ id: options.primaryBackendId,
989
+ type: options.backendInfo.type,
990
+ options: options.backendInfo.options
991
+ }
992
+ ]
993
+ };
994
+ console.log(`[createConfigRepo] First init: using bootstrap backends: ${backendsMeta.backends.map((b) => b.id).join(", ")}`);
998
995
  } else {
999
996
  console.log(`[createConfigRepo] Reconnect: using stored backends: ${backendsMeta.backends.map((b) => b.id).join(", ")}`);
1000
997
  }
@@ -1009,45 +1006,6 @@ async function createConfigRepo(appId, options) {
1009
1006
  });
1010
1007
  }
1011
1008
  await tempRepo.writeMetaFile(BACKENDS_FILE, backendsMeta);
1012
- let syncRulesMeta = await tempRepo.readMetaFile(SYNC_RULES_FILE);
1013
- if (!syncRulesMeta) {
1014
- if (options.bootstrap) {
1015
- syncRulesMeta = {
1016
- version: 1,
1017
- rules: options.bootstrap.syncRules
1018
- };
1019
- console.log(`[createConfigRepo] First init: using bootstrap syncRules: ${syncRulesMeta.rules.length} rules`);
1020
- } else {
1021
- syncRulesMeta = {
1022
- version: 1,
1023
- rules: [
1024
- {
1025
- prefix: `/${appId}/`,
1026
- direction: "one-way",
1027
- conflictStrategy: "source-wins",
1028
- replicas: backendsMeta.backends.map((b) => b.id)
1029
- },
1030
- {
1031
- prefix: `${SHARED_DIR}/`,
1032
- direction: "bi-directional",
1033
- conflictStrategy: "merge",
1034
- replicas: backendsMeta.backends.map((b) => b.id)
1035
- },
1036
- { prefix: `${NODES_DIR}/`, direction: "none" },
1037
- {
1038
- prefix: `${META_DIR}/`,
1039
- direction: "one-way",
1040
- conflictStrategy: "source-wins",
1041
- replicas: backendsMeta.backends.map((b) => b.id)
1042
- }
1043
- ]
1044
- };
1045
- }
1046
- }
1047
- if (syncRulesMeta) {
1048
- console.log(`[createConfigRepo] Reconnect: using stored syncRules: ${syncRulesMeta.rules.length} rules`);
1049
- }
1050
- await tempRepo.writeMetaFile(SYNC_RULES_FILE, syncRulesMeta);
1051
1009
  let nodeId = options.nodeId;
1052
1010
  if (!nodeId) {
1053
1011
  nodeId = process.env.NODE_ID;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zen-fs-config",
3
- "version": "0.3.21",
3
+ "version": "0.3.22",
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",