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 +17 -43
- package/dist/index.d.ts +17 -43
- package/dist/index.js +80 -117
- package/dist/index.mjs +80 -117
- 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,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
|
|
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
|
|
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
|
|
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 {
|
|
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,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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
|
695
|
+
const metaPath = `${CONFLICTS_DIR}/${conflictId}`;
|
|
704
696
|
try {
|
|
705
|
-
const raw = await this.cachedFS.readFile(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
731
|
+
const metaPath = `${CONFLICTS_DIR}/${entry}/meta.json`;
|
|
736
732
|
try {
|
|
737
|
-
const raw = await this.cachedFS.readFile(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
878
|
+
conflictId: `${conflictId}/meta.json`,
|
|
864
879
|
path: conflict.path,
|
|
865
880
|
sourceAuthor: archive.sourceAuthor,
|
|
866
881
|
targetAuthor: archive.targetAuthor,
|
|
867
|
-
sourceContent:
|
|
868
|
-
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(
|
|
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
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
);
|
|
935
|
-
|
|
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
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
|
646
|
+
const metaPath = `${CONFLICTS_DIR}/${conflictId}`;
|
|
655
647
|
try {
|
|
656
|
-
const raw = await this.cachedFS.readFile(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
682
|
+
const metaPath = `${CONFLICTS_DIR}/${entry}/meta.json`;
|
|
687
683
|
try {
|
|
688
|
-
const raw = await this.cachedFS.readFile(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
829
|
+
conflictId: `${conflictId}/meta.json`,
|
|
815
830
|
path: conflict.path,
|
|
816
831
|
sourceAuthor: archive.sourceAuthor,
|
|
817
832
|
targetAuthor: archive.targetAuthor,
|
|
818
|
-
sourceContent:
|
|
819
|
-
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(
|
|
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
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
);
|
|
886
|
-
|
|
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
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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;
|