zen-fs-config 0.3.25 → 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 +80 -49
- package/dist/index.d.ts +80 -49
- package/dist/index.js +143 -65
- package/dist/index.mjs +143 -65
- 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
|
@@ -252,8 +252,7 @@ function backendToSyncableFS(backend, name) {
|
|
|
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
|
};
|
|
@@ -274,49 +273,6 @@ function backendToSyncableFS(backend, name) {
|
|
|
274
273
|
}
|
|
275
274
|
return syncable;
|
|
276
275
|
}
|
|
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
276
|
|
|
321
277
|
// src/backend-registry.ts
|
|
322
278
|
var registry = /* @__PURE__ */ new Map();
|
|
@@ -375,12 +331,10 @@ async function wrapZenFSFileSystem(config) {
|
|
|
375
331
|
},
|
|
376
332
|
async stat(path, ..._args) {
|
|
377
333
|
const st = await isolatedFS.stat(path);
|
|
378
|
-
const isDir = typeof st.isDirectory === "function" ? st.isDirectory() : st.mode !== void 0 && (st.mode & 61440) === 16384;
|
|
379
334
|
return {
|
|
380
|
-
|
|
381
|
-
isDirectory: () => isDir,
|
|
335
|
+
mode: typeof st.mode === "number" ? st.mode : void 0,
|
|
382
336
|
size: st.size,
|
|
383
|
-
|
|
337
|
+
mtimeMs: st.mtimeMs ?? st.mtime ?? 0
|
|
384
338
|
};
|
|
385
339
|
},
|
|
386
340
|
async exists(path) {
|
|
@@ -495,8 +449,12 @@ async function verifyOrRepairVersion(fs, configFilePath, author) {
|
|
|
495
449
|
var META_DIR = "/.meta";
|
|
496
450
|
var BACKENDS_FILE = `${META_DIR}/backends.json`;
|
|
497
451
|
var CONFLICTS_DIR = `${META_DIR}/.conflicts`;
|
|
452
|
+
var DELETIONS_DIR = `${META_DIR}/.deleted`;
|
|
498
453
|
var NODES_DIR = "/nodes";
|
|
499
454
|
var NODE_ID_FILE = `${NODES_DIR}/.node-id`;
|
|
455
|
+
function tombstoneFileName(filePath) {
|
|
456
|
+
return filePath.replace(/^\//, "").replace(/\//g, "__").replace(/\./g, "++") + ".json";
|
|
457
|
+
}
|
|
500
458
|
var ConfigRepo = class {
|
|
501
459
|
appId;
|
|
502
460
|
nodeId;
|
|
@@ -512,15 +470,17 @@ var ConfigRepo = class {
|
|
|
512
470
|
onConflictCallback;
|
|
513
471
|
disposed = false;
|
|
514
472
|
configCache = /* @__PURE__ */ new Map();
|
|
473
|
+
primaryBackendId;
|
|
515
474
|
constructor(appId, nodeId, primaryBackendId, cachedFS, serializer, onConflict) {
|
|
516
475
|
this.appId = appId;
|
|
517
476
|
this.nodeId = nodeId;
|
|
477
|
+
this.primaryBackendId = primaryBackendId;
|
|
518
478
|
this.cachedFS = cachedFS;
|
|
519
479
|
this.serializer = serializer;
|
|
520
480
|
this.syncEngine = new import_zen_fs_sync.ZenFSSync();
|
|
521
481
|
this.replicaBackends = /* @__PURE__ */ new Map();
|
|
522
482
|
this.onConflictCallback = onConflict;
|
|
523
|
-
this.fullFS =
|
|
483
|
+
this.fullFS = backendToSyncableFS(cachedFS, primaryBackendId);
|
|
524
484
|
this.fs = createChrootFS(cachedFS, `/${appId}`);
|
|
525
485
|
this.rootFS = createChrootFS(cachedFS, "/");
|
|
526
486
|
}
|
|
@@ -664,9 +624,141 @@ var ConfigRepo = class {
|
|
|
664
624
|
// -----------------------------------------------------------------------
|
|
665
625
|
async flush() {
|
|
666
626
|
this.assertNotDisposed();
|
|
627
|
+
await this.processTombstones();
|
|
667
628
|
const resultsMap = await this.syncEngine.syncAll();
|
|
629
|
+
await this.updateTombstoneConfirmations();
|
|
630
|
+
await this.gcTombstones();
|
|
668
631
|
return Array.from(resultsMap.values());
|
|
669
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
|
+
}
|
|
670
762
|
/**
|
|
671
763
|
* Sync .meta/ files (backends.json) to all replica backends.
|
|
672
764
|
*
|
|
@@ -996,21 +1088,7 @@ async function createConfigRepo(appId, options) {
|
|
|
996
1088
|
type: options.backendInfo.type,
|
|
997
1089
|
options: options.backendInfo.options
|
|
998
1090
|
});
|
|
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
|
-
);
|
|
1091
|
+
const cachedFS = primaryInstance;
|
|
1014
1092
|
try {
|
|
1015
1093
|
const metaExists = await primaryInstance.exists(META_DIR);
|
|
1016
1094
|
console.log(`[createConfigRepo] /.meta/ exists: ${metaExists}`);
|
package/dist/index.mjs
CHANGED
|
@@ -203,8 +203,7 @@ function backendToSyncableFS(backend, name) {
|
|
|
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
|
};
|
|
@@ -225,49 +224,6 @@ function backendToSyncableFS(backend, name) {
|
|
|
225
224
|
}
|
|
226
225
|
return syncable;
|
|
227
226
|
}
|
|
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
227
|
|
|
272
228
|
// src/backend-registry.ts
|
|
273
229
|
var registry = /* @__PURE__ */ new Map();
|
|
@@ -326,12 +282,10 @@ async function wrapZenFSFileSystem(config) {
|
|
|
326
282
|
},
|
|
327
283
|
async stat(path, ..._args) {
|
|
328
284
|
const st = await isolatedFS.stat(path);
|
|
329
|
-
const isDir = typeof st.isDirectory === "function" ? st.isDirectory() : st.mode !== void 0 && (st.mode & 61440) === 16384;
|
|
330
285
|
return {
|
|
331
|
-
|
|
332
|
-
isDirectory: () => isDir,
|
|
286
|
+
mode: typeof st.mode === "number" ? st.mode : void 0,
|
|
333
287
|
size: st.size,
|
|
334
|
-
|
|
288
|
+
mtimeMs: st.mtimeMs ?? st.mtime ?? 0
|
|
335
289
|
};
|
|
336
290
|
},
|
|
337
291
|
async exists(path) {
|
|
@@ -446,8 +400,12 @@ async function verifyOrRepairVersion(fs, configFilePath, author) {
|
|
|
446
400
|
var META_DIR = "/.meta";
|
|
447
401
|
var BACKENDS_FILE = `${META_DIR}/backends.json`;
|
|
448
402
|
var CONFLICTS_DIR = `${META_DIR}/.conflicts`;
|
|
403
|
+
var DELETIONS_DIR = `${META_DIR}/.deleted`;
|
|
449
404
|
var NODES_DIR = "/nodes";
|
|
450
405
|
var NODE_ID_FILE = `${NODES_DIR}/.node-id`;
|
|
406
|
+
function tombstoneFileName(filePath) {
|
|
407
|
+
return filePath.replace(/^\//, "").replace(/\//g, "__").replace(/\./g, "++") + ".json";
|
|
408
|
+
}
|
|
451
409
|
var ConfigRepo = class {
|
|
452
410
|
appId;
|
|
453
411
|
nodeId;
|
|
@@ -463,15 +421,17 @@ var ConfigRepo = class {
|
|
|
463
421
|
onConflictCallback;
|
|
464
422
|
disposed = false;
|
|
465
423
|
configCache = /* @__PURE__ */ new Map();
|
|
424
|
+
primaryBackendId;
|
|
466
425
|
constructor(appId, nodeId, primaryBackendId, cachedFS, serializer, onConflict) {
|
|
467
426
|
this.appId = appId;
|
|
468
427
|
this.nodeId = nodeId;
|
|
428
|
+
this.primaryBackendId = primaryBackendId;
|
|
469
429
|
this.cachedFS = cachedFS;
|
|
470
430
|
this.serializer = serializer;
|
|
471
431
|
this.syncEngine = new ZenFSSync();
|
|
472
432
|
this.replicaBackends = /* @__PURE__ */ new Map();
|
|
473
433
|
this.onConflictCallback = onConflict;
|
|
474
|
-
this.fullFS =
|
|
434
|
+
this.fullFS = backendToSyncableFS(cachedFS, primaryBackendId);
|
|
475
435
|
this.fs = createChrootFS(cachedFS, `/${appId}`);
|
|
476
436
|
this.rootFS = createChrootFS(cachedFS, "/");
|
|
477
437
|
}
|
|
@@ -615,9 +575,141 @@ var ConfigRepo = class {
|
|
|
615
575
|
// -----------------------------------------------------------------------
|
|
616
576
|
async flush() {
|
|
617
577
|
this.assertNotDisposed();
|
|
578
|
+
await this.processTombstones();
|
|
618
579
|
const resultsMap = await this.syncEngine.syncAll();
|
|
580
|
+
await this.updateTombstoneConfirmations();
|
|
581
|
+
await this.gcTombstones();
|
|
619
582
|
return Array.from(resultsMap.values());
|
|
620
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
|
+
}
|
|
621
713
|
/**
|
|
622
714
|
* Sync .meta/ files (backends.json) to all replica backends.
|
|
623
715
|
*
|
|
@@ -947,21 +1039,7 @@ async function createConfigRepo(appId, options) {
|
|
|
947
1039
|
type: options.backendInfo.type,
|
|
948
1040
|
options: options.backendInfo.options
|
|
949
1041
|
});
|
|
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
|
-
);
|
|
1042
|
+
const cachedFS = primaryInstance;
|
|
965
1043
|
try {
|
|
966
1044
|
const metaExists = await primaryInstance.exists(META_DIR);
|
|
967
1045
|
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.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
|
}
|