zen-fs-config 0.5.8 → 0.5.10
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/cache-wrapper-XQCPZJCD.mjs +22 -0
- package/dist/index.d.mts +38 -6
- package/dist/index.d.ts +38 -6
- package/dist/index.js +99 -15
- package/dist/index.mjs +66 -15
- package/package.json +1 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// src/cache-wrapper.ts
|
|
2
|
+
import { CachedFileSystem, IdbCacheStore, MemoryCacheStore } from "zen-fs-cache";
|
|
3
|
+
function wrapWithCache(backend, backendId, options) {
|
|
4
|
+
const storeType = options.storeType ?? "IdbCacheStore";
|
|
5
|
+
const prefix = options.storePrefix ?? `zen-fs-config:${backendId}:`;
|
|
6
|
+
let store;
|
|
7
|
+
if (storeType === "IdbCacheStore") {
|
|
8
|
+
store = new IdbCacheStore(prefix);
|
|
9
|
+
} else {
|
|
10
|
+
store = new MemoryCacheStore();
|
|
11
|
+
}
|
|
12
|
+
const wrapped = new CachedFileSystem(backend, store, {
|
|
13
|
+
ttlMs: options.ttlMs ?? 0
|
|
14
|
+
});
|
|
15
|
+
if (typeof backend.shouldSync === "function") {
|
|
16
|
+
wrapped.shouldSync = (...args) => backend.shouldSync(...args);
|
|
17
|
+
}
|
|
18
|
+
return wrapped;
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
wrapWithCache
|
|
22
|
+
};
|
package/dist/index.d.mts
CHANGED
|
@@ -89,11 +89,20 @@ interface ConfigSerializer {
|
|
|
89
89
|
}
|
|
90
90
|
/** Cache configuration. */
|
|
91
91
|
interface CacheOptions {
|
|
92
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* Type of cache store for replica backends.
|
|
94
|
+
* Default: `'IdbCacheStore'` — persists across page reloads so cached
|
|
95
|
+
* content + revision tokens survive. Use `'MemoryCacheStore'` for
|
|
96
|
+
* session-only caching (lost on reload).
|
|
97
|
+
*/
|
|
93
98
|
storeType?: 'MemoryCacheStore' | 'IdbCacheStore';
|
|
94
|
-
/** Cache store prefix (for IdbCacheStore). */
|
|
99
|
+
/** Cache store prefix (for IdbCacheStore). Default: `'zen-fs-config:'`. */
|
|
95
100
|
storePrefix?: string;
|
|
96
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* TTL in milliseconds for cache hits without revalidation.
|
|
103
|
+
* Default: 0 (always revalidate via `getRevision` — exact, no stale data).
|
|
104
|
+
* Ignored when the backend implements `getRevision`.
|
|
105
|
+
*/
|
|
97
106
|
ttlMs?: number;
|
|
98
107
|
}
|
|
99
108
|
/** Options for creating a ConfigRepo. */
|
|
@@ -119,8 +128,19 @@ interface ConfigRepoOptions {
|
|
|
119
128
|
idbStoreName?: string;
|
|
120
129
|
/** Node identifier. Auto-detected if not provided (see DESIGN.md §8.2). */
|
|
121
130
|
nodeId?: string;
|
|
122
|
-
/**
|
|
123
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Cache configuration for replica backends.
|
|
133
|
+
*
|
|
134
|
+
* - **Default** (omitted): Caching is enabled with `IdbCacheStore`
|
|
135
|
+
* (persists across page reloads). Replica backends (Gitee,
|
|
136
|
+
* RemoteStorage, etc.) are wrapped with `CachedFileSystem` to avoid
|
|
137
|
+
* redundant network reads via `getRevision` revalidation.
|
|
138
|
+
* - Pass a `CacheOptions` object to customize the store type, prefix,
|
|
139
|
+
* or TTL.
|
|
140
|
+
* - Pass `false` to disable caching entirely (all reads go directly
|
|
141
|
+
* to the backend).
|
|
142
|
+
*/
|
|
143
|
+
cache?: CacheOptions | false;
|
|
124
144
|
/** Custom serializer. */
|
|
125
145
|
serializer?: ConfigSerializer;
|
|
126
146
|
/** Custom conflict handler. Called before auto-resolution. */
|
|
@@ -461,10 +481,11 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
461
481
|
private disposed;
|
|
462
482
|
private configCache;
|
|
463
483
|
private readonly primaryBackendId;
|
|
484
|
+
private readonly cacheOptions?;
|
|
464
485
|
private readonly pollIntervalMs?;
|
|
465
486
|
/** Tombstone cache — avoids redundant reads within a single flush() cycle. */
|
|
466
487
|
private tombstoneCache;
|
|
467
|
-
constructor(appId: string, nodeId: string, primaryBackendId: string, cachedFS: MinimalAsyncFS, serializer: PathAwareSerializer, onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>, pollIntervalMs?: number);
|
|
488
|
+
constructor(appId: string, nodeId: string, primaryBackendId: string, cachedFS: MinimalAsyncFS, serializer: PathAwareSerializer, onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>, pollIntervalMs?: number, cacheOptions?: CacheOptions);
|
|
468
489
|
/** Full path to this node's directory on the primary backend. */
|
|
469
490
|
get nodePath(): string;
|
|
470
491
|
/** Number of replica backends registered (excludes the local primary). */
|
|
@@ -494,8 +515,19 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
494
515
|
/**
|
|
495
516
|
* Before sync: for each tombstone, delete the actual file on all replicas.
|
|
496
517
|
* This prevents bi-directional sync from copying the file back.
|
|
518
|
+
*
|
|
519
|
+
* Before calling unlink() on each backend, we check exists() first.
|
|
520
|
+
* This avoids sending wasteful DELETE requests (or GET-then-404) to
|
|
521
|
+
* remote backends when the file was already removed on a previous cycle.
|
|
522
|
+
* Local backends (IndexedDB) are cheap to check, so the guard is
|
|
523
|
+
* effectively free for them.
|
|
497
524
|
*/
|
|
498
525
|
private processTombstones;
|
|
526
|
+
/**
|
|
527
|
+
* Safe existence check — returns false on any error instead of throwing.
|
|
528
|
+
* Used by processTombstones to avoid unnecessary unlink() calls.
|
|
529
|
+
*/
|
|
530
|
+
private safeExists;
|
|
499
531
|
/** Public wrapper for processTombstones — used by createConfigRepo. */
|
|
500
532
|
processTombstonesPublic(): Promise<void>;
|
|
501
533
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -89,11 +89,20 @@ interface ConfigSerializer {
|
|
|
89
89
|
}
|
|
90
90
|
/** Cache configuration. */
|
|
91
91
|
interface CacheOptions {
|
|
92
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* Type of cache store for replica backends.
|
|
94
|
+
* Default: `'IdbCacheStore'` — persists across page reloads so cached
|
|
95
|
+
* content + revision tokens survive. Use `'MemoryCacheStore'` for
|
|
96
|
+
* session-only caching (lost on reload).
|
|
97
|
+
*/
|
|
93
98
|
storeType?: 'MemoryCacheStore' | 'IdbCacheStore';
|
|
94
|
-
/** Cache store prefix (for IdbCacheStore). */
|
|
99
|
+
/** Cache store prefix (for IdbCacheStore). Default: `'zen-fs-config:'`. */
|
|
95
100
|
storePrefix?: string;
|
|
96
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* TTL in milliseconds for cache hits without revalidation.
|
|
103
|
+
* Default: 0 (always revalidate via `getRevision` — exact, no stale data).
|
|
104
|
+
* Ignored when the backend implements `getRevision`.
|
|
105
|
+
*/
|
|
97
106
|
ttlMs?: number;
|
|
98
107
|
}
|
|
99
108
|
/** Options for creating a ConfigRepo. */
|
|
@@ -119,8 +128,19 @@ interface ConfigRepoOptions {
|
|
|
119
128
|
idbStoreName?: string;
|
|
120
129
|
/** Node identifier. Auto-detected if not provided (see DESIGN.md §8.2). */
|
|
121
130
|
nodeId?: string;
|
|
122
|
-
/**
|
|
123
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Cache configuration for replica backends.
|
|
133
|
+
*
|
|
134
|
+
* - **Default** (omitted): Caching is enabled with `IdbCacheStore`
|
|
135
|
+
* (persists across page reloads). Replica backends (Gitee,
|
|
136
|
+
* RemoteStorage, etc.) are wrapped with `CachedFileSystem` to avoid
|
|
137
|
+
* redundant network reads via `getRevision` revalidation.
|
|
138
|
+
* - Pass a `CacheOptions` object to customize the store type, prefix,
|
|
139
|
+
* or TTL.
|
|
140
|
+
* - Pass `false` to disable caching entirely (all reads go directly
|
|
141
|
+
* to the backend).
|
|
142
|
+
*/
|
|
143
|
+
cache?: CacheOptions | false;
|
|
124
144
|
/** Custom serializer. */
|
|
125
145
|
serializer?: ConfigSerializer;
|
|
126
146
|
/** Custom conflict handler. Called before auto-resolution. */
|
|
@@ -461,10 +481,11 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
461
481
|
private disposed;
|
|
462
482
|
private configCache;
|
|
463
483
|
private readonly primaryBackendId;
|
|
484
|
+
private readonly cacheOptions?;
|
|
464
485
|
private readonly pollIntervalMs?;
|
|
465
486
|
/** Tombstone cache — avoids redundant reads within a single flush() cycle. */
|
|
466
487
|
private tombstoneCache;
|
|
467
|
-
constructor(appId: string, nodeId: string, primaryBackendId: string, cachedFS: MinimalAsyncFS, serializer: PathAwareSerializer, onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>, pollIntervalMs?: number);
|
|
488
|
+
constructor(appId: string, nodeId: string, primaryBackendId: string, cachedFS: MinimalAsyncFS, serializer: PathAwareSerializer, onConflict?: (conflict: ConflictInfo) => Promise<unknown | null>, pollIntervalMs?: number, cacheOptions?: CacheOptions);
|
|
468
489
|
/** Full path to this node's directory on the primary backend. */
|
|
469
490
|
get nodePath(): string;
|
|
470
491
|
/** Number of replica backends registered (excludes the local primary). */
|
|
@@ -494,8 +515,19 @@ declare class ConfigRepo implements IConfigRepo {
|
|
|
494
515
|
/**
|
|
495
516
|
* Before sync: for each tombstone, delete the actual file on all replicas.
|
|
496
517
|
* This prevents bi-directional sync from copying the file back.
|
|
518
|
+
*
|
|
519
|
+
* Before calling unlink() on each backend, we check exists() first.
|
|
520
|
+
* This avoids sending wasteful DELETE requests (or GET-then-404) to
|
|
521
|
+
* remote backends when the file was already removed on a previous cycle.
|
|
522
|
+
* Local backends (IndexedDB) are cheap to check, so the guard is
|
|
523
|
+
* effectively free for them.
|
|
497
524
|
*/
|
|
498
525
|
private processTombstones;
|
|
526
|
+
/**
|
|
527
|
+
* Safe existence check — returns false on any error instead of throwing.
|
|
528
|
+
* Used by processTombstones to avoid unnecessary unlink() calls.
|
|
529
|
+
*/
|
|
530
|
+
private safeExists;
|
|
499
531
|
/** Public wrapper for processTombstones — used by createConfigRepo. */
|
|
500
532
|
processTombstonesPublic(): Promise<void>;
|
|
501
533
|
/**
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
8
11
|
var __export = (target, all) => {
|
|
9
12
|
for (var name in all)
|
|
10
13
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -27,6 +30,36 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
30
|
));
|
|
28
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
32
|
|
|
33
|
+
// src/cache-wrapper.ts
|
|
34
|
+
var cache_wrapper_exports = {};
|
|
35
|
+
__export(cache_wrapper_exports, {
|
|
36
|
+
wrapWithCache: () => wrapWithCache
|
|
37
|
+
});
|
|
38
|
+
function wrapWithCache(backend, backendId, options) {
|
|
39
|
+
const storeType = options.storeType ?? "IdbCacheStore";
|
|
40
|
+
const prefix = options.storePrefix ?? `zen-fs-config:${backendId}:`;
|
|
41
|
+
let store;
|
|
42
|
+
if (storeType === "IdbCacheStore") {
|
|
43
|
+
store = new import_zen_fs_cache.IdbCacheStore(prefix);
|
|
44
|
+
} else {
|
|
45
|
+
store = new import_zen_fs_cache.MemoryCacheStore();
|
|
46
|
+
}
|
|
47
|
+
const wrapped = new import_zen_fs_cache.CachedFileSystem(backend, store, {
|
|
48
|
+
ttlMs: options.ttlMs ?? 0
|
|
49
|
+
});
|
|
50
|
+
if (typeof backend.shouldSync === "function") {
|
|
51
|
+
wrapped.shouldSync = (...args) => backend.shouldSync(...args);
|
|
52
|
+
}
|
|
53
|
+
return wrapped;
|
|
54
|
+
}
|
|
55
|
+
var import_zen_fs_cache;
|
|
56
|
+
var init_cache_wrapper = __esm({
|
|
57
|
+
"src/cache-wrapper.ts"() {
|
|
58
|
+
"use strict";
|
|
59
|
+
import_zen_fs_cache = require("zen-fs-cache");
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
30
63
|
// src/index.ts
|
|
31
64
|
var index_exports = {};
|
|
32
65
|
__export(index_exports, {
|
|
@@ -585,10 +618,11 @@ var ConfigRepo = class {
|
|
|
585
618
|
disposed = false;
|
|
586
619
|
configCache = /* @__PURE__ */ new Map();
|
|
587
620
|
primaryBackendId;
|
|
621
|
+
cacheOptions;
|
|
588
622
|
pollIntervalMs;
|
|
589
623
|
/** Tombstone cache — avoids redundant reads within a single flush() cycle. */
|
|
590
624
|
tombstoneCache = null;
|
|
591
|
-
constructor(appId, nodeId, primaryBackendId, cachedFS, serializer, onConflict, pollIntervalMs) {
|
|
625
|
+
constructor(appId, nodeId, primaryBackendId, cachedFS, serializer, onConflict, pollIntervalMs, cacheOptions) {
|
|
592
626
|
this.appId = appId;
|
|
593
627
|
this.nodeId = nodeId;
|
|
594
628
|
this.primaryBackendId = primaryBackendId;
|
|
@@ -598,6 +632,7 @@ var ConfigRepo = class {
|
|
|
598
632
|
this.replicaBackends = /* @__PURE__ */ new Map();
|
|
599
633
|
this.onConflictCallback = onConflict;
|
|
600
634
|
this.pollIntervalMs = pollIntervalMs;
|
|
635
|
+
this.cacheOptions = cacheOptions;
|
|
601
636
|
this.fullFS = backendToSyncableFS(cachedFS, primaryBackendId);
|
|
602
637
|
this.fs = createChrootFS(cachedFS, `/${appId}`);
|
|
603
638
|
this.rootFS = createChrootFS(cachedFS, "/");
|
|
@@ -826,37 +861,77 @@ var ConfigRepo = class {
|
|
|
826
861
|
/**
|
|
827
862
|
* Before sync: for each tombstone, delete the actual file on all replicas.
|
|
828
863
|
* This prevents bi-directional sync from copying the file back.
|
|
864
|
+
*
|
|
865
|
+
* Before calling unlink() on each backend, we check exists() first.
|
|
866
|
+
* This avoids sending wasteful DELETE requests (or GET-then-404) to
|
|
867
|
+
* remote backends when the file was already removed on a previous cycle.
|
|
868
|
+
* Local backends (IndexedDB) are cheap to check, so the guard is
|
|
869
|
+
* effectively free for them.
|
|
829
870
|
*/
|
|
830
871
|
async processTombstones() {
|
|
831
872
|
const tombstones = await this.readTombstones();
|
|
832
873
|
if (tombstones.length === 0) return;
|
|
833
|
-
|
|
874
|
+
let processed = 0;
|
|
875
|
+
let alreadyDeleted = 0;
|
|
834
876
|
for (const tombstone of tombstones) {
|
|
835
877
|
const tVersionPath = versionPathFor(tombstone.path);
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
878
|
+
const existedOnPrimary = await this.safeExists(this.cachedFS, tombstone.path);
|
|
879
|
+
console.log(`[ConfigRepo] tombstone check: ${tombstone.path} on primary \u2192 ${existedOnPrimary ? "EXISTS" : "not found"}`);
|
|
880
|
+
if (existedOnPrimary) {
|
|
881
|
+
try {
|
|
882
|
+
await this.cachedFS.unlink(tombstone.path);
|
|
883
|
+
processed++;
|
|
884
|
+
} catch {
|
|
885
|
+
}
|
|
839
886
|
}
|
|
840
|
-
if (tVersionPath) {
|
|
887
|
+
if (tVersionPath && await this.safeExists(this.cachedFS, tVersionPath)) {
|
|
841
888
|
try {
|
|
842
889
|
await this.cachedFS.unlink(tVersionPath);
|
|
843
890
|
} catch {
|
|
844
891
|
}
|
|
845
892
|
}
|
|
846
893
|
for (const [replicaId, replica] of this.replicaBackends) {
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
894
|
+
const existed = await this.safeExists(replica.instance, tombstone.path);
|
|
895
|
+
console.log(`[ConfigRepo] tombstone check: ${tombstone.path} on ${replicaId} \u2192 ${existed ? "EXISTS" : "not found"}`);
|
|
896
|
+
if (existed) {
|
|
897
|
+
try {
|
|
898
|
+
await replica.instance.unlink(tombstone.path);
|
|
899
|
+
console.log(`[ConfigRepo] tombstone ${tombstone.path}: deleted on ${replicaId}`);
|
|
900
|
+
processed++;
|
|
901
|
+
} catch {
|
|
902
|
+
alreadyDeleted++;
|
|
903
|
+
}
|
|
904
|
+
} else {
|
|
905
|
+
alreadyDeleted++;
|
|
850
906
|
}
|
|
851
|
-
if (tVersionPath) {
|
|
907
|
+
if (tVersionPath && await this.safeExists(replica.instance, tVersionPath)) {
|
|
852
908
|
try {
|
|
853
909
|
await replica.instance.unlink(tVersionPath);
|
|
854
910
|
} catch {
|
|
855
911
|
}
|
|
856
912
|
}
|
|
857
|
-
console.log(`[ConfigRepo] tombstone ${tombstone.path}: deleted on ${replicaId}`);
|
|
858
913
|
}
|
|
859
914
|
}
|
|
915
|
+
if (processed > 0 || alreadyDeleted > 0) {
|
|
916
|
+
console.log(`[ConfigRepo] processTombstones: ${tombstones.length} tombstone(s), ${processed} deleted, ${alreadyDeleted} already gone`);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Safe existence check — returns false on any error instead of throwing.
|
|
921
|
+
* Used by processTombstones to avoid unnecessary unlink() calls.
|
|
922
|
+
*/
|
|
923
|
+
async safeExists(fs, path) {
|
|
924
|
+
try {
|
|
925
|
+
if (typeof fs.exists === "function") {
|
|
926
|
+
const result = await fs.exists(path);
|
|
927
|
+
return result;
|
|
928
|
+
}
|
|
929
|
+
await fs.stat(path);
|
|
930
|
+
return true;
|
|
931
|
+
} catch (err) {
|
|
932
|
+
console.log(`[ConfigRepo] safeExists(${path}): threw ${err?.code ?? err?.status ?? ""} ${err?.message ?? err}`);
|
|
933
|
+
return false;
|
|
934
|
+
}
|
|
860
935
|
}
|
|
861
936
|
/** Public wrapper for processTombstones — used by createConfigRepo. */
|
|
862
937
|
async processTombstonesPublic() {
|
|
@@ -1036,7 +1111,13 @@ var ConfigRepo = class {
|
|
|
1036
1111
|
console.log(`[ConfigRepo] Creating replica backend: id=${desc.id}, type=${desc.type}`);
|
|
1037
1112
|
try {
|
|
1038
1113
|
const instance = await createBackend(desc);
|
|
1039
|
-
|
|
1114
|
+
let fsInstance = instance;
|
|
1115
|
+
if (this.cacheOptions) {
|
|
1116
|
+
const { wrapWithCache: wrapWithCache2 } = await Promise.resolve().then(() => (init_cache_wrapper(), cache_wrapper_exports));
|
|
1117
|
+
fsInstance = wrapWithCache2(instance, desc.id, this.cacheOptions);
|
|
1118
|
+
console.log(`[ConfigRepo] Replica ${desc.id} wrapped with CachedFileSystem (store=${this.cacheOptions.storeType ?? "IdbCacheStore"})`);
|
|
1119
|
+
}
|
|
1120
|
+
const syncable = backendToSyncableFS(fsInstance, `${desc.type}(${desc.id})`);
|
|
1040
1121
|
const pair = this.syncEngine.addPair(
|
|
1041
1122
|
this.fullFS,
|
|
1042
1123
|
syncable,
|
|
@@ -1047,7 +1128,7 @@ var ConfigRepo = class {
|
|
|
1047
1128
|
},
|
|
1048
1129
|
"/"
|
|
1049
1130
|
);
|
|
1050
|
-
this.replicaBackends.set(desc.id, { instance, syncable, pairId: pair.pairId });
|
|
1131
|
+
this.replicaBackends.set(desc.id, { instance: fsInstance, syncable, pairId: pair.pairId });
|
|
1051
1132
|
const conflictHandler = (event) => {
|
|
1052
1133
|
this.handleConflict(event);
|
|
1053
1134
|
};
|
|
@@ -1753,6 +1834,7 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
1753
1834
|
options: { storeName: idbStoreName }
|
|
1754
1835
|
});
|
|
1755
1836
|
const cachedFS = primaryInstance;
|
|
1837
|
+
const cacheOptions = options.cache === false ? void 0 : options.cache ?? {};
|
|
1756
1838
|
try {
|
|
1757
1839
|
await primaryInstance.mkdir(META_DIR);
|
|
1758
1840
|
console.log(`[createConfigRepo] /.meta/ ready`);
|
|
@@ -1777,7 +1859,8 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
1777
1859
|
cachedFS,
|
|
1778
1860
|
createSerializerChain(),
|
|
1779
1861
|
void 0,
|
|
1780
|
-
options.syncPollIntervalMs
|
|
1862
|
+
options.syncPollIntervalMs,
|
|
1863
|
+
cacheOptions
|
|
1781
1864
|
);
|
|
1782
1865
|
const oldBackendsMeta = await tempRepo.readMetaFile(BACKENDS_FILE);
|
|
1783
1866
|
if (oldBackendsMeta && oldBackendsMeta.backends?.length > 0) {
|
|
@@ -1843,7 +1926,8 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
1843
1926
|
cachedFS,
|
|
1844
1927
|
serializer,
|
|
1845
1928
|
options.onConflict,
|
|
1846
|
-
options.syncPollIntervalMs
|
|
1929
|
+
options.syncPollIntervalMs,
|
|
1930
|
+
cacheOptions
|
|
1847
1931
|
);
|
|
1848
1932
|
await repo.setupSync(allBackends, LOCAL_IDB_BACKEND_ID, options.syncPollIntervalMs);
|
|
1849
1933
|
await repo.load();
|
package/dist/index.mjs
CHANGED
|
@@ -528,10 +528,11 @@ var ConfigRepo = class {
|
|
|
528
528
|
disposed = false;
|
|
529
529
|
configCache = /* @__PURE__ */ new Map();
|
|
530
530
|
primaryBackendId;
|
|
531
|
+
cacheOptions;
|
|
531
532
|
pollIntervalMs;
|
|
532
533
|
/** Tombstone cache — avoids redundant reads within a single flush() cycle. */
|
|
533
534
|
tombstoneCache = null;
|
|
534
|
-
constructor(appId, nodeId, primaryBackendId, cachedFS, serializer, onConflict, pollIntervalMs) {
|
|
535
|
+
constructor(appId, nodeId, primaryBackendId, cachedFS, serializer, onConflict, pollIntervalMs, cacheOptions) {
|
|
535
536
|
this.appId = appId;
|
|
536
537
|
this.nodeId = nodeId;
|
|
537
538
|
this.primaryBackendId = primaryBackendId;
|
|
@@ -541,6 +542,7 @@ var ConfigRepo = class {
|
|
|
541
542
|
this.replicaBackends = /* @__PURE__ */ new Map();
|
|
542
543
|
this.onConflictCallback = onConflict;
|
|
543
544
|
this.pollIntervalMs = pollIntervalMs;
|
|
545
|
+
this.cacheOptions = cacheOptions;
|
|
544
546
|
this.fullFS = backendToSyncableFS(cachedFS, primaryBackendId);
|
|
545
547
|
this.fs = createChrootFS(cachedFS, `/${appId}`);
|
|
546
548
|
this.rootFS = createChrootFS(cachedFS, "/");
|
|
@@ -769,37 +771,77 @@ var ConfigRepo = class {
|
|
|
769
771
|
/**
|
|
770
772
|
* Before sync: for each tombstone, delete the actual file on all replicas.
|
|
771
773
|
* This prevents bi-directional sync from copying the file back.
|
|
774
|
+
*
|
|
775
|
+
* Before calling unlink() on each backend, we check exists() first.
|
|
776
|
+
* This avoids sending wasteful DELETE requests (or GET-then-404) to
|
|
777
|
+
* remote backends when the file was already removed on a previous cycle.
|
|
778
|
+
* Local backends (IndexedDB) are cheap to check, so the guard is
|
|
779
|
+
* effectively free for them.
|
|
772
780
|
*/
|
|
773
781
|
async processTombstones() {
|
|
774
782
|
const tombstones = await this.readTombstones();
|
|
775
783
|
if (tombstones.length === 0) return;
|
|
776
|
-
|
|
784
|
+
let processed = 0;
|
|
785
|
+
let alreadyDeleted = 0;
|
|
777
786
|
for (const tombstone of tombstones) {
|
|
778
787
|
const tVersionPath = versionPathFor(tombstone.path);
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
788
|
+
const existedOnPrimary = await this.safeExists(this.cachedFS, tombstone.path);
|
|
789
|
+
console.log(`[ConfigRepo] tombstone check: ${tombstone.path} on primary \u2192 ${existedOnPrimary ? "EXISTS" : "not found"}`);
|
|
790
|
+
if (existedOnPrimary) {
|
|
791
|
+
try {
|
|
792
|
+
await this.cachedFS.unlink(tombstone.path);
|
|
793
|
+
processed++;
|
|
794
|
+
} catch {
|
|
795
|
+
}
|
|
782
796
|
}
|
|
783
|
-
if (tVersionPath) {
|
|
797
|
+
if (tVersionPath && await this.safeExists(this.cachedFS, tVersionPath)) {
|
|
784
798
|
try {
|
|
785
799
|
await this.cachedFS.unlink(tVersionPath);
|
|
786
800
|
} catch {
|
|
787
801
|
}
|
|
788
802
|
}
|
|
789
803
|
for (const [replicaId, replica] of this.replicaBackends) {
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
804
|
+
const existed = await this.safeExists(replica.instance, tombstone.path);
|
|
805
|
+
console.log(`[ConfigRepo] tombstone check: ${tombstone.path} on ${replicaId} \u2192 ${existed ? "EXISTS" : "not found"}`);
|
|
806
|
+
if (existed) {
|
|
807
|
+
try {
|
|
808
|
+
await replica.instance.unlink(tombstone.path);
|
|
809
|
+
console.log(`[ConfigRepo] tombstone ${tombstone.path}: deleted on ${replicaId}`);
|
|
810
|
+
processed++;
|
|
811
|
+
} catch {
|
|
812
|
+
alreadyDeleted++;
|
|
813
|
+
}
|
|
814
|
+
} else {
|
|
815
|
+
alreadyDeleted++;
|
|
793
816
|
}
|
|
794
|
-
if (tVersionPath) {
|
|
817
|
+
if (tVersionPath && await this.safeExists(replica.instance, tVersionPath)) {
|
|
795
818
|
try {
|
|
796
819
|
await replica.instance.unlink(tVersionPath);
|
|
797
820
|
} catch {
|
|
798
821
|
}
|
|
799
822
|
}
|
|
800
|
-
console.log(`[ConfigRepo] tombstone ${tombstone.path}: deleted on ${replicaId}`);
|
|
801
823
|
}
|
|
802
824
|
}
|
|
825
|
+
if (processed > 0 || alreadyDeleted > 0) {
|
|
826
|
+
console.log(`[ConfigRepo] processTombstones: ${tombstones.length} tombstone(s), ${processed} deleted, ${alreadyDeleted} already gone`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Safe existence check — returns false on any error instead of throwing.
|
|
831
|
+
* Used by processTombstones to avoid unnecessary unlink() calls.
|
|
832
|
+
*/
|
|
833
|
+
async safeExists(fs, path) {
|
|
834
|
+
try {
|
|
835
|
+
if (typeof fs.exists === "function") {
|
|
836
|
+
const result = await fs.exists(path);
|
|
837
|
+
return result;
|
|
838
|
+
}
|
|
839
|
+
await fs.stat(path);
|
|
840
|
+
return true;
|
|
841
|
+
} catch (err) {
|
|
842
|
+
console.log(`[ConfigRepo] safeExists(${path}): threw ${err?.code ?? err?.status ?? ""} ${err?.message ?? err}`);
|
|
843
|
+
return false;
|
|
844
|
+
}
|
|
803
845
|
}
|
|
804
846
|
/** Public wrapper for processTombstones — used by createConfigRepo. */
|
|
805
847
|
async processTombstonesPublic() {
|
|
@@ -979,7 +1021,13 @@ var ConfigRepo = class {
|
|
|
979
1021
|
console.log(`[ConfigRepo] Creating replica backend: id=${desc.id}, type=${desc.type}`);
|
|
980
1022
|
try {
|
|
981
1023
|
const instance = await createBackend(desc);
|
|
982
|
-
|
|
1024
|
+
let fsInstance = instance;
|
|
1025
|
+
if (this.cacheOptions) {
|
|
1026
|
+
const { wrapWithCache } = await import("./cache-wrapper-XQCPZJCD.mjs");
|
|
1027
|
+
fsInstance = wrapWithCache(instance, desc.id, this.cacheOptions);
|
|
1028
|
+
console.log(`[ConfigRepo] Replica ${desc.id} wrapped with CachedFileSystem (store=${this.cacheOptions.storeType ?? "IdbCacheStore"})`);
|
|
1029
|
+
}
|
|
1030
|
+
const syncable = backendToSyncableFS(fsInstance, `${desc.type}(${desc.id})`);
|
|
983
1031
|
const pair = this.syncEngine.addPair(
|
|
984
1032
|
this.fullFS,
|
|
985
1033
|
syncable,
|
|
@@ -990,7 +1038,7 @@ var ConfigRepo = class {
|
|
|
990
1038
|
},
|
|
991
1039
|
"/"
|
|
992
1040
|
);
|
|
993
|
-
this.replicaBackends.set(desc.id, { instance, syncable, pairId: pair.pairId });
|
|
1041
|
+
this.replicaBackends.set(desc.id, { instance: fsInstance, syncable, pairId: pair.pairId });
|
|
994
1042
|
const conflictHandler = (event) => {
|
|
995
1043
|
this.handleConflict(event);
|
|
996
1044
|
};
|
|
@@ -1696,6 +1744,7 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
1696
1744
|
options: { storeName: idbStoreName }
|
|
1697
1745
|
});
|
|
1698
1746
|
const cachedFS = primaryInstance;
|
|
1747
|
+
const cacheOptions = options.cache === false ? void 0 : options.cache ?? {};
|
|
1699
1748
|
try {
|
|
1700
1749
|
await primaryInstance.mkdir(META_DIR);
|
|
1701
1750
|
console.log(`[createConfigRepo] /.meta/ ready`);
|
|
@@ -1720,7 +1769,8 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
1720
1769
|
cachedFS,
|
|
1721
1770
|
createSerializerChain(),
|
|
1722
1771
|
void 0,
|
|
1723
|
-
options.syncPollIntervalMs
|
|
1772
|
+
options.syncPollIntervalMs,
|
|
1773
|
+
cacheOptions
|
|
1724
1774
|
);
|
|
1725
1775
|
const oldBackendsMeta = await tempRepo.readMetaFile(BACKENDS_FILE);
|
|
1726
1776
|
if (oldBackendsMeta && oldBackendsMeta.backends?.length > 0) {
|
|
@@ -1786,7 +1836,8 @@ async function createConfigRepo(appId, options = {}) {
|
|
|
1786
1836
|
cachedFS,
|
|
1787
1837
|
serializer,
|
|
1788
1838
|
options.onConflict,
|
|
1789
|
-
options.syncPollIntervalMs
|
|
1839
|
+
options.syncPollIntervalMs,
|
|
1840
|
+
cacheOptions
|
|
1790
1841
|
);
|
|
1791
1842
|
await repo.setupSync(allBackends, LOCAL_IDB_BACKEND_ID, options.syncPollIntervalMs);
|
|
1792
1843
|
await repo.load();
|