@emdzej/bimmerz-vfs 0.1.0 → 0.2.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.
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Storage backend for `CachedHttpDirectory`. Two implementations:
3
+ *
4
+ * • **OPFS** (Origin Private File System) — preferred. Real
5
+ * filesystem semantics inside the browser sandbox, no quota
6
+ * prompts on writes ≤ 1 GB, and survives across tabs / refreshes.
7
+ * Requires Chrome 86+ / Safari 16+ / Firefox 111+ (gated on
8
+ * `navigator.storage.getDirectory`).
9
+ * • **IndexedDB** — fallback. Same durability story but with the
10
+ * awkward request/cursor API. Works everywhere.
11
+ *
12
+ * Each cached entry has TWO pieces:
13
+ *
14
+ * 1. The raw response bytes (one blob per key).
15
+ * 2. A small metadata record (`etag`, `last-modified`, `storedAt`,
16
+ * `validatedAt`, `url`). Used by `CachedHttpDirectory` to make
17
+ * conditional-GET requests and to surface staleness in `stats()`.
18
+ *
19
+ * The OPFS layout stores them as sibling files: `<key>` and
20
+ * `<key>.meta`. The IDB layout merges them into one object-store
21
+ * row keyed by `<key>`.
22
+ *
23
+ * Keys are origin-scoped: every backend instance is namespaced by a
24
+ * caller-supplied string (typically a hash of the `baseUrl`). The
25
+ * namespace is the OPFS subdirectory or the IDB key prefix. Clearing
26
+ * a namespace wipes one consumer's cache without touching anything
27
+ * else.
28
+ */
29
+ /** Per-entry metadata persisted alongside the bytes. */
30
+ export interface CacheMetadata {
31
+ /** Original request URL. Useful for telemetry / debugging. */
32
+ url: string;
33
+ /** Strong / weak ETag header value, if the server returned one. */
34
+ etag?: string;
35
+ /** `Last-Modified` header value (RFC 7231 IMF-fixdate). */
36
+ lastModified?: string;
37
+ /** Content-Type header, captured so callers can serve via Response. */
38
+ contentType?: string;
39
+ /** Bytes (length of the data blob; redundant with the blob but
40
+ * cheap to keep — lets `stats()` answer without reading every file). */
41
+ size: number;
42
+ /** Wall-clock ms when the body was first stored. */
43
+ storedAt: number;
44
+ /** Wall-clock ms when the body was last confirmed fresh (200 or
45
+ * 304). Drives the `maxAgeMs` staleness check. */
46
+ validatedAt: number;
47
+ }
48
+ /** Combined entry returned from `get()`. */
49
+ export interface CacheEntry {
50
+ bytes: ArrayBuffer;
51
+ meta: CacheMetadata;
52
+ }
53
+ /** What every backend must implement. Async by necessity — both
54
+ * OPFS and IDB are Promise-only. */
55
+ export interface CacheBackend {
56
+ /** Backend name, surfaced in `stats()` so consumers can show the
57
+ * user which path the cache landed on. */
58
+ readonly kind: 'opfs' | 'idb' | 'memory';
59
+ get(key: string): Promise<CacheEntry | null>;
60
+ put(key: string, entry: CacheEntry): Promise<void>;
61
+ delete(key: string): Promise<void>;
62
+ /** List keys with optional prefix filter. Order is unspecified. */
63
+ keys(prefix?: string): Promise<string[]>;
64
+ /** Remove every entry whose key starts with `prefix` (or all if
65
+ * no prefix is given). */
66
+ clear(prefix?: string): Promise<void>;
67
+ /** Aggregate counts for `stats()`. */
68
+ size(prefix?: string): Promise<{
69
+ entries: number;
70
+ totalBytes: number;
71
+ }>;
72
+ }
73
+ /** Implementation of `CacheBackend` over OPFS. Construct via
74
+ * `OpfsCacheBackend.open()` which resolves the root directory; the
75
+ * promise rejects when OPFS isn't available so callers can fall
76
+ * back to IDB. */
77
+ export declare class OpfsCacheBackend implements CacheBackend {
78
+ #private;
79
+ readonly kind: "opfs";
80
+ private constructor();
81
+ static open(namespace: string): Promise<OpfsCacheBackend>;
82
+ get(key: string): Promise<CacheEntry | null>;
83
+ put(key: string, entry: CacheEntry): Promise<void>;
84
+ delete(key: string): Promise<void>;
85
+ keys(prefix?: string): Promise<string[]>;
86
+ clear(prefix?: string): Promise<void>;
87
+ size(prefix?: string): Promise<{
88
+ entries: number;
89
+ totalBytes: number;
90
+ }>;
91
+ }
92
+ /** Implementation of `CacheBackend` over IndexedDB. Construct via
93
+ * `IdbCacheBackend.open()`; one shared connection per origin is
94
+ * cached at the module level. */
95
+ export declare class IdbCacheBackend implements CacheBackend {
96
+ #private;
97
+ readonly kind: "idb";
98
+ constructor(namespace: string);
99
+ static open(namespace: string): Promise<IdbCacheBackend>;
100
+ get(key: string): Promise<CacheEntry | null>;
101
+ put(key: string, entry: CacheEntry): Promise<void>;
102
+ delete(key: string): Promise<void>;
103
+ keys(prefix?: string): Promise<string[]>;
104
+ clear(prefix?: string): Promise<void>;
105
+ size(prefix?: string): Promise<{
106
+ entries: number;
107
+ totalBytes: number;
108
+ }>;
109
+ }
110
+ /** In-memory backend. Doesn't persist anything — used by tests and
111
+ * as a graceful no-op when neither OPFS nor IDB is available. */
112
+ export declare class MemoryCacheBackend implements CacheBackend {
113
+ #private;
114
+ readonly kind: "memory";
115
+ constructor(namespace: string);
116
+ get(key: string): Promise<CacheEntry | null>;
117
+ put(key: string, entry: CacheEntry): Promise<void>;
118
+ delete(key: string): Promise<void>;
119
+ keys(prefix?: string): Promise<string[]>;
120
+ clear(prefix?: string): Promise<void>;
121
+ size(prefix?: string): Promise<{
122
+ entries: number;
123
+ totalBytes: number;
124
+ }>;
125
+ }
126
+ export interface OpenCacheOptions {
127
+ /** Namespace for this cache instance. Typically a stable hash of
128
+ * the consumer's baseUrl so different `CachedHttpDirectory`
129
+ * instances don't trample each other. */
130
+ namespace: string;
131
+ /** Force a specific backend. Default: try OPFS, fall back to IDB,
132
+ * fall back to memory. */
133
+ prefer?: 'opfs' | 'idb' | 'memory';
134
+ }
135
+ /** Pick the best available backend at runtime. Returns the
136
+ * preferred one if it works, otherwise walks the fallback chain. */
137
+ export declare function openCacheBackend(options: OpenCacheOptions): Promise<CacheBackend>;
138
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
package/dist/cache.js ADDED
@@ -0,0 +1,368 @@
1
+ /**
2
+ * Storage backend for `CachedHttpDirectory`. Two implementations:
3
+ *
4
+ * • **OPFS** (Origin Private File System) — preferred. Real
5
+ * filesystem semantics inside the browser sandbox, no quota
6
+ * prompts on writes ≤ 1 GB, and survives across tabs / refreshes.
7
+ * Requires Chrome 86+ / Safari 16+ / Firefox 111+ (gated on
8
+ * `navigator.storage.getDirectory`).
9
+ * • **IndexedDB** — fallback. Same durability story but with the
10
+ * awkward request/cursor API. Works everywhere.
11
+ *
12
+ * Each cached entry has TWO pieces:
13
+ *
14
+ * 1. The raw response bytes (one blob per key).
15
+ * 2. A small metadata record (`etag`, `last-modified`, `storedAt`,
16
+ * `validatedAt`, `url`). Used by `CachedHttpDirectory` to make
17
+ * conditional-GET requests and to surface staleness in `stats()`.
18
+ *
19
+ * The OPFS layout stores them as sibling files: `<key>` and
20
+ * `<key>.meta`. The IDB layout merges them into one object-store
21
+ * row keyed by `<key>`.
22
+ *
23
+ * Keys are origin-scoped: every backend instance is namespaced by a
24
+ * caller-supplied string (typically a hash of the `baseUrl`). The
25
+ * namespace is the OPFS subdirectory or the IDB key prefix. Clearing
26
+ * a namespace wipes one consumer's cache without touching anything
27
+ * else.
28
+ */
29
+ /* ── OPFS backend ──────────────────────────────────────────────── */
30
+ /** OPFS file naming:
31
+ *
32
+ * <root>/<namespace>/<encoded-key> ← raw bytes
33
+ * <root>/<namespace>/<encoded-key>.meta ← JSON metadata
34
+ *
35
+ * `root` defaults to `bimmerz-vfs-cache`. Keys are URL-encoded so
36
+ * any character is safe — OPFS rejects `/` in names which we'd
37
+ * otherwise hit constantly. */
38
+ const OPFS_ROOT = 'bimmerz-vfs-cache';
39
+ const META_SUFFIX = '.meta';
40
+ function encodeKey(key) {
41
+ /* Replace forbidden OPFS characters. encodeURIComponent handles
42
+ most, but `:` (in URLs) is rejected on some platforms — strip
43
+ it explicitly. */
44
+ return encodeURIComponent(key).replace(/:/g, '%3A');
45
+ }
46
+ function decodeKey(encoded) {
47
+ return decodeURIComponent(encoded);
48
+ }
49
+ /** Implementation of `CacheBackend` over OPFS. Construct via
50
+ * `OpfsCacheBackend.open()` which resolves the root directory; the
51
+ * promise rejects when OPFS isn't available so callers can fall
52
+ * back to IDB. */
53
+ export class OpfsCacheBackend {
54
+ kind = 'opfs';
55
+ #root;
56
+ constructor(root) {
57
+ this.#root = root;
58
+ }
59
+ static async open(namespace) {
60
+ if (typeof navigator === 'undefined' || !navigator.storage?.getDirectory) {
61
+ throw new Error('OPFS not available');
62
+ }
63
+ const opfs = await navigator.storage.getDirectory();
64
+ const cacheRoot = await opfs.getDirectoryHandle(OPFS_ROOT, { create: true });
65
+ const nsDir = await cacheRoot.getDirectoryHandle(encodeKey(namespace), { create: true });
66
+ return new OpfsCacheBackend(nsDir);
67
+ }
68
+ async get(key) {
69
+ const encoded = encodeKey(key);
70
+ try {
71
+ const [bodyFile, metaFile] = await Promise.all([
72
+ this.#root.getFileHandle(encoded),
73
+ this.#root.getFileHandle(encoded + META_SUFFIX),
74
+ ]);
75
+ const [bodyBlob, metaBlob] = await Promise.all([
76
+ bodyFile.getFile(),
77
+ metaFile.getFile(),
78
+ ]);
79
+ const [bytes, metaText] = await Promise.all([
80
+ bodyBlob.arrayBuffer(),
81
+ metaBlob.text(),
82
+ ]);
83
+ const meta = JSON.parse(metaText);
84
+ return { bytes, meta };
85
+ }
86
+ catch {
87
+ /* `getFileHandle` throws when the entry doesn't exist; any
88
+ other failure (corrupted JSON, partial write from a prior
89
+ crash) is also treated as a cache miss so the caller falls
90
+ back to a real fetch rather than serving garbage. */
91
+ return null;
92
+ }
93
+ }
94
+ async put(key, entry) {
95
+ const encoded = encodeKey(key);
96
+ /* Write metadata FIRST so a crash mid-put leaves stale meta +
97
+ missing body rather than fresh body + stale meta. The reader
98
+ handles missing files defensively, so either failure mode
99
+ reads as a cache miss. */
100
+ const [bodyHandle, metaHandle] = await Promise.all([
101
+ this.#root.getFileHandle(encoded, { create: true }),
102
+ this.#root.getFileHandle(encoded + META_SUFFIX, { create: true }),
103
+ ]);
104
+ const metaWriter = await metaHandle.createWritable();
105
+ await metaWriter.write(JSON.stringify(entry.meta));
106
+ await metaWriter.close();
107
+ const bodyWriter = await bodyHandle.createWritable();
108
+ await bodyWriter.write(entry.bytes);
109
+ await bodyWriter.close();
110
+ }
111
+ async delete(key) {
112
+ const encoded = encodeKey(key);
113
+ await Promise.allSettled([
114
+ this.#root.removeEntry(encoded),
115
+ this.#root.removeEntry(encoded + META_SUFFIX),
116
+ ]);
117
+ }
118
+ async keys(prefix) {
119
+ const out = [];
120
+ /* The iterator API isn't typed yet on FileSystemDirectoryHandle
121
+ in some lib targets; cast through `unknown`. */
122
+ const dir = this.#root;
123
+ for await (const handle of dir.values()) {
124
+ if (handle.kind !== 'file')
125
+ continue;
126
+ if (handle.name.endsWith(META_SUFFIX))
127
+ continue;
128
+ const key = decodeKey(handle.name);
129
+ if (prefix && !key.startsWith(prefix))
130
+ continue;
131
+ out.push(key);
132
+ }
133
+ return out;
134
+ }
135
+ async clear(prefix) {
136
+ const keys = await this.keys(prefix);
137
+ await Promise.all(keys.map((k) => this.delete(k)));
138
+ }
139
+ async size(prefix) {
140
+ const keys = await this.keys(prefix);
141
+ let totalBytes = 0;
142
+ /* We pull byte sizes from metadata (no full body read needed). */
143
+ await Promise.all(keys.map(async (key) => {
144
+ const meta = await this.#readMeta(key);
145
+ if (meta)
146
+ totalBytes += meta.size;
147
+ }));
148
+ return { entries: keys.length, totalBytes };
149
+ }
150
+ async #readMeta(key) {
151
+ try {
152
+ const handle = await this.#root.getFileHandle(encodeKey(key) + META_SUFFIX);
153
+ const blob = await handle.getFile();
154
+ return JSON.parse(await blob.text());
155
+ }
156
+ catch {
157
+ return null;
158
+ }
159
+ }
160
+ }
161
+ /* ── IDB backend ───────────────────────────────────────────────── */
162
+ /** Stable DB name. One DB per origin, one object store keyed by
163
+ * `${namespace}:${key}` so all consumers share a single connection. */
164
+ const IDB_NAME = 'bimmerz-vfs-cache';
165
+ const IDB_STORE = 'entries';
166
+ const IDB_VERSION = 1;
167
+ /** Implementation of `CacheBackend` over IndexedDB. Construct via
168
+ * `IdbCacheBackend.open()`; one shared connection per origin is
169
+ * cached at the module level. */
170
+ export class IdbCacheBackend {
171
+ kind = 'idb';
172
+ #namespace;
173
+ #dbPromise;
174
+ constructor(namespace) {
175
+ this.#namespace = namespace;
176
+ this.#dbPromise = openDb();
177
+ }
178
+ static async open(namespace) {
179
+ if (typeof indexedDB === 'undefined') {
180
+ throw new Error('IndexedDB not available');
181
+ }
182
+ const instance = new IdbCacheBackend(namespace);
183
+ /* Eagerly resolve the DB connection so a failure surfaces
184
+ at construction time rather than first-use. */
185
+ await instance.#dbPromise;
186
+ return instance;
187
+ }
188
+ #rowKey(key) {
189
+ return `${this.#namespace}:${key}`;
190
+ }
191
+ async get(key) {
192
+ const db = await this.#dbPromise;
193
+ return new Promise((resolve, reject) => {
194
+ const tx = db.transaction(IDB_STORE, 'readonly');
195
+ const req = tx.objectStore(IDB_STORE).get(this.#rowKey(key));
196
+ req.onsuccess = () => {
197
+ const row = req.result;
198
+ resolve(row ? { bytes: row.bytes, meta: row.meta } : null);
199
+ };
200
+ req.onerror = () => reject(req.error);
201
+ });
202
+ }
203
+ async put(key, entry) {
204
+ const db = await this.#dbPromise;
205
+ const row = {
206
+ k: this.#rowKey(key),
207
+ ns: this.#namespace,
208
+ bytes: entry.bytes,
209
+ meta: entry.meta,
210
+ };
211
+ return new Promise((resolve, reject) => {
212
+ const tx = db.transaction(IDB_STORE, 'readwrite');
213
+ const req = tx.objectStore(IDB_STORE).put(row);
214
+ req.onsuccess = () => resolve();
215
+ req.onerror = () => reject(req.error);
216
+ });
217
+ }
218
+ async delete(key) {
219
+ const db = await this.#dbPromise;
220
+ return new Promise((resolve, reject) => {
221
+ const tx = db.transaction(IDB_STORE, 'readwrite');
222
+ const req = tx.objectStore(IDB_STORE).delete(this.#rowKey(key));
223
+ req.onsuccess = () => resolve();
224
+ req.onerror = () => reject(req.error);
225
+ });
226
+ }
227
+ async keys(prefix) {
228
+ const db = await this.#dbPromise;
229
+ return new Promise((resolve, reject) => {
230
+ const tx = db.transaction(IDB_STORE, 'readonly');
231
+ const req = tx.objectStore(IDB_STORE).index('ns').openKeyCursor(IDBKeyRange.only(this.#namespace));
232
+ const out = [];
233
+ req.onsuccess = () => {
234
+ const cursor = req.result;
235
+ if (!cursor)
236
+ return resolve(out);
237
+ const rowKey = String(cursor.primaryKey);
238
+ const key = rowKey.slice(this.#namespace.length + 1);
239
+ if (!prefix || key.startsWith(prefix))
240
+ out.push(key);
241
+ cursor.continue();
242
+ };
243
+ req.onerror = () => reject(req.error);
244
+ });
245
+ }
246
+ async clear(prefix) {
247
+ const keys = await this.keys(prefix);
248
+ await Promise.all(keys.map((k) => this.delete(k)));
249
+ }
250
+ async size(prefix) {
251
+ const db = await this.#dbPromise;
252
+ return new Promise((resolve, reject) => {
253
+ const tx = db.transaction(IDB_STORE, 'readonly');
254
+ const req = tx.objectStore(IDB_STORE).index('ns').openCursor(IDBKeyRange.only(this.#namespace));
255
+ let entries = 0;
256
+ let totalBytes = 0;
257
+ req.onsuccess = () => {
258
+ const cursor = req.result;
259
+ if (!cursor)
260
+ return resolve({ entries, totalBytes });
261
+ const row = cursor.value;
262
+ const key = row.k.slice(this.#namespace.length + 1);
263
+ if (!prefix || key.startsWith(prefix)) {
264
+ entries += 1;
265
+ totalBytes += row.meta.size;
266
+ }
267
+ cursor.continue();
268
+ };
269
+ req.onerror = () => reject(req.error);
270
+ });
271
+ }
272
+ }
273
+ function openDb() {
274
+ return new Promise((resolve, reject) => {
275
+ const req = indexedDB.open(IDB_NAME, IDB_VERSION);
276
+ req.onupgradeneeded = () => {
277
+ const db = req.result;
278
+ const store = db.createObjectStore(IDB_STORE, { keyPath: 'k' });
279
+ store.createIndex('ns', 'ns', { unique: false });
280
+ };
281
+ req.onsuccess = () => resolve(req.result);
282
+ req.onerror = () => reject(req.error);
283
+ });
284
+ }
285
+ /* ── Memory backend (for tests + SSR) ─────────────────────────── */
286
+ /** In-memory backend. Doesn't persist anything — used by tests and
287
+ * as a graceful no-op when neither OPFS nor IDB is available. */
288
+ export class MemoryCacheBackend {
289
+ kind = 'memory';
290
+ #store = new Map();
291
+ #namespace;
292
+ constructor(namespace) {
293
+ this.#namespace = namespace;
294
+ }
295
+ #ns(key) {
296
+ return `${this.#namespace}:${key}`;
297
+ }
298
+ async get(key) {
299
+ const e = this.#store.get(this.#ns(key));
300
+ return e ? { bytes: e.bytes, meta: { ...e.meta } } : null;
301
+ }
302
+ async put(key, entry) {
303
+ this.#store.set(this.#ns(key), entry);
304
+ }
305
+ async delete(key) {
306
+ this.#store.delete(this.#ns(key));
307
+ }
308
+ async keys(prefix) {
309
+ const out = [];
310
+ for (const k of this.#store.keys()) {
311
+ if (!k.startsWith(`${this.#namespace}:`))
312
+ continue;
313
+ const local = k.slice(this.#namespace.length + 1);
314
+ if (!prefix || local.startsWith(prefix))
315
+ out.push(local);
316
+ }
317
+ return out;
318
+ }
319
+ async clear(prefix) {
320
+ for (const k of [...this.#store.keys()]) {
321
+ if (!k.startsWith(`${this.#namespace}:`))
322
+ continue;
323
+ const local = k.slice(this.#namespace.length + 1);
324
+ if (!prefix || local.startsWith(prefix))
325
+ this.#store.delete(k);
326
+ }
327
+ }
328
+ async size(prefix) {
329
+ let entries = 0;
330
+ let totalBytes = 0;
331
+ for (const [k, v] of this.#store) {
332
+ if (!k.startsWith(`${this.#namespace}:`))
333
+ continue;
334
+ const local = k.slice(this.#namespace.length + 1);
335
+ if (prefix && !local.startsWith(prefix))
336
+ continue;
337
+ entries += 1;
338
+ totalBytes += v.meta.size;
339
+ }
340
+ return { entries, totalBytes };
341
+ }
342
+ }
343
+ /** Pick the best available backend at runtime. Returns the
344
+ * preferred one if it works, otherwise walks the fallback chain. */
345
+ export async function openCacheBackend(options) {
346
+ const order = options.prefer
347
+ ? options.prefer === 'opfs'
348
+ ? ['opfs', 'idb', 'memory']
349
+ : options.prefer === 'idb'
350
+ ? ['idb', 'memory']
351
+ : ['memory']
352
+ : ['opfs', 'idb', 'memory'];
353
+ let lastError;
354
+ for (const kind of order) {
355
+ try {
356
+ if (kind === 'opfs')
357
+ return await OpfsCacheBackend.open(options.namespace);
358
+ if (kind === 'idb')
359
+ return await IdbCacheBackend.open(options.namespace);
360
+ return new MemoryCacheBackend(options.namespace);
361
+ }
362
+ catch (err) {
363
+ lastError = err;
364
+ }
365
+ }
366
+ throw new Error(`No usable cache backend (last error: ${lastError instanceof Error ? lastError.message : String(lastError)})`);
367
+ }
368
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * `CachedHttpDirectory` — drop-in replacement for `HttpDirectory`
3
+ * that persists fetched bytes (index + files) in OPFS (preferred)
4
+ * or IndexedDB.
5
+ *
6
+ * Why a wrapper, not changes to `HttpDirectory`: the base class is
7
+ * the right minimum surface for read-only HTTP. Caching is an
8
+ * orthogonal concern that some consumers don't want (development,
9
+ * one-shot CLI tools). Keeping the two separate also means a bug in
10
+ * the cache layer can't poison the simple path.
11
+ *
12
+ * What gets cached:
13
+ *
14
+ * • The `index.json` per directory.
15
+ * • The body of every `file(name).arrayBuffer()` call.
16
+ *
17
+ * Staleness policy: **stale-while-revalidate.**
18
+ *
19
+ * • First read of any URL: do a normal fetch, store body + ETag +
20
+ * Last-Modified + the `Content-Type`, return.
21
+ * • Subsequent reads inside `maxAgeMs` (default 5 min): serve
22
+ * cached bytes immediately, no network at all.
23
+ * • Subsequent reads OUTSIDE `maxAgeMs`: serve cached bytes
24
+ * immediately AND kick off a background conditional GET (`If-
25
+ * None-Match` / `If-Modified-Since`). When the server replies:
26
+ * - `304 Not Modified` → bump `validatedAt`. Body untouched.
27
+ * - `200 OK` → write the new body to cache. The next call
28
+ * then sees the fresh bytes.
29
+ * • Explicit `revalidate(file)` forces an immediate conditional
30
+ * fetch and returns once the network response settles.
31
+ *
32
+ * The first-after-update read returns the OLD bytes. That's the
33
+ * standard SWR tradeoff — favours latency over freshness, lets
34
+ * the dashboard render instantly on every load.
35
+ */
36
+ import type { VirtualFile, VirtualDirectory, VirtualEntry } from './types.js';
37
+ import type { HttpDirectoryOptions } from './http.js';
38
+ import { type CacheBackend } from './cache.js';
39
+ /** Construction options for a `CachedHttpDirectory`. */
40
+ export interface CachedHttpDirectoryOptions extends HttpDirectoryOptions {
41
+ /** Cache backend. If omitted, one is auto-selected from OPFS / IDB
42
+ * / memory at first use. Sharing one backend across multiple
43
+ * directories is fine — namespaces keep entries separate. */
44
+ backend?: CacheBackend;
45
+ /** Override the auto-selected backend kind. */
46
+ preferBackend?: 'opfs' | 'idb' | 'memory';
47
+ /**
48
+ * Namespace string for this directory's cached entries. Default:
49
+ * the baseUrl with its scheme stripped. Two `CachedHttpDirectory`
50
+ * instances sharing a namespace share their cache — usually fine
51
+ * (same baseUrl = same content) but expose the knob for testing
52
+ * and for the case where two CDN URLs serve the same content and
53
+ * you want one cache hit covering both.
54
+ */
55
+ namespace?: string;
56
+ /**
57
+ * Optional canonicaliser for cache keys. Useful when URLs carry
58
+ * signed-token query strings that change per request — strip the
59
+ * token so equivalent URLs hit the same cache entry.
60
+ */
61
+ cacheKey?: (url: string) => string;
62
+ /**
63
+ * Cache entries are served without revalidation when their age is
64
+ * below this threshold. Default 5 min (300 000 ms). Set to 0 for
65
+ * always-revalidate, or `Infinity` for never-revalidate.
66
+ */
67
+ maxAgeMs?: number;
68
+ }
69
+ /** Public API exposed by every `CachedHttpDirectory` for cache
70
+ * management — same surface across nested subdirs. */
71
+ export interface CacheControl {
72
+ /** Bytes + entry count for this directory's namespace. */
73
+ stats(): Promise<{
74
+ backend: 'opfs' | 'idb' | 'memory';
75
+ entries: number;
76
+ totalBytes: number;
77
+ }>;
78
+ /** Drop every cached entry in this namespace. */
79
+ clear(): Promise<void>;
80
+ /** Force a fresh conditional GET on the given path. Resolves once
81
+ * the cache is consistent with the server. */
82
+ revalidate(path: string): Promise<void>;
83
+ /** Force-refresh the index for this directory (subset of
84
+ * `revalidate(<indexFile>)` exposed for clarity). */
85
+ revalidateIndex(): Promise<void>;
86
+ }
87
+ /** `VirtualFile` backed by a cache lookup + conditional fetch. */
88
+ export declare class CachedHttpFile implements VirtualFile {
89
+ #private;
90
+ readonly name: string;
91
+ readonly size: number;
92
+ constructor(args: {
93
+ name: string;
94
+ size: number;
95
+ url: string;
96
+ fetch: typeof globalThis.fetch;
97
+ backend: Promise<CacheBackend>;
98
+ cacheKey: string;
99
+ maxAgeMs: number;
100
+ });
101
+ arrayBuffer(): Promise<ArrayBuffer>;
102
+ /** Force a fresh conditional fetch and resolve once the cache is
103
+ * consistent with the server. */
104
+ revalidate(): Promise<void>;
105
+ }
106
+ export declare class CachedHttpDirectory implements VirtualDirectory, CacheControl {
107
+ #private;
108
+ readonly name: string;
109
+ constructor(baseUrl: string, options?: CachedHttpDirectoryOptions);
110
+ file(name: string): Promise<VirtualFile | null>;
111
+ dir(name: string): Promise<VirtualDirectory | null>;
112
+ entries(): Promise<VirtualEntry[]>;
113
+ stats(): Promise<{
114
+ backend: 'opfs' | 'idb' | 'memory';
115
+ entries: number;
116
+ totalBytes: number;
117
+ }>;
118
+ clear(): Promise<void>;
119
+ revalidate(path: string): Promise<void>;
120
+ revalidateIndex(): Promise<void>;
121
+ }
122
+ //# sourceMappingURL=cached-http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cached-http.d.ts","sourceRoot":"","sources":["../src/cached-http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC9E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,EAEL,KAAK,YAAY,EAGlB,MAAM,YAAY,CAAC;AAapB,wDAAwD;AACxD,MAAM,WAAW,0BAA2B,SAAQ,oBAAoB;IACtE;;kEAE8D;IAC9D,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,+CAA+C;IAC/C,aAAa,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;IAC1C;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;uDACuD;AACvD,MAAM,WAAW,YAAY;IAC3B,0DAA0D;IAC1D,KAAK,IAAI,OAAO,CAAC;QACf,OAAO,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;QACnC,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC,CAAC;IACH,iDAAiD;IACjD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB;mDAC+C;IAC/C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC;0DACsD;IACtD,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC;AAwBD,kEAAkE;AAClE,qBAAa,cAAe,YAAW,WAAW;;IAChD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAOV,IAAI,EAAE;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;QAC/B,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;QAC/B,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB;IAUK,WAAW,IAAI,OAAO,CAAC,WAAW,CAAC;IAwEzC;sCACkC;IAC5B,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;CAKlC;AAID,qBAAa,mBAAoB,YAAW,gBAAgB,EAAE,YAAY;;IACxE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAcV,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,0BAA+B;IA6B/D,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAmB/C,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IAkBnD,OAAO,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAelC,KAAK,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IAM7F,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAKtB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBvC,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;CAGvC"}