zen-fs-config 0.3.21 → 0.3.23
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 +20 -43
- package/dist/index.d.ts +20 -43
- package/dist/index.js +107 -111
- package/dist/index.mjs +107 -111
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
/**
|
|
71
|
-
|
|
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
|
|
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,21 @@ 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
|
|
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>;
|
|
247
|
+
/** Compare two file contents (handles Uint8Array, ArrayBuffer, string, Buffer). */
|
|
248
|
+
private bufferEqual;
|
|
249
|
+
private toUint8Array;
|
|
272
250
|
getSyncStatuses(): Map<string, SyncPairStatus>;
|
|
273
251
|
resolveConflict(conflictId: string, mergedContent: unknown): Promise<void>;
|
|
274
252
|
listConflicts(): Promise<ConflictArchive[]>;
|
|
253
|
+
readConflictBackup(conflictId: string, fileType: 'source' | 'target' | 'resolved'): Promise<string>;
|
|
275
254
|
dispose(): Promise<void>;
|
|
276
255
|
setupSync(backends: BackendDescriptor[], primaryBackendId: string): Promise<void>;
|
|
277
256
|
private persistConfig;
|
|
@@ -279,12 +258,10 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
279
258
|
private handleConflict;
|
|
280
259
|
private ensureDir;
|
|
281
260
|
private walkDir;
|
|
282
|
-
writeMetaFile(path: string, data: BackendsMeta
|
|
261
|
+
writeMetaFile(path: string, data: BackendsMeta): Promise<void>;
|
|
283
262
|
readMetaFile<T>(path: string): Promise<T | null>;
|
|
284
263
|
getBackends(): Promise<BackendsMeta | null>;
|
|
285
264
|
updateBackends(meta: BackendsMeta): Promise<void>;
|
|
286
|
-
getSyncRules(): Promise<SyncRulesMeta | null>;
|
|
287
|
-
updateSyncRules(meta: SyncRulesMeta): Promise<void>;
|
|
288
265
|
private tryParse;
|
|
289
266
|
private assertNotDisposed;
|
|
290
267
|
}
|
|
@@ -374,4 +351,4 @@ declare function incrementVersion(fs: SyncableFS, configFilePath: string, newCon
|
|
|
374
351
|
*/
|
|
375
352
|
declare function verifyOrRepairVersion(fs: SyncableFS, configFilePath: string, author: string): Promise<VersionMeta | null>;
|
|
376
353
|
|
|
377
|
-
export { type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendsMeta, type
|
|
354
|
+
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 {
|
|
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
|
-
/**
|
|
71
|
-
|
|
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
|
|
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,21 @@ 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
|
|
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>;
|
|
247
|
+
/** Compare two file contents (handles Uint8Array, ArrayBuffer, string, Buffer). */
|
|
248
|
+
private bufferEqual;
|
|
249
|
+
private toUint8Array;
|
|
272
250
|
getSyncStatuses(): Map<string, SyncPairStatus>;
|
|
273
251
|
resolveConflict(conflictId: string, mergedContent: unknown): Promise<void>;
|
|
274
252
|
listConflicts(): Promise<ConflictArchive[]>;
|
|
253
|
+
readConflictBackup(conflictId: string, fileType: 'source' | 'target' | 'resolved'): Promise<string>;
|
|
275
254
|
dispose(): Promise<void>;
|
|
276
255
|
setupSync(backends: BackendDescriptor[], primaryBackendId: string): Promise<void>;
|
|
277
256
|
private persistConfig;
|
|
@@ -279,12 +258,10 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
279
258
|
private handleConflict;
|
|
280
259
|
private ensureDir;
|
|
281
260
|
private walkDir;
|
|
282
|
-
writeMetaFile(path: string, data: BackendsMeta
|
|
261
|
+
writeMetaFile(path: string, data: BackendsMeta): Promise<void>;
|
|
283
262
|
readMetaFile<T>(path: string): Promise<T | null>;
|
|
284
263
|
getBackends(): Promise<BackendsMeta | null>;
|
|
285
264
|
updateBackends(meta: BackendsMeta): Promise<void>;
|
|
286
|
-
getSyncRules(): Promise<SyncRulesMeta | null>;
|
|
287
|
-
updateSyncRules(meta: SyncRulesMeta): Promise<void>;
|
|
288
265
|
private tryParse;
|
|
289
266
|
private assertNotDisposed;
|
|
290
267
|
}
|
|
@@ -374,4 +351,4 @@ declare function incrementVersion(fs: SyncableFS, configFilePath: string, newCon
|
|
|
374
351
|
*/
|
|
375
352
|
declare function verifyOrRepairVersion(fs: SyncableFS, configFilePath: string, author: string): Promise<VersionMeta | null>;
|
|
376
353
|
|
|
377
|
-
export { type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendsMeta, type
|
|
354
|
+
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,33 +658,68 @@ var ConfigRepo = class {
|
|
|
666
658
|
return Array.from(resultsMap.values());
|
|
667
659
|
}
|
|
668
660
|
/**
|
|
669
|
-
* Sync .meta/ files (backends.json
|
|
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
|
-
|
|
670
|
+
try {
|
|
671
|
+
const content = await this.cachedFS.readFile(BACKENDS_FILE);
|
|
672
|
+
const vPath = versionPathFor(BACKENDS_FILE);
|
|
673
|
+
let vContent;
|
|
680
674
|
try {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
675
|
+
vContent = await this.cachedFS.readFile(vPath);
|
|
676
|
+
} catch {
|
|
677
|
+
}
|
|
678
|
+
for (const [id, replica] of this.replicaBackends) {
|
|
679
|
+
try {
|
|
680
|
+
let needWrite = true;
|
|
685
681
|
try {
|
|
686
|
-
await replica.syncable.
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
682
|
+
const existing = await replica.syncable.readFile(BACKENDS_FILE);
|
|
683
|
+
if (this.bufferEqual(content, existing)) {
|
|
684
|
+
needWrite = false;
|
|
685
|
+
console.log(`[ConfigRepo] ${BACKENDS_FILE} already up-to-date on ${id}, skipping`);
|
|
686
|
+
}
|
|
687
|
+
} catch {
|
|
688
|
+
}
|
|
689
|
+
if (needWrite) {
|
|
690
|
+
await replica.syncable.writeFile(BACKENDS_FILE, content);
|
|
691
|
+
console.log(`[ConfigRepo] Synced ${BACKENDS_FILE} to replica ${id}`);
|
|
691
692
|
}
|
|
693
|
+
if (vContent) {
|
|
694
|
+
try {
|
|
695
|
+
await replica.syncable.writeFile(vPath, vContent);
|
|
696
|
+
} catch (err) {
|
|
697
|
+
console.error(`[ConfigRepo] Failed to sync ${vPath} to ${id}:`, err.message);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
} catch (err) {
|
|
701
|
+
console.error(`[ConfigRepo] Failed to sync ${BACKENDS_FILE} to ${id}:`, err.message);
|
|
692
702
|
}
|
|
693
|
-
} catch {
|
|
694
703
|
}
|
|
704
|
+
} catch {
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
/** Compare two file contents (handles Uint8Array, ArrayBuffer, string, Buffer). */
|
|
708
|
+
bufferEqual(a, b) {
|
|
709
|
+
const ua = this.toUint8Array(a);
|
|
710
|
+
const ub = this.toUint8Array(b);
|
|
711
|
+
if (ua.length !== ub.length) return false;
|
|
712
|
+
for (let i = 0; i < ua.length; i++) {
|
|
713
|
+
if (ua[i] !== ub[i]) return false;
|
|
695
714
|
}
|
|
715
|
+
return true;
|
|
716
|
+
}
|
|
717
|
+
toUint8Array(raw) {
|
|
718
|
+
if (raw instanceof ArrayBuffer) return new Uint8Array(raw);
|
|
719
|
+
if (raw instanceof Uint8Array) return raw;
|
|
720
|
+
if (typeof raw === "string") return new TextEncoder().encode(raw);
|
|
721
|
+
if (Buffer.isBuffer(raw)) return new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
|
|
722
|
+
return new Uint8Array(raw);
|
|
696
723
|
}
|
|
697
724
|
getSyncStatuses() {
|
|
698
725
|
this.assertNotDisposed();
|
|
@@ -703,9 +730,9 @@ var ConfigRepo = class {
|
|
|
703
730
|
// -----------------------------------------------------------------------
|
|
704
731
|
async resolveConflict(conflictId, mergedContent) {
|
|
705
732
|
this.assertNotDisposed();
|
|
706
|
-
const
|
|
733
|
+
const metaPath = `${CONFLICTS_DIR}/${conflictId}`;
|
|
707
734
|
try {
|
|
708
|
-
const raw = await this.cachedFS.readFile(
|
|
735
|
+
const raw = await this.cachedFS.readFile(metaPath);
|
|
709
736
|
const archive = JSON.parse(
|
|
710
737
|
new TextDecoder().decode(toUint8Array(raw))
|
|
711
738
|
);
|
|
@@ -720,9 +747,13 @@ var ConfigRepo = class {
|
|
|
720
747
|
author
|
|
721
748
|
);
|
|
722
749
|
await writeVersion(this.fullFS, versionPathFor(configPath), version);
|
|
723
|
-
|
|
750
|
+
const conflictDir = metaPath.substring(0, metaPath.lastIndexOf("/"));
|
|
751
|
+
const resolvedBackupPath = `${conflictDir}/resolved`;
|
|
752
|
+
const resolvedBytes = typeof mergedContent === "string" ? new TextEncoder().encode(mergedContent) : new TextEncoder().encode(JSON.stringify(mergedContent, null, 2));
|
|
753
|
+
await this.cachedFS.writeFile(resolvedBackupPath, resolvedBytes);
|
|
754
|
+
archive.resolvedBackupPath = `./resolved`;
|
|
724
755
|
await this.cachedFS.writeFile(
|
|
725
|
-
|
|
756
|
+
metaPath,
|
|
726
757
|
new TextEncoder().encode(JSON.stringify(archive, null, 2))
|
|
727
758
|
);
|
|
728
759
|
} catch (err) {
|
|
@@ -735,9 +766,9 @@ var ConfigRepo = class {
|
|
|
735
766
|
try {
|
|
736
767
|
const entries = await this.cachedFS.readdir(CONFLICTS_DIR);
|
|
737
768
|
for (const entry of entries) {
|
|
738
|
-
|
|
769
|
+
const metaPath = `${CONFLICTS_DIR}/${entry}/meta.json`;
|
|
739
770
|
try {
|
|
740
|
-
const raw = await this.cachedFS.readFile(
|
|
771
|
+
const raw = await this.cachedFS.readFile(metaPath);
|
|
741
772
|
const archive = JSON.parse(
|
|
742
773
|
new TextDecoder().decode(toUint8Array(raw))
|
|
743
774
|
);
|
|
@@ -749,6 +780,13 @@ var ConfigRepo = class {
|
|
|
749
780
|
}
|
|
750
781
|
return archives.sort((a, b) => a.timestamp - b.timestamp);
|
|
751
782
|
}
|
|
783
|
+
async readConflictBackup(conflictId, fileType) {
|
|
784
|
+
this.assertNotDisposed();
|
|
785
|
+
const conflictDir = `${CONFLICTS_DIR}/${conflictId}`.replace(/\/meta\.json$/, "");
|
|
786
|
+
const filePath = `${conflictDir}/${fileType}`;
|
|
787
|
+
const raw = await this.cachedFS.readFile(filePath);
|
|
788
|
+
return new TextDecoder().decode(toUint8Array(raw));
|
|
789
|
+
}
|
|
752
790
|
// -----------------------------------------------------------------------
|
|
753
791
|
// IConfigRepo — Lifecycle
|
|
754
792
|
// -----------------------------------------------------------------------
|
|
@@ -799,7 +837,7 @@ var ConfigRepo = class {
|
|
|
799
837
|
);
|
|
800
838
|
console.log(`[ConfigRepo] Sync pair added: pairId=${pair.pairId}, replica=${replicaId}, root=/`);
|
|
801
839
|
const conflictHandler = (event) => {
|
|
802
|
-
this.handleConflict(event
|
|
840
|
+
this.handleConflict(event);
|
|
803
841
|
};
|
|
804
842
|
this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
|
|
805
843
|
this.syncEngine.watch(pair.pairId);
|
|
@@ -835,45 +873,57 @@ var ConfigRepo = class {
|
|
|
835
873
|
// -----------------------------------------------------------------------
|
|
836
874
|
// Internal — Conflict Handling
|
|
837
875
|
// -----------------------------------------------------------------------
|
|
838
|
-
async handleConflict(event
|
|
876
|
+
async handleConflict(event) {
|
|
839
877
|
const conflict = event.conflict;
|
|
840
878
|
if (!conflict) return;
|
|
879
|
+
const conflictId = `${event.timestamp}_${conflict.path.replace(/\//g, "_")}`;
|
|
880
|
+
const conflictDir = `${CONFLICTS_DIR}/${conflictId}`;
|
|
881
|
+
const sourceBackupPath = `${conflictDir}/source`;
|
|
882
|
+
const targetBackupPath = `${conflictDir}/target`;
|
|
883
|
+
await this.ensureDir(conflictDir);
|
|
884
|
+
await this.cachedFS.writeFile(
|
|
885
|
+
sourceBackupPath,
|
|
886
|
+
new TextEncoder().encode(conflict.sourceContent)
|
|
887
|
+
);
|
|
888
|
+
await this.cachedFS.writeFile(
|
|
889
|
+
targetBackupPath,
|
|
890
|
+
new TextEncoder().encode(conflict.targetContent)
|
|
891
|
+
);
|
|
892
|
+
let sourceVersion = 0;
|
|
893
|
+
try {
|
|
894
|
+
const srcVer = await readVersion(this.fullFS, versionPathFor(conflict.path));
|
|
895
|
+
if (srcVer) sourceVersion = srcVer.version;
|
|
896
|
+
} catch {
|
|
897
|
+
}
|
|
841
898
|
const archive = {
|
|
842
899
|
conflictPath: conflict.path,
|
|
843
900
|
timestamp: event.timestamp,
|
|
844
901
|
sourceAuthor: `${this.appId}/${this.nodeId}`,
|
|
845
902
|
targetAuthor: "unknown",
|
|
846
|
-
|
|
847
|
-
targetContent: this.tryParse(conflict.targetContent),
|
|
848
|
-
sourceVersion: 0,
|
|
903
|
+
sourceVersion,
|
|
849
904
|
targetVersion: 0,
|
|
850
|
-
resolvedStrategy: conflict.resolvedWith
|
|
905
|
+
resolvedStrategy: conflict.resolvedWith,
|
|
906
|
+
sourceBackupPath: `./source`,
|
|
907
|
+
targetBackupPath: `./target`
|
|
851
908
|
};
|
|
852
|
-
|
|
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);
|
|
909
|
+
const metaPath = `${conflictDir}/meta.json`;
|
|
860
910
|
await this.cachedFS.writeFile(
|
|
861
|
-
|
|
911
|
+
metaPath,
|
|
862
912
|
new TextEncoder().encode(JSON.stringify(archive, null, 2))
|
|
863
913
|
);
|
|
864
914
|
if (this.onConflictCallback) {
|
|
865
915
|
const info = {
|
|
866
|
-
conflictId:
|
|
916
|
+
conflictId: `${conflictId}/meta.json`,
|
|
867
917
|
path: conflict.path,
|
|
868
918
|
sourceAuthor: archive.sourceAuthor,
|
|
869
919
|
targetAuthor: archive.targetAuthor,
|
|
870
|
-
sourceContent:
|
|
871
|
-
targetContent:
|
|
920
|
+
sourceContent: this.tryParse(conflict.sourceContent),
|
|
921
|
+
targetContent: this.tryParse(conflict.targetContent)
|
|
872
922
|
};
|
|
873
923
|
try {
|
|
874
924
|
const customMerge = await this.onConflictCallback(info);
|
|
875
925
|
if (customMerge !== null && customMerge !== void 0) {
|
|
876
|
-
await this.resolveConflict(
|
|
926
|
+
await this.resolveConflict(`${conflictId}/meta.json`, customMerge);
|
|
877
927
|
}
|
|
878
928
|
} catch (err) {
|
|
879
929
|
console.error("[zen-fs-config] Conflict handler error:", err);
|
|
@@ -958,14 +1008,6 @@ var ConfigRepo = class {
|
|
|
958
1008
|
this.assertNotDisposed();
|
|
959
1009
|
await this.writeMetaFile(BACKENDS_FILE, meta);
|
|
960
1010
|
}
|
|
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
1011
|
tryParse(content) {
|
|
970
1012
|
try {
|
|
971
1013
|
return JSON.parse(content);
|
|
@@ -1026,24 +1068,17 @@ async function createConfigRepo(appId, options) {
|
|
|
1026
1068
|
);
|
|
1027
1069
|
let backendsMeta = await tempRepo.readMetaFile(BACKENDS_FILE);
|
|
1028
1070
|
if (!backendsMeta) {
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
id: options.primaryBackendId,
|
|
1041
|
-
type: options.backendInfo.type,
|
|
1042
|
-
options: options.backendInfo.options
|
|
1043
|
-
}
|
|
1044
|
-
]
|
|
1045
|
-
};
|
|
1046
|
-
}
|
|
1071
|
+
backendsMeta = {
|
|
1072
|
+
version: 1,
|
|
1073
|
+
backends: [
|
|
1074
|
+
{
|
|
1075
|
+
id: options.primaryBackendId,
|
|
1076
|
+
type: options.backendInfo.type,
|
|
1077
|
+
options: options.backendInfo.options
|
|
1078
|
+
}
|
|
1079
|
+
]
|
|
1080
|
+
};
|
|
1081
|
+
console.log(`[createConfigRepo] First init: using bootstrap backends: ${backendsMeta.backends.map((b) => b.id).join(", ")}`);
|
|
1047
1082
|
} else {
|
|
1048
1083
|
console.log(`[createConfigRepo] Reconnect: using stored backends: ${backendsMeta.backends.map((b) => b.id).join(", ")}`);
|
|
1049
1084
|
}
|
|
@@ -1058,45 +1093,6 @@ async function createConfigRepo(appId, options) {
|
|
|
1058
1093
|
});
|
|
1059
1094
|
}
|
|
1060
1095
|
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
1096
|
let nodeId = options.nodeId;
|
|
1101
1097
|
if (!nodeId) {
|
|
1102
1098
|
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,33 +609,68 @@ var ConfigRepo = class {
|
|
|
617
609
|
return Array.from(resultsMap.values());
|
|
618
610
|
}
|
|
619
611
|
/**
|
|
620
|
-
* Sync .meta/ files (backends.json
|
|
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
|
-
|
|
621
|
+
try {
|
|
622
|
+
const content = await this.cachedFS.readFile(BACKENDS_FILE);
|
|
623
|
+
const vPath = versionPathFor(BACKENDS_FILE);
|
|
624
|
+
let vContent;
|
|
631
625
|
try {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
626
|
+
vContent = await this.cachedFS.readFile(vPath);
|
|
627
|
+
} catch {
|
|
628
|
+
}
|
|
629
|
+
for (const [id, replica] of this.replicaBackends) {
|
|
630
|
+
try {
|
|
631
|
+
let needWrite = true;
|
|
636
632
|
try {
|
|
637
|
-
await replica.syncable.
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
633
|
+
const existing = await replica.syncable.readFile(BACKENDS_FILE);
|
|
634
|
+
if (this.bufferEqual(content, existing)) {
|
|
635
|
+
needWrite = false;
|
|
636
|
+
console.log(`[ConfigRepo] ${BACKENDS_FILE} already up-to-date on ${id}, skipping`);
|
|
637
|
+
}
|
|
638
|
+
} catch {
|
|
639
|
+
}
|
|
640
|
+
if (needWrite) {
|
|
641
|
+
await replica.syncable.writeFile(BACKENDS_FILE, content);
|
|
642
|
+
console.log(`[ConfigRepo] Synced ${BACKENDS_FILE} to replica ${id}`);
|
|
642
643
|
}
|
|
644
|
+
if (vContent) {
|
|
645
|
+
try {
|
|
646
|
+
await replica.syncable.writeFile(vPath, vContent);
|
|
647
|
+
} catch (err) {
|
|
648
|
+
console.error(`[ConfigRepo] Failed to sync ${vPath} to ${id}:`, err.message);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
} catch (err) {
|
|
652
|
+
console.error(`[ConfigRepo] Failed to sync ${BACKENDS_FILE} to ${id}:`, err.message);
|
|
643
653
|
}
|
|
644
|
-
} catch {
|
|
645
654
|
}
|
|
655
|
+
} catch {
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
/** Compare two file contents (handles Uint8Array, ArrayBuffer, string, Buffer). */
|
|
659
|
+
bufferEqual(a, b) {
|
|
660
|
+
const ua = this.toUint8Array(a);
|
|
661
|
+
const ub = this.toUint8Array(b);
|
|
662
|
+
if (ua.length !== ub.length) return false;
|
|
663
|
+
for (let i = 0; i < ua.length; i++) {
|
|
664
|
+
if (ua[i] !== ub[i]) return false;
|
|
646
665
|
}
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
toUint8Array(raw) {
|
|
669
|
+
if (raw instanceof ArrayBuffer) return new Uint8Array(raw);
|
|
670
|
+
if (raw instanceof Uint8Array) return raw;
|
|
671
|
+
if (typeof raw === "string") return new TextEncoder().encode(raw);
|
|
672
|
+
if (Buffer.isBuffer(raw)) return new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
|
|
673
|
+
return new Uint8Array(raw);
|
|
647
674
|
}
|
|
648
675
|
getSyncStatuses() {
|
|
649
676
|
this.assertNotDisposed();
|
|
@@ -654,9 +681,9 @@ var ConfigRepo = class {
|
|
|
654
681
|
// -----------------------------------------------------------------------
|
|
655
682
|
async resolveConflict(conflictId, mergedContent) {
|
|
656
683
|
this.assertNotDisposed();
|
|
657
|
-
const
|
|
684
|
+
const metaPath = `${CONFLICTS_DIR}/${conflictId}`;
|
|
658
685
|
try {
|
|
659
|
-
const raw = await this.cachedFS.readFile(
|
|
686
|
+
const raw = await this.cachedFS.readFile(metaPath);
|
|
660
687
|
const archive = JSON.parse(
|
|
661
688
|
new TextDecoder().decode(toUint8Array(raw))
|
|
662
689
|
);
|
|
@@ -671,9 +698,13 @@ var ConfigRepo = class {
|
|
|
671
698
|
author
|
|
672
699
|
);
|
|
673
700
|
await writeVersion(this.fullFS, versionPathFor(configPath), version);
|
|
674
|
-
|
|
701
|
+
const conflictDir = metaPath.substring(0, metaPath.lastIndexOf("/"));
|
|
702
|
+
const resolvedBackupPath = `${conflictDir}/resolved`;
|
|
703
|
+
const resolvedBytes = typeof mergedContent === "string" ? new TextEncoder().encode(mergedContent) : new TextEncoder().encode(JSON.stringify(mergedContent, null, 2));
|
|
704
|
+
await this.cachedFS.writeFile(resolvedBackupPath, resolvedBytes);
|
|
705
|
+
archive.resolvedBackupPath = `./resolved`;
|
|
675
706
|
await this.cachedFS.writeFile(
|
|
676
|
-
|
|
707
|
+
metaPath,
|
|
677
708
|
new TextEncoder().encode(JSON.stringify(archive, null, 2))
|
|
678
709
|
);
|
|
679
710
|
} catch (err) {
|
|
@@ -686,9 +717,9 @@ var ConfigRepo = class {
|
|
|
686
717
|
try {
|
|
687
718
|
const entries = await this.cachedFS.readdir(CONFLICTS_DIR);
|
|
688
719
|
for (const entry of entries) {
|
|
689
|
-
|
|
720
|
+
const metaPath = `${CONFLICTS_DIR}/${entry}/meta.json`;
|
|
690
721
|
try {
|
|
691
|
-
const raw = await this.cachedFS.readFile(
|
|
722
|
+
const raw = await this.cachedFS.readFile(metaPath);
|
|
692
723
|
const archive = JSON.parse(
|
|
693
724
|
new TextDecoder().decode(toUint8Array(raw))
|
|
694
725
|
);
|
|
@@ -700,6 +731,13 @@ var ConfigRepo = class {
|
|
|
700
731
|
}
|
|
701
732
|
return archives.sort((a, b) => a.timestamp - b.timestamp);
|
|
702
733
|
}
|
|
734
|
+
async readConflictBackup(conflictId, fileType) {
|
|
735
|
+
this.assertNotDisposed();
|
|
736
|
+
const conflictDir = `${CONFLICTS_DIR}/${conflictId}`.replace(/\/meta\.json$/, "");
|
|
737
|
+
const filePath = `${conflictDir}/${fileType}`;
|
|
738
|
+
const raw = await this.cachedFS.readFile(filePath);
|
|
739
|
+
return new TextDecoder().decode(toUint8Array(raw));
|
|
740
|
+
}
|
|
703
741
|
// -----------------------------------------------------------------------
|
|
704
742
|
// IConfigRepo — Lifecycle
|
|
705
743
|
// -----------------------------------------------------------------------
|
|
@@ -750,7 +788,7 @@ var ConfigRepo = class {
|
|
|
750
788
|
);
|
|
751
789
|
console.log(`[ConfigRepo] Sync pair added: pairId=${pair.pairId}, replica=${replicaId}, root=/`);
|
|
752
790
|
const conflictHandler = (event) => {
|
|
753
|
-
this.handleConflict(event
|
|
791
|
+
this.handleConflict(event);
|
|
754
792
|
};
|
|
755
793
|
this.syncEngine.on(pair.pairId, "conflict", conflictHandler);
|
|
756
794
|
this.syncEngine.watch(pair.pairId);
|
|
@@ -786,45 +824,57 @@ var ConfigRepo = class {
|
|
|
786
824
|
// -----------------------------------------------------------------------
|
|
787
825
|
// Internal — Conflict Handling
|
|
788
826
|
// -----------------------------------------------------------------------
|
|
789
|
-
async handleConflict(event
|
|
827
|
+
async handleConflict(event) {
|
|
790
828
|
const conflict = event.conflict;
|
|
791
829
|
if (!conflict) return;
|
|
830
|
+
const conflictId = `${event.timestamp}_${conflict.path.replace(/\//g, "_")}`;
|
|
831
|
+
const conflictDir = `${CONFLICTS_DIR}/${conflictId}`;
|
|
832
|
+
const sourceBackupPath = `${conflictDir}/source`;
|
|
833
|
+
const targetBackupPath = `${conflictDir}/target`;
|
|
834
|
+
await this.ensureDir(conflictDir);
|
|
835
|
+
await this.cachedFS.writeFile(
|
|
836
|
+
sourceBackupPath,
|
|
837
|
+
new TextEncoder().encode(conflict.sourceContent)
|
|
838
|
+
);
|
|
839
|
+
await this.cachedFS.writeFile(
|
|
840
|
+
targetBackupPath,
|
|
841
|
+
new TextEncoder().encode(conflict.targetContent)
|
|
842
|
+
);
|
|
843
|
+
let sourceVersion = 0;
|
|
844
|
+
try {
|
|
845
|
+
const srcVer = await readVersion(this.fullFS, versionPathFor(conflict.path));
|
|
846
|
+
if (srcVer) sourceVersion = srcVer.version;
|
|
847
|
+
} catch {
|
|
848
|
+
}
|
|
792
849
|
const archive = {
|
|
793
850
|
conflictPath: conflict.path,
|
|
794
851
|
timestamp: event.timestamp,
|
|
795
852
|
sourceAuthor: `${this.appId}/${this.nodeId}`,
|
|
796
853
|
targetAuthor: "unknown",
|
|
797
|
-
|
|
798
|
-
targetContent: this.tryParse(conflict.targetContent),
|
|
799
|
-
sourceVersion: 0,
|
|
854
|
+
sourceVersion,
|
|
800
855
|
targetVersion: 0,
|
|
801
|
-
resolvedStrategy: conflict.resolvedWith
|
|
856
|
+
resolvedStrategy: conflict.resolvedWith,
|
|
857
|
+
sourceBackupPath: `./source`,
|
|
858
|
+
targetBackupPath: `./target`
|
|
802
859
|
};
|
|
803
|
-
|
|
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);
|
|
860
|
+
const metaPath = `${conflictDir}/meta.json`;
|
|
811
861
|
await this.cachedFS.writeFile(
|
|
812
|
-
|
|
862
|
+
metaPath,
|
|
813
863
|
new TextEncoder().encode(JSON.stringify(archive, null, 2))
|
|
814
864
|
);
|
|
815
865
|
if (this.onConflictCallback) {
|
|
816
866
|
const info = {
|
|
817
|
-
conflictId:
|
|
867
|
+
conflictId: `${conflictId}/meta.json`,
|
|
818
868
|
path: conflict.path,
|
|
819
869
|
sourceAuthor: archive.sourceAuthor,
|
|
820
870
|
targetAuthor: archive.targetAuthor,
|
|
821
|
-
sourceContent:
|
|
822
|
-
targetContent:
|
|
871
|
+
sourceContent: this.tryParse(conflict.sourceContent),
|
|
872
|
+
targetContent: this.tryParse(conflict.targetContent)
|
|
823
873
|
};
|
|
824
874
|
try {
|
|
825
875
|
const customMerge = await this.onConflictCallback(info);
|
|
826
876
|
if (customMerge !== null && customMerge !== void 0) {
|
|
827
|
-
await this.resolveConflict(
|
|
877
|
+
await this.resolveConflict(`${conflictId}/meta.json`, customMerge);
|
|
828
878
|
}
|
|
829
879
|
} catch (err) {
|
|
830
880
|
console.error("[zen-fs-config] Conflict handler error:", err);
|
|
@@ -909,14 +959,6 @@ var ConfigRepo = class {
|
|
|
909
959
|
this.assertNotDisposed();
|
|
910
960
|
await this.writeMetaFile(BACKENDS_FILE, meta);
|
|
911
961
|
}
|
|
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
962
|
tryParse(content) {
|
|
921
963
|
try {
|
|
922
964
|
return JSON.parse(content);
|
|
@@ -977,24 +1019,17 @@ async function createConfigRepo(appId, options) {
|
|
|
977
1019
|
);
|
|
978
1020
|
let backendsMeta = await tempRepo.readMetaFile(BACKENDS_FILE);
|
|
979
1021
|
if (!backendsMeta) {
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
id: options.primaryBackendId,
|
|
992
|
-
type: options.backendInfo.type,
|
|
993
|
-
options: options.backendInfo.options
|
|
994
|
-
}
|
|
995
|
-
]
|
|
996
|
-
};
|
|
997
|
-
}
|
|
1022
|
+
backendsMeta = {
|
|
1023
|
+
version: 1,
|
|
1024
|
+
backends: [
|
|
1025
|
+
{
|
|
1026
|
+
id: options.primaryBackendId,
|
|
1027
|
+
type: options.backendInfo.type,
|
|
1028
|
+
options: options.backendInfo.options
|
|
1029
|
+
}
|
|
1030
|
+
]
|
|
1031
|
+
};
|
|
1032
|
+
console.log(`[createConfigRepo] First init: using bootstrap backends: ${backendsMeta.backends.map((b) => b.id).join(", ")}`);
|
|
998
1033
|
} else {
|
|
999
1034
|
console.log(`[createConfigRepo] Reconnect: using stored backends: ${backendsMeta.backends.map((b) => b.id).join(", ")}`);
|
|
1000
1035
|
}
|
|
@@ -1009,45 +1044,6 @@ async function createConfigRepo(appId, options) {
|
|
|
1009
1044
|
});
|
|
1010
1045
|
}
|
|
1011
1046
|
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
1047
|
let nodeId = options.nodeId;
|
|
1052
1048
|
if (!nodeId) {
|
|
1053
1049
|
nodeId = process.env.NODE_ID;
|