@toapi/worker 1.2.1 → 1.2.3

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,7 @@
1
+ import type { Logger } from "@toapi/common";
2
+ export declare const consoleFallback: (logger?: Logger) => {
3
+ error: (error: unknown) => import("@toapi/common").MaybePromise<void>;
4
+ warn: (message: string) => import("@toapi/common").MaybePromise<void>;
5
+ info: (message: string) => import("@toapi/common").MaybePromise<void>;
6
+ };
7
+ //# sourceMappingURL=console-fallback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console-fallback.d.ts","sourceRoot":"","sources":["../src/console-fallback.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAE5C,eAAO,MAAM,eAAe,GAAI,SAAS,MAAM;;;;CAK1B,CAAC"}
@@ -0,0 +1,5 @@
1
+ export const consoleFallback = (logger) => ({
2
+ error: logger?.error ?? ((error) => console.error("Toapi Worker:", error)),
3
+ warn: logger?.warn ?? ((message) => console.warn("Toapi Worker:", message)),
4
+ info: logger?.info ?? ((message) => console.info("Toapi Worker:", message)),
5
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"handle-toapi-request.d.ts","sourceRoot":"","sources":["../src/handle-toapi-request.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAM5C,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,OAAO,EACZ,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,qBAsC9B;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,2BAAqB,CAAC"}
1
+ {"version":3,"file":"handle-toapi-request.d.ts","sourceRoot":"","sources":["../src/handle-toapi-request.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAO5C,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,OAAO,EACZ,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,qBA8B9B;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,2BAAqB,CAAC"}
@@ -2,9 +2,9 @@ import { isMutation } from "@toapi/common";
2
2
  import { getCachedEntry, getMetadata } from "./cache";
3
3
  import { mutateAndInvalidate } from "./mutate-and-invalidate";
4
4
  import { serveFromNetwork } from "./serve-from-network";
5
+ import { consoleFallback } from "./console-fallback";
5
6
  export async function handleToapiRequest(req, options) {
6
- const errorLog = options?.logger?.error ??
7
- ((err) => console.error("Toapi Worker fetch failed", err));
7
+ const logger = consoleFallback(options?.logger);
8
8
  if (isMutation(req)) {
9
9
  return mutateAndInvalidate(req);
10
10
  }
@@ -15,26 +15,21 @@ export async function handleToapiRequest(req, options) {
15
15
  return serveFromNetwork(req);
16
16
  }
17
17
  const meta = await getMetadata(req.url);
18
- if (meta?.expiresAt) {
19
- if (meta.expiresAt > Date.now()) {
20
- // cached response is still valid
21
- return cachedResponse;
18
+ if (meta && (!meta.expiresAt || meta.expiresAt > Date.now())) {
19
+ return cachedResponse;
20
+ }
21
+ else {
22
+ // cached response is expired
23
+ try {
24
+ // try to serve from network
25
+ return await serveFromNetwork(req);
22
26
  }
23
- else {
24
- // cached response is expired
25
- try {
26
- // try to serve from network
27
- return await serveFromNetwork(req);
28
- }
29
- catch (error) {
30
- // probably network not available, serve old response
31
- errorLog(error);
32
- return cachedResponse;
33
- }
27
+ catch (error) {
28
+ // probably network not available, serve old response
29
+ logger.error(error);
30
+ return cachedResponse;
34
31
  }
35
32
  }
36
- // no expiration header, serve cached response
37
- return cachedResponse;
38
33
  }
39
34
  }
40
35
  /**
@@ -1,6 +1,9 @@
1
+ import { type Logger } from "@toapi/common";
1
2
  interface Options {
2
3
  url: string;
4
+ timeout?: number;
5
+ logger?: Logger;
3
6
  }
4
- export declare function listenForInvalidations({ url }: Options): Promise<void>;
7
+ export declare function listenForInvalidations({ url, timeout, logger: customLogger, }: Options): Promise<void>;
5
8
  export {};
6
9
  //# sourceMappingURL=revalidation-stream.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"revalidation-stream.d.ts","sourceRoot":"","sources":["../src/revalidation-stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,MAAM,EACZ,MAAM,eAAe,CAAC;AAMvB,UAAU,OAAO;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,sBAAsB,CAAC,EAC3C,GAAG,EACH,OAAc,EACd,MAAM,EAAE,YAAY,GACrB,EAAE,OAAO,iBAqFT"}
@@ -1,7 +1,9 @@
1
1
  import { INVALIDATION_POST_EVENT, TAGS_CONTENT_TYPE, } from "@toapi/common";
2
2
  import { deleteCache, expireAll, invalidateTags } from "./cache";
3
- export async function listenForInvalidations({ url }) {
4
- console.info("TApi: Listening for invalidations...");
3
+ import { consoleFallback } from "./console-fallback";
4
+ export async function listenForInvalidations({ url, timeout = 5000, logger: customLogger, }) {
5
+ const logger = consoleFallback(customLogger);
6
+ logger.info("Listening for invalidations...");
5
7
  let res = null;
6
8
  const MAX_ATTEMPTS = 1000;
7
9
  for (let retry = 0; retry < MAX_ATTEMPTS; retry++) {
@@ -10,32 +12,32 @@ export async function listenForInvalidations({ url }) {
10
12
  break;
11
13
  }
12
14
  catch (error) {
13
- console.warn(`TApi: Failed attempt #${retry + 1} to open invalidation stream`, error);
15
+ logger.warn(`Failed attempt #${retry + 1} to open invalidation stream\n${String(error)}`);
14
16
  }
15
17
  await new Promise((resolve) => setTimeout(resolve, 500 * Math.pow(2, retry)));
16
18
  }
17
19
  if (!res) {
18
- console.error(`TApi: Failed to open invalidation stream after ${MAX_ATTEMPTS} attempts, giving up.`);
20
+ logger.error(new Error(`Failed to open invalidation stream after ${MAX_ATTEMPTS} attempts, giving up.`));
19
21
  return;
20
22
  }
21
23
  const contentType = res.headers.get("Content-Type");
22
24
  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);
25
+ logger.error(new Error(`Failed to open invalidation stream: Received ${res.status} ${res.statusText} with content type ${contentType}. Cleaning up and unregistering service worker.`));
24
26
  await deleteCache();
25
27
  await self.registration.unregister();
26
28
  return;
27
29
  }
28
- console.info("TApi: Invalidation Stream Connection Established");
30
+ logger.info("Invalidation Stream Connection Established");
29
31
  try {
30
32
  const tags = await expireAll();
31
33
  const clients = await self.clients.matchAll();
32
34
  for (const client of clients) {
33
35
  client.postMessage({ type: INVALIDATION_POST_EVENT, tags });
34
36
  }
35
- console.info("TApi: Marked all cached entries as expired");
37
+ logger.info("Marked all cached entries as expired");
36
38
  }
37
39
  catch {
38
- console.warn("TApi: Failed to expire existing cache entries");
40
+ logger.warn("Failed to expire existing cache entries");
39
41
  }
40
42
  try {
41
43
  let buffer = "";
@@ -50,7 +52,7 @@ export async function listenForInvalidations({ url }) {
50
52
  if (!rawTags)
51
53
  continue;
52
54
  const tags = rawTags.split(" ");
53
- console.info("TApi: Remote-Invalidating tags", tags);
55
+ logger.info(`Remote-Invalidating tags: ${tags}`);
54
56
  await invalidateTags(tags);
55
57
  for (const client of clients) {
56
58
  client.postMessage({ type: INVALIDATION_POST_EVENT, tags });
@@ -59,12 +61,9 @@ export async function listenForInvalidations({ url }) {
59
61
  }
60
62
  }
61
63
  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);
64
+ logger.warn(`Invalidation stream failure, retrying in ${Math.round(timeout / 1000)}s`);
65
+ setTimeout(() => {
66
+ listenForInvalidations({ url, timeout: Math.round(timeout * 1.5) });
67
+ }, timeout);
69
68
  }
70
69
  }
package/dist/setup.d.ts CHANGED
@@ -17,24 +17,30 @@ export interface SetupToapiWorkerOptions {
17
17
  invalidationsUrl?: string;
18
18
  /**
19
19
  * Grace period in seconds past a cache entry's `expiresAt` before it is
20
- * dropped on the next `activate`. See {@link CleanupOptions}.
20
+ * dropped by the cleanup pass that runs on every worker startup. See
21
+ * {@link CleanupOptions}.
21
22
  *
22
23
  * @default 60 * 60 * 24 * 7 // 7 days
23
24
  */
24
25
  maximumStaleAge?: number;
25
26
  /**
26
- * Optional logger. Its `error` method is used for failed network refetches
27
- * and for a fatal failure of the invalidation stream. Defaults to
28
- * `console.error`.
27
+ * Optional logger. `error` reports failed network refetches and a fatal
28
+ * failure of the invalidation stream, `warn` reports invalidation-stream
29
+ * retries, and `info` reports stream progress. Each method that is omitted
30
+ * falls back to the matching `console` method.
29
31
  */
30
32
  logger?: Logger;
31
33
  }
32
34
  /**
33
35
  * Wire up the whole Toapi service worker in a single call. This registers the
34
- * `activate` and `fetch` listeners and opens the revalidation stream, and is
35
- * equivalent to calling {@link cleanup}, {@link handleToapiRequest}, and
36
+ * `fetch` listener, runs {@link cleanup}, and opens the revalidation stream,
37
+ * and is equivalent to calling {@link cleanup}, {@link handleToapiRequest}, and
36
38
  * {@link listenForInvalidations} by hand.
37
39
  *
40
+ * Cleanup runs at worker startup rather than from the `activate` event, which
41
+ * only fires when a new worker takes over — an installed worker that is never
42
+ * updated would otherwise never clean up.
43
+ *
38
44
  * ```ts
39
45
  * // service-worker.ts
40
46
  * import { setupToapiWorker } from "@toapi/worker";
@@ -49,5 +55,5 @@ export interface SetupToapiWorkerOptions {
49
55
  * `vite-plugin-pwa`'s precaching) — requests it doesn't handle fall through to
50
56
  * them.
51
57
  */
52
- export declare function setupToapiWorker({ basePath, invalidationsUrl, maximumStaleAge, logger, }?: SetupToapiWorkerOptions): void;
58
+ export declare function setupToapiWorker({ basePath, invalidationsUrl, maximumStaleAge, logger: customLogger, }?: SetupToapiWorkerOptions): void;
53
59
  //# sourceMappingURL=setup.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAU5C,MAAM,WAAW,uBAAuB;IACtC;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,QAAiB,EACjB,gBAAsD,EACtD,eAAkC,EAClC,MAAM,GACP,GAAE,uBAA4B,QAwB9B"}
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAW5C,MAAM,WAAW,uBAAuB;IACtC;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,QAAiB,EACjB,gBAAsD,EACtD,eAAkC,EAClC,MAAM,EAAE,YAAY,GACrB,GAAE,uBAA4B,QAkB9B"}
package/dist/setup.js CHANGED
@@ -2,13 +2,18 @@ import { INVALIDATIONS_ROUTE } from "@toapi/common";
2
2
  import { cleanup } from "./cleanup";
3
3
  import { handleToapiRequest } from "./handle-toapi-request";
4
4
  import { listenForInvalidations } from "./revalidation-stream";
5
+ import { consoleFallback } from "./console-fallback";
5
6
  const ONE_WEEK_SECONDS = 60 * 60 * 24 * 7;
6
7
  /**
7
8
  * Wire up the whole Toapi service worker in a single call. This registers the
8
- * `activate` and `fetch` listeners and opens the revalidation stream, and is
9
- * equivalent to calling {@link cleanup}, {@link handleToapiRequest}, and
9
+ * `fetch` listener, runs {@link cleanup}, and opens the revalidation stream,
10
+ * and is equivalent to calling {@link cleanup}, {@link handleToapiRequest}, and
10
11
  * {@link listenForInvalidations} by hand.
11
12
  *
13
+ * Cleanup runs at worker startup rather than from the `activate` event, which
14
+ * only fires when a new worker takes over — an installed worker that is never
15
+ * updated would otherwise never clean up.
16
+ *
12
17
  * ```ts
13
18
  * // service-worker.ts
14
19
  * import { setupToapiWorker } from "@toapi/worker";
@@ -23,13 +28,9 @@ const ONE_WEEK_SECONDS = 60 * 60 * 24 * 7;
23
28
  * `vite-plugin-pwa`'s precaching) — requests it doesn't handle fall through to
24
29
  * them.
25
30
  */
26
- export function setupToapiWorker({ basePath = "/api", invalidationsUrl = `${basePath}${INVALIDATIONS_ROUTE}`, maximumStaleAge = ONE_WEEK_SECONDS, logger, } = {}) {
31
+ export function setupToapiWorker({ basePath = "/api", invalidationsUrl = `${basePath}${INVALIDATIONS_ROUTE}`, maximumStaleAge = ONE_WEEK_SECONDS, logger: customLogger, } = {}) {
32
+ const logger = consoleFallback(customLogger);
27
33
  const controlPrefix = `${basePath}/__tapi`;
28
- const errorLog = logger?.error ??
29
- ((err) => console.error("Toapi Worker: invalidation stream failed", err));
30
- self.addEventListener("activate", (event) => {
31
- event.waitUntil(cleanup({ maximumStaleAge }));
32
- });
33
34
  self.addEventListener("fetch", (event) => {
34
35
  const url = new URL(event.request.url);
35
36
  // only handle same-origin API requests, and never the control endpoints
@@ -40,5 +41,6 @@ export function setupToapiWorker({ basePath = "/api", invalidationsUrl = `${base
40
41
  event.respondWith(handleToapiRequest(event.request, { logger }));
41
42
  }
42
43
  });
43
- listenForInvalidations({ url: invalidationsUrl }).catch(errorLog);
44
+ listenForInvalidations({ url: invalidationsUrl, logger }).catch(logger.error);
45
+ cleanup({ maximumStaleAge }).catch(logger.error);
44
46
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toapi/worker",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "author": {
5
5
  "name": "Michel Smola",
6
6
  "email": "michel.smola@farbenmeer.de"
@@ -20,7 +20,7 @@
20
20
  }
21
21
  },
22
22
  "dependencies": {
23
- "@toapi/common": "^1.2.1"
23
+ "@toapi/common": "^1.2.3"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/node": "^25.0.3",