@toapi/worker 0.12.1

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,16 @@
1
+ export declare const META_STORE_NAME = "meta";
2
+ export declare const TAGS_STORE_NAME = "tags";
3
+ export interface CacheMeta {
4
+ tags: string[];
5
+ expiresAt: number | null;
6
+ }
7
+ export declare function openCacheMetaDB(): Promise<IDBDatabase>;
8
+ export declare function openCache(): Promise<Cache>;
9
+ export declare function deleteCache(): Promise<void>;
10
+ export declare function storeCacheEntry(req: Request, res: Response): Promise<void>;
11
+ export declare function deleteCacheEntry(req: Request): Promise<void>;
12
+ export declare function getMetadata(url: string): Promise<CacheMeta | null>;
13
+ export declare function getCachedEntry(req: Request): Promise<Response | undefined>;
14
+ export declare function invalidateTags(tags: string[]): Promise<void>;
15
+ export declare function expireAll(): Promise<string[]>;
16
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,eAAe,SAAS,CAAC;AACtC,eAAO,MAAM,eAAe,SAAS,CAAC;AAEtC,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAKD,wBAAgB,eAAe,yBAS9B;AAED,wBAAgB,SAAS,mBAMxB;AAED,wBAAsB,WAAW,kBAIhC;AAED,wBAAsB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,iBAiChE;AAED,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,OAAO,iBAmClD;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,6BAU5C;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,iCAGhD;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,iBA4BlD;AAED,wBAAsB,SAAS,sBAwB9B"}
package/dist/cache.js ADDED
@@ -0,0 +1,141 @@
1
+ import { EXPIRES_AT_HEADER, TAGS_HEADER } from "@toapi/common";
2
+ import { deleteDB, openDB } from "./idb";
3
+ const DB_NAME = "tapi-cache-meta";
4
+ const CACHE_NAME = "tapi-cache";
5
+ const DB_VERSION = 1;
6
+ export const META_STORE_NAME = "meta";
7
+ export const TAGS_STORE_NAME = "tags";
8
+ let metaDb = null;
9
+ let cache = null;
10
+ export function openCacheMetaDB() {
11
+ if (metaDb)
12
+ return metaDb;
13
+ metaDb = openDB(DB_NAME, DB_VERSION, (db) => {
14
+ db.createObjectStore(META_STORE_NAME);
15
+ db.createObjectStore(TAGS_STORE_NAME);
16
+ });
17
+ return metaDb;
18
+ }
19
+ export function openCache() {
20
+ if (cache)
21
+ return cache;
22
+ cache = self.caches.open(CACHE_NAME);
23
+ return cache;
24
+ }
25
+ export async function deleteCache() {
26
+ metaDb = null;
27
+ cache = null;
28
+ await Promise.all([deleteDB(DB_NAME), self.caches.delete(CACHE_NAME)]);
29
+ }
30
+ export async function storeCacheEntry(req, res) {
31
+ const [metaDb, cache] = await Promise.all([openCacheMetaDB(), openCache()]);
32
+ const url = req.url;
33
+ const expiresAt = res.headers.get(EXPIRES_AT_HEADER);
34
+ const meta = {
35
+ tags: res.headers.get(TAGS_HEADER)?.split(" ").filter(Boolean) ?? [],
36
+ expiresAt: expiresAt ? parseInt(expiresAt, 10) : null,
37
+ };
38
+ await new Promise(async (resolve, reject) => {
39
+ const tx = metaDb.transaction([META_STORE_NAME, TAGS_STORE_NAME], "readwrite");
40
+ tx.oncomplete = () => resolve();
41
+ tx.onerror = () => reject(new Error(`Failed to store metadata for "${url}"`));
42
+ const metaStore = tx.objectStore(META_STORE_NAME);
43
+ metaStore.put(meta, url);
44
+ const tagsStore = tx.objectStore(TAGS_STORE_NAME);
45
+ for (const tag of meta.tags) {
46
+ const req = tagsStore.get(tag);
47
+ req.onsuccess = () => {
48
+ const urls = req.result ?? [];
49
+ urls.push(url);
50
+ tagsStore.put(urls, tag);
51
+ };
52
+ }
53
+ });
54
+ await cache.put(req, res.clone());
55
+ }
56
+ export async function deleteCacheEntry(req) {
57
+ const [metaDb, cache] = await Promise.all([openCacheMetaDB(), openCache()]);
58
+ const url = req.url;
59
+ await new Promise((resolve, reject) => {
60
+ const tx = metaDb.transaction([META_STORE_NAME, TAGS_STORE_NAME], "readwrite");
61
+ tx.oncomplete = () => resolve();
62
+ tx.onerror = () => reject(new Error(`Failed to clear metadata for "${url}"`));
63
+ const metaStore = tx.objectStore(META_STORE_NAME);
64
+ const tagsStore = tx.objectStore(TAGS_STORE_NAME);
65
+ const meta = metaStore.get(url);
66
+ meta.onsuccess = () => {
67
+ const tags = meta.result?.tags ?? [];
68
+ for (const tag of tags) {
69
+ const req = tagsStore.get(tag);
70
+ req.onsuccess = () => {
71
+ const urls = req.result ?? [];
72
+ tagsStore.put(urls.filter((u) => u !== url), tag);
73
+ };
74
+ }
75
+ };
76
+ metaStore.delete(url);
77
+ });
78
+ await cache.delete(req);
79
+ }
80
+ export async function getMetadata(url) {
81
+ const db = await openCacheMetaDB();
82
+ return new Promise((resolve, reject) => {
83
+ const tx = db.transaction(META_STORE_NAME, "readonly");
84
+ const metaStore = tx.objectStore(META_STORE_NAME);
85
+ const req = metaStore.get(url);
86
+ req.onsuccess = () => resolve(req.result ?? null);
87
+ req.onerror = () => reject(new Error(`Failed to retrieve metadata for "${url}"`));
88
+ });
89
+ }
90
+ export async function getCachedEntry(req) {
91
+ const cache = await openCache();
92
+ return cache.match(req);
93
+ }
94
+ export async function invalidateTags(tags) {
95
+ const [metaDb, cache] = await Promise.all([openCacheMetaDB(), openCache()]);
96
+ return new Promise((resolve, reject) => {
97
+ const deletes = [];
98
+ const tx = metaDb.transaction([TAGS_STORE_NAME, META_STORE_NAME], "readwrite");
99
+ tx.oncomplete = async () => {
100
+ await Promise.all(deletes);
101
+ resolve();
102
+ };
103
+ tx.onerror = () => reject(new Error(`Failed to invalidate tags ${tags.join(", ")}`));
104
+ const tagsStore = tx.objectStore(TAGS_STORE_NAME);
105
+ const metaStore = tx.objectStore(META_STORE_NAME);
106
+ for (const tag of tags) {
107
+ const req = tagsStore.get(tag);
108
+ req.onsuccess = () => {
109
+ const urls = req.result ?? [];
110
+ for (const url of urls) {
111
+ metaStore.delete(url);
112
+ deletes.push(cache.delete(url));
113
+ }
114
+ tagsStore.delete(tag);
115
+ };
116
+ }
117
+ });
118
+ }
119
+ export async function expireAll() {
120
+ const metaDb = await openCacheMetaDB();
121
+ return new Promise((resolve, reject) => {
122
+ const tags = new Set();
123
+ const tx = metaDb.transaction([META_STORE_NAME], "readwrite");
124
+ tx.oncomplete = () => resolve(Array.from(tags));
125
+ tx.onerror = () => reject(new Error(`Failed to expire all cache entries`));
126
+ const metaStore = tx.objectStore(META_STORE_NAME);
127
+ const cursorRequest = metaStore.openCursor();
128
+ cursorRequest.onsuccess = () => {
129
+ const cursor = cursorRequest.result;
130
+ if (!cursor)
131
+ return;
132
+ const value = cursor.value;
133
+ value.expiresAt = Date.now();
134
+ for (const tag of value.tags) {
135
+ tags.add(tag);
136
+ }
137
+ metaStore.put(value, cursor.key);
138
+ cursor.continue();
139
+ };
140
+ });
141
+ }
@@ -0,0 +1,28 @@
1
+ export interface CleanupOptions {
2
+ /**
3
+ * Grace period in seconds past a cache entry's `expiresAt` before the
4
+ * entry is dropped. Entries expired within this window are kept; entries
5
+ * expired longer than this window are deleted from both the cache and
6
+ * the meta store.
7
+ */
8
+ maximumStaleAge: number;
9
+ }
10
+ /**
11
+ * Reconcile the worker's cache and metadata stores. Intended to be called
12
+ * from the service worker's `activate` event:
13
+ *
14
+ * ```ts
15
+ * self.addEventListener("activate", (event) => {
16
+ * event.waitUntil(cleanup({ maximumStaleAge: 60 * 60 * 24 * 7 }));
17
+ * });
18
+ * ```
19
+ *
20
+ * Performs three things:
21
+ * 1. Drops cache + meta entries whose `expiresAt` is older than
22
+ * `maximumStaleAge` seconds.
23
+ * 2. Drops cache entries that have no corresponding meta record (orphans).
24
+ * 3. Rebuilds the tags store from the surviving meta records, healing any
25
+ * drift between the two stores.
26
+ */
27
+ export declare function cleanup({ maximumStaleAge, }: CleanupOptions): Promise<void>;
28
+ //# sourceMappingURL=cleanup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cleanup.d.ts","sourceRoot":"","sources":["../src/cleanup.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,OAAO,CAAC,EAC5B,eAAe,GAChB,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAoEhC"}
@@ -0,0 +1,80 @@
1
+ import { META_STORE_NAME, TAGS_STORE_NAME, openCache, openCacheMetaDB, } from "./cache";
2
+ /**
3
+ * Reconcile the worker's cache and metadata stores. Intended to be called
4
+ * from the service worker's `activate` event:
5
+ *
6
+ * ```ts
7
+ * self.addEventListener("activate", (event) => {
8
+ * event.waitUntil(cleanup({ maximumStaleAge: 60 * 60 * 24 * 7 }));
9
+ * });
10
+ * ```
11
+ *
12
+ * Performs three things:
13
+ * 1. Drops cache + meta entries whose `expiresAt` is older than
14
+ * `maximumStaleAge` seconds.
15
+ * 2. Drops cache entries that have no corresponding meta record (orphans).
16
+ * 3. Rebuilds the tags store from the surviving meta records, healing any
17
+ * drift between the two stores.
18
+ */
19
+ export async function cleanup({ maximumStaleAge, }) {
20
+ const [metaDb, cache] = await Promise.all([openCacheMetaDB(), openCache()]);
21
+ const cutoff = Date.now() - maximumStaleAge * 1000;
22
+ const survivors = new Map();
23
+ const dropped = [];
24
+ // Pass 1: cursor meta, drop long-expired records, rebuild tags store
25
+ // from survivors. All in a single readwrite transaction so meta and tags
26
+ // can't disagree halfway through.
27
+ await new Promise((resolve, reject) => {
28
+ const tx = metaDb.transaction([META_STORE_NAME, TAGS_STORE_NAME], "readwrite");
29
+ tx.oncomplete = () => resolve();
30
+ tx.onerror = () => reject(new Error("Failed to clean up tapi cache"));
31
+ const metaStore = tx.objectStore(META_STORE_NAME);
32
+ const tagsStore = tx.objectStore(TAGS_STORE_NAME);
33
+ const cursorReq = metaStore.openCursor();
34
+ cursorReq.onsuccess = () => {
35
+ const cursor = cursorReq.result;
36
+ if (!cursor) {
37
+ // cursor done — rebuild tags store from `survivors`
38
+ tagsStore.clear();
39
+ const tagToUrls = new Map();
40
+ for (const [url, tags] of survivors) {
41
+ for (const tag of tags) {
42
+ const urls = tagToUrls.get(tag);
43
+ if (urls)
44
+ urls.push(url);
45
+ else
46
+ tagToUrls.set(tag, [url]);
47
+ }
48
+ }
49
+ for (const [tag, urls] of tagToUrls) {
50
+ tagsStore.put(urls, tag);
51
+ }
52
+ return;
53
+ }
54
+ const url = cursor.key;
55
+ const value = cursor.value;
56
+ if (value.expiresAt !== null && value.expiresAt < cutoff) {
57
+ dropped.push(url);
58
+ cursor.delete();
59
+ }
60
+ else {
61
+ survivors.set(url, value.tags);
62
+ }
63
+ cursor.continue();
64
+ };
65
+ });
66
+ // Delete dropped URLs from Cache Storage. Outside the IDB transaction
67
+ // because the Cache API is async and would auto-commit the tx.
68
+ await Promise.all(dropped.map((url) => cache.delete(url)));
69
+ // Pass 2: drop cache entries that have no surviving meta record.
70
+ // `survivors` already contains every URL with a (kept) meta record, so
71
+ // anything in the cache not in `survivors` and not in `dropped` is an
72
+ // orphan from a prior interrupted write.
73
+ const cacheKeys = await cache.keys();
74
+ const knownUrls = new Set(survivors.keys());
75
+ await Promise.all(cacheKeys.map((req) => {
76
+ if (knownUrls.has(req.url))
77
+ return Promise.resolve(true);
78
+ return cache.delete(req);
79
+ }));
80
+ }
package/dist/idb.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare function openDB(name: string, version: number, migrate: (db: IDBDatabase) => void): Promise<IDBDatabase>;
2
+ export declare function deleteDB(name: string): Promise<void>;
3
+ //# sourceMappingURL=idb.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idb.d.ts","sourceRoot":"","sources":["../src/idb.ts"],"names":[],"mappings":"AAAA,wBAAgB,MAAM,CACpB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,GACjC,OAAO,CAAC,WAAW,CAAC,CAatB;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUpD"}
package/dist/idb.js ADDED
@@ -0,0 +1,25 @@
1
+ export function openDB(name, version, migrate) {
2
+ return new Promise((resolve, reject) => {
3
+ const req = self.indexedDB.open(name, version);
4
+ req.onsuccess = () => {
5
+ resolve(req.result);
6
+ };
7
+ req.onerror = () => {
8
+ reject(new Error(`Failed to open IndexedDB Database ${name}@${version}`));
9
+ };
10
+ req.onupgradeneeded = () => {
11
+ migrate(req.result);
12
+ };
13
+ });
14
+ }
15
+ export function deleteDB(name) {
16
+ return new Promise((resolve, reject) => {
17
+ const req = self.indexedDB.deleteDatabase(name);
18
+ req.onsuccess = () => {
19
+ resolve();
20
+ };
21
+ req.onerror = () => {
22
+ reject(new Error(`Failed to delete IndexedDB Database ${name}`));
23
+ };
24
+ });
25
+ }
@@ -0,0 +1,9 @@
1
+ import type { Logger } from "@toapi/common";
2
+ export { listenForInvalidations } from "./revalidation-stream";
3
+ export { cleanup } from "./cleanup";
4
+ export type { CleanupOptions } from "./cleanup";
5
+ export type { Logger } from "@toapi/common";
6
+ export declare function handleTapiRequest(req: Request, options?: {
7
+ logger?: Logger;
8
+ }): Promise<Response>;
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAI5C,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,YAAY,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,YAAY,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAE5C,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,qBAmClF"}
package/dist/index.js ADDED
@@ -0,0 +1,40 @@
1
+ import { isMutation } from "@toapi/common";
2
+ import { getCachedEntry, getMetadata } from "./cache";
3
+ import { mutateAndInvalidate } from "./mutate-and-invalidate";
4
+ import { serveFromNetwork } from "./serve-from-network";
5
+ export { listenForInvalidations } from "./revalidation-stream";
6
+ export { cleanup } from "./cleanup";
7
+ export async function handleTapiRequest(req, options) {
8
+ const errorLog = options?.logger?.error ?? ((err) => console.error("TApi Worker fetch failed", err));
9
+ if (isMutation(req)) {
10
+ return mutateAndInvalidate(req);
11
+ }
12
+ else {
13
+ const cachedResponse = await getCachedEntry(req);
14
+ if (!cachedResponse) {
15
+ // no cached response, serve from network
16
+ return serveFromNetwork(req);
17
+ }
18
+ const meta = await getMetadata(req.url);
19
+ if (meta?.expiresAt) {
20
+ if (meta.expiresAt > Date.now()) {
21
+ // cached response is still valid
22
+ return cachedResponse;
23
+ }
24
+ else {
25
+ // cached response is expired
26
+ try {
27
+ // try to serve from network
28
+ return serveFromNetwork(req);
29
+ }
30
+ catch (error) {
31
+ // probably network not available, serve old response
32
+ errorLog(error);
33
+ return cachedResponse;
34
+ }
35
+ }
36
+ }
37
+ // no expiration header, serve cached response
38
+ return cachedResponse;
39
+ }
40
+ }
@@ -0,0 +1,2 @@
1
+ export declare function mutateAndInvalidate(req: Request): Promise<Response>;
2
+ //# sourceMappingURL=mutate-and-invalidate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mutate-and-invalidate.d.ts","sourceRoot":"","sources":["../src/mutate-and-invalidate.ts"],"names":[],"mappings":"AAGA,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,OAAO,qBAUrD"}
@@ -0,0 +1,11 @@
1
+ import { TAGS_HEADER } from "@toapi/common";
2
+ import { invalidateTags } from "./cache";
3
+ export async function mutateAndInvalidate(req) {
4
+ const res = await fetch(req);
5
+ const tags = res.headers.get(TAGS_HEADER)?.split(" ")?.filter(Boolean) ?? [];
6
+ if (tags.length === 0) {
7
+ return res;
8
+ }
9
+ await invalidateTags(tags);
10
+ return res;
11
+ }
@@ -0,0 +1,6 @@
1
+ interface Options {
2
+ url: string;
3
+ }
4
+ export declare function listenForInvalidations({ url }: Options): Promise<void>;
5
+ export {};
6
+ //# sourceMappingURL=revalidation-stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"revalidation-stream.d.ts","sourceRoot":"","sources":["../src/revalidation-stream.ts"],"names":[],"mappings":"AAQA,UAAU,OAAO;IACf,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAsB,sBAAsB,CAAC,EAAE,GAAG,EAAE,EAAE,OAAO,iBAoF5D"}
@@ -0,0 +1,70 @@
1
+ import { INVALIDATION_POST_EVENT, TAGS_CONTENT_TYPE, } from "@toapi/common";
2
+ import { deleteCache, expireAll, invalidateTags } from "./cache";
3
+ export async function listenForInvalidations({ url }) {
4
+ console.info("TApi: Listening for invalidations...");
5
+ let res = null;
6
+ const MAX_ATTEMPTS = 1000;
7
+ for (let retry = 0; retry < MAX_ATTEMPTS; retry++) {
8
+ try {
9
+ res = await fetch(url);
10
+ break;
11
+ }
12
+ catch (error) {
13
+ console.warn(`TApi: Failed attempt #${retry + 1} to open invalidation stream`, error);
14
+ }
15
+ await new Promise((resolve) => setTimeout(resolve, 500 * Math.pow(2, retry)));
16
+ }
17
+ if (!res) {
18
+ console.error(`TApi: Failed to open invalidation stream after ${MAX_ATTEMPTS} attempts, giving up.`);
19
+ return;
20
+ }
21
+ const contentType = res.headers.get("Content-Type");
22
+ if (!res.ok || contentType !== TAGS_CONTENT_TYPE || !res.body) {
23
+ console.error("TApi: Failed to open invalidation stream. Cleaning up and unregistering service worker.", res.status, res.statusText);
24
+ await deleteCache();
25
+ await self.registration.unregister();
26
+ return;
27
+ }
28
+ console.info("TApi: Invalidation Stream Connection Established");
29
+ try {
30
+ const tags = await expireAll();
31
+ const clients = await self.clients.matchAll();
32
+ for (const client of clients) {
33
+ client.postMessage({ type: INVALIDATION_POST_EVENT, tags });
34
+ }
35
+ console.info("TApi: Marked all cached entries as expired");
36
+ }
37
+ catch {
38
+ console.warn("TApi: Failed to expire existing cache entries");
39
+ }
40
+ try {
41
+ let buffer = "";
42
+ const decoder = new TextDecoder();
43
+ for await (const chunk of res.body) {
44
+ buffer += decoder.decode(chunk);
45
+ const lines = buffer.split("\n");
46
+ buffer = lines.pop() || "";
47
+ const clients = await self.clients.matchAll();
48
+ for (const line of lines) {
49
+ const rawTags = line.trim();
50
+ if (!rawTags)
51
+ continue;
52
+ const tags = rawTags.split(" ");
53
+ console.info("TApi: Remote-Invalidating tags", tags);
54
+ await invalidateTags(tags);
55
+ for (const client of clients) {
56
+ client.postMessage({ type: INVALIDATION_POST_EVENT, tags });
57
+ }
58
+ }
59
+ }
60
+ }
61
+ catch (error) {
62
+ if (error instanceof Error && error.name === "NetworkError") {
63
+ console.info("TApi: Network disconnected, retrying revalidation connection...");
64
+ setTimeout(() => {
65
+ listenForInvalidations({ url });
66
+ }, 5000);
67
+ }
68
+ console.error("TApi: Failed to read invalidation stream", error);
69
+ }
70
+ }
@@ -0,0 +1,2 @@
1
+ export declare function serveFromNetwork(req: Request): Promise<Response>;
2
+ //# sourceMappingURL=serve-from-network.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve-from-network.d.ts","sourceRoot":"","sources":["../src/serve-from-network.ts"],"names":[],"mappings":"AAGA,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,OAAO,qBAYlD"}
@@ -0,0 +1,14 @@
1
+ import { EXPIRES_AT_HEADER, TAGS_HEADER } from "@toapi/common";
2
+ import { deleteCacheEntry, storeCacheEntry } from "./cache";
3
+ export async function serveFromNetwork(req) {
4
+ const res = await fetch(req);
5
+ // only cache ok responses with tags or expires-at header
6
+ if (res.ok &&
7
+ (res.headers.has(TAGS_HEADER) || res.headers.has(EXPIRES_AT_HEADER))) {
8
+ await storeCacheEntry(req, res);
9
+ }
10
+ else {
11
+ await deleteCacheEntry(req);
12
+ }
13
+ return res;
14
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@toapi/worker",
3
+ "version": "0.12.1",
4
+ "author": {
5
+ "name": "Michel Smola",
6
+ "email": "michel.smola@farbenmeer.de"
7
+ },
8
+ "type": "module",
9
+ "module": "dist/index.js",
10
+ "main": "dist/index.js",
11
+ "private": false,
12
+ "license": "MIT",
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "dependencies": {
23
+ "@toapi/common": "^0.12.1"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^25.0.3",
27
+ "vitest": "^4.0.16"
28
+ },
29
+ "peerDependencies": {
30
+ "typescript": "^5 || ^6.0.0"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/farbenmeer/tapi.git"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc --noEmit false",
38
+ "release": "pnpm build && pnpm publish --no-git-checks",
39
+ "test": "vitest run --passWithNoTests"
40
+ }
41
+ }