@offlinejs/http-cache 0.1.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/index.d.ts +77 -0
- package/dist/index.js +238 -0
- package/dist/index.js.map +1 -0
- package/package.json +33 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** Stored HTTP/JSON cache entry. */
|
|
2
|
+
interface CacheEntry<TBody = unknown> {
|
|
3
|
+
body: TBody;
|
|
4
|
+
cachedAt: number;
|
|
5
|
+
/** Soft expiry — may still be served while revalidating. */
|
|
6
|
+
expiresAt: number;
|
|
7
|
+
headers: Record<string, string>;
|
|
8
|
+
key: string;
|
|
9
|
+
status: number;
|
|
10
|
+
/** Hard expiry — never serve after this (defaults to expiresAt). */
|
|
11
|
+
staleAt: number;
|
|
12
|
+
}
|
|
13
|
+
interface HttpCacheStore {
|
|
14
|
+
clear(): Promise<void>;
|
|
15
|
+
delete(key: string): Promise<void>;
|
|
16
|
+
get<TBody = unknown>(key: string): Promise<CacheEntry<TBody> | null>;
|
|
17
|
+
set<TBody = unknown>(entry: CacheEntry<TBody>): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
type CachePolicy = {
|
|
20
|
+
/** Fresh window (ms). Default 60_000. */
|
|
21
|
+
ttlMs?: number;
|
|
22
|
+
/** Extra time to serve stale while revalidating (ms). Default 0. */
|
|
23
|
+
staleWhileRevalidateMs?: number;
|
|
24
|
+
};
|
|
25
|
+
type CachedFetchOptions = CachePolicy & {
|
|
26
|
+
/** Override cache key (default: METHOD + URL). */
|
|
27
|
+
key?: string;
|
|
28
|
+
/** Only cache these methods. Default: GET, HEAD. */
|
|
29
|
+
methods?: string[];
|
|
30
|
+
/** Cache store. Default: memory. */
|
|
31
|
+
store?: HttpCacheStore;
|
|
32
|
+
/** Custom fetch implementation. */
|
|
33
|
+
fetch?: typeof globalThis.fetch;
|
|
34
|
+
/** Parse JSON instead of returning Response. */
|
|
35
|
+
json?: boolean;
|
|
36
|
+
};
|
|
37
|
+
type CachedResult<TBody> = {
|
|
38
|
+
data: TBody;
|
|
39
|
+
fromCache: boolean;
|
|
40
|
+
headers: Record<string, string>;
|
|
41
|
+
key: string;
|
|
42
|
+
stale: boolean;
|
|
43
|
+
status: number;
|
|
44
|
+
};
|
|
45
|
+
declare const buildCacheKey: (input: RequestInfo | URL, init?: RequestInit) => string;
|
|
46
|
+
/** In-memory HTTP cache (fast, tab-local). */
|
|
47
|
+
declare const createMemoryHttpCache: () => HttpCacheStore;
|
|
48
|
+
type IndexedDBHttpCacheOptions = {
|
|
49
|
+
databaseName?: string;
|
|
50
|
+
};
|
|
51
|
+
/** Durable browser HTTP cache backed by IndexedDB. */
|
|
52
|
+
declare const createIndexedDBHttpCache: (options?: IndexedDBHttpCacheOptions) => HttpCacheStore;
|
|
53
|
+
type CacheApiStoreOptions = {
|
|
54
|
+
cacheName?: string;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Browser Cache API store for opaque Response bodies (assets / raw HTTP).
|
|
58
|
+
* Keys are request URLs (GET).
|
|
59
|
+
*/
|
|
60
|
+
declare const createCacheApiStore: (options?: CacheApiStoreOptions) => {
|
|
61
|
+
match(request: RequestInfo | URL): Promise<Response | undefined>;
|
|
62
|
+
put(request: RequestInfo | URL, response: Response): Promise<void>;
|
|
63
|
+
delete(request: RequestInfo | URL): Promise<boolean>;
|
|
64
|
+
clear(): Promise<void>;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Cached JSON/text fetch with TTL + optional stale-while-revalidate.
|
|
68
|
+
* Use this for read-through API caching — not for offline mutation queues
|
|
69
|
+
* (that’s `@offlinejs/client` / createOfflineDB).
|
|
70
|
+
*/
|
|
71
|
+
declare const cachedFetch: <TBody = unknown>(input: RequestInfo | URL, init?: RequestInit, options?: CachedFetchOptions) => Promise<CachedResult<TBody>>;
|
|
72
|
+
/** Convenience helper — always parses JSON. */
|
|
73
|
+
declare const cachedJson: <TBody = unknown>(input: RequestInfo | URL, init?: RequestInit, options?: Omit<CachedFetchOptions, "json">) => Promise<CachedResult<TBody>>;
|
|
74
|
+
declare const invalidateCacheKey: (key: string, store?: HttpCacheStore) => Promise<void>;
|
|
75
|
+
declare const clearHttpCache: (store?: HttpCacheStore) => Promise<void>;
|
|
76
|
+
|
|
77
|
+
export { type CacheApiStoreOptions, type CacheEntry, type CachePolicy, type CachedFetchOptions, type CachedResult, type HttpCacheStore, type IndexedDBHttpCacheOptions, buildCacheKey, cachedFetch, cachedJson, clearHttpCache, createCacheApiStore, createIndexedDBHttpCache, createMemoryHttpCache, invalidateCacheKey };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { now } from '@offlinejs/utils';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
var DEFAULT_TTL_MS = 6e4;
|
|
5
|
+
var DEFAULT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
|
|
6
|
+
var normalizeHeaders = (headers) => {
|
|
7
|
+
const out = {};
|
|
8
|
+
if (!headers) {
|
|
9
|
+
return out;
|
|
10
|
+
}
|
|
11
|
+
if (headers instanceof Headers) {
|
|
12
|
+
headers.forEach((value, key) => {
|
|
13
|
+
out[key.toLowerCase()] = value;
|
|
14
|
+
});
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
if (Array.isArray(headers)) {
|
|
18
|
+
for (const [key, value] of headers) {
|
|
19
|
+
out[key.toLowerCase()] = value;
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
24
|
+
out[key.toLowerCase()] = String(value);
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
};
|
|
28
|
+
var buildCacheKey = (input, init) => {
|
|
29
|
+
const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
|
|
30
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
31
|
+
return `${method} ${url}`;
|
|
32
|
+
};
|
|
33
|
+
var resolvePolicy = (policy = {}) => {
|
|
34
|
+
const ttlMs = policy.ttlMs ?? DEFAULT_TTL_MS;
|
|
35
|
+
const staleWhileRevalidateMs = policy.staleWhileRevalidateMs ?? 0;
|
|
36
|
+
return { ttlMs, staleWhileRevalidateMs };
|
|
37
|
+
};
|
|
38
|
+
var isFresh = (entry, at = now()) => at < entry.expiresAt;
|
|
39
|
+
var isUsable = (entry, at = now()) => at < entry.staleAt;
|
|
40
|
+
var createMemoryHttpCache = () => {
|
|
41
|
+
const map = /* @__PURE__ */ new Map();
|
|
42
|
+
return {
|
|
43
|
+
async get(key) {
|
|
44
|
+
const entry = map.get(key);
|
|
45
|
+
return entry ? structuredClone(entry) : null;
|
|
46
|
+
},
|
|
47
|
+
async set(entry) {
|
|
48
|
+
map.set(entry.key, structuredClone(entry));
|
|
49
|
+
},
|
|
50
|
+
async delete(key) {
|
|
51
|
+
map.delete(key);
|
|
52
|
+
},
|
|
53
|
+
async clear() {
|
|
54
|
+
map.clear();
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
var openCacheDb = (databaseName) => new Promise((resolve, reject) => {
|
|
59
|
+
const request = globalThis.indexedDB.open(databaseName, 1);
|
|
60
|
+
request.onupgradeneeded = () => {
|
|
61
|
+
const db = request.result;
|
|
62
|
+
if (!db.objectStoreNames.contains("entries")) {
|
|
63
|
+
db.createObjectStore("entries", { keyPath: "key" });
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
request.onsuccess = () => resolve(request.result);
|
|
67
|
+
request.onerror = () => reject(request.error ?? new Error("IndexedDB open failed"));
|
|
68
|
+
});
|
|
69
|
+
var idbRequest = (request) => new Promise((resolve, reject) => {
|
|
70
|
+
request.onsuccess = () => resolve(request.result);
|
|
71
|
+
request.onerror = () => reject(request.error ?? new Error("IndexedDB request failed"));
|
|
72
|
+
});
|
|
73
|
+
var createIndexedDBHttpCache = (options = {}) => {
|
|
74
|
+
const databaseName = options.databaseName ?? "offlinejs-http-cache";
|
|
75
|
+
let dbPromise = null;
|
|
76
|
+
const db = () => {
|
|
77
|
+
if (!globalThis.indexedDB) {
|
|
78
|
+
throw new Error("@offlinejs/http-cache IndexedDB store requires indexedDB");
|
|
79
|
+
}
|
|
80
|
+
dbPromise ??= openCacheDb(databaseName);
|
|
81
|
+
return dbPromise;
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
async get(key) {
|
|
85
|
+
const database = await db();
|
|
86
|
+
const entry = await idbRequest(
|
|
87
|
+
database.transaction("entries", "readonly").objectStore("entries").get(key)
|
|
88
|
+
);
|
|
89
|
+
return entry ?? null;
|
|
90
|
+
},
|
|
91
|
+
async set(entry) {
|
|
92
|
+
const database = await db();
|
|
93
|
+
await idbRequest(database.transaction("entries", "readwrite").objectStore("entries").put(entry));
|
|
94
|
+
},
|
|
95
|
+
async delete(key) {
|
|
96
|
+
const database = await db();
|
|
97
|
+
await idbRequest(database.transaction("entries", "readwrite").objectStore("entries").delete(key));
|
|
98
|
+
},
|
|
99
|
+
async clear() {
|
|
100
|
+
const database = await db();
|
|
101
|
+
await idbRequest(database.transaction("entries", "readwrite").objectStore("entries").clear());
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
var createCacheApiStore = (options = {}) => {
|
|
106
|
+
const cacheName = options.cacheName ?? "offlinejs-cache-api";
|
|
107
|
+
const open = async () => {
|
|
108
|
+
if (!globalThis.caches) {
|
|
109
|
+
throw new Error("@offlinejs/http-cache Cache API store requires caches");
|
|
110
|
+
}
|
|
111
|
+
return globalThis.caches.open(cacheName);
|
|
112
|
+
};
|
|
113
|
+
return {
|
|
114
|
+
async match(request) {
|
|
115
|
+
const cache = await open();
|
|
116
|
+
return await cache.match(request) ?? void 0;
|
|
117
|
+
},
|
|
118
|
+
async put(request, response) {
|
|
119
|
+
const cache = await open();
|
|
120
|
+
await cache.put(request, response.clone());
|
|
121
|
+
},
|
|
122
|
+
async delete(request) {
|
|
123
|
+
const cache = await open();
|
|
124
|
+
return cache.delete(request);
|
|
125
|
+
},
|
|
126
|
+
async clear() {
|
|
127
|
+
await globalThis.caches.delete(cacheName);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
var defaultStore = createMemoryHttpCache();
|
|
132
|
+
var inflightRevalidate = /* @__PURE__ */ new Map();
|
|
133
|
+
var writeEntry = async (store, key, status, headers, body, policy) => {
|
|
134
|
+
const { ttlMs, staleWhileRevalidateMs } = resolvePolicy(policy);
|
|
135
|
+
const cachedAt = now();
|
|
136
|
+
const entry = {
|
|
137
|
+
key,
|
|
138
|
+
status,
|
|
139
|
+
headers,
|
|
140
|
+
body,
|
|
141
|
+
cachedAt,
|
|
142
|
+
expiresAt: cachedAt + ttlMs,
|
|
143
|
+
staleAt: cachedAt + ttlMs + staleWhileRevalidateMs
|
|
144
|
+
};
|
|
145
|
+
await store.set(entry);
|
|
146
|
+
return entry;
|
|
147
|
+
};
|
|
148
|
+
var revalidateInBackground = (key, input, init, store, policy, fetchImpl) => {
|
|
149
|
+
if (inflightRevalidate.has(key)) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const task = (async () => {
|
|
153
|
+
const response = await fetchImpl(input, init);
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
158
|
+
const body = contentType.includes("application/json") ? await response.clone().json() : await response.clone().text();
|
|
159
|
+
await writeEntry(store, key, response.status, normalizeHeaders(response.headers), body, policy);
|
|
160
|
+
})().catch(() => {
|
|
161
|
+
}).finally(() => {
|
|
162
|
+
inflightRevalidate.delete(key);
|
|
163
|
+
});
|
|
164
|
+
inflightRevalidate.set(key, task);
|
|
165
|
+
};
|
|
166
|
+
var cachedFetch = async (input, init, options = {}) => {
|
|
167
|
+
const store = options.store ?? defaultStore;
|
|
168
|
+
const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
169
|
+
const methods = new Set((options.methods ?? [...DEFAULT_METHODS]).map((m) => m.toUpperCase()));
|
|
170
|
+
const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
|
|
171
|
+
const key = options.key ?? buildCacheKey(input, init);
|
|
172
|
+
const policy = {
|
|
173
|
+
...options.ttlMs !== void 0 ? { ttlMs: options.ttlMs } : {},
|
|
174
|
+
...options.staleWhileRevalidateMs !== void 0 ? { staleWhileRevalidateMs: options.staleWhileRevalidateMs } : {}
|
|
175
|
+
};
|
|
176
|
+
if (!methods.has(method)) {
|
|
177
|
+
const response2 = await fetchImpl(input, init);
|
|
178
|
+
const contentType2 = response2.headers.get("content-type") ?? "";
|
|
179
|
+
const data2 = options.json || contentType2.includes("application/json") ? await response2.json() : await response2.text();
|
|
180
|
+
return {
|
|
181
|
+
data: data2,
|
|
182
|
+
fromCache: false,
|
|
183
|
+
stale: false,
|
|
184
|
+
status: response2.status,
|
|
185
|
+
headers: normalizeHeaders(response2.headers),
|
|
186
|
+
key
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const cached = await store.get(key);
|
|
190
|
+
const at = now();
|
|
191
|
+
if (cached && isFresh(cached, at)) {
|
|
192
|
+
return {
|
|
193
|
+
data: cached.body,
|
|
194
|
+
fromCache: true,
|
|
195
|
+
stale: false,
|
|
196
|
+
status: cached.status,
|
|
197
|
+
headers: cached.headers,
|
|
198
|
+
key
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (cached && isUsable(cached, at)) {
|
|
202
|
+
revalidateInBackground(key, input, init, store, policy, fetchImpl);
|
|
203
|
+
return {
|
|
204
|
+
data: cached.body,
|
|
205
|
+
fromCache: true,
|
|
206
|
+
stale: true,
|
|
207
|
+
status: cached.status,
|
|
208
|
+
headers: cached.headers,
|
|
209
|
+
key
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const response = await fetchImpl(input, init);
|
|
213
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
214
|
+
const shouldJson = options.json ?? contentType.includes("application/json");
|
|
215
|
+
const data = shouldJson ? await response.json() : await response.text();
|
|
216
|
+
if (response.ok) {
|
|
217
|
+
await writeEntry(store, key, response.status, normalizeHeaders(response.headers), data, policy);
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
data,
|
|
221
|
+
fromCache: false,
|
|
222
|
+
stale: false,
|
|
223
|
+
status: response.status,
|
|
224
|
+
headers: normalizeHeaders(response.headers),
|
|
225
|
+
key
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
var cachedJson = async (input, init, options = {}) => cachedFetch(input, init, { ...options, json: true });
|
|
229
|
+
var invalidateCacheKey = async (key, store = defaultStore) => {
|
|
230
|
+
await store.delete(key);
|
|
231
|
+
};
|
|
232
|
+
var clearHttpCache = async (store = defaultStore) => {
|
|
233
|
+
await store.clear();
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export { buildCacheKey, cachedFetch, cachedJson, clearHttpCache, createCacheApiStore, createIndexedDBHttpCache, createMemoryHttpCache, invalidateCacheKey };
|
|
237
|
+
//# sourceMappingURL=index.js.map
|
|
238
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["response","contentType","data"],"mappings":"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","file":"index.js","sourcesContent":["import { now } from \"@offlinejs/utils\";\n\n/** Stored HTTP/JSON cache entry. */\nexport interface CacheEntry<TBody = unknown> {\n body: TBody;\n cachedAt: number;\n /** Soft expiry — may still be served while revalidating. */\n expiresAt: number;\n headers: Record<string, string>;\n key: string;\n status: number;\n /** Hard expiry — never serve after this (defaults to expiresAt). */\n staleAt: number;\n}\n\nexport interface HttpCacheStore {\n clear(): Promise<void>;\n delete(key: string): Promise<void>;\n get<TBody = unknown>(key: string): Promise<CacheEntry<TBody> | null>;\n set<TBody = unknown>(entry: CacheEntry<TBody>): Promise<void>;\n}\n\nexport type CachePolicy = {\n /** Fresh window (ms). Default 60_000. */\n ttlMs?: number;\n /** Extra time to serve stale while revalidating (ms). Default 0. */\n staleWhileRevalidateMs?: number;\n};\n\nexport type CachedFetchOptions = CachePolicy & {\n /** Override cache key (default: METHOD + URL). */\n key?: string;\n /** Only cache these methods. Default: GET, HEAD. */\n methods?: string[];\n /** Cache store. Default: memory. */\n store?: HttpCacheStore;\n /** Custom fetch implementation. */\n fetch?: typeof globalThis.fetch;\n /** Parse JSON instead of returning Response. */\n json?: boolean;\n};\n\nexport type CachedResult<TBody> = {\n data: TBody;\n fromCache: boolean;\n headers: Record<string, string>;\n key: string;\n stale: boolean;\n status: number;\n};\n\nconst DEFAULT_TTL_MS = 60_000;\nconst DEFAULT_METHODS = new Set([\"GET\", \"HEAD\"]);\n\nconst normalizeHeaders = (headers?: HeadersInit): Record<string, string> => {\n const out: Record<string, string> = {};\n if (!headers) {\n return out;\n }\n if (headers instanceof Headers) {\n headers.forEach((value, key) => {\n out[key.toLowerCase()] = value;\n });\n return out;\n }\n if (Array.isArray(headers)) {\n for (const [key, value] of headers) {\n out[key.toLowerCase()] = value;\n }\n return out;\n }\n for (const [key, value] of Object.entries(headers)) {\n out[key.toLowerCase()] = String(value);\n }\n return out;\n};\n\nexport const buildCacheKey = (input: RequestInfo | URL, init?: RequestInit): string => {\n const method = (init?.method ?? (input instanceof Request ? input.method : \"GET\")).toUpperCase();\n const url =\n typeof input === \"string\"\n ? input\n : input instanceof URL\n ? input.toString()\n : input.url;\n return `${method} ${url}`;\n};\n\nconst resolvePolicy = (policy: CachePolicy = {}) => {\n const ttlMs = policy.ttlMs ?? DEFAULT_TTL_MS;\n const staleWhileRevalidateMs = policy.staleWhileRevalidateMs ?? 0;\n return { ttlMs, staleWhileRevalidateMs };\n};\n\nconst isFresh = (entry: CacheEntry, at = now()): boolean => at < entry.expiresAt;\nconst isUsable = (entry: CacheEntry, at = now()): boolean => at < entry.staleAt;\n\n/** In-memory HTTP cache (fast, tab-local). */\nexport const createMemoryHttpCache = (): HttpCacheStore => {\n const map = new Map<string, CacheEntry>();\n\n return {\n async get<TBody = unknown>(key: string) {\n const entry = map.get(key);\n return entry ? (structuredClone(entry) as CacheEntry<TBody>) : null;\n },\n async set(entry) {\n map.set(entry.key, structuredClone(entry));\n },\n async delete(key) {\n map.delete(key);\n },\n async clear() {\n map.clear();\n }\n };\n};\n\nconst openCacheDb = (databaseName: string): Promise<IDBDatabase> =>\n new Promise((resolve, reject) => {\n const request = globalThis.indexedDB.open(databaseName, 1);\n request.onupgradeneeded = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(\"entries\")) {\n db.createObjectStore(\"entries\", { keyPath: \"key\" });\n }\n };\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error ?? new Error(\"IndexedDB open failed\"));\n });\n\nconst idbRequest = <T>(request: IDBRequest<T>): Promise<T> =>\n new Promise((resolve, reject) => {\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error ?? new Error(\"IndexedDB request failed\"));\n });\n\nexport type IndexedDBHttpCacheOptions = {\n databaseName?: string;\n};\n\n/** Durable browser HTTP cache backed by IndexedDB. */\nexport const createIndexedDBHttpCache = (\n options: IndexedDBHttpCacheOptions = {}\n): HttpCacheStore => {\n const databaseName = options.databaseName ?? \"offlinejs-http-cache\";\n let dbPromise: Promise<IDBDatabase> | null = null;\n\n const db = () => {\n if (!globalThis.indexedDB) {\n throw new Error(\"@offlinejs/http-cache IndexedDB store requires indexedDB\");\n }\n dbPromise ??= openCacheDb(databaseName);\n return dbPromise;\n };\n\n return {\n async get<TBody = unknown>(key: string) {\n const database = await db();\n const entry = await idbRequest(\n database.transaction(\"entries\", \"readonly\").objectStore(\"entries\").get(key)\n );\n return (entry as CacheEntry<TBody> | undefined) ?? null;\n },\n async set(entry) {\n const database = await db();\n await idbRequest(database.transaction(\"entries\", \"readwrite\").objectStore(\"entries\").put(entry));\n },\n async delete(key) {\n const database = await db();\n await idbRequest(database.transaction(\"entries\", \"readwrite\").objectStore(\"entries\").delete(key));\n },\n async clear() {\n const database = await db();\n await idbRequest(database.transaction(\"entries\", \"readwrite\").objectStore(\"entries\").clear());\n }\n };\n};\n\nexport type CacheApiStoreOptions = {\n cacheName?: string;\n};\n\n/**\n * Browser Cache API store for opaque Response bodies (assets / raw HTTP).\n * Keys are request URLs (GET).\n */\nexport const createCacheApiStore = (options: CacheApiStoreOptions = {}) => {\n const cacheName = options.cacheName ?? \"offlinejs-cache-api\";\n\n const open = async () => {\n if (!globalThis.caches) {\n throw new Error(\"@offlinejs/http-cache Cache API store requires caches\");\n }\n return globalThis.caches.open(cacheName);\n };\n\n return {\n async match(request: RequestInfo | URL): Promise<Response | undefined> {\n const cache = await open();\n return (await cache.match(request)) ?? undefined;\n },\n async put(request: RequestInfo | URL, response: Response): Promise<void> {\n const cache = await open();\n await cache.put(request, response.clone());\n },\n async delete(request: RequestInfo | URL): Promise<boolean> {\n const cache = await open();\n return cache.delete(request);\n },\n async clear(): Promise<void> {\n await globalThis.caches.delete(cacheName);\n }\n };\n};\n\ntype RevalidateMap = Map<string, Promise<void>>;\n\nconst defaultStore = createMemoryHttpCache();\nconst inflightRevalidate: RevalidateMap = new Map();\n\nconst writeEntry = async <TBody>(\n store: HttpCacheStore,\n key: string,\n status: number,\n headers: Record<string, string>,\n body: TBody,\n policy: CachePolicy\n): Promise<CacheEntry<TBody>> => {\n const { ttlMs, staleWhileRevalidateMs } = resolvePolicy(policy);\n const cachedAt = now();\n const entry: CacheEntry<TBody> = {\n key,\n status,\n headers,\n body,\n cachedAt,\n expiresAt: cachedAt + ttlMs,\n staleAt: cachedAt + ttlMs + staleWhileRevalidateMs\n };\n await store.set(entry);\n return entry;\n};\n\nconst revalidateInBackground = (\n key: string,\n input: RequestInfo | URL,\n init: RequestInit | undefined,\n store: HttpCacheStore,\n policy: CachePolicy,\n fetchImpl: typeof globalThis.fetch\n): void => {\n if (inflightRevalidate.has(key)) {\n return;\n }\n\n const task = (async () => {\n const response = await fetchImpl(input, init);\n if (!response.ok) {\n return;\n }\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n const body = contentType.includes(\"application/json\")\n ? await response.clone().json()\n : await response.clone().text();\n await writeEntry(store, key, response.status, normalizeHeaders(response.headers), body, policy);\n })()\n .catch(() => {\n // Swallow revalidation errors — stale cache remains usable until staleAt.\n })\n .finally(() => {\n inflightRevalidate.delete(key);\n });\n\n inflightRevalidate.set(key, task);\n};\n\n/**\n * Cached JSON/text fetch with TTL + optional stale-while-revalidate.\n * Use this for read-through API caching — not for offline mutation queues\n * (that’s `@offlinejs/client` / createOfflineDB).\n */\nexport const cachedFetch = async <TBody = unknown>(\n input: RequestInfo | URL,\n init?: RequestInit,\n options: CachedFetchOptions = {}\n): Promise<CachedResult<TBody>> => {\n const store = options.store ?? defaultStore;\n const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);\n const methods = new Set((options.methods ?? [...DEFAULT_METHODS]).map((m) => m.toUpperCase()));\n const method = (init?.method ?? (input instanceof Request ? input.method : \"GET\")).toUpperCase();\n const key = options.key ?? buildCacheKey(input, init);\n const policy: CachePolicy = {\n ...(options.ttlMs !== undefined ? { ttlMs: options.ttlMs } : {}),\n ...(options.staleWhileRevalidateMs !== undefined\n ? { staleWhileRevalidateMs: options.staleWhileRevalidateMs }\n : {})\n };\n\n if (!methods.has(method)) {\n const response = await fetchImpl(input, init);\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n const data = (\n options.json || contentType.includes(\"application/json\")\n ? await response.json()\n : await response.text()\n ) as TBody;\n return {\n data,\n fromCache: false,\n stale: false,\n status: response.status,\n headers: normalizeHeaders(response.headers),\n key\n };\n }\n\n const cached = await store.get<TBody>(key);\n const at = now();\n\n if (cached && isFresh(cached, at)) {\n return {\n data: cached.body,\n fromCache: true,\n stale: false,\n status: cached.status,\n headers: cached.headers,\n key\n };\n }\n\n if (cached && isUsable(cached, at)) {\n revalidateInBackground(key, input, init, store, policy, fetchImpl);\n return {\n data: cached.body,\n fromCache: true,\n stale: true,\n status: cached.status,\n headers: cached.headers,\n key\n };\n }\n\n const response = await fetchImpl(input, init);\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n const shouldJson = options.json ?? contentType.includes(\"application/json\");\n const data = (shouldJson ? await response.json() : await response.text()) as TBody;\n\n if (response.ok) {\n await writeEntry(store, key, response.status, normalizeHeaders(response.headers), data, policy);\n }\n\n return {\n data,\n fromCache: false,\n stale: false,\n status: response.status,\n headers: normalizeHeaders(response.headers),\n key\n };\n};\n\n/** Convenience helper — always parses JSON. */\nexport const cachedJson = async <TBody = unknown>(\n input: RequestInfo | URL,\n init?: RequestInit,\n options: Omit<CachedFetchOptions, \"json\"> = {}\n): Promise<CachedResult<TBody>> => cachedFetch<TBody>(input, init, { ...options, json: true });\n\nexport const invalidateCacheKey = async (\n key: string,\n store: HttpCacheStore = defaultStore\n): Promise<void> => {\n await store.delete(key);\n};\n\nexport const clearHttpCache = async (store: HttpCacheStore = defaultStore): Promise<void> => {\n await store.clear();\n};\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@offlinejs/http-cache",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "General-purpose HTTP / JSON cache layer (TTL + stale-while-revalidate) for OfflineJS apps.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@offlinejs/utils": "0.1.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"fake-indexeddb": "^6.2.5",
|
|
21
|
+
"tsup": "latest",
|
|
22
|
+
"typescript": "latest",
|
|
23
|
+
"vitest": "latest"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public",
|
|
27
|
+
"registry": "https://registry.npmjs.org/"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsup",
|
|
31
|
+
"typecheck": "tsc --noEmit"
|
|
32
|
+
}
|
|
33
|
+
}
|