zen-fs-config 0.3.20 → 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,29 +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
- for (const [id, replica] of this.replicaBackends) {
683
- try {
684
- await replica.syncable.writeFile(metaFile, content);
685
- console.log(`[ConfigRepo] Synced ${metaFile} to replica ${id}`);
686
- } catch (err) {
687
- console.error(`[ConfigRepo] Failed to sync ${metaFile} to ${id}:`, err.message);
688
- }
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);
689
681
  }
690
- } catch {
691
682
  }
683
+ } catch {
692
684
  }
693
685
  }
694
686
  getSyncStatuses() {
@@ -700,9 +692,9 @@ var ConfigRepo = class {
700
692
  // -----------------------------------------------------------------------
701
693
  async resolveConflict(conflictId, mergedContent) {
702
694
  this.assertNotDisposed();
703
- const archivePath = `${CONFLICTS_DIR}/${conflictId}`;
695
+ const metaPath = `${CONFLICTS_DIR}/${conflictId}`;
704
696
  try {
705
- const raw = await this.cachedFS.readFile(archivePath);
697
+ const raw = await this.cachedFS.readFile(metaPath);
706
698
  const archive = JSON.parse(
707
699
  new TextDecoder().decode(toUint8Array(raw))
708
700
  );
@@ -717,9 +709,13 @@ var ConfigRepo = class {
717
709
  author
718
710
  );
719
711
  await writeVersion(this.fullFS, versionPathFor(configPath), version);
720
- 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`;
721
717
  await this.cachedFS.writeFile(
722
- archivePath,
718
+ metaPath,
723
719
  new TextEncoder().encode(JSON.stringify(archive, null, 2))
724
720
  );
725
721
  } catch (err) {
@@ -732,9 +728,9 @@ var ConfigRepo = class {
732
728
  try {
733
729
  const entries = await this.cachedFS.readdir(CONFLICTS_DIR);
734
730
  for (const entry of entries) {
735
- if (!entry.endsWith(".json")) continue;
731
+ const metaPath = `${CONFLICTS_DIR}/${entry}/meta.json`;
736
732
  try {
737
- const raw = await this.cachedFS.readFile(`${CONFLICTS_DIR}/${entry}`);
733
+ const raw = await this.cachedFS.readFile(metaPath);
738
734
  const archive = JSON.parse(
739
735
  new TextDecoder().decode(toUint8Array(raw))
740
736
  );
@@ -746,6 +742,13 @@ var ConfigRepo = class {
746
742
  }
747
743
  return archives.sort((a, b) => a.timestamp - b.timestamp);
748
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
+ }
749
752
  // -----------------------------------------------------------------------
750
753
  // IConfigRepo — Lifecycle
751
754
  // -----------------------------------------------------------------------
@@ -796,7 +799,7 @@ var ConfigRepo = class {
796
799
  );
797
800
  console.log(`[ConfigRepo] Sync pair added: pairId=${pair.pairId}, replica=${replicaId}, root=/`);
798
801
  const conflictHandler = (event) => {
799
- this.handleConflict(event, { prefix: "/", direction: "one-way" });
802
+ this.handleConflict(event);
800
803
  };
801
804
  this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
802
805
  this.syncEngine.watch(pair.pairId);
@@ -832,45 +835,57 @@ var ConfigRepo = class {
832
835
  // -----------------------------------------------------------------------
833
836
  // Internal — Conflict Handling
834
837
  // -----------------------------------------------------------------------
835
- async handleConflict(event, _rule) {
838
+ async handleConflict(event) {
836
839
  const conflict = event.conflict;
837
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
+ }
838
860
  const archive = {
839
861
  conflictPath: conflict.path,
840
862
  timestamp: event.timestamp,
841
863
  sourceAuthor: `${this.appId}/${this.nodeId}`,
842
864
  targetAuthor: "unknown",
843
- sourceContent: this.tryParse(conflict.sourceContent),
844
- targetContent: this.tryParse(conflict.targetContent),
845
- sourceVersion: 0,
865
+ sourceVersion,
846
866
  targetVersion: 0,
847
- resolvedStrategy: conflict.resolvedWith
867
+ resolvedStrategy: conflict.resolvedWith,
868
+ sourceBackupPath: `./source`,
869
+ targetBackupPath: `./target`
848
870
  };
849
- try {
850
- const srcVer = await readVersion(this.fullFS, versionPathFor(conflict.path));
851
- if (srcVer) archive.sourceVersion = srcVer.version;
852
- } catch {
853
- }
854
- const archiveFileName = `${event.timestamp}_${conflict.path.replace(/\//g, "_")}.conflict.json`;
855
- const archivePath = `${CONFLICTS_DIR}/${archiveFileName}`;
856
- await this.ensureDir(archivePath);
871
+ const metaPath = `${conflictDir}/meta.json`;
857
872
  await this.cachedFS.writeFile(
858
- archivePath,
873
+ metaPath,
859
874
  new TextEncoder().encode(JSON.stringify(archive, null, 2))
860
875
  );
861
876
  if (this.onConflictCallback) {
862
877
  const info = {
863
- conflictId: archiveFileName,
878
+ conflictId: `${conflictId}/meta.json`,
864
879
  path: conflict.path,
865
880
  sourceAuthor: archive.sourceAuthor,
866
881
  targetAuthor: archive.targetAuthor,
867
- sourceContent: archive.sourceContent,
868
- targetContent: archive.targetContent
882
+ sourceContent: this.tryParse(conflict.sourceContent),
883
+ targetContent: this.tryParse(conflict.targetContent)
869
884
  };
870
885
  try {
871
886
  const customMerge = await this.onConflictCallback(info);
872
887
  if (customMerge !== null && customMerge !== void 0) {
873
- await this.resolveConflict(archiveFileName, customMerge);
888
+ await this.resolveConflict(`${conflictId}/meta.json`, customMerge);
874
889
  }
875
890
  } catch (err) {
876
891
  console.error("[zen-fs-config] Conflict handler error:", err);
@@ -927,12 +942,14 @@ var ConfigRepo = class {
927
942
  async writeMetaFile(path, data) {
928
943
  console.log(`[writeMetaFile] ${path}, ensuring dir...`);
929
944
  await this.ensureDir(path);
930
- console.log(`[writeMetaFile] ${path}, writing ${JSON.stringify(data).length} bytes...`);
931
- await this.cachedFS.writeFile(
932
- path,
933
- new TextEncoder().encode(JSON.stringify(data, null, 2))
934
- );
935
- console.log(`[writeMetaFile] ${path} done`);
945
+ const bytes = new TextEncoder().encode(JSON.stringify(data, null, 2));
946
+ console.log(`[writeMetaFile] ${path}, writing ${bytes.length} bytes...`);
947
+ await this.cachedFS.writeFile(path, bytes);
948
+ const author = `${this.appId}/${this.nodeId}`;
949
+ const version = await incrementVersion(this.fullFS, path, bytes, author);
950
+ await this.ensureDir(versionPathFor(path));
951
+ await writeVersion(this.fullFS, versionPathFor(path), version);
952
+ console.log(`[writeMetaFile] ${path} done (version=${version.version})`);
936
953
  }
937
954
  async readMetaFile(path) {
938
955
  try {
@@ -953,14 +970,6 @@ var ConfigRepo = class {
953
970
  this.assertNotDisposed();
954
971
  await this.writeMetaFile(BACKENDS_FILE, meta);
955
972
  }
956
- async getSyncRules() {
957
- this.assertNotDisposed();
958
- return this.readMetaFile(SYNC_RULES_FILE);
959
- }
960
- async updateSyncRules(meta) {
961
- this.assertNotDisposed();
962
- await this.writeMetaFile(SYNC_RULES_FILE, meta);
963
- }
964
973
  tryParse(content) {
965
974
  try {
966
975
  return JSON.parse(content);
@@ -1021,24 +1030,17 @@ async function createConfigRepo(appId, options) {
1021
1030
  );
1022
1031
  let backendsMeta = await tempRepo.readMetaFile(BACKENDS_FILE);
1023
1032
  if (!backendsMeta) {
1024
- if (options.bootstrap) {
1025
- backendsMeta = {
1026
- version: 1,
1027
- backends: options.bootstrap.backends
1028
- };
1029
- console.log(`[createConfigRepo] First init: using bootstrap backends: ${backendsMeta.backends.map((b) => b.id).join(", ")}`);
1030
- } else {
1031
- backendsMeta = {
1032
- version: 1,
1033
- backends: [
1034
- {
1035
- id: options.primaryBackendId,
1036
- type: options.backendInfo.type,
1037
- options: options.backendInfo.options
1038
- }
1039
- ]
1040
- };
1041
- }
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(", ")}`);
1042
1044
  } else {
1043
1045
  console.log(`[createConfigRepo] Reconnect: using stored backends: ${backendsMeta.backends.map((b) => b.id).join(", ")}`);
1044
1046
  }
@@ -1053,45 +1055,6 @@ async function createConfigRepo(appId, options) {
1053
1055
  });
1054
1056
  }
1055
1057
  await tempRepo.writeMetaFile(BACKENDS_FILE, backendsMeta);
1056
- let syncRulesMeta = await tempRepo.readMetaFile(SYNC_RULES_FILE);
1057
- if (!syncRulesMeta) {
1058
- if (options.bootstrap) {
1059
- syncRulesMeta = {
1060
- version: 1,
1061
- rules: options.bootstrap.syncRules
1062
- };
1063
- console.log(`[createConfigRepo] First init: using bootstrap syncRules: ${syncRulesMeta.rules.length} rules`);
1064
- } else {
1065
- syncRulesMeta = {
1066
- version: 1,
1067
- rules: [
1068
- {
1069
- prefix: `/${appId}/`,
1070
- direction: "one-way",
1071
- conflictStrategy: "source-wins",
1072
- replicas: backendsMeta.backends.map((b) => b.id)
1073
- },
1074
- {
1075
- prefix: `${SHARED_DIR}/`,
1076
- direction: "bi-directional",
1077
- conflictStrategy: "merge",
1078
- replicas: backendsMeta.backends.map((b) => b.id)
1079
- },
1080
- { prefix: `${NODES_DIR}/`, direction: "none" },
1081
- {
1082
- prefix: `${META_DIR}/`,
1083
- direction: "one-way",
1084
- conflictStrategy: "source-wins",
1085
- replicas: backendsMeta.backends.map((b) => b.id)
1086
- }
1087
- ]
1088
- };
1089
- }
1090
- }
1091
- if (syncRulesMeta) {
1092
- console.log(`[createConfigRepo] Reconnect: using stored syncRules: ${syncRulesMeta.rules.length} rules`);
1093
- }
1094
- await tempRepo.writeMetaFile(SYNC_RULES_FILE, syncRulesMeta);
1095
1058
  let nodeId = options.nodeId;
1096
1059
  if (!nodeId) {
1097
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,29 +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
- for (const [id, replica] of this.replicaBackends) {
634
- try {
635
- await replica.syncable.writeFile(metaFile, content);
636
- console.log(`[ConfigRepo] Synced ${metaFile} to replica ${id}`);
637
- } catch (err) {
638
- console.error(`[ConfigRepo] Failed to sync ${metaFile} to ${id}:`, err.message);
639
- }
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);
640
632
  }
641
- } catch {
642
633
  }
634
+ } catch {
643
635
  }
644
636
  }
645
637
  getSyncStatuses() {
@@ -651,9 +643,9 @@ var ConfigRepo = class {
651
643
  // -----------------------------------------------------------------------
652
644
  async resolveConflict(conflictId, mergedContent) {
653
645
  this.assertNotDisposed();
654
- const archivePath = `${CONFLICTS_DIR}/${conflictId}`;
646
+ const metaPath = `${CONFLICTS_DIR}/${conflictId}`;
655
647
  try {
656
- const raw = await this.cachedFS.readFile(archivePath);
648
+ const raw = await this.cachedFS.readFile(metaPath);
657
649
  const archive = JSON.parse(
658
650
  new TextDecoder().decode(toUint8Array(raw))
659
651
  );
@@ -668,9 +660,13 @@ var ConfigRepo = class {
668
660
  author
669
661
  );
670
662
  await writeVersion(this.fullFS, versionPathFor(configPath), version);
671
- 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`;
672
668
  await this.cachedFS.writeFile(
673
- archivePath,
669
+ metaPath,
674
670
  new TextEncoder().encode(JSON.stringify(archive, null, 2))
675
671
  );
676
672
  } catch (err) {
@@ -683,9 +679,9 @@ var ConfigRepo = class {
683
679
  try {
684
680
  const entries = await this.cachedFS.readdir(CONFLICTS_DIR);
685
681
  for (const entry of entries) {
686
- if (!entry.endsWith(".json")) continue;
682
+ const metaPath = `${CONFLICTS_DIR}/${entry}/meta.json`;
687
683
  try {
688
- const raw = await this.cachedFS.readFile(`${CONFLICTS_DIR}/${entry}`);
684
+ const raw = await this.cachedFS.readFile(metaPath);
689
685
  const archive = JSON.parse(
690
686
  new TextDecoder().decode(toUint8Array(raw))
691
687
  );
@@ -697,6 +693,13 @@ var ConfigRepo = class {
697
693
  }
698
694
  return archives.sort((a, b) => a.timestamp - b.timestamp);
699
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
+ }
700
703
  // -----------------------------------------------------------------------
701
704
  // IConfigRepo — Lifecycle
702
705
  // -----------------------------------------------------------------------
@@ -747,7 +750,7 @@ var ConfigRepo = class {
747
750
  );
748
751
  console.log(`[ConfigRepo] Sync pair added: pairId=${pair.pairId}, replica=${replicaId}, root=/`);
749
752
  const conflictHandler = (event) => {
750
- this.handleConflict(event, { prefix: "/", direction: "one-way" });
753
+ this.handleConflict(event);
751
754
  };
752
755
  this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
753
756
  this.syncEngine.watch(pair.pairId);
@@ -783,45 +786,57 @@ var ConfigRepo = class {
783
786
  // -----------------------------------------------------------------------
784
787
  // Internal — Conflict Handling
785
788
  // -----------------------------------------------------------------------
786
- async handleConflict(event, _rule) {
789
+ async handleConflict(event) {
787
790
  const conflict = event.conflict;
788
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
+ }
789
811
  const archive = {
790
812
  conflictPath: conflict.path,
791
813
  timestamp: event.timestamp,
792
814
  sourceAuthor: `${this.appId}/${this.nodeId}`,
793
815
  targetAuthor: "unknown",
794
- sourceContent: this.tryParse(conflict.sourceContent),
795
- targetContent: this.tryParse(conflict.targetContent),
796
- sourceVersion: 0,
816
+ sourceVersion,
797
817
  targetVersion: 0,
798
- resolvedStrategy: conflict.resolvedWith
818
+ resolvedStrategy: conflict.resolvedWith,
819
+ sourceBackupPath: `./source`,
820
+ targetBackupPath: `./target`
799
821
  };
800
- try {
801
- const srcVer = await readVersion(this.fullFS, versionPathFor(conflict.path));
802
- if (srcVer) archive.sourceVersion = srcVer.version;
803
- } catch {
804
- }
805
- const archiveFileName = `${event.timestamp}_${conflict.path.replace(/\//g, "_")}.conflict.json`;
806
- const archivePath = `${CONFLICTS_DIR}/${archiveFileName}`;
807
- await this.ensureDir(archivePath);
822
+ const metaPath = `${conflictDir}/meta.json`;
808
823
  await this.cachedFS.writeFile(
809
- archivePath,
824
+ metaPath,
810
825
  new TextEncoder().encode(JSON.stringify(archive, null, 2))
811
826
  );
812
827
  if (this.onConflictCallback) {
813
828
  const info = {
814
- conflictId: archiveFileName,
829
+ conflictId: `${conflictId}/meta.json`,
815
830
  path: conflict.path,
816
831
  sourceAuthor: archive.sourceAuthor,
817
832
  targetAuthor: archive.targetAuthor,
818
- sourceContent: archive.sourceContent,
819
- targetContent: archive.targetContent
833
+ sourceContent: this.tryParse(conflict.sourceContent),
834
+ targetContent: this.tryParse(conflict.targetContent)
820
835
  };
821
836
  try {
822
837
  const customMerge = await this.onConflictCallback(info);
823
838
  if (customMerge !== null && customMerge !== void 0) {
824
- await this.resolveConflict(archiveFileName, customMerge);
839
+ await this.resolveConflict(`${conflictId}/meta.json`, customMerge);
825
840
  }
826
841
  } catch (err) {
827
842
  console.error("[zen-fs-config] Conflict handler error:", err);
@@ -878,12 +893,14 @@ var ConfigRepo = class {
878
893
  async writeMetaFile(path, data) {
879
894
  console.log(`[writeMetaFile] ${path}, ensuring dir...`);
880
895
  await this.ensureDir(path);
881
- console.log(`[writeMetaFile] ${path}, writing ${JSON.stringify(data).length} bytes...`);
882
- await this.cachedFS.writeFile(
883
- path,
884
- new TextEncoder().encode(JSON.stringify(data, null, 2))
885
- );
886
- console.log(`[writeMetaFile] ${path} done`);
896
+ const bytes = new TextEncoder().encode(JSON.stringify(data, null, 2));
897
+ console.log(`[writeMetaFile] ${path}, writing ${bytes.length} bytes...`);
898
+ await this.cachedFS.writeFile(path, bytes);
899
+ const author = `${this.appId}/${this.nodeId}`;
900
+ const version = await incrementVersion(this.fullFS, path, bytes, author);
901
+ await this.ensureDir(versionPathFor(path));
902
+ await writeVersion(this.fullFS, versionPathFor(path), version);
903
+ console.log(`[writeMetaFile] ${path} done (version=${version.version})`);
887
904
  }
888
905
  async readMetaFile(path) {
889
906
  try {
@@ -904,14 +921,6 @@ var ConfigRepo = class {
904
921
  this.assertNotDisposed();
905
922
  await this.writeMetaFile(BACKENDS_FILE, meta);
906
923
  }
907
- async getSyncRules() {
908
- this.assertNotDisposed();
909
- return this.readMetaFile(SYNC_RULES_FILE);
910
- }
911
- async updateSyncRules(meta) {
912
- this.assertNotDisposed();
913
- await this.writeMetaFile(SYNC_RULES_FILE, meta);
914
- }
915
924
  tryParse(content) {
916
925
  try {
917
926
  return JSON.parse(content);
@@ -972,24 +981,17 @@ async function createConfigRepo(appId, options) {
972
981
  );
973
982
  let backendsMeta = await tempRepo.readMetaFile(BACKENDS_FILE);
974
983
  if (!backendsMeta) {
975
- if (options.bootstrap) {
976
- backendsMeta = {
977
- version: 1,
978
- backends: options.bootstrap.backends
979
- };
980
- console.log(`[createConfigRepo] First init: using bootstrap backends: ${backendsMeta.backends.map((b) => b.id).join(", ")}`);
981
- } else {
982
- backendsMeta = {
983
- version: 1,
984
- backends: [
985
- {
986
- id: options.primaryBackendId,
987
- type: options.backendInfo.type,
988
- options: options.backendInfo.options
989
- }
990
- ]
991
- };
992
- }
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(", ")}`);
993
995
  } else {
994
996
  console.log(`[createConfigRepo] Reconnect: using stored backends: ${backendsMeta.backends.map((b) => b.id).join(", ")}`);
995
997
  }
@@ -1004,45 +1006,6 @@ async function createConfigRepo(appId, options) {
1004
1006
  });
1005
1007
  }
1006
1008
  await tempRepo.writeMetaFile(BACKENDS_FILE, backendsMeta);
1007
- let syncRulesMeta = await tempRepo.readMetaFile(SYNC_RULES_FILE);
1008
- if (!syncRulesMeta) {
1009
- if (options.bootstrap) {
1010
- syncRulesMeta = {
1011
- version: 1,
1012
- rules: options.bootstrap.syncRules
1013
- };
1014
- console.log(`[createConfigRepo] First init: using bootstrap syncRules: ${syncRulesMeta.rules.length} rules`);
1015
- } else {
1016
- syncRulesMeta = {
1017
- version: 1,
1018
- rules: [
1019
- {
1020
- prefix: `/${appId}/`,
1021
- direction: "one-way",
1022
- conflictStrategy: "source-wins",
1023
- replicas: backendsMeta.backends.map((b) => b.id)
1024
- },
1025
- {
1026
- prefix: `${SHARED_DIR}/`,
1027
- direction: "bi-directional",
1028
- conflictStrategy: "merge",
1029
- replicas: backendsMeta.backends.map((b) => b.id)
1030
- },
1031
- { prefix: `${NODES_DIR}/`, direction: "none" },
1032
- {
1033
- prefix: `${META_DIR}/`,
1034
- direction: "one-way",
1035
- conflictStrategy: "source-wins",
1036
- replicas: backendsMeta.backends.map((b) => b.id)
1037
- }
1038
- ]
1039
- };
1040
- }
1041
- }
1042
- if (syncRulesMeta) {
1043
- console.log(`[createConfigRepo] Reconnect: using stored syncRules: ${syncRulesMeta.rules.length} rules`);
1044
- }
1045
- await tempRepo.writeMetaFile(SYNC_RULES_FILE, syncRulesMeta);
1046
1009
  let nodeId = options.nodeId;
1047
1010
  if (!nodeId) {
1048
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.20",
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",