@floegence/floe-webapp-core 0.65.2 → 0.66.0
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/resource-cache.d.ts +49 -0
- package/dist/resource-cache.js +162 -0
- package/package.json +6 -2
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Disposable presentation snapshots. Authorization and polling belong to the consumer. */
|
|
2
|
+
export interface ResourceCacheStorage {
|
|
3
|
+
/** Reads also update the entry's last-access time. */
|
|
4
|
+
get(key: string): Promise<string | null>;
|
|
5
|
+
set(key: string, value: string): Promise<void>;
|
|
6
|
+
remove(key: string): Promise<void>;
|
|
7
|
+
list(): Promise<readonly {
|
|
8
|
+
key: string;
|
|
9
|
+
bytes: number;
|
|
10
|
+
lastAccessedAt: number;
|
|
11
|
+
}[]>;
|
|
12
|
+
}
|
|
13
|
+
export interface ResourceSnapshot<T> {
|
|
14
|
+
readonly data: T | undefined;
|
|
15
|
+
readonly refreshing: boolean;
|
|
16
|
+
readonly stale: boolean;
|
|
17
|
+
readonly error: unknown;
|
|
18
|
+
}
|
|
19
|
+
export interface CachedResource<T> {
|
|
20
|
+
snapshot(): ResourceSnapshot<T>;
|
|
21
|
+
subscribe(listener: () => void): () => void;
|
|
22
|
+
hydrate(): Promise<void>;
|
|
23
|
+
refresh(fetcher: (signal: AbortSignal) => Promise<T>): Promise<T>;
|
|
24
|
+
set(value: T): void;
|
|
25
|
+
/** Keeps the visible snapshot by default, but removes its durable copy. */
|
|
26
|
+
invalidate(clear?: boolean): void;
|
|
27
|
+
}
|
|
28
|
+
export interface CachedResourceDefinition<T> {
|
|
29
|
+
scope: string;
|
|
30
|
+
key: string;
|
|
31
|
+
version: number;
|
|
32
|
+
/** Validate and decode untrusted persisted content; throw to reject it. */
|
|
33
|
+
decode(value: unknown): T;
|
|
34
|
+
/** Persist an explicit presentation projection when live data contains private fields. */
|
|
35
|
+
persist?(value: T): unknown;
|
|
36
|
+
}
|
|
37
|
+
export interface ResourceCache {
|
|
38
|
+
resource<T>(definition: CachedResourceDefinition<T>): CachedResource<T>;
|
|
39
|
+
clearScope(scope: string): Promise<void>;
|
|
40
|
+
flush(): Promise<void>;
|
|
41
|
+
dispose(): void;
|
|
42
|
+
}
|
|
43
|
+
export declare function createResourceCache(options: {
|
|
44
|
+
storage: ResourceCacheStorage;
|
|
45
|
+
maxBytes?: number;
|
|
46
|
+
writeDelayMs?: number;
|
|
47
|
+
}): ResourceCache;
|
|
48
|
+
/** A dedicated database containing disposable snapshots only, never application settings. */
|
|
49
|
+
export declare function createIndexedDBResourceCacheStorage(name?: string): ResourceCacheStorage;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
function I(m) {
|
|
2
|
+
const d = m.storage, E = Math.max(0, m.maxBytes ?? 32 * 1024 * 1024), v = /* @__PURE__ */ new Map(), c = /* @__PURE__ */ new Map();
|
|
3
|
+
let n, t = Promise.resolve(), o = !1;
|
|
4
|
+
const i = (e) => (t = t.then(e).catch(() => {
|
|
5
|
+
}), t), h = async () => {
|
|
6
|
+
const e = [...await d.list()].sort((l, s) => l.lastAccessedAt - s.lastAccessedAt);
|
|
7
|
+
let r = e.reduce((l, s) => l + s.bytes, 0);
|
|
8
|
+
for (const l of e) {
|
|
9
|
+
if (r <= E) break;
|
|
10
|
+
await d.remove(l.key), r -= l.bytes;
|
|
11
|
+
}
|
|
12
|
+
}, f = async () => {
|
|
13
|
+
clearTimeout(n), n = void 0;
|
|
14
|
+
const e = [...c];
|
|
15
|
+
c.clear(), e.length && i(async () => {
|
|
16
|
+
for (const [r, l] of e)
|
|
17
|
+
if (l.valid())
|
|
18
|
+
try {
|
|
19
|
+
await d.set(r, l.value), l.stored();
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
await h();
|
|
23
|
+
}), await t;
|
|
24
|
+
}, k = (e, r, l, s) => {
|
|
25
|
+
c.set(e, { value: r, valid: l, stored: s }), n === void 0 && (n = setTimeout(() => {
|
|
26
|
+
f();
|
|
27
|
+
}, m.writeDelayMs ?? 100));
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
resource: (e) => {
|
|
31
|
+
if (o) throw new Error("Resource cache is disposed");
|
|
32
|
+
if (!e.scope || !e.key || !Number.isSafeInteger(e.version)) throw new Error("Invalid resource identity");
|
|
33
|
+
const r = JSON.stringify([e.scope, e.key, e.version]), l = v.get(r);
|
|
34
|
+
if (l) return l.handle;
|
|
35
|
+
let s = { data: void 0, refreshing: !1, stale: !0, error: void 0 };
|
|
36
|
+
const S = /* @__PURE__ */ new Set();
|
|
37
|
+
let x = 0, y = 0, q, D, w, p;
|
|
38
|
+
const b = () => {
|
|
39
|
+
for (const a of S) a();
|
|
40
|
+
}, R = (a) => {
|
|
41
|
+
++x, s = { data: a, refreshing: s.refreshing, stale: !1, error: void 0 };
|
|
42
|
+
try {
|
|
43
|
+
const u = JSON.stringify({ version: e.version, data: e.persist ? e.persist(a) : a });
|
|
44
|
+
q = u, u !== D ? k(r, u, () => q === u, () => {
|
|
45
|
+
D = u;
|
|
46
|
+
}) : c.delete(r);
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
b();
|
|
50
|
+
}, O = x, P = d.get(r).then((a) => {
|
|
51
|
+
if (!(o || x !== O || a === null))
|
|
52
|
+
try {
|
|
53
|
+
const u = JSON.parse(a);
|
|
54
|
+
if (u.version !== e.version) throw new Error("Unsupported snapshot");
|
|
55
|
+
const g = e.decode(u.data);
|
|
56
|
+
s = { ...s, data: g, stale: !0 }, q = a, D = a, b();
|
|
57
|
+
} catch {
|
|
58
|
+
i(() => d.remove(r));
|
|
59
|
+
}
|
|
60
|
+
}).catch(() => {
|
|
61
|
+
}), N = {
|
|
62
|
+
snapshot: () => s,
|
|
63
|
+
subscribe: (a) => (S.add(a), () => {
|
|
64
|
+
S.delete(a);
|
|
65
|
+
}),
|
|
66
|
+
hydrate: () => P,
|
|
67
|
+
refresh: (a) => {
|
|
68
|
+
if (p) return p;
|
|
69
|
+
const u = ++y, g = new AbortController();
|
|
70
|
+
return w = g, s = { ...s, refreshing: !0, error: void 0 }, b(), p = Promise.resolve().then(() => a(g.signal)).then((A) => (!o && u === y && !g.signal.aborted && R(A), A), (A) => {
|
|
71
|
+
throw !o && u === y && !g.signal.aborted && (s = { ...s, stale: !0, error: A }, b()), A;
|
|
72
|
+
}).finally(() => {
|
|
73
|
+
u === y && (p = void 0, w = void 0, s = { ...s, refreshing: !1 }, b());
|
|
74
|
+
}), p;
|
|
75
|
+
},
|
|
76
|
+
set: (a) => {
|
|
77
|
+
++y, w?.abort(), p = void 0, s = { ...s, refreshing: !1 }, R(a);
|
|
78
|
+
},
|
|
79
|
+
invalidate: (a = !1) => {
|
|
80
|
+
++x, ++y, w?.abort(), w = void 0, p = void 0, q = void 0, D = void 0, c.delete(r), s = { data: a ? void 0 : s.data, refreshing: !1, stale: !0, error: void 0 }, b(), i(() => d.remove(r));
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
return v.set(r, { scope: e.scope, handle: N, stop: () => {
|
|
84
|
+
++y, w?.abort(), S.clear();
|
|
85
|
+
} }), N;
|
|
86
|
+
},
|
|
87
|
+
flush: f,
|
|
88
|
+
clearScope: async (e) => {
|
|
89
|
+
for (const r of v.values()) r.scope === e && r.handle.invalidate(!0);
|
|
90
|
+
await i(async () => {
|
|
91
|
+
for (const r of await d.list())
|
|
92
|
+
try {
|
|
93
|
+
JSON.parse(r.key)[0] === e && await d.remove(r.key);
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
dispose: () => {
|
|
99
|
+
if (!o) {
|
|
100
|
+
o = !0;
|
|
101
|
+
for (const e of v.values()) e.stop();
|
|
102
|
+
f(), v.clear();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function J(m = "floe-resource-cache") {
|
|
108
|
+
let d;
|
|
109
|
+
const E = () => d ??= new Promise((c, n) => {
|
|
110
|
+
const t = indexedDB.open(m, 1);
|
|
111
|
+
t.onupgradeneeded = () => t.result.createObjectStore("snapshots", { keyPath: "key" }), t.onsuccess = () => {
|
|
112
|
+
const o = t.result;
|
|
113
|
+
o.onversionchange = () => {
|
|
114
|
+
o.close(), d = void 0;
|
|
115
|
+
}, c(o);
|
|
116
|
+
}, t.onerror = () => {
|
|
117
|
+
d = void 0, n(t.error);
|
|
118
|
+
}, t.onblocked = () => {
|
|
119
|
+
d = void 0, n(new Error("Resource cache database is blocked"));
|
|
120
|
+
};
|
|
121
|
+
}), v = async (c, n) => {
|
|
122
|
+
const t = await E();
|
|
123
|
+
return new Promise((o, i) => {
|
|
124
|
+
const h = t.transaction("snapshots", c);
|
|
125
|
+
let f;
|
|
126
|
+
h.oncomplete = () => o(f), h.onerror = () => i(h.error), h.onabort = () => i(h.error ?? new Error("Resource cache transaction aborted")), n(h.objectStore("snapshots"), (k) => {
|
|
127
|
+
f = k;
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
return {
|
|
132
|
+
get: (c) => v("readwrite", (n, t) => {
|
|
133
|
+
const o = n.get(c);
|
|
134
|
+
o.onsuccess = () => {
|
|
135
|
+
const i = o.result;
|
|
136
|
+
i && n.put({ ...i, lastAccessedAt: Date.now() }), t(i?.value ?? null);
|
|
137
|
+
};
|
|
138
|
+
}),
|
|
139
|
+
set: (c, n) => v("readwrite", (t, o) => {
|
|
140
|
+
t.put({ key: c, value: n, bytes: new TextEncoder().encode(n).byteLength, lastAccessedAt: Date.now() }), o(void 0);
|
|
141
|
+
}),
|
|
142
|
+
remove: (c) => v("readwrite", (n, t) => {
|
|
143
|
+
n.delete(c), t(void 0);
|
|
144
|
+
}),
|
|
145
|
+
list: () => v("readonly", (c, n) => {
|
|
146
|
+
const t = [], o = c.openCursor();
|
|
147
|
+
o.onsuccess = () => {
|
|
148
|
+
const i = o.result;
|
|
149
|
+
if (!i) {
|
|
150
|
+
n(t);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const { key: h, bytes: f, lastAccessedAt: k } = i.value;
|
|
154
|
+
t.push({ key: h, bytes: f, lastAccessedAt: k }), i.continue();
|
|
155
|
+
};
|
|
156
|
+
})
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
export {
|
|
160
|
+
J as createIndexedDBResourceCacheStorage,
|
|
161
|
+
I as createResourceCache
|
|
162
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floegence/floe-webapp-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.66.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -101,7 +101,11 @@
|
|
|
101
101
|
"types": "./scripts/pdf-assets.d.mts",
|
|
102
102
|
"import": "./scripts/pdf-assets.mjs"
|
|
103
103
|
},
|
|
104
|
-
"./standalone.css": "./dist/standalone.css"
|
|
104
|
+
"./standalone.css": "./dist/standalone.css",
|
|
105
|
+
"./resource-cache": {
|
|
106
|
+
"types": "./dist/resource-cache.d.ts",
|
|
107
|
+
"import": "./dist/resource-cache.js"
|
|
108
|
+
}
|
|
105
109
|
},
|
|
106
110
|
"files": [
|
|
107
111
|
"dist",
|