zen-fs-config 0.3.24 → 0.3.27
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 +81 -50
- package/dist/index.d.ts +81 -50
- package/dist/index.js +157 -67
- package/dist/index.mjs +157 -67
- package/package.json +3 -4
package/dist/index.d.mts
CHANGED
|
@@ -29,6 +29,17 @@ interface VersionMeta {
|
|
|
29
29
|
/** Timestamp when the version was created. */
|
|
30
30
|
timestamp: number;
|
|
31
31
|
}
|
|
32
|
+
/** Content of a tombstone file in `.meta/.deleted/`. */
|
|
33
|
+
interface TombstoneMeta {
|
|
34
|
+
/** The deleted file path. */
|
|
35
|
+
path: string;
|
|
36
|
+
/** Timestamp of deletion. */
|
|
37
|
+
deletedAt: number;
|
|
38
|
+
/** Backend ID that initiated the deletion. */
|
|
39
|
+
deletedBy: string;
|
|
40
|
+
/** Backend IDs that have confirmed the deletion (synced). */
|
|
41
|
+
confirmedBy: string[];
|
|
42
|
+
}
|
|
32
43
|
/** Content of a conflict archive file in `.meta/.conflicts/`. */
|
|
33
44
|
interface ConflictArchive {
|
|
34
45
|
/** The config file path that conflicted. */
|
|
@@ -146,6 +157,12 @@ interface IConfigRepo {
|
|
|
146
157
|
getBackends(): Promise<BackendsMeta | null>;
|
|
147
158
|
/** Write .meta/backends.json. */
|
|
148
159
|
updateBackends(meta: BackendsMeta): Promise<void>;
|
|
160
|
+
/**
|
|
161
|
+
* Delete a file and record a tombstone for cross-backend sync.
|
|
162
|
+
* The tombstone ensures the deletion propagates to all backends
|
|
163
|
+
* instead of being treated as a "missing file" that gets re-created.
|
|
164
|
+
*/
|
|
165
|
+
deleteFile(path: string): Promise<void>;
|
|
149
166
|
/**
|
|
150
167
|
* Sync .meta/ files (backends.json) to all replica backends.
|
|
151
168
|
* Called automatically by createConfigRepo() after setupSync().
|
|
@@ -191,21 +208,54 @@ declare function configKeyToFilePath(configPath: string): string;
|
|
|
191
208
|
declare function getExtension(path: string): string;
|
|
192
209
|
|
|
193
210
|
/**
|
|
194
|
-
* zen-fs-config —
|
|
211
|
+
* zen-fs-config — Backend Registry
|
|
195
212
|
*
|
|
196
|
-
*
|
|
213
|
+
* A pluggable registry that maps backend type names to factory functions.
|
|
214
|
+
*
|
|
215
|
+
* Core principle: zen-fs-config does NOT hardcode every ZenFS backend.
|
|
216
|
+
* Instead, it provides:
|
|
217
|
+
* 1. A simple registry API (registerBackend, createBackend, etc.)
|
|
218
|
+
* 2. One built-in backend (InMemory) — zero extra dependencies
|
|
219
|
+
* 3. A wrapZenFSFileSystem() helper to adapt any ZenFS FileSystem
|
|
220
|
+
* implementation into the BackendInstance interface
|
|
221
|
+
*
|
|
222
|
+
* Applications (like zen-fs-config-admin) register whatever backends
|
|
223
|
+
* they need at startup. Adding a new backend never requires changing
|
|
224
|
+
* zen-fs-config itself.
|
|
197
225
|
*/
|
|
198
226
|
|
|
199
|
-
|
|
227
|
+
type BackendFactory = (options: Record<string, unknown>) => Promise<BackendInstance>;
|
|
228
|
+
/**
|
|
229
|
+
* The minimal interface a backend instance must satisfy.
|
|
230
|
+
* Matches zen-fs-cache's CacheableFileSystem requirements.
|
|
231
|
+
*/
|
|
232
|
+
interface BackendInstance {
|
|
200
233
|
readFile(path: string, ...args: any[]): Promise<any>;
|
|
201
|
-
writeFile(path: string, data:
|
|
234
|
+
writeFile(path: string, data: string | Uint8Array | ArrayBuffer, options?: any): Promise<void>;
|
|
202
235
|
readdir(path: string): Promise<string[]>;
|
|
203
|
-
stat(path: string): Promise<any>;
|
|
236
|
+
stat(path: string, ...args: any[]): Promise<any>;
|
|
204
237
|
exists(path: string): Promise<boolean>;
|
|
205
238
|
mkdir(path: string, options?: any): Promise<any>;
|
|
206
239
|
unlink(path: string): Promise<void>;
|
|
207
|
-
rmdir
|
|
240
|
+
rmdir(path: string): Promise<void>;
|
|
208
241
|
rename?(oldPath: string, newPath: string): Promise<void>;
|
|
242
|
+
readFileMeta?(path: string, opts?: any): Promise<any>;
|
|
243
|
+
getRevision?(path: string): Promise<string | number | undefined>;
|
|
244
|
+
}
|
|
245
|
+
declare function registerBackend(type: string, factory: BackendFactory): void;
|
|
246
|
+
declare function unregisterBackend(type: string): boolean;
|
|
247
|
+
declare function createBackend(descriptor: Pick<BackendDescriptor, 'type' | 'options'>): Promise<BackendInstance>;
|
|
248
|
+
declare function hasBackend(type: string): boolean;
|
|
249
|
+
declare function listBackends(): string[];
|
|
250
|
+
declare function wrapZenFSFileSystem(config: any): Promise<BackendInstance>;
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* zen-fs-config — ConfigRepo Implementation
|
|
254
|
+
*
|
|
255
|
+
* Core implementation of IConfigRepo and the createConfigRepo factory.
|
|
256
|
+
*/
|
|
257
|
+
|
|
258
|
+
interface MinimalAsyncFS extends BackendInstance {
|
|
209
259
|
}
|
|
210
260
|
declare class ConfigRepo implements IConfigRepo {
|
|
211
261
|
readonly appId: string;
|
|
@@ -222,7 +272,8 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
222
272
|
private onConflictCallback?;
|
|
223
273
|
private disposed;
|
|
224
274
|
private configCache;
|
|
225
|
-
|
|
275
|
+
private readonly primaryBackendId;
|
|
276
|
+
constructor(appId: string, nodeId: string, primaryBackendId: string, cachedFS: MinimalAsyncFS, serializer: PathAwareSerializer, onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>);
|
|
226
277
|
/** Full path to this node's directory on the primary backend. */
|
|
227
278
|
get nodePath(): string;
|
|
228
279
|
load(rawConfig?: string): Promise<void>;
|
|
@@ -235,6 +286,28 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
235
286
|
}): Promise<SyncResult>;
|
|
236
287
|
peekNodeConfig<T = unknown>(nodeId: string, path: string): Promise<T>;
|
|
237
288
|
flush(): Promise<SyncResult[]>;
|
|
289
|
+
/**
|
|
290
|
+
* Delete a file and write a tombstone so the deletion propagates
|
|
291
|
+
* to all backends instead of being treated as "missing file → re-create".
|
|
292
|
+
*/
|
|
293
|
+
deleteFile(path: string): Promise<void>;
|
|
294
|
+
/**
|
|
295
|
+
* Read all tombstones from the primary backend.
|
|
296
|
+
*/
|
|
297
|
+
private readTombstones;
|
|
298
|
+
/**
|
|
299
|
+
* Before sync: for each tombstone, delete the actual file on all replicas.
|
|
300
|
+
* This prevents bi-directional sync from copying the file back.
|
|
301
|
+
*/
|
|
302
|
+
private processTombstones;
|
|
303
|
+
/**
|
|
304
|
+
* After sync: mark each tombstone as confirmed by all replica backends.
|
|
305
|
+
*/
|
|
306
|
+
private updateTombstoneConfirmations;
|
|
307
|
+
/**
|
|
308
|
+
* GC: remove tombstones where all backends in backends.json have confirmed.
|
|
309
|
+
*/
|
|
310
|
+
private gcTombstones;
|
|
238
311
|
/**
|
|
239
312
|
* Sync .meta/ files (backends.json) to all replica backends.
|
|
240
313
|
*
|
|
@@ -264,48 +337,6 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
264
337
|
}
|
|
265
338
|
declare function createConfigRepo(appId: string, options: ConfigRepoOptions): Promise<IConfigRepo>;
|
|
266
339
|
|
|
267
|
-
/**
|
|
268
|
-
* zen-fs-config — Backend Registry
|
|
269
|
-
*
|
|
270
|
-
* A pluggable registry that maps backend type names to factory functions.
|
|
271
|
-
*
|
|
272
|
-
* Core principle: zen-fs-config does NOT hardcode every ZenFS backend.
|
|
273
|
-
* Instead, it provides:
|
|
274
|
-
* 1. A simple registry API (registerBackend, createBackend, etc.)
|
|
275
|
-
* 2. One built-in backend (InMemory) — zero extra dependencies
|
|
276
|
-
* 3. A wrapZenFSFileSystem() helper to adapt any ZenFS FileSystem
|
|
277
|
-
* implementation into the BackendInstance interface
|
|
278
|
-
*
|
|
279
|
-
* Applications (like zen-fs-config-admin) register whatever backends
|
|
280
|
-
* they need at startup. Adding a new backend never requires changing
|
|
281
|
-
* zen-fs-config itself.
|
|
282
|
-
*/
|
|
283
|
-
|
|
284
|
-
type BackendFactory = (options: Record<string, unknown>) => Promise<BackendInstance>;
|
|
285
|
-
/**
|
|
286
|
-
* The minimal interface a backend instance must satisfy.
|
|
287
|
-
* Matches zen-fs-cache's CacheableFileSystem requirements.
|
|
288
|
-
*/
|
|
289
|
-
interface BackendInstance {
|
|
290
|
-
readFile(path: string, ...args: any[]): Promise<any>;
|
|
291
|
-
writeFile(path: string, data: string | Uint8Array | ArrayBuffer, options?: any): Promise<void>;
|
|
292
|
-
readdir(path: string): Promise<string[]>;
|
|
293
|
-
stat(path: string, ...args: any[]): Promise<any>;
|
|
294
|
-
exists(path: string): Promise<boolean>;
|
|
295
|
-
mkdir(path: string, options?: any): Promise<any>;
|
|
296
|
-
unlink(path: string): Promise<void>;
|
|
297
|
-
rmdir(path: string): Promise<void>;
|
|
298
|
-
rename?(oldPath: string, newPath: string): Promise<void>;
|
|
299
|
-
readFileMeta?(path: string, opts?: any): Promise<any>;
|
|
300
|
-
getRevision?(path: string): Promise<string | number | undefined>;
|
|
301
|
-
}
|
|
302
|
-
declare function registerBackend(type: string, factory: BackendFactory): void;
|
|
303
|
-
declare function unregisterBackend(type: string): boolean;
|
|
304
|
-
declare function createBackend(descriptor: Pick<BackendDescriptor, 'type' | 'options'>): Promise<BackendInstance>;
|
|
305
|
-
declare function hasBackend(type: string): boolean;
|
|
306
|
-
declare function listBackends(): string[];
|
|
307
|
-
declare function wrapZenFSFileSystem(config: any): Promise<BackendInstance>;
|
|
308
|
-
|
|
309
340
|
/**
|
|
310
341
|
* zen-fs-config — Sidecar Version File Management
|
|
311
342
|
*
|
|
@@ -348,4 +379,4 @@ declare function incrementVersion(fs: SyncableFS, configFilePath: string, newCon
|
|
|
348
379
|
*/
|
|
349
380
|
declare function verifyOrRepairVersion(fs: SyncableFS, configFilePath: string, author: string): Promise<VersionMeta | null>;
|
|
350
381
|
|
|
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 };
|
|
382
|
+
export { type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendsMeta, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type IConfigRepo, type TombstoneMeta, type VersionMeta, configKeyToFilePath, createBackend, createConfigRepo, createSerializerChain, getExtension, hasBackend, incrementVersion, listBackends, readVersion, registerBackend, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
|
package/dist/index.d.ts
CHANGED
|
@@ -29,6 +29,17 @@ interface VersionMeta {
|
|
|
29
29
|
/** Timestamp when the version was created. */
|
|
30
30
|
timestamp: number;
|
|
31
31
|
}
|
|
32
|
+
/** Content of a tombstone file in `.meta/.deleted/`. */
|
|
33
|
+
interface TombstoneMeta {
|
|
34
|
+
/** The deleted file path. */
|
|
35
|
+
path: string;
|
|
36
|
+
/** Timestamp of deletion. */
|
|
37
|
+
deletedAt: number;
|
|
38
|
+
/** Backend ID that initiated the deletion. */
|
|
39
|
+
deletedBy: string;
|
|
40
|
+
/** Backend IDs that have confirmed the deletion (synced). */
|
|
41
|
+
confirmedBy: string[];
|
|
42
|
+
}
|
|
32
43
|
/** Content of a conflict archive file in `.meta/.conflicts/`. */
|
|
33
44
|
interface ConflictArchive {
|
|
34
45
|
/** The config file path that conflicted. */
|
|
@@ -146,6 +157,12 @@ interface IConfigRepo {
|
|
|
146
157
|
getBackends(): Promise<BackendsMeta | null>;
|
|
147
158
|
/** Write .meta/backends.json. */
|
|
148
159
|
updateBackends(meta: BackendsMeta): Promise<void>;
|
|
160
|
+
/**
|
|
161
|
+
* Delete a file and record a tombstone for cross-backend sync.
|
|
162
|
+
* The tombstone ensures the deletion propagates to all backends
|
|
163
|
+
* instead of being treated as a "missing file" that gets re-created.
|
|
164
|
+
*/
|
|
165
|
+
deleteFile(path: string): Promise<void>;
|
|
149
166
|
/**
|
|
150
167
|
* Sync .meta/ files (backends.json) to all replica backends.
|
|
151
168
|
* Called automatically by createConfigRepo() after setupSync().
|
|
@@ -191,21 +208,54 @@ declare function configKeyToFilePath(configPath: string): string;
|
|
|
191
208
|
declare function getExtension(path: string): string;
|
|
192
209
|
|
|
193
210
|
/**
|
|
194
|
-
* zen-fs-config —
|
|
211
|
+
* zen-fs-config — Backend Registry
|
|
195
212
|
*
|
|
196
|
-
*
|
|
213
|
+
* A pluggable registry that maps backend type names to factory functions.
|
|
214
|
+
*
|
|
215
|
+
* Core principle: zen-fs-config does NOT hardcode every ZenFS backend.
|
|
216
|
+
* Instead, it provides:
|
|
217
|
+
* 1. A simple registry API (registerBackend, createBackend, etc.)
|
|
218
|
+
* 2. One built-in backend (InMemory) — zero extra dependencies
|
|
219
|
+
* 3. A wrapZenFSFileSystem() helper to adapt any ZenFS FileSystem
|
|
220
|
+
* implementation into the BackendInstance interface
|
|
221
|
+
*
|
|
222
|
+
* Applications (like zen-fs-config-admin) register whatever backends
|
|
223
|
+
* they need at startup. Adding a new backend never requires changing
|
|
224
|
+
* zen-fs-config itself.
|
|
197
225
|
*/
|
|
198
226
|
|
|
199
|
-
|
|
227
|
+
type BackendFactory = (options: Record<string, unknown>) => Promise<BackendInstance>;
|
|
228
|
+
/**
|
|
229
|
+
* The minimal interface a backend instance must satisfy.
|
|
230
|
+
* Matches zen-fs-cache's CacheableFileSystem requirements.
|
|
231
|
+
*/
|
|
232
|
+
interface BackendInstance {
|
|
200
233
|
readFile(path: string, ...args: any[]): Promise<any>;
|
|
201
|
-
writeFile(path: string, data:
|
|
234
|
+
writeFile(path: string, data: string | Uint8Array | ArrayBuffer, options?: any): Promise<void>;
|
|
202
235
|
readdir(path: string): Promise<string[]>;
|
|
203
|
-
stat(path: string): Promise<any>;
|
|
236
|
+
stat(path: string, ...args: any[]): Promise<any>;
|
|
204
237
|
exists(path: string): Promise<boolean>;
|
|
205
238
|
mkdir(path: string, options?: any): Promise<any>;
|
|
206
239
|
unlink(path: string): Promise<void>;
|
|
207
|
-
rmdir
|
|
240
|
+
rmdir(path: string): Promise<void>;
|
|
208
241
|
rename?(oldPath: string, newPath: string): Promise<void>;
|
|
242
|
+
readFileMeta?(path: string, opts?: any): Promise<any>;
|
|
243
|
+
getRevision?(path: string): Promise<string | number | undefined>;
|
|
244
|
+
}
|
|
245
|
+
declare function registerBackend(type: string, factory: BackendFactory): void;
|
|
246
|
+
declare function unregisterBackend(type: string): boolean;
|
|
247
|
+
declare function createBackend(descriptor: Pick<BackendDescriptor, 'type' | 'options'>): Promise<BackendInstance>;
|
|
248
|
+
declare function hasBackend(type: string): boolean;
|
|
249
|
+
declare function listBackends(): string[];
|
|
250
|
+
declare function wrapZenFSFileSystem(config: any): Promise<BackendInstance>;
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* zen-fs-config — ConfigRepo Implementation
|
|
254
|
+
*
|
|
255
|
+
* Core implementation of IConfigRepo and the createConfigRepo factory.
|
|
256
|
+
*/
|
|
257
|
+
|
|
258
|
+
interface MinimalAsyncFS extends BackendInstance {
|
|
209
259
|
}
|
|
210
260
|
declare class ConfigRepo implements IConfigRepo {
|
|
211
261
|
readonly appId: string;
|
|
@@ -222,7 +272,8 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
222
272
|
private onConflictCallback?;
|
|
223
273
|
private disposed;
|
|
224
274
|
private configCache;
|
|
225
|
-
|
|
275
|
+
private readonly primaryBackendId;
|
|
276
|
+
constructor(appId: string, nodeId: string, primaryBackendId: string, cachedFS: MinimalAsyncFS, serializer: PathAwareSerializer, onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>);
|
|
226
277
|
/** Full path to this node's directory on the primary backend. */
|
|
227
278
|
get nodePath(): string;
|
|
228
279
|
load(rawConfig?: string): Promise<void>;
|
|
@@ -235,6 +286,28 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
235
286
|
}): Promise<SyncResult>;
|
|
236
287
|
peekNodeConfig<T = unknown>(nodeId: string, path: string): Promise<T>;
|
|
237
288
|
flush(): Promise<SyncResult[]>;
|
|
289
|
+
/**
|
|
290
|
+
* Delete a file and write a tombstone so the deletion propagates
|
|
291
|
+
* to all backends instead of being treated as "missing file → re-create".
|
|
292
|
+
*/
|
|
293
|
+
deleteFile(path: string): Promise<void>;
|
|
294
|
+
/**
|
|
295
|
+
* Read all tombstones from the primary backend.
|
|
296
|
+
*/
|
|
297
|
+
private readTombstones;
|
|
298
|
+
/**
|
|
299
|
+
* Before sync: for each tombstone, delete the actual file on all replicas.
|
|
300
|
+
* This prevents bi-directional sync from copying the file back.
|
|
301
|
+
*/
|
|
302
|
+
private processTombstones;
|
|
303
|
+
/**
|
|
304
|
+
* After sync: mark each tombstone as confirmed by all replica backends.
|
|
305
|
+
*/
|
|
306
|
+
private updateTombstoneConfirmations;
|
|
307
|
+
/**
|
|
308
|
+
* GC: remove tombstones where all backends in backends.json have confirmed.
|
|
309
|
+
*/
|
|
310
|
+
private gcTombstones;
|
|
238
311
|
/**
|
|
239
312
|
* Sync .meta/ files (backends.json) to all replica backends.
|
|
240
313
|
*
|
|
@@ -264,48 +337,6 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
264
337
|
}
|
|
265
338
|
declare function createConfigRepo(appId: string, options: ConfigRepoOptions): Promise<IConfigRepo>;
|
|
266
339
|
|
|
267
|
-
/**
|
|
268
|
-
* zen-fs-config — Backend Registry
|
|
269
|
-
*
|
|
270
|
-
* A pluggable registry that maps backend type names to factory functions.
|
|
271
|
-
*
|
|
272
|
-
* Core principle: zen-fs-config does NOT hardcode every ZenFS backend.
|
|
273
|
-
* Instead, it provides:
|
|
274
|
-
* 1. A simple registry API (registerBackend, createBackend, etc.)
|
|
275
|
-
* 2. One built-in backend (InMemory) — zero extra dependencies
|
|
276
|
-
* 3. A wrapZenFSFileSystem() helper to adapt any ZenFS FileSystem
|
|
277
|
-
* implementation into the BackendInstance interface
|
|
278
|
-
*
|
|
279
|
-
* Applications (like zen-fs-config-admin) register whatever backends
|
|
280
|
-
* they need at startup. Adding a new backend never requires changing
|
|
281
|
-
* zen-fs-config itself.
|
|
282
|
-
*/
|
|
283
|
-
|
|
284
|
-
type BackendFactory = (options: Record<string, unknown>) => Promise<BackendInstance>;
|
|
285
|
-
/**
|
|
286
|
-
* The minimal interface a backend instance must satisfy.
|
|
287
|
-
* Matches zen-fs-cache's CacheableFileSystem requirements.
|
|
288
|
-
*/
|
|
289
|
-
interface BackendInstance {
|
|
290
|
-
readFile(path: string, ...args: any[]): Promise<any>;
|
|
291
|
-
writeFile(path: string, data: string | Uint8Array | ArrayBuffer, options?: any): Promise<void>;
|
|
292
|
-
readdir(path: string): Promise<string[]>;
|
|
293
|
-
stat(path: string, ...args: any[]): Promise<any>;
|
|
294
|
-
exists(path: string): Promise<boolean>;
|
|
295
|
-
mkdir(path: string, options?: any): Promise<any>;
|
|
296
|
-
unlink(path: string): Promise<void>;
|
|
297
|
-
rmdir(path: string): Promise<void>;
|
|
298
|
-
rename?(oldPath: string, newPath: string): Promise<void>;
|
|
299
|
-
readFileMeta?(path: string, opts?: any): Promise<any>;
|
|
300
|
-
getRevision?(path: string): Promise<string | number | undefined>;
|
|
301
|
-
}
|
|
302
|
-
declare function registerBackend(type: string, factory: BackendFactory): void;
|
|
303
|
-
declare function unregisterBackend(type: string): boolean;
|
|
304
|
-
declare function createBackend(descriptor: Pick<BackendDescriptor, 'type' | 'options'>): Promise<BackendInstance>;
|
|
305
|
-
declare function hasBackend(type: string): boolean;
|
|
306
|
-
declare function listBackends(): string[];
|
|
307
|
-
declare function wrapZenFSFileSystem(config: any): Promise<BackendInstance>;
|
|
308
|
-
|
|
309
340
|
/**
|
|
310
341
|
* zen-fs-config — Sidecar Version File Management
|
|
311
342
|
*
|
|
@@ -348,4 +379,4 @@ declare function incrementVersion(fs: SyncableFS, configFilePath: string, newCon
|
|
|
348
379
|
*/
|
|
349
380
|
declare function verifyOrRepairVersion(fs: SyncableFS, configFilePath: string, author: string): Promise<VersionMeta | null>;
|
|
350
381
|
|
|
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 };
|
|
382
|
+
export { type BackendDescriptor, type BackendFactory, type BackendInstance, type BackendsMeta, type CacheOptions, ConfigRepo, type ConfigRepoOptions, type ConfigSerializer, type ConflictArchive, type ConflictInfo, type IConfigRepo, type TombstoneMeta, type VersionMeta, configKeyToFilePath, createBackend, createConfigRepo, createSerializerChain, getExtension, hasBackend, incrementVersion, listBackends, readVersion, registerBackend, sha256, unregisterBackend, verifyOrRepairVersion, versionPathFor, wrapZenFSFileSystem, writeVersion };
|
package/dist/index.js
CHANGED
|
@@ -225,8 +225,8 @@ async function ensureParentDir(absolutePath) {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
// src/adapters.ts
|
|
228
|
-
function backendToSyncableFS(backend) {
|
|
229
|
-
|
|
228
|
+
function backendToSyncableFS(backend, name) {
|
|
229
|
+
const syncable = {
|
|
230
230
|
async readdir(path) {
|
|
231
231
|
return backend.readdir(path);
|
|
232
232
|
},
|
|
@@ -252,8 +252,7 @@ function backendToSyncableFS(backend) {
|
|
|
252
252
|
async stat(path) {
|
|
253
253
|
const s = await backend.stat(path);
|
|
254
254
|
return {
|
|
255
|
-
|
|
256
|
-
isDirectory: typeof s.isDirectory === "function" ? () => s.isDirectory() : () => !!(s.mode && s.mode & 16384),
|
|
255
|
+
mode: typeof s.mode === "number" ? s.mode : void 0,
|
|
257
256
|
size: s.size ?? 0,
|
|
258
257
|
mtimeMs: typeof s.mtimeMs === "number" ? s.mtimeMs : s.mtime ? new Date(s.mtime).getTime() : 0
|
|
259
258
|
};
|
|
@@ -265,47 +264,14 @@ function backendToSyncableFS(backend) {
|
|
|
265
264
|
return backend.exists(path);
|
|
266
265
|
}
|
|
267
266
|
};
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (encoding) {
|
|
277
|
-
if (typeof data === "string") return data;
|
|
278
|
-
return new TextDecoder().decode(
|
|
279
|
-
data instanceof ArrayBuffer ? new Uint8Array(data) : data
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
if (typeof data === "string") return Buffer.from(data);
|
|
283
|
-
if (data instanceof ArrayBuffer) return Buffer.from(new Uint8Array(data));
|
|
284
|
-
return Buffer.from(data);
|
|
285
|
-
},
|
|
286
|
-
async writeFile(path, data) {
|
|
287
|
-
return cached.writeFile(path, data);
|
|
288
|
-
},
|
|
289
|
-
async unlink(path) {
|
|
290
|
-
return cached.unlink(path);
|
|
291
|
-
},
|
|
292
|
-
async stat(path) {
|
|
293
|
-
const s = await cached.stat(path);
|
|
294
|
-
const isDir = typeof s.isDirectory === "function" ? s.isDirectory() : typeof s.isDirectory === "boolean" ? s.isDirectory : s.mode !== void 0 && (s.mode & 61440) === 16384;
|
|
295
|
-
return {
|
|
296
|
-
isFile: () => !isDir,
|
|
297
|
-
isDirectory: () => isDir,
|
|
298
|
-
size: s.size,
|
|
299
|
-
mtimeMs: s.mtimeMs ?? s.mtime
|
|
300
|
-
};
|
|
301
|
-
},
|
|
302
|
-
async mkdir(path, options) {
|
|
303
|
-
return cached.mkdir(path, options);
|
|
304
|
-
},
|
|
305
|
-
async exists(path) {
|
|
306
|
-
return cached.exists(path);
|
|
307
|
-
}
|
|
308
|
-
};
|
|
267
|
+
if (name) {
|
|
268
|
+
syncable.backendName = name;
|
|
269
|
+
} else if (backend.backendName) {
|
|
270
|
+
syncable.backendName = backend.backendName;
|
|
271
|
+
} else {
|
|
272
|
+
syncable.backendName = backend.constructor.name || "Backend";
|
|
273
|
+
}
|
|
274
|
+
return syncable;
|
|
309
275
|
}
|
|
310
276
|
|
|
311
277
|
// src/backend-registry.ts
|
|
@@ -365,12 +331,10 @@ async function wrapZenFSFileSystem(config) {
|
|
|
365
331
|
},
|
|
366
332
|
async stat(path, ..._args) {
|
|
367
333
|
const st = await isolatedFS.stat(path);
|
|
368
|
-
const isDir = typeof st.isDirectory === "function" ? st.isDirectory() : st.mode !== void 0 && (st.mode & 61440) === 16384;
|
|
369
334
|
return {
|
|
370
|
-
|
|
371
|
-
isDirectory: () => isDir,
|
|
335
|
+
mode: typeof st.mode === "number" ? st.mode : void 0,
|
|
372
336
|
size: st.size,
|
|
373
|
-
|
|
337
|
+
mtimeMs: st.mtimeMs ?? st.mtime ?? 0
|
|
374
338
|
};
|
|
375
339
|
},
|
|
376
340
|
async exists(path) {
|
|
@@ -485,8 +449,12 @@ async function verifyOrRepairVersion(fs, configFilePath, author) {
|
|
|
485
449
|
var META_DIR = "/.meta";
|
|
486
450
|
var BACKENDS_FILE = `${META_DIR}/backends.json`;
|
|
487
451
|
var CONFLICTS_DIR = `${META_DIR}/.conflicts`;
|
|
452
|
+
var DELETIONS_DIR = `${META_DIR}/.deleted`;
|
|
488
453
|
var NODES_DIR = "/nodes";
|
|
489
454
|
var NODE_ID_FILE = `${NODES_DIR}/.node-id`;
|
|
455
|
+
function tombstoneFileName(filePath) {
|
|
456
|
+
return filePath.replace(/^\//, "").replace(/\//g, "__").replace(/\./g, "++") + ".json";
|
|
457
|
+
}
|
|
490
458
|
var ConfigRepo = class {
|
|
491
459
|
appId;
|
|
492
460
|
nodeId;
|
|
@@ -502,15 +470,17 @@ var ConfigRepo = class {
|
|
|
502
470
|
onConflictCallback;
|
|
503
471
|
disposed = false;
|
|
504
472
|
configCache = /* @__PURE__ */ new Map();
|
|
505
|
-
|
|
473
|
+
primaryBackendId;
|
|
474
|
+
constructor(appId, nodeId, primaryBackendId, cachedFS, serializer, onConflict) {
|
|
506
475
|
this.appId = appId;
|
|
507
476
|
this.nodeId = nodeId;
|
|
477
|
+
this.primaryBackendId = primaryBackendId;
|
|
508
478
|
this.cachedFS = cachedFS;
|
|
509
479
|
this.serializer = serializer;
|
|
510
480
|
this.syncEngine = new import_zen_fs_sync.ZenFSSync();
|
|
511
481
|
this.replicaBackends = /* @__PURE__ */ new Map();
|
|
512
482
|
this.onConflictCallback = onConflict;
|
|
513
|
-
this.fullFS =
|
|
483
|
+
this.fullFS = backendToSyncableFS(cachedFS, primaryBackendId);
|
|
514
484
|
this.fs = createChrootFS(cachedFS, `/${appId}`);
|
|
515
485
|
this.rootFS = createChrootFS(cachedFS, "/");
|
|
516
486
|
}
|
|
@@ -654,9 +624,141 @@ var ConfigRepo = class {
|
|
|
654
624
|
// -----------------------------------------------------------------------
|
|
655
625
|
async flush() {
|
|
656
626
|
this.assertNotDisposed();
|
|
627
|
+
await this.processTombstones();
|
|
657
628
|
const resultsMap = await this.syncEngine.syncAll();
|
|
629
|
+
await this.updateTombstoneConfirmations();
|
|
630
|
+
await this.gcTombstones();
|
|
658
631
|
return Array.from(resultsMap.values());
|
|
659
632
|
}
|
|
633
|
+
// -----------------------------------------------------------------------
|
|
634
|
+
// Tombstone (Deletion Tracking)
|
|
635
|
+
// -----------------------------------------------------------------------
|
|
636
|
+
/**
|
|
637
|
+
* Delete a file and write a tombstone so the deletion propagates
|
|
638
|
+
* to all backends instead of being treated as "missing file → re-create".
|
|
639
|
+
*/
|
|
640
|
+
async deleteFile(path) {
|
|
641
|
+
this.assertNotDisposed();
|
|
642
|
+
const normalizedPath = path.startsWith("/") ? path : "/" + path;
|
|
643
|
+
const tombstonePath = `${DELETIONS_DIR}/${tombstoneFileName(normalizedPath)}`;
|
|
644
|
+
const tombstone = {
|
|
645
|
+
path: normalizedPath,
|
|
646
|
+
deletedAt: Date.now(),
|
|
647
|
+
deletedBy: this.primaryBackendId,
|
|
648
|
+
confirmedBy: [this.primaryBackendId]
|
|
649
|
+
};
|
|
650
|
+
await this.ensureDir(tombstonePath);
|
|
651
|
+
await this.cachedFS.writeFile(
|
|
652
|
+
tombstonePath,
|
|
653
|
+
new TextEncoder().encode(JSON.stringify(tombstone, null, 2))
|
|
654
|
+
);
|
|
655
|
+
try {
|
|
656
|
+
await this.cachedFS.unlink(normalizedPath);
|
|
657
|
+
} catch {
|
|
658
|
+
}
|
|
659
|
+
const versionPath = versionPathFor(normalizedPath);
|
|
660
|
+
try {
|
|
661
|
+
await this.cachedFS.unlink(versionPath);
|
|
662
|
+
} catch {
|
|
663
|
+
}
|
|
664
|
+
console.log(`[ConfigRepo] deleteFile: ${normalizedPath} (tombstone at ${tombstonePath})`);
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Read all tombstones from the primary backend.
|
|
668
|
+
*/
|
|
669
|
+
async readTombstones() {
|
|
670
|
+
try {
|
|
671
|
+
const entries = await this.cachedFS.readdir(DELETIONS_DIR);
|
|
672
|
+
const tombstones = [];
|
|
673
|
+
for (const entry of entries) {
|
|
674
|
+
if (!entry.endsWith(".json")) continue;
|
|
675
|
+
try {
|
|
676
|
+
const raw = await this.cachedFS.readFile(`${DELETIONS_DIR}/${entry}`);
|
|
677
|
+
const data = JSON.parse(new TextDecoder().decode(toUint8Array(raw)));
|
|
678
|
+
tombstones.push(data);
|
|
679
|
+
} catch {
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return tombstones;
|
|
683
|
+
} catch {
|
|
684
|
+
return [];
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Before sync: for each tombstone, delete the actual file on all replicas.
|
|
689
|
+
* This prevents bi-directional sync from copying the file back.
|
|
690
|
+
*/
|
|
691
|
+
async processTombstones() {
|
|
692
|
+
const tombstones = await this.readTombstones();
|
|
693
|
+
if (tombstones.length === 0) return;
|
|
694
|
+
console.log(`[ConfigRepo] processTombstones: ${tombstones.length} tombstone(s)`);
|
|
695
|
+
for (const tombstone of tombstones) {
|
|
696
|
+
try {
|
|
697
|
+
await this.cachedFS.unlink(tombstone.path);
|
|
698
|
+
} catch {
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
await this.cachedFS.unlink(versionPathFor(tombstone.path));
|
|
702
|
+
} catch {
|
|
703
|
+
}
|
|
704
|
+
for (const [replicaId, replica] of this.replicaBackends) {
|
|
705
|
+
try {
|
|
706
|
+
await replica.instance.unlink(tombstone.path);
|
|
707
|
+
} catch {
|
|
708
|
+
}
|
|
709
|
+
try {
|
|
710
|
+
await replica.instance.unlink(versionPathFor(tombstone.path));
|
|
711
|
+
} catch {
|
|
712
|
+
}
|
|
713
|
+
console.log(`[ConfigRepo] tombstone ${tombstone.path}: deleted on ${replicaId}`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* After sync: mark each tombstone as confirmed by all replica backends.
|
|
719
|
+
*/
|
|
720
|
+
async updateTombstoneConfirmations() {
|
|
721
|
+
const tombstones = await this.readTombstones();
|
|
722
|
+
if (tombstones.length === 0) return;
|
|
723
|
+
const backendsMeta = await this.getBackends();
|
|
724
|
+
const allBackendIds = backendsMeta?.backends.map((b) => b.id) ?? [this.primaryBackendId];
|
|
725
|
+
for (const tombstone of tombstones) {
|
|
726
|
+
const tombstonePath = `${DELETIONS_DIR}/${tombstoneFileName(tombstone.path)}`;
|
|
727
|
+
for (const replicaId of this.replicaBackends.keys()) {
|
|
728
|
+
if (!tombstone.confirmedBy.includes(replicaId)) {
|
|
729
|
+
tombstone.confirmedBy.push(replicaId);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
try {
|
|
733
|
+
await this.cachedFS.writeFile(
|
|
734
|
+
tombstonePath,
|
|
735
|
+
new TextEncoder().encode(JSON.stringify(tombstone, null, 2))
|
|
736
|
+
);
|
|
737
|
+
} catch {
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
console.log(`[ConfigRepo] updateTombstoneConfirmations: ${tombstones.length} tombstone(s) updated`);
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* GC: remove tombstones where all backends in backends.json have confirmed.
|
|
744
|
+
*/
|
|
745
|
+
async gcTombstones() {
|
|
746
|
+
const tombstones = await this.readTombstones();
|
|
747
|
+
if (tombstones.length === 0) return;
|
|
748
|
+
const backendsMeta = await this.getBackends();
|
|
749
|
+
const allBackendIds = backendsMeta?.backends.map((b) => b.id) ?? [this.primaryBackendId];
|
|
750
|
+
for (const tombstone of tombstones) {
|
|
751
|
+
const allConfirmed = allBackendIds.every((id) => tombstone.confirmedBy.includes(id));
|
|
752
|
+
if (allConfirmed) {
|
|
753
|
+
const tombstonePath = `${DELETIONS_DIR}/${tombstoneFileName(tombstone.path)}`;
|
|
754
|
+
try {
|
|
755
|
+
await this.cachedFS.unlink(tombstonePath);
|
|
756
|
+
console.log(`[ConfigRepo] gcTombstones: removed ${tombstonePath} (all ${allBackendIds.length} backends confirmed)`);
|
|
757
|
+
} catch {
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
660
762
|
/**
|
|
661
763
|
* Sync .meta/ files (backends.json) to all replica backends.
|
|
662
764
|
*
|
|
@@ -769,7 +871,7 @@ var ConfigRepo = class {
|
|
|
769
871
|
console.log(`[ConfigRepo] Creating replica backend: id=${desc.id}, type=${desc.type}`);
|
|
770
872
|
try {
|
|
771
873
|
const instance = await createBackend(desc);
|
|
772
|
-
const syncable = backendToSyncableFS(instance);
|
|
874
|
+
const syncable = backendToSyncableFS(instance, `${desc.type}(${desc.id})`);
|
|
773
875
|
this.replicaBackends.set(desc.id, { instance, syncable });
|
|
774
876
|
console.log(`[ConfigRepo] Replica ${desc.id} created successfully`);
|
|
775
877
|
} catch (err) {
|
|
@@ -986,21 +1088,7 @@ async function createConfigRepo(appId, options) {
|
|
|
986
1088
|
type: options.backendInfo.type,
|
|
987
1089
|
options: options.backendInfo.options
|
|
988
1090
|
});
|
|
989
|
-
const
|
|
990
|
-
let cacheStore;
|
|
991
|
-
const storeType = options.cache?.storeType ?? "MemoryCacheStore";
|
|
992
|
-
if (storeType === "IdbCacheStore") {
|
|
993
|
-
cacheStore = new zenCache.IdbCacheStore(options.cache?.storePrefix);
|
|
994
|
-
} else {
|
|
995
|
-
cacheStore = new zenCache.MemoryCacheStore();
|
|
996
|
-
}
|
|
997
|
-
const cachedFS = new zenCache.CachedFileSystem(
|
|
998
|
-
primaryInstance,
|
|
999
|
-
cacheStore,
|
|
1000
|
-
{
|
|
1001
|
-
ttlMs: options.cache?.ttlMs ?? 0
|
|
1002
|
-
}
|
|
1003
|
-
);
|
|
1091
|
+
const cachedFS = primaryInstance;
|
|
1004
1092
|
try {
|
|
1005
1093
|
const metaExists = await primaryInstance.exists(META_DIR);
|
|
1006
1094
|
console.log(`[createConfigRepo] /.meta/ exists: ${metaExists}`);
|
|
@@ -1015,6 +1103,7 @@ async function createConfigRepo(appId, options) {
|
|
|
1015
1103
|
const tempRepo = new ConfigRepo(
|
|
1016
1104
|
appId,
|
|
1017
1105
|
"",
|
|
1106
|
+
options.primaryBackendId,
|
|
1018
1107
|
cachedFS,
|
|
1019
1108
|
createSerializerChain(),
|
|
1020
1109
|
void 0
|
|
@@ -1071,6 +1160,7 @@ async function createConfigRepo(appId, options) {
|
|
|
1071
1160
|
const repo = new ConfigRepo(
|
|
1072
1161
|
appId,
|
|
1073
1162
|
nodeId,
|
|
1163
|
+
options.primaryBackendId,
|
|
1074
1164
|
cachedFS,
|
|
1075
1165
|
serializer,
|
|
1076
1166
|
options.onConflict
|
package/dist/index.mjs
CHANGED
|
@@ -176,8 +176,8 @@ async function ensureParentDir(absolutePath) {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
// src/adapters.ts
|
|
179
|
-
function backendToSyncableFS(backend) {
|
|
180
|
-
|
|
179
|
+
function backendToSyncableFS(backend, name) {
|
|
180
|
+
const syncable = {
|
|
181
181
|
async readdir(path) {
|
|
182
182
|
return backend.readdir(path);
|
|
183
183
|
},
|
|
@@ -203,8 +203,7 @@ function backendToSyncableFS(backend) {
|
|
|
203
203
|
async stat(path) {
|
|
204
204
|
const s = await backend.stat(path);
|
|
205
205
|
return {
|
|
206
|
-
|
|
207
|
-
isDirectory: typeof s.isDirectory === "function" ? () => s.isDirectory() : () => !!(s.mode && s.mode & 16384),
|
|
206
|
+
mode: typeof s.mode === "number" ? s.mode : void 0,
|
|
208
207
|
size: s.size ?? 0,
|
|
209
208
|
mtimeMs: typeof s.mtimeMs === "number" ? s.mtimeMs : s.mtime ? new Date(s.mtime).getTime() : 0
|
|
210
209
|
};
|
|
@@ -216,47 +215,14 @@ function backendToSyncableFS(backend) {
|
|
|
216
215
|
return backend.exists(path);
|
|
217
216
|
}
|
|
218
217
|
};
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (encoding) {
|
|
228
|
-
if (typeof data === "string") return data;
|
|
229
|
-
return new TextDecoder().decode(
|
|
230
|
-
data instanceof ArrayBuffer ? new Uint8Array(data) : data
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
if (typeof data === "string") return Buffer.from(data);
|
|
234
|
-
if (data instanceof ArrayBuffer) return Buffer.from(new Uint8Array(data));
|
|
235
|
-
return Buffer.from(data);
|
|
236
|
-
},
|
|
237
|
-
async writeFile(path, data) {
|
|
238
|
-
return cached.writeFile(path, data);
|
|
239
|
-
},
|
|
240
|
-
async unlink(path) {
|
|
241
|
-
return cached.unlink(path);
|
|
242
|
-
},
|
|
243
|
-
async stat(path) {
|
|
244
|
-
const s = await cached.stat(path);
|
|
245
|
-
const isDir = typeof s.isDirectory === "function" ? s.isDirectory() : typeof s.isDirectory === "boolean" ? s.isDirectory : s.mode !== void 0 && (s.mode & 61440) === 16384;
|
|
246
|
-
return {
|
|
247
|
-
isFile: () => !isDir,
|
|
248
|
-
isDirectory: () => isDir,
|
|
249
|
-
size: s.size,
|
|
250
|
-
mtimeMs: s.mtimeMs ?? s.mtime
|
|
251
|
-
};
|
|
252
|
-
},
|
|
253
|
-
async mkdir(path, options) {
|
|
254
|
-
return cached.mkdir(path, options);
|
|
255
|
-
},
|
|
256
|
-
async exists(path) {
|
|
257
|
-
return cached.exists(path);
|
|
258
|
-
}
|
|
259
|
-
};
|
|
218
|
+
if (name) {
|
|
219
|
+
syncable.backendName = name;
|
|
220
|
+
} else if (backend.backendName) {
|
|
221
|
+
syncable.backendName = backend.backendName;
|
|
222
|
+
} else {
|
|
223
|
+
syncable.backendName = backend.constructor.name || "Backend";
|
|
224
|
+
}
|
|
225
|
+
return syncable;
|
|
260
226
|
}
|
|
261
227
|
|
|
262
228
|
// src/backend-registry.ts
|
|
@@ -316,12 +282,10 @@ async function wrapZenFSFileSystem(config) {
|
|
|
316
282
|
},
|
|
317
283
|
async stat(path, ..._args) {
|
|
318
284
|
const st = await isolatedFS.stat(path);
|
|
319
|
-
const isDir = typeof st.isDirectory === "function" ? st.isDirectory() : st.mode !== void 0 && (st.mode & 61440) === 16384;
|
|
320
285
|
return {
|
|
321
|
-
|
|
322
|
-
isDirectory: () => isDir,
|
|
286
|
+
mode: typeof st.mode === "number" ? st.mode : void 0,
|
|
323
287
|
size: st.size,
|
|
324
|
-
|
|
288
|
+
mtimeMs: st.mtimeMs ?? st.mtime ?? 0
|
|
325
289
|
};
|
|
326
290
|
},
|
|
327
291
|
async exists(path) {
|
|
@@ -436,8 +400,12 @@ async function verifyOrRepairVersion(fs, configFilePath, author) {
|
|
|
436
400
|
var META_DIR = "/.meta";
|
|
437
401
|
var BACKENDS_FILE = `${META_DIR}/backends.json`;
|
|
438
402
|
var CONFLICTS_DIR = `${META_DIR}/.conflicts`;
|
|
403
|
+
var DELETIONS_DIR = `${META_DIR}/.deleted`;
|
|
439
404
|
var NODES_DIR = "/nodes";
|
|
440
405
|
var NODE_ID_FILE = `${NODES_DIR}/.node-id`;
|
|
406
|
+
function tombstoneFileName(filePath) {
|
|
407
|
+
return filePath.replace(/^\//, "").replace(/\//g, "__").replace(/\./g, "++") + ".json";
|
|
408
|
+
}
|
|
441
409
|
var ConfigRepo = class {
|
|
442
410
|
appId;
|
|
443
411
|
nodeId;
|
|
@@ -453,15 +421,17 @@ var ConfigRepo = class {
|
|
|
453
421
|
onConflictCallback;
|
|
454
422
|
disposed = false;
|
|
455
423
|
configCache = /* @__PURE__ */ new Map();
|
|
456
|
-
|
|
424
|
+
primaryBackendId;
|
|
425
|
+
constructor(appId, nodeId, primaryBackendId, cachedFS, serializer, onConflict) {
|
|
457
426
|
this.appId = appId;
|
|
458
427
|
this.nodeId = nodeId;
|
|
428
|
+
this.primaryBackendId = primaryBackendId;
|
|
459
429
|
this.cachedFS = cachedFS;
|
|
460
430
|
this.serializer = serializer;
|
|
461
431
|
this.syncEngine = new ZenFSSync();
|
|
462
432
|
this.replicaBackends = /* @__PURE__ */ new Map();
|
|
463
433
|
this.onConflictCallback = onConflict;
|
|
464
|
-
this.fullFS =
|
|
434
|
+
this.fullFS = backendToSyncableFS(cachedFS, primaryBackendId);
|
|
465
435
|
this.fs = createChrootFS(cachedFS, `/${appId}`);
|
|
466
436
|
this.rootFS = createChrootFS(cachedFS, "/");
|
|
467
437
|
}
|
|
@@ -605,9 +575,141 @@ var ConfigRepo = class {
|
|
|
605
575
|
// -----------------------------------------------------------------------
|
|
606
576
|
async flush() {
|
|
607
577
|
this.assertNotDisposed();
|
|
578
|
+
await this.processTombstones();
|
|
608
579
|
const resultsMap = await this.syncEngine.syncAll();
|
|
580
|
+
await this.updateTombstoneConfirmations();
|
|
581
|
+
await this.gcTombstones();
|
|
609
582
|
return Array.from(resultsMap.values());
|
|
610
583
|
}
|
|
584
|
+
// -----------------------------------------------------------------------
|
|
585
|
+
// Tombstone (Deletion Tracking)
|
|
586
|
+
// -----------------------------------------------------------------------
|
|
587
|
+
/**
|
|
588
|
+
* Delete a file and write a tombstone so the deletion propagates
|
|
589
|
+
* to all backends instead of being treated as "missing file → re-create".
|
|
590
|
+
*/
|
|
591
|
+
async deleteFile(path) {
|
|
592
|
+
this.assertNotDisposed();
|
|
593
|
+
const normalizedPath = path.startsWith("/") ? path : "/" + path;
|
|
594
|
+
const tombstonePath = `${DELETIONS_DIR}/${tombstoneFileName(normalizedPath)}`;
|
|
595
|
+
const tombstone = {
|
|
596
|
+
path: normalizedPath,
|
|
597
|
+
deletedAt: Date.now(),
|
|
598
|
+
deletedBy: this.primaryBackendId,
|
|
599
|
+
confirmedBy: [this.primaryBackendId]
|
|
600
|
+
};
|
|
601
|
+
await this.ensureDir(tombstonePath);
|
|
602
|
+
await this.cachedFS.writeFile(
|
|
603
|
+
tombstonePath,
|
|
604
|
+
new TextEncoder().encode(JSON.stringify(tombstone, null, 2))
|
|
605
|
+
);
|
|
606
|
+
try {
|
|
607
|
+
await this.cachedFS.unlink(normalizedPath);
|
|
608
|
+
} catch {
|
|
609
|
+
}
|
|
610
|
+
const versionPath = versionPathFor(normalizedPath);
|
|
611
|
+
try {
|
|
612
|
+
await this.cachedFS.unlink(versionPath);
|
|
613
|
+
} catch {
|
|
614
|
+
}
|
|
615
|
+
console.log(`[ConfigRepo] deleteFile: ${normalizedPath} (tombstone at ${tombstonePath})`);
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Read all tombstones from the primary backend.
|
|
619
|
+
*/
|
|
620
|
+
async readTombstones() {
|
|
621
|
+
try {
|
|
622
|
+
const entries = await this.cachedFS.readdir(DELETIONS_DIR);
|
|
623
|
+
const tombstones = [];
|
|
624
|
+
for (const entry of entries) {
|
|
625
|
+
if (!entry.endsWith(".json")) continue;
|
|
626
|
+
try {
|
|
627
|
+
const raw = await this.cachedFS.readFile(`${DELETIONS_DIR}/${entry}`);
|
|
628
|
+
const data = JSON.parse(new TextDecoder().decode(toUint8Array(raw)));
|
|
629
|
+
tombstones.push(data);
|
|
630
|
+
} catch {
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return tombstones;
|
|
634
|
+
} catch {
|
|
635
|
+
return [];
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Before sync: for each tombstone, delete the actual file on all replicas.
|
|
640
|
+
* This prevents bi-directional sync from copying the file back.
|
|
641
|
+
*/
|
|
642
|
+
async processTombstones() {
|
|
643
|
+
const tombstones = await this.readTombstones();
|
|
644
|
+
if (tombstones.length === 0) return;
|
|
645
|
+
console.log(`[ConfigRepo] processTombstones: ${tombstones.length} tombstone(s)`);
|
|
646
|
+
for (const tombstone of tombstones) {
|
|
647
|
+
try {
|
|
648
|
+
await this.cachedFS.unlink(tombstone.path);
|
|
649
|
+
} catch {
|
|
650
|
+
}
|
|
651
|
+
try {
|
|
652
|
+
await this.cachedFS.unlink(versionPathFor(tombstone.path));
|
|
653
|
+
} catch {
|
|
654
|
+
}
|
|
655
|
+
for (const [replicaId, replica] of this.replicaBackends) {
|
|
656
|
+
try {
|
|
657
|
+
await replica.instance.unlink(tombstone.path);
|
|
658
|
+
} catch {
|
|
659
|
+
}
|
|
660
|
+
try {
|
|
661
|
+
await replica.instance.unlink(versionPathFor(tombstone.path));
|
|
662
|
+
} catch {
|
|
663
|
+
}
|
|
664
|
+
console.log(`[ConfigRepo] tombstone ${tombstone.path}: deleted on ${replicaId}`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* After sync: mark each tombstone as confirmed by all replica backends.
|
|
670
|
+
*/
|
|
671
|
+
async updateTombstoneConfirmations() {
|
|
672
|
+
const tombstones = await this.readTombstones();
|
|
673
|
+
if (tombstones.length === 0) return;
|
|
674
|
+
const backendsMeta = await this.getBackends();
|
|
675
|
+
const allBackendIds = backendsMeta?.backends.map((b) => b.id) ?? [this.primaryBackendId];
|
|
676
|
+
for (const tombstone of tombstones) {
|
|
677
|
+
const tombstonePath = `${DELETIONS_DIR}/${tombstoneFileName(tombstone.path)}`;
|
|
678
|
+
for (const replicaId of this.replicaBackends.keys()) {
|
|
679
|
+
if (!tombstone.confirmedBy.includes(replicaId)) {
|
|
680
|
+
tombstone.confirmedBy.push(replicaId);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
try {
|
|
684
|
+
await this.cachedFS.writeFile(
|
|
685
|
+
tombstonePath,
|
|
686
|
+
new TextEncoder().encode(JSON.stringify(tombstone, null, 2))
|
|
687
|
+
);
|
|
688
|
+
} catch {
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
console.log(`[ConfigRepo] updateTombstoneConfirmations: ${tombstones.length} tombstone(s) updated`);
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* GC: remove tombstones where all backends in backends.json have confirmed.
|
|
695
|
+
*/
|
|
696
|
+
async gcTombstones() {
|
|
697
|
+
const tombstones = await this.readTombstones();
|
|
698
|
+
if (tombstones.length === 0) return;
|
|
699
|
+
const backendsMeta = await this.getBackends();
|
|
700
|
+
const allBackendIds = backendsMeta?.backends.map((b) => b.id) ?? [this.primaryBackendId];
|
|
701
|
+
for (const tombstone of tombstones) {
|
|
702
|
+
const allConfirmed = allBackendIds.every((id) => tombstone.confirmedBy.includes(id));
|
|
703
|
+
if (allConfirmed) {
|
|
704
|
+
const tombstonePath = `${DELETIONS_DIR}/${tombstoneFileName(tombstone.path)}`;
|
|
705
|
+
try {
|
|
706
|
+
await this.cachedFS.unlink(tombstonePath);
|
|
707
|
+
console.log(`[ConfigRepo] gcTombstones: removed ${tombstonePath} (all ${allBackendIds.length} backends confirmed)`);
|
|
708
|
+
} catch {
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
611
713
|
/**
|
|
612
714
|
* Sync .meta/ files (backends.json) to all replica backends.
|
|
613
715
|
*
|
|
@@ -720,7 +822,7 @@ var ConfigRepo = class {
|
|
|
720
822
|
console.log(`[ConfigRepo] Creating replica backend: id=${desc.id}, type=${desc.type}`);
|
|
721
823
|
try {
|
|
722
824
|
const instance = await createBackend(desc);
|
|
723
|
-
const syncable = backendToSyncableFS(instance);
|
|
825
|
+
const syncable = backendToSyncableFS(instance, `${desc.type}(${desc.id})`);
|
|
724
826
|
this.replicaBackends.set(desc.id, { instance, syncable });
|
|
725
827
|
console.log(`[ConfigRepo] Replica ${desc.id} created successfully`);
|
|
726
828
|
} catch (err) {
|
|
@@ -937,21 +1039,7 @@ async function createConfigRepo(appId, options) {
|
|
|
937
1039
|
type: options.backendInfo.type,
|
|
938
1040
|
options: options.backendInfo.options
|
|
939
1041
|
});
|
|
940
|
-
const
|
|
941
|
-
let cacheStore;
|
|
942
|
-
const storeType = options.cache?.storeType ?? "MemoryCacheStore";
|
|
943
|
-
if (storeType === "IdbCacheStore") {
|
|
944
|
-
cacheStore = new zenCache.IdbCacheStore(options.cache?.storePrefix);
|
|
945
|
-
} else {
|
|
946
|
-
cacheStore = new zenCache.MemoryCacheStore();
|
|
947
|
-
}
|
|
948
|
-
const cachedFS = new zenCache.CachedFileSystem(
|
|
949
|
-
primaryInstance,
|
|
950
|
-
cacheStore,
|
|
951
|
-
{
|
|
952
|
-
ttlMs: options.cache?.ttlMs ?? 0
|
|
953
|
-
}
|
|
954
|
-
);
|
|
1042
|
+
const cachedFS = primaryInstance;
|
|
955
1043
|
try {
|
|
956
1044
|
const metaExists = await primaryInstance.exists(META_DIR);
|
|
957
1045
|
console.log(`[createConfigRepo] /.meta/ exists: ${metaExists}`);
|
|
@@ -966,6 +1054,7 @@ async function createConfigRepo(appId, options) {
|
|
|
966
1054
|
const tempRepo = new ConfigRepo(
|
|
967
1055
|
appId,
|
|
968
1056
|
"",
|
|
1057
|
+
options.primaryBackendId,
|
|
969
1058
|
cachedFS,
|
|
970
1059
|
createSerializerChain(),
|
|
971
1060
|
void 0
|
|
@@ -1022,6 +1111,7 @@ async function createConfigRepo(appId, options) {
|
|
|
1022
1111
|
const repo = new ConfigRepo(
|
|
1023
1112
|
appId,
|
|
1024
1113
|
nodeId,
|
|
1114
|
+
options.primaryBackendId,
|
|
1025
1115
|
cachedFS,
|
|
1026
1116
|
serializer,
|
|
1027
1117
|
options.onConflict
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zen-fs-config",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.27",
|
|
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",
|
|
@@ -49,7 +49,6 @@
|
|
|
49
49
|
"typescript": "^5.9.3",
|
|
50
50
|
"vitest": "^1.6.1",
|
|
51
51
|
"zen-fs-cache": "^1.0.1",
|
|
52
|
-
"zen-fs-sync": "^0.
|
|
53
|
-
}
|
|
54
|
-
"dependencies": {}
|
|
52
|
+
"zen-fs-sync": "^0.2.2"
|
|
53
|
+
}
|
|
55
54
|
}
|