zen-fs-config 0.3.25 → 0.3.28
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 +80 -49
- package/dist/index.d.ts +80 -49
- package/dist/index.js +148 -74
- package/dist/index.mjs +148 -74
- 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,6 +272,7 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
222
272
|
private onConflictCallback?;
|
|
223
273
|
private disposed;
|
|
224
274
|
private configCache;
|
|
275
|
+
private readonly primaryBackendId;
|
|
225
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;
|
|
@@ -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,6 +272,7 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
222
272
|
private onConflictCallback?;
|
|
223
273
|
private disposed;
|
|
224
274
|
private configCache;
|
|
275
|
+
private readonly primaryBackendId;
|
|
225
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;
|
|
@@ -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
|
@@ -150,14 +150,10 @@ function createChrootFS(inner, root) {
|
|
|
150
150
|
},
|
|
151
151
|
async stat(path) {
|
|
152
152
|
const s = await inner.stat(rp(path));
|
|
153
|
-
if (typeof s.isFile === "function" && typeof s.isDirectory === "function") {
|
|
154
|
-
return s;
|
|
155
|
-
}
|
|
156
|
-
const isDir = typeof s.isDirectory === "function" ? s.isDirectory() : typeof s.isDirectory === "boolean" ? s.isDirectory : s.mode !== void 0 && (s.mode & 61440) === 16384;
|
|
157
153
|
return {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
154
|
+
mode: typeof s.mode === "number" ? s.mode : void 0,
|
|
155
|
+
size: s.size ?? 0,
|
|
156
|
+
mtimeMs: s.mtimeMs ?? s.mtime ?? 0
|
|
161
157
|
};
|
|
162
158
|
},
|
|
163
159
|
async access(path) {
|
|
@@ -252,8 +248,7 @@ function backendToSyncableFS(backend, name) {
|
|
|
252
248
|
async stat(path) {
|
|
253
249
|
const s = await backend.stat(path);
|
|
254
250
|
return {
|
|
255
|
-
|
|
256
|
-
isDirectory: typeof s.isDirectory === "function" ? () => s.isDirectory() : () => !!(s.mode && s.mode & 16384),
|
|
251
|
+
mode: typeof s.mode === "number" ? s.mode : void 0,
|
|
257
252
|
size: s.size ?? 0,
|
|
258
253
|
mtimeMs: typeof s.mtimeMs === "number" ? s.mtimeMs : s.mtime ? new Date(s.mtime).getTime() : 0
|
|
259
254
|
};
|
|
@@ -274,49 +269,6 @@ function backendToSyncableFS(backend, name) {
|
|
|
274
269
|
}
|
|
275
270
|
return syncable;
|
|
276
271
|
}
|
|
277
|
-
function cachedFSToSyncableFS(cached, name) {
|
|
278
|
-
const syncable = {
|
|
279
|
-
async readdir(path) {
|
|
280
|
-
return cached.readdir(path);
|
|
281
|
-
},
|
|
282
|
-
async readFile(path, encoding) {
|
|
283
|
-
const data = await cached.readFile(path);
|
|
284
|
-
if (encoding) {
|
|
285
|
-
if (typeof data === "string") return data;
|
|
286
|
-
return new TextDecoder().decode(
|
|
287
|
-
data instanceof ArrayBuffer ? new Uint8Array(data) : data
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
if (typeof data === "string") return Buffer.from(data);
|
|
291
|
-
if (data instanceof ArrayBuffer) return Buffer.from(new Uint8Array(data));
|
|
292
|
-
return Buffer.from(data);
|
|
293
|
-
},
|
|
294
|
-
async writeFile(path, data) {
|
|
295
|
-
return cached.writeFile(path, data);
|
|
296
|
-
},
|
|
297
|
-
async unlink(path) {
|
|
298
|
-
return cached.unlink(path);
|
|
299
|
-
},
|
|
300
|
-
async stat(path) {
|
|
301
|
-
const s = await cached.stat(path);
|
|
302
|
-
const isDir = typeof s.isDirectory === "function" ? s.isDirectory() : typeof s.isDirectory === "boolean" ? s.isDirectory : s.mode !== void 0 && (s.mode & 61440) === 16384;
|
|
303
|
-
return {
|
|
304
|
-
isFile: () => !isDir,
|
|
305
|
-
isDirectory: () => isDir,
|
|
306
|
-
size: s.size,
|
|
307
|
-
mtimeMs: s.mtimeMs ?? s.mtime
|
|
308
|
-
};
|
|
309
|
-
},
|
|
310
|
-
async mkdir(path, options) {
|
|
311
|
-
return cached.mkdir(path, options);
|
|
312
|
-
},
|
|
313
|
-
async exists(path) {
|
|
314
|
-
return cached.exists(path);
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
syncable.backendName = name || "CachedFS";
|
|
318
|
-
return syncable;
|
|
319
|
-
}
|
|
320
272
|
|
|
321
273
|
// src/backend-registry.ts
|
|
322
274
|
var registry = /* @__PURE__ */ new Map();
|
|
@@ -375,12 +327,10 @@ async function wrapZenFSFileSystem(config) {
|
|
|
375
327
|
},
|
|
376
328
|
async stat(path, ..._args) {
|
|
377
329
|
const st = await isolatedFS.stat(path);
|
|
378
|
-
const isDir = typeof st.isDirectory === "function" ? st.isDirectory() : st.mode !== void 0 && (st.mode & 61440) === 16384;
|
|
379
330
|
return {
|
|
380
|
-
|
|
381
|
-
isDirectory: () => isDir,
|
|
331
|
+
mode: typeof st.mode === "number" ? st.mode : void 0,
|
|
382
332
|
size: st.size,
|
|
383
|
-
|
|
333
|
+
mtimeMs: st.mtimeMs ?? st.mtime ?? 0
|
|
384
334
|
};
|
|
385
335
|
},
|
|
386
336
|
async exists(path) {
|
|
@@ -495,8 +445,12 @@ async function verifyOrRepairVersion(fs, configFilePath, author) {
|
|
|
495
445
|
var META_DIR = "/.meta";
|
|
496
446
|
var BACKENDS_FILE = `${META_DIR}/backends.json`;
|
|
497
447
|
var CONFLICTS_DIR = `${META_DIR}/.conflicts`;
|
|
448
|
+
var DELETIONS_DIR = `${META_DIR}/.deleted`;
|
|
498
449
|
var NODES_DIR = "/nodes";
|
|
499
450
|
var NODE_ID_FILE = `${NODES_DIR}/.node-id`;
|
|
451
|
+
function tombstoneFileName(filePath) {
|
|
452
|
+
return filePath.replace(/^\//, "").replace(/\//g, "__").replace(/\./g, "++") + ".json";
|
|
453
|
+
}
|
|
500
454
|
var ConfigRepo = class {
|
|
501
455
|
appId;
|
|
502
456
|
nodeId;
|
|
@@ -512,15 +466,17 @@ var ConfigRepo = class {
|
|
|
512
466
|
onConflictCallback;
|
|
513
467
|
disposed = false;
|
|
514
468
|
configCache = /* @__PURE__ */ new Map();
|
|
469
|
+
primaryBackendId;
|
|
515
470
|
constructor(appId, nodeId, primaryBackendId, cachedFS, serializer, onConflict) {
|
|
516
471
|
this.appId = appId;
|
|
517
472
|
this.nodeId = nodeId;
|
|
473
|
+
this.primaryBackendId = primaryBackendId;
|
|
518
474
|
this.cachedFS = cachedFS;
|
|
519
475
|
this.serializer = serializer;
|
|
520
476
|
this.syncEngine = new import_zen_fs_sync.ZenFSSync();
|
|
521
477
|
this.replicaBackends = /* @__PURE__ */ new Map();
|
|
522
478
|
this.onConflictCallback = onConflict;
|
|
523
|
-
this.fullFS =
|
|
479
|
+
this.fullFS = backendToSyncableFS(cachedFS, primaryBackendId);
|
|
524
480
|
this.fs = createChrootFS(cachedFS, `/${appId}`);
|
|
525
481
|
this.rootFS = createChrootFS(cachedFS, "/");
|
|
526
482
|
}
|
|
@@ -664,9 +620,141 @@ var ConfigRepo = class {
|
|
|
664
620
|
// -----------------------------------------------------------------------
|
|
665
621
|
async flush() {
|
|
666
622
|
this.assertNotDisposed();
|
|
623
|
+
await this.processTombstones();
|
|
667
624
|
const resultsMap = await this.syncEngine.syncAll();
|
|
625
|
+
await this.updateTombstoneConfirmations();
|
|
626
|
+
await this.gcTombstones();
|
|
668
627
|
return Array.from(resultsMap.values());
|
|
669
628
|
}
|
|
629
|
+
// -----------------------------------------------------------------------
|
|
630
|
+
// Tombstone (Deletion Tracking)
|
|
631
|
+
// -----------------------------------------------------------------------
|
|
632
|
+
/**
|
|
633
|
+
* Delete a file and write a tombstone so the deletion propagates
|
|
634
|
+
* to all backends instead of being treated as "missing file → re-create".
|
|
635
|
+
*/
|
|
636
|
+
async deleteFile(path) {
|
|
637
|
+
this.assertNotDisposed();
|
|
638
|
+
const normalizedPath = path.startsWith("/") ? path : "/" + path;
|
|
639
|
+
const tombstonePath = `${DELETIONS_DIR}/${tombstoneFileName(normalizedPath)}`;
|
|
640
|
+
const tombstone = {
|
|
641
|
+
path: normalizedPath,
|
|
642
|
+
deletedAt: Date.now(),
|
|
643
|
+
deletedBy: this.primaryBackendId,
|
|
644
|
+
confirmedBy: [this.primaryBackendId]
|
|
645
|
+
};
|
|
646
|
+
await this.ensureDir(tombstonePath);
|
|
647
|
+
await this.cachedFS.writeFile(
|
|
648
|
+
tombstonePath,
|
|
649
|
+
new TextEncoder().encode(JSON.stringify(tombstone, null, 2))
|
|
650
|
+
);
|
|
651
|
+
try {
|
|
652
|
+
await this.cachedFS.unlink(normalizedPath);
|
|
653
|
+
} catch {
|
|
654
|
+
}
|
|
655
|
+
const versionPath = versionPathFor(normalizedPath);
|
|
656
|
+
try {
|
|
657
|
+
await this.cachedFS.unlink(versionPath);
|
|
658
|
+
} catch {
|
|
659
|
+
}
|
|
660
|
+
console.log(`[ConfigRepo] deleteFile: ${normalizedPath} (tombstone at ${tombstonePath})`);
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Read all tombstones from the primary backend.
|
|
664
|
+
*/
|
|
665
|
+
async readTombstones() {
|
|
666
|
+
try {
|
|
667
|
+
const entries = await this.cachedFS.readdir(DELETIONS_DIR);
|
|
668
|
+
const tombstones = [];
|
|
669
|
+
for (const entry of entries) {
|
|
670
|
+
if (!entry.endsWith(".json")) continue;
|
|
671
|
+
try {
|
|
672
|
+
const raw = await this.cachedFS.readFile(`${DELETIONS_DIR}/${entry}`);
|
|
673
|
+
const data = JSON.parse(new TextDecoder().decode(toUint8Array(raw)));
|
|
674
|
+
tombstones.push(data);
|
|
675
|
+
} catch {
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return tombstones;
|
|
679
|
+
} catch {
|
|
680
|
+
return [];
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Before sync: for each tombstone, delete the actual file on all replicas.
|
|
685
|
+
* This prevents bi-directional sync from copying the file back.
|
|
686
|
+
*/
|
|
687
|
+
async processTombstones() {
|
|
688
|
+
const tombstones = await this.readTombstones();
|
|
689
|
+
if (tombstones.length === 0) return;
|
|
690
|
+
console.log(`[ConfigRepo] processTombstones: ${tombstones.length} tombstone(s)`);
|
|
691
|
+
for (const tombstone of tombstones) {
|
|
692
|
+
try {
|
|
693
|
+
await this.cachedFS.unlink(tombstone.path);
|
|
694
|
+
} catch {
|
|
695
|
+
}
|
|
696
|
+
try {
|
|
697
|
+
await this.cachedFS.unlink(versionPathFor(tombstone.path));
|
|
698
|
+
} catch {
|
|
699
|
+
}
|
|
700
|
+
for (const [replicaId, replica] of this.replicaBackends) {
|
|
701
|
+
try {
|
|
702
|
+
await replica.instance.unlink(tombstone.path);
|
|
703
|
+
} catch {
|
|
704
|
+
}
|
|
705
|
+
try {
|
|
706
|
+
await replica.instance.unlink(versionPathFor(tombstone.path));
|
|
707
|
+
} catch {
|
|
708
|
+
}
|
|
709
|
+
console.log(`[ConfigRepo] tombstone ${tombstone.path}: deleted on ${replicaId}`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* After sync: mark each tombstone as confirmed by all replica backends.
|
|
715
|
+
*/
|
|
716
|
+
async updateTombstoneConfirmations() {
|
|
717
|
+
const tombstones = await this.readTombstones();
|
|
718
|
+
if (tombstones.length === 0) return;
|
|
719
|
+
const backendsMeta = await this.getBackends();
|
|
720
|
+
const allBackendIds = backendsMeta?.backends.map((b) => b.id) ?? [this.primaryBackendId];
|
|
721
|
+
for (const tombstone of tombstones) {
|
|
722
|
+
const tombstonePath = `${DELETIONS_DIR}/${tombstoneFileName(tombstone.path)}`;
|
|
723
|
+
for (const replicaId of this.replicaBackends.keys()) {
|
|
724
|
+
if (!tombstone.confirmedBy.includes(replicaId)) {
|
|
725
|
+
tombstone.confirmedBy.push(replicaId);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
try {
|
|
729
|
+
await this.cachedFS.writeFile(
|
|
730
|
+
tombstonePath,
|
|
731
|
+
new TextEncoder().encode(JSON.stringify(tombstone, null, 2))
|
|
732
|
+
);
|
|
733
|
+
} catch {
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
console.log(`[ConfigRepo] updateTombstoneConfirmations: ${tombstones.length} tombstone(s) updated`);
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* GC: remove tombstones where all backends in backends.json have confirmed.
|
|
740
|
+
*/
|
|
741
|
+
async gcTombstones() {
|
|
742
|
+
const tombstones = await this.readTombstones();
|
|
743
|
+
if (tombstones.length === 0) return;
|
|
744
|
+
const backendsMeta = await this.getBackends();
|
|
745
|
+
const allBackendIds = backendsMeta?.backends.map((b) => b.id) ?? [this.primaryBackendId];
|
|
746
|
+
for (const tombstone of tombstones) {
|
|
747
|
+
const allConfirmed = allBackendIds.every((id) => tombstone.confirmedBy.includes(id));
|
|
748
|
+
if (allConfirmed) {
|
|
749
|
+
const tombstonePath = `${DELETIONS_DIR}/${tombstoneFileName(tombstone.path)}`;
|
|
750
|
+
try {
|
|
751
|
+
await this.cachedFS.unlink(tombstonePath);
|
|
752
|
+
console.log(`[ConfigRepo] gcTombstones: removed ${tombstonePath} (all ${allBackendIds.length} backends confirmed)`);
|
|
753
|
+
} catch {
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
670
758
|
/**
|
|
671
759
|
* Sync .meta/ files (backends.json) to all replica backends.
|
|
672
760
|
*
|
|
@@ -927,9 +1015,9 @@ var ConfigRepo = class {
|
|
|
927
1015
|
const fullPath = current === "/" ? `/${entry}` : `${current}/${entry}`;
|
|
928
1016
|
try {
|
|
929
1017
|
const stat = await this.cachedFS.stat(fullPath);
|
|
930
|
-
if (stat.
|
|
1018
|
+
if (stat.mode !== void 0 && (stat.mode & 16384) === 16384) {
|
|
931
1019
|
stack.push(fullPath);
|
|
932
|
-
} else
|
|
1020
|
+
} else {
|
|
933
1021
|
results.push(fullPath);
|
|
934
1022
|
}
|
|
935
1023
|
} catch {
|
|
@@ -996,21 +1084,7 @@ async function createConfigRepo(appId, options) {
|
|
|
996
1084
|
type: options.backendInfo.type,
|
|
997
1085
|
options: options.backendInfo.options
|
|
998
1086
|
});
|
|
999
|
-
const
|
|
1000
|
-
let cacheStore;
|
|
1001
|
-
const storeType = options.cache?.storeType ?? "MemoryCacheStore";
|
|
1002
|
-
if (storeType === "IdbCacheStore") {
|
|
1003
|
-
cacheStore = new zenCache.IdbCacheStore(options.cache?.storePrefix);
|
|
1004
|
-
} else {
|
|
1005
|
-
cacheStore = new zenCache.MemoryCacheStore();
|
|
1006
|
-
}
|
|
1007
|
-
const cachedFS = new zenCache.CachedFileSystem(
|
|
1008
|
-
primaryInstance,
|
|
1009
|
-
cacheStore,
|
|
1010
|
-
{
|
|
1011
|
-
ttlMs: options.cache?.ttlMs ?? 0
|
|
1012
|
-
}
|
|
1013
|
-
);
|
|
1087
|
+
const cachedFS = primaryInstance;
|
|
1014
1088
|
try {
|
|
1015
1089
|
const metaExists = await primaryInstance.exists(META_DIR);
|
|
1016
1090
|
console.log(`[createConfigRepo] /.meta/ exists: ${metaExists}`);
|
package/dist/index.mjs
CHANGED
|
@@ -101,14 +101,10 @@ function createChrootFS(inner, root) {
|
|
|
101
101
|
},
|
|
102
102
|
async stat(path) {
|
|
103
103
|
const s = await inner.stat(rp(path));
|
|
104
|
-
if (typeof s.isFile === "function" && typeof s.isDirectory === "function") {
|
|
105
|
-
return s;
|
|
106
|
-
}
|
|
107
|
-
const isDir = typeof s.isDirectory === "function" ? s.isDirectory() : typeof s.isDirectory === "boolean" ? s.isDirectory : s.mode !== void 0 && (s.mode & 61440) === 16384;
|
|
108
104
|
return {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
105
|
+
mode: typeof s.mode === "number" ? s.mode : void 0,
|
|
106
|
+
size: s.size ?? 0,
|
|
107
|
+
mtimeMs: s.mtimeMs ?? s.mtime ?? 0
|
|
112
108
|
};
|
|
113
109
|
},
|
|
114
110
|
async access(path) {
|
|
@@ -203,8 +199,7 @@ function backendToSyncableFS(backend, name) {
|
|
|
203
199
|
async stat(path) {
|
|
204
200
|
const s = await backend.stat(path);
|
|
205
201
|
return {
|
|
206
|
-
|
|
207
|
-
isDirectory: typeof s.isDirectory === "function" ? () => s.isDirectory() : () => !!(s.mode && s.mode & 16384),
|
|
202
|
+
mode: typeof s.mode === "number" ? s.mode : void 0,
|
|
208
203
|
size: s.size ?? 0,
|
|
209
204
|
mtimeMs: typeof s.mtimeMs === "number" ? s.mtimeMs : s.mtime ? new Date(s.mtime).getTime() : 0
|
|
210
205
|
};
|
|
@@ -225,49 +220,6 @@ function backendToSyncableFS(backend, name) {
|
|
|
225
220
|
}
|
|
226
221
|
return syncable;
|
|
227
222
|
}
|
|
228
|
-
function cachedFSToSyncableFS(cached, name) {
|
|
229
|
-
const syncable = {
|
|
230
|
-
async readdir(path) {
|
|
231
|
-
return cached.readdir(path);
|
|
232
|
-
},
|
|
233
|
-
async readFile(path, encoding) {
|
|
234
|
-
const data = await cached.readFile(path);
|
|
235
|
-
if (encoding) {
|
|
236
|
-
if (typeof data === "string") return data;
|
|
237
|
-
return new TextDecoder().decode(
|
|
238
|
-
data instanceof ArrayBuffer ? new Uint8Array(data) : data
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
if (typeof data === "string") return Buffer.from(data);
|
|
242
|
-
if (data instanceof ArrayBuffer) return Buffer.from(new Uint8Array(data));
|
|
243
|
-
return Buffer.from(data);
|
|
244
|
-
},
|
|
245
|
-
async writeFile(path, data) {
|
|
246
|
-
return cached.writeFile(path, data);
|
|
247
|
-
},
|
|
248
|
-
async unlink(path) {
|
|
249
|
-
return cached.unlink(path);
|
|
250
|
-
},
|
|
251
|
-
async stat(path) {
|
|
252
|
-
const s = await cached.stat(path);
|
|
253
|
-
const isDir = typeof s.isDirectory === "function" ? s.isDirectory() : typeof s.isDirectory === "boolean" ? s.isDirectory : s.mode !== void 0 && (s.mode & 61440) === 16384;
|
|
254
|
-
return {
|
|
255
|
-
isFile: () => !isDir,
|
|
256
|
-
isDirectory: () => isDir,
|
|
257
|
-
size: s.size,
|
|
258
|
-
mtimeMs: s.mtimeMs ?? s.mtime
|
|
259
|
-
};
|
|
260
|
-
},
|
|
261
|
-
async mkdir(path, options) {
|
|
262
|
-
return cached.mkdir(path, options);
|
|
263
|
-
},
|
|
264
|
-
async exists(path) {
|
|
265
|
-
return cached.exists(path);
|
|
266
|
-
}
|
|
267
|
-
};
|
|
268
|
-
syncable.backendName = name || "CachedFS";
|
|
269
|
-
return syncable;
|
|
270
|
-
}
|
|
271
223
|
|
|
272
224
|
// src/backend-registry.ts
|
|
273
225
|
var registry = /* @__PURE__ */ new Map();
|
|
@@ -326,12 +278,10 @@ async function wrapZenFSFileSystem(config) {
|
|
|
326
278
|
},
|
|
327
279
|
async stat(path, ..._args) {
|
|
328
280
|
const st = await isolatedFS.stat(path);
|
|
329
|
-
const isDir = typeof st.isDirectory === "function" ? st.isDirectory() : st.mode !== void 0 && (st.mode & 61440) === 16384;
|
|
330
281
|
return {
|
|
331
|
-
|
|
332
|
-
isDirectory: () => isDir,
|
|
282
|
+
mode: typeof st.mode === "number" ? st.mode : void 0,
|
|
333
283
|
size: st.size,
|
|
334
|
-
|
|
284
|
+
mtimeMs: st.mtimeMs ?? st.mtime ?? 0
|
|
335
285
|
};
|
|
336
286
|
},
|
|
337
287
|
async exists(path) {
|
|
@@ -446,8 +396,12 @@ async function verifyOrRepairVersion(fs, configFilePath, author) {
|
|
|
446
396
|
var META_DIR = "/.meta";
|
|
447
397
|
var BACKENDS_FILE = `${META_DIR}/backends.json`;
|
|
448
398
|
var CONFLICTS_DIR = `${META_DIR}/.conflicts`;
|
|
399
|
+
var DELETIONS_DIR = `${META_DIR}/.deleted`;
|
|
449
400
|
var NODES_DIR = "/nodes";
|
|
450
401
|
var NODE_ID_FILE = `${NODES_DIR}/.node-id`;
|
|
402
|
+
function tombstoneFileName(filePath) {
|
|
403
|
+
return filePath.replace(/^\//, "").replace(/\//g, "__").replace(/\./g, "++") + ".json";
|
|
404
|
+
}
|
|
451
405
|
var ConfigRepo = class {
|
|
452
406
|
appId;
|
|
453
407
|
nodeId;
|
|
@@ -463,15 +417,17 @@ var ConfigRepo = class {
|
|
|
463
417
|
onConflictCallback;
|
|
464
418
|
disposed = false;
|
|
465
419
|
configCache = /* @__PURE__ */ new Map();
|
|
420
|
+
primaryBackendId;
|
|
466
421
|
constructor(appId, nodeId, primaryBackendId, cachedFS, serializer, onConflict) {
|
|
467
422
|
this.appId = appId;
|
|
468
423
|
this.nodeId = nodeId;
|
|
424
|
+
this.primaryBackendId = primaryBackendId;
|
|
469
425
|
this.cachedFS = cachedFS;
|
|
470
426
|
this.serializer = serializer;
|
|
471
427
|
this.syncEngine = new ZenFSSync();
|
|
472
428
|
this.replicaBackends = /* @__PURE__ */ new Map();
|
|
473
429
|
this.onConflictCallback = onConflict;
|
|
474
|
-
this.fullFS =
|
|
430
|
+
this.fullFS = backendToSyncableFS(cachedFS, primaryBackendId);
|
|
475
431
|
this.fs = createChrootFS(cachedFS, `/${appId}`);
|
|
476
432
|
this.rootFS = createChrootFS(cachedFS, "/");
|
|
477
433
|
}
|
|
@@ -615,9 +571,141 @@ var ConfigRepo = class {
|
|
|
615
571
|
// -----------------------------------------------------------------------
|
|
616
572
|
async flush() {
|
|
617
573
|
this.assertNotDisposed();
|
|
574
|
+
await this.processTombstones();
|
|
618
575
|
const resultsMap = await this.syncEngine.syncAll();
|
|
576
|
+
await this.updateTombstoneConfirmations();
|
|
577
|
+
await this.gcTombstones();
|
|
619
578
|
return Array.from(resultsMap.values());
|
|
620
579
|
}
|
|
580
|
+
// -----------------------------------------------------------------------
|
|
581
|
+
// Tombstone (Deletion Tracking)
|
|
582
|
+
// -----------------------------------------------------------------------
|
|
583
|
+
/**
|
|
584
|
+
* Delete a file and write a tombstone so the deletion propagates
|
|
585
|
+
* to all backends instead of being treated as "missing file → re-create".
|
|
586
|
+
*/
|
|
587
|
+
async deleteFile(path) {
|
|
588
|
+
this.assertNotDisposed();
|
|
589
|
+
const normalizedPath = path.startsWith("/") ? path : "/" + path;
|
|
590
|
+
const tombstonePath = `${DELETIONS_DIR}/${tombstoneFileName(normalizedPath)}`;
|
|
591
|
+
const tombstone = {
|
|
592
|
+
path: normalizedPath,
|
|
593
|
+
deletedAt: Date.now(),
|
|
594
|
+
deletedBy: this.primaryBackendId,
|
|
595
|
+
confirmedBy: [this.primaryBackendId]
|
|
596
|
+
};
|
|
597
|
+
await this.ensureDir(tombstonePath);
|
|
598
|
+
await this.cachedFS.writeFile(
|
|
599
|
+
tombstonePath,
|
|
600
|
+
new TextEncoder().encode(JSON.stringify(tombstone, null, 2))
|
|
601
|
+
);
|
|
602
|
+
try {
|
|
603
|
+
await this.cachedFS.unlink(normalizedPath);
|
|
604
|
+
} catch {
|
|
605
|
+
}
|
|
606
|
+
const versionPath = versionPathFor(normalizedPath);
|
|
607
|
+
try {
|
|
608
|
+
await this.cachedFS.unlink(versionPath);
|
|
609
|
+
} catch {
|
|
610
|
+
}
|
|
611
|
+
console.log(`[ConfigRepo] deleteFile: ${normalizedPath} (tombstone at ${tombstonePath})`);
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Read all tombstones from the primary backend.
|
|
615
|
+
*/
|
|
616
|
+
async readTombstones() {
|
|
617
|
+
try {
|
|
618
|
+
const entries = await this.cachedFS.readdir(DELETIONS_DIR);
|
|
619
|
+
const tombstones = [];
|
|
620
|
+
for (const entry of entries) {
|
|
621
|
+
if (!entry.endsWith(".json")) continue;
|
|
622
|
+
try {
|
|
623
|
+
const raw = await this.cachedFS.readFile(`${DELETIONS_DIR}/${entry}`);
|
|
624
|
+
const data = JSON.parse(new TextDecoder().decode(toUint8Array(raw)));
|
|
625
|
+
tombstones.push(data);
|
|
626
|
+
} catch {
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return tombstones;
|
|
630
|
+
} catch {
|
|
631
|
+
return [];
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Before sync: for each tombstone, delete the actual file on all replicas.
|
|
636
|
+
* This prevents bi-directional sync from copying the file back.
|
|
637
|
+
*/
|
|
638
|
+
async processTombstones() {
|
|
639
|
+
const tombstones = await this.readTombstones();
|
|
640
|
+
if (tombstones.length === 0) return;
|
|
641
|
+
console.log(`[ConfigRepo] processTombstones: ${tombstones.length} tombstone(s)`);
|
|
642
|
+
for (const tombstone of tombstones) {
|
|
643
|
+
try {
|
|
644
|
+
await this.cachedFS.unlink(tombstone.path);
|
|
645
|
+
} catch {
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
await this.cachedFS.unlink(versionPathFor(tombstone.path));
|
|
649
|
+
} catch {
|
|
650
|
+
}
|
|
651
|
+
for (const [replicaId, replica] of this.replicaBackends) {
|
|
652
|
+
try {
|
|
653
|
+
await replica.instance.unlink(tombstone.path);
|
|
654
|
+
} catch {
|
|
655
|
+
}
|
|
656
|
+
try {
|
|
657
|
+
await replica.instance.unlink(versionPathFor(tombstone.path));
|
|
658
|
+
} catch {
|
|
659
|
+
}
|
|
660
|
+
console.log(`[ConfigRepo] tombstone ${tombstone.path}: deleted on ${replicaId}`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* After sync: mark each tombstone as confirmed by all replica backends.
|
|
666
|
+
*/
|
|
667
|
+
async updateTombstoneConfirmations() {
|
|
668
|
+
const tombstones = await this.readTombstones();
|
|
669
|
+
if (tombstones.length === 0) return;
|
|
670
|
+
const backendsMeta = await this.getBackends();
|
|
671
|
+
const allBackendIds = backendsMeta?.backends.map((b) => b.id) ?? [this.primaryBackendId];
|
|
672
|
+
for (const tombstone of tombstones) {
|
|
673
|
+
const tombstonePath = `${DELETIONS_DIR}/${tombstoneFileName(tombstone.path)}`;
|
|
674
|
+
for (const replicaId of this.replicaBackends.keys()) {
|
|
675
|
+
if (!tombstone.confirmedBy.includes(replicaId)) {
|
|
676
|
+
tombstone.confirmedBy.push(replicaId);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
try {
|
|
680
|
+
await this.cachedFS.writeFile(
|
|
681
|
+
tombstonePath,
|
|
682
|
+
new TextEncoder().encode(JSON.stringify(tombstone, null, 2))
|
|
683
|
+
);
|
|
684
|
+
} catch {
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
console.log(`[ConfigRepo] updateTombstoneConfirmations: ${tombstones.length} tombstone(s) updated`);
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* GC: remove tombstones where all backends in backends.json have confirmed.
|
|
691
|
+
*/
|
|
692
|
+
async gcTombstones() {
|
|
693
|
+
const tombstones = await this.readTombstones();
|
|
694
|
+
if (tombstones.length === 0) return;
|
|
695
|
+
const backendsMeta = await this.getBackends();
|
|
696
|
+
const allBackendIds = backendsMeta?.backends.map((b) => b.id) ?? [this.primaryBackendId];
|
|
697
|
+
for (const tombstone of tombstones) {
|
|
698
|
+
const allConfirmed = allBackendIds.every((id) => tombstone.confirmedBy.includes(id));
|
|
699
|
+
if (allConfirmed) {
|
|
700
|
+
const tombstonePath = `${DELETIONS_DIR}/${tombstoneFileName(tombstone.path)}`;
|
|
701
|
+
try {
|
|
702
|
+
await this.cachedFS.unlink(tombstonePath);
|
|
703
|
+
console.log(`[ConfigRepo] gcTombstones: removed ${tombstonePath} (all ${allBackendIds.length} backends confirmed)`);
|
|
704
|
+
} catch {
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
621
709
|
/**
|
|
622
710
|
* Sync .meta/ files (backends.json) to all replica backends.
|
|
623
711
|
*
|
|
@@ -878,9 +966,9 @@ var ConfigRepo = class {
|
|
|
878
966
|
const fullPath = current === "/" ? `/${entry}` : `${current}/${entry}`;
|
|
879
967
|
try {
|
|
880
968
|
const stat = await this.cachedFS.stat(fullPath);
|
|
881
|
-
if (stat.
|
|
969
|
+
if (stat.mode !== void 0 && (stat.mode & 16384) === 16384) {
|
|
882
970
|
stack.push(fullPath);
|
|
883
|
-
} else
|
|
971
|
+
} else {
|
|
884
972
|
results.push(fullPath);
|
|
885
973
|
}
|
|
886
974
|
} catch {
|
|
@@ -947,21 +1035,7 @@ async function createConfigRepo(appId, options) {
|
|
|
947
1035
|
type: options.backendInfo.type,
|
|
948
1036
|
options: options.backendInfo.options
|
|
949
1037
|
});
|
|
950
|
-
const
|
|
951
|
-
let cacheStore;
|
|
952
|
-
const storeType = options.cache?.storeType ?? "MemoryCacheStore";
|
|
953
|
-
if (storeType === "IdbCacheStore") {
|
|
954
|
-
cacheStore = new zenCache.IdbCacheStore(options.cache?.storePrefix);
|
|
955
|
-
} else {
|
|
956
|
-
cacheStore = new zenCache.MemoryCacheStore();
|
|
957
|
-
}
|
|
958
|
-
const cachedFS = new zenCache.CachedFileSystem(
|
|
959
|
-
primaryInstance,
|
|
960
|
-
cacheStore,
|
|
961
|
-
{
|
|
962
|
-
ttlMs: options.cache?.ttlMs ?? 0
|
|
963
|
-
}
|
|
964
|
-
);
|
|
1038
|
+
const cachedFS = primaryInstance;
|
|
965
1039
|
try {
|
|
966
1040
|
const metaExists = await primaryInstance.exists(META_DIR);
|
|
967
1041
|
console.log(`[createConfigRepo] /.meta/ exists: ${metaExists}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zen-fs-config",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.28",
|
|
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
|
}
|