@alxia/react-router 0.9.0 → 0.11.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.
Files changed (100) hide show
  1. package/README.md +301 -13
  2. package/dist/chunks/index-59vhfj6n.js +10 -0
  3. package/dist/chunks/index-59vhfj6n.js.map +10 -0
  4. package/dist/chunks/index-5gqcpyyg.js +34 -0
  5. package/dist/chunks/index-5gqcpyyg.js.map +10 -0
  6. package/dist/chunks/{index-zqa30gr7.js → index-9xv2a8et.js} +7 -38
  7. package/dist/chunks/index-9xv2a8et.js.map +11 -0
  8. package/dist/chunks/index-mppz0806.js +119 -0
  9. package/dist/chunks/index-mppz0806.js.map +11 -0
  10. package/dist/chunks/index-rfr02vg4.js +40 -0
  11. package/dist/chunks/index-rfr02vg4.js.map +10 -0
  12. package/dist/chunks/index-zdbqzcjp.js +97 -0
  13. package/dist/chunks/index-zdbqzcjp.js.map +12 -0
  14. package/dist/errors/api-error.d.ts +86 -0
  15. package/dist/errors/api-error.d.ts.map +1 -0
  16. package/dist/errors/api-errors.d.ts +96 -0
  17. package/dist/errors/api-errors.d.ts.map +1 -0
  18. package/dist/errors/call-failure.d.ts +45 -0
  19. package/dist/errors/call-failure.d.ts.map +1 -0
  20. package/dist/errors/catalogue-check.d.ts +24 -0
  21. package/dist/errors/catalogue-check.d.ts.map +1 -0
  22. package/dist/errors/catalogue-read.d.ts +33 -0
  23. package/dist/errors/catalogue-read.d.ts.map +1 -0
  24. package/dist/errors/catalogue.d.ts +59 -0
  25. package/dist/errors/catalogue.d.ts.map +1 -0
  26. package/dist/errors/code-resolver.d.ts +44 -0
  27. package/dist/errors/code-resolver.d.ts.map +1 -0
  28. package/dist/errors/describe-refusal.d.ts +6 -0
  29. package/dist/errors/describe-refusal.d.ts.map +1 -0
  30. package/dist/errors/describe.d.ts +3 -0
  31. package/dist/errors/describe.d.ts.map +1 -0
  32. package/dist/errors/fetcher-result.d.ts +21 -0
  33. package/dist/errors/fetcher-result.d.ts.map +1 -0
  34. package/dist/errors/form-errors.d.ts +8 -0
  35. package/dist/errors/form-errors.d.ts.map +1 -0
  36. package/dist/errors/index.d.ts +15 -0
  37. package/dist/errors/index.d.ts.map +1 -0
  38. package/dist/errors/index.js +602 -0
  39. package/dist/errors/index.js.map +29 -0
  40. package/dist/errors/json-actions.d.ts +38 -0
  41. package/dist/errors/json-actions.d.ts.map +1 -0
  42. package/dist/errors/log.d.ts +8 -0
  43. package/dist/errors/log.d.ts.map +1 -0
  44. package/dist/errors/message-values.d.ts +11 -0
  45. package/dist/errors/message-values.d.ts.map +1 -0
  46. package/dist/errors/messages.d.ts +63 -0
  47. package/dist/errors/messages.d.ts.map +1 -0
  48. package/dist/errors/page-path.d.ts +9 -0
  49. package/dist/errors/page-path.d.ts.map +1 -0
  50. package/dist/errors/provider.d.ts +20 -0
  51. package/dist/errors/provider.d.ts.map +1 -0
  52. package/dist/errors/request-id.d.ts +5 -0
  53. package/dist/errors/request-id.d.ts.map +1 -0
  54. package/dist/errors/route-error.d.ts +31 -0
  55. package/dist/errors/route-error.d.ts.map +1 -0
  56. package/dist/errors/standard-schema.d.ts +34 -0
  57. package/dist/errors/standard-schema.d.ts.map +1 -0
  58. package/dist/errors/submit-form.d.ts +16 -0
  59. package/dist/errors/submit-form.d.ts.map +1 -0
  60. package/dist/errors/use-json-fetcher.d.ts +18 -0
  61. package/dist/errors/use-json-fetcher.d.ts.map +1 -0
  62. package/dist/pwa/build/build-worker.d.ts +41 -0
  63. package/dist/pwa/build/build-worker.d.ts.map +1 -0
  64. package/dist/pwa/build/index.d.ts +9 -0
  65. package/dist/pwa/build/index.d.ts.map +1 -0
  66. package/dist/pwa/build/index.js +9 -0
  67. package/dist/pwa/build/index.js.map +9 -0
  68. package/dist/pwa/build/precache.d.ts +17 -0
  69. package/dist/pwa/build/precache.d.ts.map +1 -0
  70. package/dist/pwa/index.js +34 -134
  71. package/dist/pwa/index.js.map +4 -8
  72. package/dist/pwa/react/index.js +1 -1
  73. package/dist/pwa/worker/index.d.ts +9 -0
  74. package/dist/pwa/worker/index.d.ts.map +1 -0
  75. package/dist/pwa/worker/index.js +144 -0
  76. package/dist/pwa/worker/index.js.map +12 -0
  77. package/dist/pwa/worker/injected.d.ts +15 -0
  78. package/dist/pwa/worker/injected.d.ts.map +1 -0
  79. package/dist/pwa/worker/plan.d.ts +65 -0
  80. package/dist/pwa/worker/plan.d.ts.map +1 -0
  81. package/dist/pwa/worker/scope.d.ts +18 -0
  82. package/dist/pwa/worker/scope.d.ts.map +1 -0
  83. package/dist/pwa/worker/start-worker.d.ts +17 -0
  84. package/dist/pwa/worker/start-worker.d.ts.map +1 -0
  85. package/dist/vite/index.d.ts +10 -0
  86. package/dist/vite/index.d.ts.map +1 -1
  87. package/dist/vite/index.js +49 -3
  88. package/dist/vite/index.js.map +6 -5
  89. package/dist/vite/runtime.d.ts +1 -1
  90. package/dist/vite/runtime.d.ts.map +1 -1
  91. package/dist/vite/worker.d.ts +16 -0
  92. package/dist/vite/worker.d.ts.map +1 -0
  93. package/docs/README.md +3 -2
  94. package/docs/guide/errors.md +838 -0
  95. package/docs/guide/how-it-works.md +7 -1
  96. package/docs/guide/pwa.md +372 -211
  97. package/docs/roadmap.md +38 -5
  98. package/docs/troubleshooting.md +689 -3
  99. package/package.json +56 -4
  100. package/dist/chunks/index-zqa30gr7.js.map +0 -12
package/README.md CHANGED
@@ -92,7 +92,10 @@ Vite's preview server, as `bun run start` would: the pages, `/api`, the
92
92
  client's files and the app's middlewares. So does React Router's prerendering.
93
93
 
94
94
  `@alxia/core` and `react-router` 8 are peers, and `vite` 7 or 8 is an
95
- optional peer, for `/vite`; `react` 19 is an optional peer, for `/pwa/react`. The package declares no dependency.
95
+ optional peer, for `/vite`; `react` 19 is an optional peer, for `/pwa/react`. The six `workbox-*`
96
+ packages (`workbox-core`, `workbox-expiration`, `workbox-navigation-preload`,
97
+ `workbox-precaching`, `workbox-routing`, `workbox-strategies`) are optional
98
+ peers too, for `/pwa/worker` (the [PWA section](#installable-and-offline-pwa) has the install line). `/errors` imports `react-router`, a required peer that is already installed. The package declares no dependency.
96
99
 
97
100
  Its `typescript` peer is 6 or 7. The template's TypeScript 5.9 typechecks,
98
101
  but `bun add` warns about it; `bun add -d typescript@^6` silences the
@@ -484,23 +487,51 @@ no UI: the service worker's contract, which requests it caches and which
484
487
  responses it may store, the precache list read from a build, the
485
488
  registration and update notice, the web app manifest, and the cleaning at
486
489
  sign-out. It imports nothing, so the worker, a build script, the page and
487
- the server can all import it. The hooks are in `/pwa/react`
488
- (`react`, an optional peer) and `noCache` in `/pwa/server`; this package
489
- never reads `import.meta.env`, so the app passes `enabled`.
490
+ the server can all import it. The worker itself is `startWorker` in
491
+ `/pwa/worker`, its build `buildWorker` in `/pwa/build`, the hooks are in
492
+ `/pwa/react` (`react`, an optional peer) and `noCache` in `/pwa/server`; this
493
+ package never reads `import.meta.env`, so the app passes `enabled`.
494
+
495
+ With `app/service-worker.ts` in the app, `react-router build` writes
496
+ `build/client/service-worker.js` itself: `alxia()` bundles the file after the
497
+ client build, with the precache list written in, and there is no script to
498
+ add. It needs Bun (`bun --bun react-router build`, or a `bunfig.toml` with
499
+ `[run] bun = true`). `alxia({ worker: 'app/pwa/worker.ts' })` names another file
500
+ and `alxia({ worker: false })` builds none.
501
+
502
+ The worker bundles Workbox, whose six packages are optional peers:
503
+
504
+ ```sh
505
+ bun add -d workbox-core workbox-expiration workbox-navigation-preload workbox-precaching workbox-routing workbox-strategies
506
+ ```
490
507
 
491
508
  ```ts
492
- // file: app/pwa-contract.ts, shared by the worker and the page:
493
- // every cache is named `my-app-<kind>-<version>`.
509
+ // file: app/pwa-contract.ts
510
+ // Shared by the worker and the page: every cache is named `my-app-<kind>-<version>`.
494
511
  import { defineWorkerContract } from '@alxia/react-router/pwa';
495
512
 
496
513
  export const workerContract = defineWorkerContract({ cachePrefix: 'my-app' });
497
514
  ```
498
515
 
516
+ ```ts
517
+ // file: app/service-worker.ts
518
+ import { startWorker } from '@alxia/react-router/pwa/worker';
519
+ import { workerContract } from './pwa-contract';
520
+
521
+ startWorker({ contract: workerContract });
522
+ ```
523
+
499
524
  ```tsx
500
525
  // file: app/root.tsx
526
+ import { manifestHref } from '@alxia/react-router/pwa';
501
527
  import { useServiceWorker } from '@alxia/react-router/pwa/react';
502
528
  import { Outlet } from 'react-router';
503
529
 
530
+ // The browser reads the manifest to offer the install.
531
+ export function links() {
532
+ return [{ rel: 'manifest', href: manifestHref('en') }];
533
+ }
534
+
504
535
  // A production build only, never under `react-router dev`.
505
536
  export default function App() {
506
537
  useServiceWorker({ enabled: import.meta.env.PROD });
@@ -511,18 +542,233 @@ export default function App() {
511
542
  ```ts
512
543
  // file: app/server.ts
513
544
  import { createServer } from '@alxia/react-router';
514
- import { SERVICE_WORKER_URL } from '@alxia/react-router/pwa';
545
+ import {
546
+ MANIFEST_HEADERS,
547
+ MANIFEST_PATH,
548
+ SERVICE_WORKER_URL,
549
+ webAppManifest,
550
+ } from '@alxia/react-router/pwa';
515
551
  import { noCache } from '@alxia/react-router/pwa/server';
516
552
 
517
- // The worker's script is revalidated; beforeAll runs before the client's files.
553
+ // beforeAll runs before the client's files: the worker's script is
554
+ // revalidated. The manifest is a route, served with its own headers.
518
555
  export default createServer({
519
556
  beforeAll: (app) => app.use(SERVICE_WORKER_URL, noCache),
557
+ configure: (app) =>
558
+ app.get(MANIFEST_PATH, ({ reply }) => {
559
+ const manifest = webAppManifest({
560
+ name: 'My app',
561
+ description: 'My installable app',
562
+ lang: 'en',
563
+ themeColor: '#1e40af',
564
+ backgroundColor: '#ffffff',
565
+ });
566
+ return reply(200, JSON.stringify(manifest), { headers: MANIFEST_HEADERS });
567
+ }),
520
568
  });
521
569
  ```
522
570
 
571
+ The manifest names four icons (`MINIMAL_2023_ICONS`): put
572
+ `pwa-64x64.png`, `pwa-192x192.png`, `pwa-512x512.png` and
573
+ `maskable-icon-512x512.png` in `public/`, which `@vite-pwa/assets-generator`'s
574
+ `minimal2023Preset` writes from one image. A browser offers the install with
575
+ a 192 and a 512.
576
+
577
+ The worker serves a page load from `public/offline.html` when the network
578
+ fails, and the build needs the file:
579
+
580
+ ```html
581
+ <!-- file: public/offline.html -->
582
+ <!doctype html>
583
+ <html lang="en">
584
+ <head>
585
+ <meta charset="utf-8" />
586
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
587
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'" />
588
+ <title>Offline</title>
589
+ </head>
590
+ <body>
591
+ <h1>You are offline</h1>
592
+ <p>Check your connection and try again.</p>
593
+ </body>
594
+ </html>
595
+ ```
596
+
597
+ Set `routeDiscovery: { mode: 'initial' }` in `react-router.config.ts`, so a
598
+ client navigation offline to a route never visited finds its modules
599
+ ([more](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md#offline-a-client-navigation-to-a-route-never-visited-shows-the-offline-page)):
600
+
601
+ ```ts
602
+ import type { Config } from '@react-router/dev/config';
603
+
604
+ export default { ssr: true, routeDiscovery: { mode: 'initial' } } satisfies Config;
605
+ ```
606
+
607
+ An app that built its worker with a script of its own has steps to drop:
608
+ [moving from a build script of your own](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/pwa.md#moving-from-a-build-script-of-your-own).
609
+
610
+ For a build pipeline of your own, `buildWorker` from `/pwa/build` does what
611
+ `alxia()` does. It needs Bun and runs after `react-router build`:
612
+
613
+ ```ts
614
+ // file: scripts/build-worker.ts
615
+ import { buildWorker } from '@alxia/react-router/pwa/build';
616
+
617
+ await buildWorker({ file: 'app/service-worker.ts' });
618
+ ```
619
+
523
620
  The [PWA guide](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/pwa.md)
524
- has the worker's fetch handler, the manifest route, the update banner and the
525
- rest.
621
+ has the worker's options, the offline page, the manifest route, the update
622
+ banner and the rest.
623
+
624
+ ## An API's refusals, worded for the page
625
+
626
+ `@alxia/react-router/errors` has no UI; its one runtime import is `react-router`, never `@alxia/core`. It
627
+ holds the shape of a GraphQL API's refusal, declared structurally (no `@nxgt/graphql-client` at
628
+ runtime), the request id an API echoes, the page a request is for, the
629
+ catalogue of the API's messages an app keeps, the words of a form's checks,
630
+ the provider that fills the catalogue in the browser, the wording of a refusal, the loaders and actions that answer with it, the JSON actions (CSRF check) with their fetcher hook, and what a failed call to the API becomes (`apiCallFailure`):
631
+
632
+ ```ts
633
+ import { isApiErrorLike, nextOf, requestIdOf } from '@alxia/react-router/errors';
634
+
635
+ export function describe(error: unknown, request: Request) {
636
+ return isApiErrorLike(error)
637
+ ? { code: error.code, status: error.status, next: nextOf(request.url) }
638
+ : undefined;
639
+ }
640
+
641
+ export const idOf = (response: Response) => requestIdOf(response);
642
+ ```
643
+
644
+ The catalogue is an instance made with the app's ICU formatter. A form's checks
645
+ read it through `apiCodeResolver`, and the provider fills it from the root's data:
646
+
647
+ ```ts
648
+ import {
649
+ ApiMessagesProvider,
650
+ apiCodeResolver,
651
+ createApiMessages,
652
+ } from '@alxia/react-router/errors';
653
+
654
+ type Language = 'en' | 'fr';
655
+
656
+ export const apiMessages = createApiMessages<Language>({
657
+ format: (message) => message, // the app's ICU formatter goes here
658
+ });
659
+
660
+ export const resolver = apiCodeResolver(apiMessages, {
661
+ translate: (key) => key,
662
+ language: () => 'en',
663
+ });
664
+
665
+ // In the root: <ApiMessagesProvider messages={apiMessages} value={data.catalogue}>…
666
+ export const provider = ApiMessagesProvider({
667
+ messages: apiMessages,
668
+ value: { en: {}, fr: {} },
669
+ children: null,
670
+ });
671
+ ```
672
+
673
+ A failed call is worded in the page's language by `createApiErrors`:
674
+
675
+ ```ts
676
+ import { createApiErrors } from '@alxia/react-router/errors';
677
+ import { apiMessages } from './api-messages';
678
+
679
+ export const apiErrors = createApiErrors<'en' | 'fr'>({
680
+ messages: apiMessages,
681
+ unavailable: (language) => (language === 'en' ? 'Unavailable' : 'Indisponible'),
682
+ });
683
+
684
+ // In an action's catch: { message, status }, or undefined (rethrow it).
685
+ export const refusal = (error: unknown) => apiErrors.apiRefusal(error, 'en');
686
+ ```
687
+
688
+ A loader throws the API's error as a route error, with the call's request id
689
+ for the error page; an action answers a refusal as data:
690
+
691
+ ```ts
692
+ import { errorRequestId, routeError } from '@alxia/react-router/errors';
693
+ import { apiErrors } from './api-errors';
694
+
695
+ // In a loader's catch: a 401, 403, 404 or 503 ApiError becomes a route error.
696
+ export const thrown = (error: unknown) => routeError(error);
697
+
698
+ // In the root's ErrorBoundary: the reference the user can quote.
699
+ export const reference = (error: unknown) => errorRequestId(error);
700
+
701
+ // A fetcher's action: { ok: true, …value }, or { ok: false, error } with a status.
702
+ export const rename = (run: () => Promise<{ title: string }>) =>
703
+ apiErrors.fetcherResult('en', { intent: 'rename' }, run);
704
+
705
+ // A form's action: { ok: true, value }, or the form's errors with a status.
706
+ export const create = (run: () => Promise<{ id: string }>) =>
707
+ apiErrors.submitForm('en', run, (path) =>
708
+ path === 'todo.title' ? 'title' : undefined,
709
+ );
710
+ ```
711
+
712
+ JSON actions refuse a post from another site, and read a Standard Schema's input:
713
+
714
+ ```ts
715
+ import { jsonActions, useJsonFetcher } from '@alxia/react-router/errors';
716
+ import { z } from 'zod';
717
+
718
+ // Behind a TLS proxy named with createServer({ proxy }), nothing to set; else: origin: () => process.env.APP_ORIGIN
719
+ export const { readInput } = jsonActions();
720
+
721
+ const schema = z.object({ title: z.string().min(1) });
722
+
723
+ export async function action({ request }: { request: Request }) {
724
+ return readInput(request, schema); // 400 when the input is refused
725
+ }
726
+
727
+ // In the component: const { post, busy } = useJsonFetcher<typeof action>();
728
+ ```
729
+
730
+ A call that failed is turned into what a loader throws, and logged with the API's request id, by `apiCallFailure(error, { api, operation: operationName(document), requestId })`
731
+ (your own `execute` calls it in its `catch`):
732
+
733
+ ```ts
734
+ import { apiCallFailure, operationName } from '@alxia/react-router/errors';
735
+ import type { DocumentNode } from 'graphql';
736
+
737
+ export function failed(error: unknown, document: DocumentNode, requestId?: string, signal?: AbortSignal) {
738
+ return apiCallFailure(error, { api: 'my-api', operation: operationName(document), requestId, signal });
739
+ }
740
+ // In execute(): catch (error) { throw failed(error, document, requestId, signal); }
741
+ ```
742
+
743
+ The API's catalogue is read and kept fresh by `createErrorCatalogue`; the root's
744
+ middleware awaits `freshAll()`, its loader returns `clientMessages()` and
745
+ `server.ts` calls `warm()`:
746
+
747
+ ```ts
748
+ import { createErrorCatalogue } from '@alxia/react-router/errors';
749
+ import { apiMessages } from './api-messages';
750
+
751
+ export const catalogue = createErrorCatalogue<'en' | 'fr'>({
752
+ messages: apiMessages,
753
+ languages: ['en', 'fr'],
754
+ read: async (language) => fetchCatalogue(language), // { version, messages }
755
+ versions: async () => fetchVersions(), // { en: '…', fr: '…' }
756
+ ttlMs: () => Number(process.env.CATALOGUE_TTL_MS ?? 60_000),
757
+ clientCodes: ['validation.errors.required'],
758
+ });
759
+
760
+ declare function fetchCatalogue(
761
+ language: string,
762
+ ): Promise<{ version: string; messages: { code: string; message: string; params: { name: string; kind: 'TEXT' }[] }[] }>;
763
+ declare function fetchVersions(): Promise<{ en: string; fr: string }>;
764
+
765
+ // The root's middleware: await catalogue.freshAll();
766
+ // The root's loader: return { messages: catalogue.clientMessages() };
767
+ // server.ts, at startup: catalogue.warm();
768
+ ```
769
+
770
+ The [errors guide](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/errors.md)
771
+ has the rest.
526
772
 
527
773
  ## Traps
528
774
 
@@ -574,8 +820,8 @@ From `@alxia/react-router/vite`:
574
820
 
575
821
  | export | |
576
822
  | --- | --- |
577
- | `alxia(options?)` | the Vite plugin, for `react-router dev`, `react-router build` and `vite preview`, building the server for Bun. `entry` is the server file: `app/server.ts` by default, or the default server when there is none |
578
- | `AlxiaOptions` | its options |
823
+ | `alxia(options?)` | the Vite plugin, for `react-router dev`, `react-router build` and `vite preview`, building the server for Bun. `entry` is the server file: `app/server.ts` by default, or the default server when there is none. `worker` is the service worker built after `react-router build`: `app/service-worker.ts` by default when it exists, a file, `AlxiaWorkerOptions`, or `false` |
824
+ | `AlxiaOptions`, `AlxiaWorkerOptions` | its options, and the worker's (`buildWorker()`'s but `root`, `client` and `file`, which is optional) |
579
825
 
580
826
  From `@alxia/react-router/pwa`:
581
827
 
@@ -623,6 +869,47 @@ From `@alxia/react-router/pwa/server` (`@alxia/core` is its only package import)
623
869
  | `revalidated(response, policy?)` | a response with those two headers set, on a copy when its headers are immutable; `policy` is `WORKER_POLICY` by default |
624
870
  | `WORKER_POLICY` | `default-src 'none'; connect-src 'self'`: the worker runs no other script and fetches this site only |
625
871
 
872
+ From `@alxia/react-router/pwa/worker` (the six `workbox-*` packages, optional peers, are its only package imports):
873
+
874
+ | export | |
875
+ | --- | --- |
876
+ | `startWorker(options)` | the worker's strategies: the precache route, `NetworkOnly` for a page load with the precached offline page as its fallback, `CacheFirst` for hashed bundles, `StaleWhileRevalidate` for other static files (both storing only what `isStorable` accepts), the obsolete caches swept on `activate`, and `SKIP_WAITING` applied |
877
+ | `WorkerOptions` | `contract` (required), `caching`, `assets`, `static`, `navigationPreload`, `precache` |
878
+ | `ExpirationOptions` | `{ maxEntries?, maxAgeSeconds? }` |
879
+
880
+ From `@alxia/react-router/pwa/build` (Bun; Node's built-ins its only imports):
881
+
882
+ | export | |
883
+ | --- | --- |
884
+ | `buildWorker(options)` | bundles the worker into `build/client/service-worker.js` with its precache list (the offline page by its content, every route's code, `precacheAssets`), after `react-router build` |
885
+ | `BuildWorkerOptions` | `file` (required), `root`, `client`, `contract`, `offline`, `precacheAssets`, `minify` |
886
+ | `WorkerBuild` | `{ file, precache, precacheBytes, bytes }` |
887
+
888
+ From `@alxia/react-router/errors` (`react-router` is its only package import, and never `@alxia/core`):
889
+
890
+ | export | |
891
+ | --- | --- |
892
+ | `isApiErrorLike(error)` | whether `error` is an `@nxgt/graphql-client` `ApiError`, like the client's `isApiError`, plus a numeric `status`, which `ApiErrorLike` promises; it holds across two copies of the client |
893
+ | `ApiErrorLike`, `ApiErrorEntryLike`, `ApiErrorExtensionsLike`, `ApiErrorParamsLike`, `ApiFieldErrorLike` | the shape of the refusal: `errors`, `code`, `status`, `requestId?`, and each error's `extensions` (`code`, `params`, `fields`, `field`, `minutes`, `retryAfter`) |
894
+ | `waitMinutes(extensions)` | the minutes of a throttle's wait: `minutes` (at least one), else `retryAfter` rounded up |
895
+ | `REQUEST_ID_HEADER`, `requestIdOf(response)` | `x-request-id`, and the id a response carries, if the API sent one |
896
+ | `nextOf(url)` | the path and search of the page a request is for, without React Router's `.data`, `/_.data`, `_routes` and empty `index`; it never starts with two slashes, but an app still validates `next` before redirecting to it |
897
+ | `FormErrors` | what a form action answers when the API refused the input: `fieldErrors` by field and a `formError` (`undefined` when every message sits under a field), already translated; `submitForm` answers it |
898
+ | `routeError(error, { statuses? }?)`, `RouteErrorOptions` | what a loader throws for the API's error: an `ApiError` of 401, 403, 404 or 503, or such a `Response` carrying a request id, becomes a route error of that status with the `code`, `message` and `requestId`; any other `ApiError` with an id becomes a 500 carrying the id; anything else is returned as it is |
899
+ | `errorRequestId(error)`, `RouteErrorData` | the support reference an error page shows: the `requestId` a route error carries, else `undefined`; `RouteErrorData` is the data `routeError` carries (`code?`, `message?`, `requestId?`) |
900
+ | `createApiMessages({ format })`, `ApiMessages`, `ApiMessagesOptions` | the catalogue store of an app, an instance (`set`, `add`, `clear` (for tests), `messageOf`, `format`, `apiMessage(code, values, language)`); `format` is the app's ICU formatter, such as `@nxgt/material`'s `formatMessage` |
901
+ | `CatalogueMessage`, `CatalogueMessages`, `ParamKind`, `MessageFormat`, `MessageValues` | one API message with the kind of each argument (`TEXT`, `NUMBER`, `DATE`, `PLURAL`, `SELECT`), a language's messages by code, and the formatter's type |
902
+ | `apiCodeResolver(messages, { translate, language, codeOf?, isApiCode? })`, `ApiCodeResolver`, `ApiCodeResolverOptions`, `CodeIssueLike` | the `{ translate, codeOf }` a form's `useCodeResolver(schema, resolver)` takes: a check's API code is worded as the API words it, any other key by the app's translator; `CodeIssueLike` is the issue a custom `codeOf` receives |
903
+ | `ApiMessagesProvider({ messages, value, children })`, `ApiMessagesProviderProps` | `messages` is the store and `value` the root loader's data; fills the store in the browser from the root's data before the children render; a server render writes nothing. A function with no JSX, so no React import |
904
+ | `createApiErrors({ messages, unavailable, refused?, byCode?, isApiCode? })`, `ApiErrors`, `ApiErrorsOptions` | what a page says of a refusal, in a language: `apiErrorMessage(error, language)` (the catalogue's text of the code, else the API's message; `refused` replaces it for an error with no API code and `byCode` words given codes, such as a query's limits), `fieldErrors(error, language)` by input path, `translateCode(code, fallback, language, values?)`, `unavailableMessage(language)` and `apiRefusal(error, language)`; for an action, `fetcherResult(language, about, run, route?)` (`{ ok: true, …about, …value }`, or `{ ok: false, …about, error }` with the refusal's status), `fetcherRefusal(error, language, route?)` (the refusal, or the thrown route error) and `submitForm(language, run, fieldOf, route?)` (`{ ok: true, value }`, or a `FormErrors` with the refusal's status); `route` is the `RouteErrorOptions` of the `routeError` they throw through |
905
+ | `apiCallFailure(error, { api, operation?, requestId?, signal?, passedThrough?, log? })`, `CallFailureOptions` | what a failed call to the API becomes, for a loader or an action to throw, logged with the request id: an `ApiError` as it is (with `requestId`), a `Response` of its own status for a 401, 403 or 404 without a GraphQL body, a 503 `Response` with no body for an unreachable, slow, garbled or aborted call (the reason is in the log), any other error as it is; the log lines start with `api`, and a `log` that throws never replaces the outcome. It recognises `@nxgt/graphql-client`'s errors by `name`, not `instanceof` |
906
+ | `operationName(document)`, `DocumentLike` | the name of the operation a document holds, for the log (else `query`, `mutation` or `subscription`); a structural `{ definitions }`, so `graphql`'s `DocumentNode` fits |
907
+ | `createErrorCatalogue({ messages, languages, read, versions, ttlMs, clientCodes, retryMs?, firstReadWaitMs?, log? })`, `ErrorCatalogue`, `ErrorCatalogueOptions`, `CatalogueReply` | keeps the API's error catalogue in the store: `fresh(language, now?)` and `freshAll(now?)` (a cold language is awaited for `firstReadWaitMs`, 1500 by default, and read once however many requests ask; a held one is served, and its version checked in the background once per `ttlMs`, a number or a getter; only a language whose version moved is read again; a failure is retried after `retryMs`, 5000 by default), `warm()` for startup, `clientMessages()` (the `clientCodes` the browser words itself, by language, for the root's loader), and, for tests, `reset()`, `heldVersion(language)`, `settled()` and the `now` parameters. `read` and `versions` are the app's calls to its API |
908
+ | `ApiRefusal` | `{ message, status }`: a failed call as a page says it; what `apiRefusal` returns for an API refusal (its status) or an unreachable API (503), else `undefined` |
909
+ | `jsonActions({ origin? })`, `JsonActions`, `JsonActionsOptions` | the JSON helpers of the actions, bound to the app's origin (a string, or a getter read at each request; the request's own by default): `readJson(request)` (403 `Cross-site request refused` for another origin or a cross-site fetch, 415 `Expected a JSON body`, 400 `Malformed JSON body`), `requireJsonPost(request)` (a native post goes back to its page) and `readInput(request, schema)` (the body's `input` through a Standard Schema, async allowed; 400 `The form sent an input the API would refuse`) |
910
+ | `useJsonFetcher(key?, action?)`, `JsonBody` | a fetcher that posts JSON: `{ fetcher, post(body, to?), busy }`; `to` is the action of one call |
911
+ | `StandardSchemaV1` | the Standard Schema v1 type `readInput` takes, declared in `/errors` (type only, so no `@alxia/core`; `readInput`'s output is inferred from the schema's `~standard.types`); a Zod 4, Valibot or ArkType schema fits |
912
+
626
913
  The `alxia-react-router` bin, run with `bunx`:
627
914
 
628
915
  | command | |
@@ -637,7 +924,8 @@ The `alxia-react-router` bin, run with `bunx`:
637
924
  - [Customising the server](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/customising-the-server.md): `createServer()`, and the escape hatches.
638
925
  - [Typing the loaders](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/typing-the-loaders.md): `Register`, and the app's own context keys.
639
926
  - [A CSP nonce](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/csp-nonce.md), [Beside the pages](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/beside-the-pages.md) (middlewares, routes, WebSockets, the client's files), [OpenAPI](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/openapi.md), [Testing](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/testing.md) and [Deploying](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/deploying.md), with Docker.
640
- - [Installable and offline](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/pwa.md): what `/pwa` holds, the worker's contract, what the worker caches, the precache list from a build, registering the worker and offering an update, the web app manifest, the hooks in React, and clearing at sign-out.
927
+ - [Installable and offline](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/pwa.md): what the subpaths hold, the worker's contract, the worker, what it caches, the offline page, building the worker with `alxia()` or `buildWorker`, the hooks in React, registering the worker and offering an update, serving the worker's script, the web app manifest, and clearing at sign-out.
928
+ - [An API's refusals](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/errors.md): what `/errors` holds, recognising an API error, the request id, the page a request is for, the catalogue store, the form checks in the API's words, the provider, wording a refusal, loaders and the error page, actions, JSON actions and CSRF, and keeping the catalogue fresh.
641
929
  - [Troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md): each message, and the traps that print none.
642
930
  - [Roadmap](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/roadmap.md): what is coming, and what is not planned.
643
931
  - [Example](https://github.com/softistx/alxia/tree/develop/examples/react-router): the official template, these four changes, then an `app/server.ts` with a session, an `/api`, secure headers with a nonce and a streamed page.
@@ -0,0 +1,10 @@
1
+ // src/pwa/worker/injected.ts
2
+ var PRECACHE_DEFINE = "globalThis.__ALXIA_PRECACHE__";
3
+ function injectedPrecache() {
4
+ return globalThis.__ALXIA_PRECACHE__;
5
+ }
6
+
7
+ export { PRECACHE_DEFINE, injectedPrecache };
8
+
9
+ //# debugId=CA917CBC5F688F4264756E2164756E21
10
+ //# sourceMappingURL=index-59vhfj6n.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/pwa/worker/injected.ts"],
4
+ "sourcesContent": [
5
+ "/** The define key the build sets: the worker's precache list, as JSON. */\nexport const PRECACHE_DEFINE = 'globalThis.__ALXIA_PRECACHE__';\n\n/** One file the worker precaches: its URL and a revision (`null` when hashed in the name). */\ntype Entry = { readonly url: string; readonly revision: string | null };\n\n/**\n * The precache list the build wrote into this worker, or `undefined` when the\n * worker did not go through the build. Read through `globalThis`, so a missing\n * define is `undefined`, never a `ReferenceError`.\n */\nexport function injectedPrecache(): readonly Entry[] | undefined {\n\treturn (globalThis as { __ALXIA_PRECACHE__?: readonly Entry[] })\n\t\t.__ALXIA_PRECACHE__;\n}\n"
6
+ ],
7
+ "mappings": ";AACO,IAAM,kBAAkB;AAUxB,SAAS,gBAAgB,GAAiC;AAAA,EAChE,OAAQ,WACN;AAAA;",
8
+ "debugId": "CA917CBC5F688F4264756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,34 @@
1
+ // src/pwa/model/precache-list.ts
2
+ var HASHED_ASSET = /^\/assets\/[^/]+-[\w-]{8}\.(?:js|css)$/;
3
+ function parseBrowserManifest2(source) {
4
+ const start = source.indexOf("{");
5
+ const end = source.lastIndexOf("}");
6
+ if (start < 0 || end < start) {
7
+ throw new Error("parseBrowserManifest(): not React Router's browser manifest (build/client/assets/manifest-*.js).");
8
+ }
9
+ return JSON.parse(source.slice(start, end + 1));
10
+ }
11
+ function precacheList2(manifest) {
12
+ const urls = new Set([manifest.url]);
13
+ for (const part of [manifest.entry, ...Object.values(manifest.routes)]) {
14
+ for (const url of [
15
+ ...part.module ? [part.module] : [],
16
+ ...part.imports ?? [],
17
+ ...part.css ?? []
18
+ ]) {
19
+ urls.add(url);
20
+ }
21
+ }
22
+ return [...urls].filter((url) => HASHED_ASSET.test(url)).map((url) => ({ url, revision: null }));
23
+ }
24
+ function assetPrecache2(assetFiles, include, base = "/assets/") {
25
+ return assetFiles.filter((file) => {
26
+ include.lastIndex = 0;
27
+ return include.test(file);
28
+ }).sort().map((file) => ({ url: `${base}${file}`, revision: null }));
29
+ }
30
+
31
+ export { parseBrowserManifest2, precacheList2, assetPrecache2 };
32
+
33
+ //# debugId=812DDA9793A6A64B64756E2164756E21
34
+ //# sourceMappingURL=index-5gqcpyyg.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/pwa/model/precache-list.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * What the service worker precaches, read from a build: every script and\n * stylesheet React Router's manifest names (the client entry and each\n * route's module, imports and CSS), and the manifest itself, so a client\n * navigation offline to a route never visited still finds its code. Only\n * hashed `/assets/` files: a new build names new files, so each precaches\n * under its own URL, revision `null`. Chunks only a component loads on\n * demand stay out: a cache-first route stores them the first time they load.\n *\n * Dependency-free: a build script runs it at build time.\n */\n\n/** A route or the entry, as `window.__reactRouterManifest` lists it. */\ntype ManifestModule = { module?: string; imports?: string[]; css?: string[] };\n\n/** The parts of React Router's browser manifest the list reads. */\nexport type BrowserManifest = {\n\turl: string;\n\tentry: ManifestModule;\n\troutes: Record<string, ManifestModule>;\n};\n\n/** A Workbox precache entry: a hashed URL needs no revision. */\nexport type PrecacheEntry = { url: string; revision: string | null };\n\n/** A Vite-hashed file under `/assets/`: `name-<8 hash chars>.js|css`. */\nconst HASHED_ASSET = /^\\/assets\\/[^/]+-[\\w-]{8}\\.(?:js|css)$/;\n\n/** Reads `window.__reactRouterManifest={…};` (build/client/assets/manifest-*.js). */\nexport function parseBrowserManifest(source: string): BrowserManifest {\n\tconst start = source.indexOf('{');\n\tconst end = source.lastIndexOf('}');\n\tif (start < 0 || end < start) {\n\t\tthrow new Error(\n\t\t\t\"parseBrowserManifest(): not React Router's browser manifest (build/client/assets/manifest-*.js).\",\n\t\t);\n\t}\n\treturn JSON.parse(source.slice(start, end + 1)) as BrowserManifest;\n}\n\n/** The hashed scripts and stylesheets every route needs, the manifest's own file first. */\nexport function precacheList(manifest: BrowserManifest): PrecacheEntry[] {\n\tconst urls = new Set<string>([manifest.url]);\n\tfor (const part of [manifest.entry, ...Object.values(manifest.routes)]) {\n\t\tfor (const url of [\n\t\t\t...(part.module ? [part.module] : []),\n\t\t\t...(part.imports ?? []),\n\t\t\t...(part.css ?? []),\n\t\t]) {\n\t\t\turls.add(url);\n\t\t}\n\t}\n\treturn [...urls]\n\t\t.filter((url) => HASHED_ASSET.test(url))\n\t\t.map((url) => ({ url, revision: null }));\n}\n\n/**\n * The precache entries of the files among `assetFiles` (the names in\n * build/client/assets) that `include` matches, sorted, under `base`: fonts\n * an app keeps offline, say. The regex is the app's.\n */\nexport function assetPrecache(\n\tassetFiles: readonly string[],\n\tinclude: RegExp,\n\tbase = '/assets/',\n): PrecacheEntry[] {\n\treturn assetFiles\n\t\t.filter((file) => {\n\t\t\tinclude.lastIndex = 0; // a /g or /y regex remembers its last match\n\t\t\treturn include.test(file);\n\t\t})\n\t\t.sort()\n\t\t.map((file) => ({ url: `${base}${file}`, revision: null }));\n}\n"
6
+ ],
7
+ "mappings": ";AA0BA,IAAM,eAAe;AAGd,SAAS,qBAAoB,CAAC,QAAiC;AAAA,EACrE,MAAM,QAAQ,OAAO,QAAQ,GAAG;AAAA,EAChC,MAAM,MAAM,OAAO,YAAY,GAAG;AAAA,EAClC,IAAI,QAAQ,KAAK,MAAM,OAAO;AAAA,IAC7B,MAAM,IAAI,MACT,kGACD;AAAA,EACD;AAAA,EACA,OAAO,KAAK,MAAM,OAAO,MAAM,OAAO,MAAM,CAAC,CAAC;AAAA;AAIxC,SAAS,aAAY,CAAC,UAA4C;AAAA,EACxE,MAAM,OAAO,IAAI,IAAY,CAAC,SAAS,GAAG,CAAC;AAAA,EAC3C,WAAW,QAAQ,CAAC,SAAS,OAAO,GAAG,OAAO,OAAO,SAAS,MAAM,CAAC,GAAG;AAAA,IACvE,WAAW,OAAO;AAAA,MACjB,GAAI,KAAK,SAAS,CAAC,KAAK,MAAM,IAAI,CAAC;AAAA,MACnC,GAAI,KAAK,WAAW,CAAC;AAAA,MACrB,GAAI,KAAK,OAAO,CAAC;AAAA,IAClB,GAAG;AAAA,MACF,KAAK,IAAI,GAAG;AAAA,IACb;AAAA,EACD;AAAA,EACA,OAAO,CAAC,GAAG,IAAI,EACb,OAAO,CAAC,QAAQ,aAAa,KAAK,GAAG,CAAC,EACtC,IAAI,CAAC,SAAS,EAAE,KAAK,UAAU,KAAK,EAAE;AAAA;AAQlC,SAAS,cAAa,CAC5B,YACA,SACA,OAAO,YACW;AAAA,EAClB,OAAO,WACL,OAAO,CAAC,SAAS;AAAA,IACjB,QAAQ,YAAY;AAAA,IACpB,OAAO,QAAQ,KAAK,IAAI;AAAA,GACxB,EACA,KAAK,EACL,IAAI,CAAC,UAAU,EAAE,KAAK,GAAG,OAAO,QAAQ,UAAU,KAAK,EAAE;AAAA;",
8
+ "debugId": "812DDA9793A6A64B64756E2164756E21",
9
+ "names": []
10
+ }
@@ -1,38 +1,7 @@
1
- // src/pwa/model/worker-contract.ts
2
- var SERVICE_WORKER_URL2 = "/service-worker.js";
3
- var OFFLINE_URL2 = "/offline.html";
4
- var SKIP_WAITING2 = "SKIP_WAITING";
5
- function defineWorkerContract2(options) {
6
- const { cachePrefix } = options;
7
- if (typeof cachePrefix !== "string" || cachePrefix === "") {
8
- throw new TypeError(`defineWorkerContract(): cachePrefix must be a non-empty name, such as 'my-app'; got '${String(cachePrefix)}'.`);
9
- }
10
- if (cachePrefix.endsWith("-")) {
11
- throw new TypeError(`defineWorkerContract(): cachePrefix must not end with '-', which the contract adds; got '${cachePrefix}'. Use '${cachePrefix.replace(/-+$/, "")}'.`);
12
- }
13
- const cacheVersion = options.cacheVersion ?? "v1";
14
- if (typeof cacheVersion !== "string" || cacheVersion === "" || cacheVersion.includes("-")) {
15
- throw new TypeError(`defineWorkerContract(): cacheVersion must be a non-empty name without '-', such as 'v2'; got '${String(cacheVersion)}'.`);
16
- }
17
- const cacheNames = Object.freeze({
18
- precache: `${cachePrefix}-precache-${cacheVersion}`,
19
- assets: `${cachePrefix}-assets-${cacheVersion}`,
20
- static: `${cachePrefix}-static-${cacheVersion}`
21
- });
22
- const current = new Set(Object.values(cacheNames));
23
- const shape = new RegExp(`^${cachePrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}-[a-z0-9]+-[^-]+$`);
24
- const owned = (name) => shape.test(name);
25
- const contract = {
26
- cachePrefix,
27
- cacheVersion,
28
- serviceWorkerUrl: options.serviceWorkerUrl ?? SERVICE_WORKER_URL2,
29
- offlineUrl: options.offlineUrl ?? OFFLINE_URL2,
30
- cacheNames,
31
- obsoleteCaches: (names) => names.filter((name) => owned(name) && !current.has(name)),
32
- clearableCaches: (names) => names.filter((name) => owned(name) && name !== cacheNames.precache)
33
- };
34
- return Object.freeze(contract);
35
- }
1
+ import {
2
+ SERVICE_WORKER_URL2,
3
+ SKIP_WAITING2
4
+ } from "./index-rfr02vg4.js";
36
5
 
37
6
  // src/pwa/model/registration.ts
38
7
  function watchForUpdates2(registration, container, onWaiting) {
@@ -133,7 +102,7 @@ function createUpdateNotice2(storage, key = UPDATE_LATER_KEY2) {
133
102
  }
134
103
  var sessionStore2 = () => typeof sessionStorage === "undefined" ? undefined : sessionStorage;
135
104
 
136
- export { SERVICE_WORKER_URL2, OFFLINE_URL2, SKIP_WAITING2, defineWorkerContract2, watchForUpdates2, createUpdater2, afterLoad2, registerServiceWorker2, UPDATE_LATER_KEY2, createUpdateNotice2, sessionStore2 };
105
+ export { watchForUpdates2, createUpdater2, afterLoad2, registerServiceWorker2, UPDATE_LATER_KEY2, createUpdateNotice2, sessionStore2 };
137
106
 
138
- //# debugId=E59639D8397E531464756E2164756E21
139
- //# sourceMappingURL=index-zqa30gr7.js.map
107
+ //# debugId=824F7298E795FD3C64756E2164756E21
108
+ //# sourceMappingURL=index-9xv2a8et.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/pwa/model/registration.ts", "../src/pwa/model/update-notice.ts"],
4
+ "sourcesContent": [
5
+ "import { SERVICE_WORKER_URL, SKIP_WAITING } from './worker-contract';\n\n/** What the registration reads of `navigator.serviceWorker`. */\nexport type ServiceWorkerContainerLike = Pick<\n\tServiceWorkerContainer,\n\t'controller' | 'addEventListener' | 'register'\n>;\n\n/**\n * Calls `onWaiting` with each new version that finished installing while an\n * older one controls the page: the one waiting now, and each found later.\n * A first install controls nothing yet, so it offers nothing.\n */\nexport function watchForUpdates(\n\tregistration: ServiceWorkerRegistration,\n\tcontainer: Pick<ServiceWorkerContainerLike, 'controller'>,\n\tonWaiting: (worker: ServiceWorker) => void,\n): void {\n\tconst offer = (worker: ServiceWorker | null) => {\n\t\tif (worker && container.controller) onWaiting(worker);\n\t};\n\toffer(registration.waiting);\n\tregistration.addEventListener('updatefound', () => {\n\t\tconst installing = registration.installing;\n\t\tinstalling?.addEventListener('statechange', () => {\n\t\t\tif (installing.state === 'installed') offer(installing);\n\t\t});\n\t});\n}\n\n/** Moves this page onto a waiting worker: `apply(worker)`. */\nexport type ApplyUpdate = (\n\tworker: Pick<ServiceWorker, 'postMessage' | 'state'>,\n) => void;\n\n/**\n * The one update path of a page, created once: `apply(worker)` activates\n * the waiting worker and reloads once, when it takes control, so the page\n * runs the version its assets came from.\n *\n * - Several tabs: another tab may have activated the worker already. Its\n * `controllerchange` reached this page before anyone asked, so this page\n * did not reload (a form may be half filled); its \"Reload\" then reloads\n * at once, the worker being active.\n * - Clicked twice, or a `controllerchange` after a direct reload: one\n * reload, never two.\n */\nexport function createUpdater(\n\tcontainer: Pick<\n\t\tServiceWorkerContainerLike,\n\t\t'controller' | 'addEventListener'\n\t>,\n\treload: () => void = () => window.location.reload(),\n): ApplyUpdate {\n\tlet requested = false;\n\tlet reloaded = false;\n\tconst reloadOnce = () => {\n\t\tif (reloaded) return;\n\t\treloaded = true;\n\t\treload();\n\t};\n\tcontainer.addEventListener('controllerchange', () => {\n\t\tif (requested) reloadOnce();\n\t});\n\treturn (worker) => {\n\t\tconst active =\n\t\t\tworker.state === 'activating' ||\n\t\t\tworker.state === 'activated' ||\n\t\t\tcontainer.controller === worker;\n\t\tif (active) return reloadOnce();\n\t\trequested = true;\n\t\tworker.postMessage({ type: SKIP_WAITING });\n\t};\n}\n\n/** Runs `run` once the page has loaded: registering never competes with it. */\nexport function afterLoad(run: () => void): void {\n\tif (document.readyState === 'complete') run();\n\telse window.addEventListener('load', run, { once: true });\n}\n\nexport interface RegisterOptions {\n\t/** The worker's path. `SERVICE_WORKER_URL` by default. */\n\turl?: string;\n\t/** The scope it controls. `'/'` by default. */\n\tscope?: string;\n}\n\nlet updater: ApplyUpdate | undefined;\n\n/**\n * Registers the worker and watches for updates, handing each waiting worker\n * to `onWaiting` with the page's one `apply` (`createUpdater`, made on the\n * first call: one per page). A tab coming back to the foreground checks for\n * an update, as a long-lived page otherwise would only on its next full load.\n */\nexport async function registerServiceWorker(\n\tcontainer: ServiceWorkerContainerLike,\n\tonWaiting: (worker: ServiceWorker, apply: ApplyUpdate) => void,\n\toptions: RegisterOptions = {},\n): Promise<ServiceWorkerRegistration> {\n\tupdater ??= createUpdater(container);\n\tconst apply = updater;\n\tconst registration = await container.register(\n\t\toptions.url ?? SERVICE_WORKER_URL,\n\t\t{ scope: options.scope ?? '/' },\n\t);\n\twatchForUpdates(registration, container, (worker) =>\n\t\tonWaiting(worker, apply),\n\t);\n\tdocument.addEventListener('visibilitychange', () => {\n\t\tif (document.visibilityState === 'visible')\n\t\t\tregistration.update().catch(() => {\n\t\t\t\t// Offline, or the server is down: the next check will tell.\n\t\t\t});\n\t});\n\treturn registration;\n}\n",
6
+ "/**\n * The session's \"Later\": the notice stays hidden until the next visit.\n * `sessionStorage` is per origin, so one key serves every app; a second\n * argument of `createUpdateNotice` names another.\n */\nexport const UPDATE_LATER_KEY = 'alxia.update-later';\n\n/** The part of `Storage` the notice reads and writes. */\nexport type NoticeStorage = {\n\tgetItem(key: string): string | null;\n\tsetItem(key: string, value: string): void;\n};\n\n/** What the notice offers while a new version waits. */\nexport type UpdateNotice = {\n\t/** Moves the page onto the new version: the worker's `apply`. */\n\treload: () => void;\n\t/** Hides the notice until the next visit (the tab's session). */\n\tlater: () => void;\n};\n\n/** What `createUpdateNotice` returns: the shape `useSyncExternalStore` takes. */\nexport interface UpdateNoticeStore {\n\t/** A new version waits: `apply` moves the page onto it. */\n\toffer(apply: () => void): void;\n\t/** The notice to show, if any (the same object until it changes). */\n\tcurrent(): UpdateNotice | undefined;\n\t/** Calls `listener` on each change; returns the function that stops it. */\n\tsubscribe(listener: () => void): () => void;\n}\n\n/**\n * The update notice's state: offered by the worker's registration, read by\n * the banners, hidden for the session by \"Later\". `storage` is the session's\n * (`sessionStorage`), which may be missing or throw (private mode, blocked\n * storage): the notice then hides for this page only. It is a function so\n * that nothing reads the global at load.\n */\nexport function createUpdateNotice(\n\tstorage: () => NoticeStorage | undefined,\n\tkey: string = UPDATE_LATER_KEY,\n): UpdateNoticeStore {\n\tlet reload: (() => void) | undefined;\n\tlet laterThisPage = false;\n\tlet notice: UpdateNotice | undefined;\n\tconst listeners = new Set<() => void>();\n\n\tconst later = () => {\n\t\tlaterThisPage = true;\n\t\ttry {\n\t\t\tstorage()?.setItem(key, '1');\n\t\t} catch {\n\t\t\t// No storage: hidden for this page only.\n\t\t}\n\t\trefresh();\n\t};\n\tconst laterAsked = () => {\n\t\tif (laterThisPage) return true;\n\t\ttry {\n\t\t\treturn storage()?.getItem(key) === '1';\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t};\n\tfunction refresh() {\n\t\tnotice = reload && !laterAsked() ? { reload, later } : undefined;\n\t\tfor (const listener of listeners) listener();\n\t}\n\n\treturn {\n\t\toffer(apply) {\n\t\t\treload = apply;\n\t\t\trefresh();\n\t\t},\n\t\tcurrent: () => notice,\n\t\tsubscribe(listener) {\n\t\t\tlisteners.add(listener);\n\t\t\treturn () => {\n\t\t\t\tlisteners.delete(listener);\n\t\t\t};\n\t\t},\n\t};\n}\n\n/** `sessionStorage`, or nothing where there is none (the server). */\nexport const sessionStore = (): NoticeStorage | undefined =>\n\ttypeof sessionStorage === 'undefined' ? undefined : sessionStorage;\n"
7
+ ],
8
+ "mappings": "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",
9
+ "debugId": "824F7298E795FD3C64756E2164756E21",
10
+ "names": []
11
+ }