@nifrajs/web 5.5.1 → 5.7.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/CHANGELOG.md +243 -0
- package/README.md +21 -11
- package/dist/boundary.d.ts +67 -7
- package/dist/boundary.d.ts.map +1 -1
- package/dist/boundary.js +6 -4
- package/dist/boundary.js.map +1 -1
- package/dist/build-plan.d.ts +3 -0
- package/dist/build-plan.d.ts.map +1 -1
- package/dist/build-plan.js.map +1 -1
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +2 -0
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +9 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +10 -0
- package/dist/build.js.map +1 -1
- package/dist/client.d.ts +3 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +174 -23
- package/dist/client.js.map +1 -1
- package/dist/csp.d.ts +8 -0
- package/dist/csp.d.ts.map +1 -1
- package/dist/csp.js +11 -2
- package/dist/csp.js.map +1 -1
- package/dist/deferred.d.ts +6 -4
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +10 -6
- package/dist/deferred.js.map +1 -1
- package/dist/dev-feed.d.ts.map +1 -1
- package/dist/dev-feed.js +11 -2
- package/dist/dev-feed.js.map +1 -1
- package/dist/dev-session.d.ts.map +1 -1
- package/dist/dev-session.js +13 -6
- package/dist/dev-session.js.map +1 -1
- package/dist/dev.d.ts +4 -1
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +12 -5
- package/dist/dev.js.map +1 -1
- package/dist/diagnostic-catalog.d.ts +4 -0
- package/dist/diagnostic-catalog.d.ts.map +1 -1
- package/dist/diagnostic-catalog.js +48 -0
- package/dist/diagnostic-catalog.js.map +1 -1
- package/dist/diagnostic.d.ts +1 -1
- package/dist/diagnostic.d.ts.map +1 -1
- package/dist/diagnostic.js +1 -1
- package/dist/diagnostic.js.map +1 -1
- package/dist/fn.d.ts +5 -3
- package/dist/fn.d.ts.map +1 -1
- package/dist/fn.js +20 -5
- package/dist/fn.js.map +1 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +13 -3
- package/dist/fs.js.map +1 -1
- package/dist/identity.d.ts +22 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +60 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/internal/cdn/cloudflare.d.ts +2 -1
- package/dist/internal/cdn/cloudflare.d.ts.map +1 -1
- package/dist/internal/cdn/cloudflare.js +2 -1
- package/dist/internal/cdn/cloudflare.js.map +1 -1
- package/dist/internal/certified-components.d.ts +1 -1
- package/dist/internal/certified-components.js +1 -1
- package/dist/internal/codegen.d.ts +11 -10
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +73 -10
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/dev-websocket.d.ts +73 -0
- package/dist/internal/dev-websocket.d.ts.map +1 -0
- package/dist/internal/dev-websocket.js +145 -0
- package/dist/internal/dev-websocket.js.map +1 -0
- package/dist/internal/hole-runtime.d.ts +66 -0
- package/dist/internal/hole-runtime.d.ts.map +1 -0
- package/dist/internal/hole-runtime.js +211 -0
- package/dist/internal/hole-runtime.js.map +1 -0
- package/dist/internal/identity-epoch.d.ts +50 -0
- package/dist/internal/identity-epoch.d.ts.map +1 -0
- package/dist/internal/identity-epoch.js +180 -0
- package/dist/internal/identity-epoch.js.map +1 -0
- package/dist/internal/invalidation-contract.d.ts +23 -0
- package/dist/internal/invalidation-contract.d.ts.map +1 -0
- package/dist/internal/invalidation-contract.js +11 -0
- package/dist/internal/invalidation-contract.js.map +1 -0
- package/dist/internal/invalidation.d.ts +62 -0
- package/dist/internal/invalidation.d.ts.map +1 -0
- package/dist/internal/invalidation.js +149 -0
- package/dist/internal/invalidation.js.map +1 -0
- package/dist/internal/island-bindings.d.ts +51 -0
- package/dist/internal/island-bindings.d.ts.map +1 -0
- package/dist/internal/island-bindings.js +147 -0
- package/dist/internal/island-bindings.js.map +1 -0
- package/dist/internal/island-list.d.ts +26 -0
- package/dist/internal/island-list.d.ts.map +1 -0
- package/dist/internal/island-list.js +78 -0
- package/dist/internal/island-list.js.map +1 -0
- package/dist/internal/island-mount.d.ts +4 -0
- package/dist/internal/island-mount.d.ts.map +1 -0
- package/dist/internal/island-mount.js +66 -0
- package/dist/internal/island-mount.js.map +1 -0
- package/dist/internal/island-owner.d.ts +5 -0
- package/dist/internal/island-owner.d.ts.map +1 -0
- package/dist/internal/island-owner.js +20 -0
- package/dist/internal/island-owner.js.map +1 -0
- package/dist/internal/island-resource.d.ts +30 -0
- package/dist/internal/island-resource.d.ts.map +1 -0
- package/dist/internal/island-resource.js +57 -0
- package/dist/internal/island-resource.js.map +1 -0
- package/dist/internal/island-signals.d.ts +51 -0
- package/dist/internal/island-signals.d.ts.map +1 -0
- package/dist/internal/island-signals.js +220 -0
- package/dist/internal/island-signals.js.map +1 -0
- package/dist/internal/keep-alive-table.d.ts +23 -0
- package/dist/internal/keep-alive-table.d.ts.map +1 -0
- package/dist/internal/keep-alive-table.js +53 -0
- package/dist/internal/keep-alive-table.js.map +1 -0
- package/dist/internal/live-contract.d.ts +101 -0
- package/dist/internal/live-contract.d.ts.map +1 -0
- package/dist/internal/live-contract.js +5 -0
- package/dist/internal/live-contract.js.map +1 -0
- package/dist/internal/live-queries.d.ts +22 -0
- package/dist/internal/live-queries.d.ts.map +1 -0
- package/dist/internal/live-queries.js +54 -0
- package/dist/internal/live-queries.js.map +1 -0
- package/dist/internal/page-events.d.ts +10 -0
- package/dist/internal/page-events.d.ts.map +1 -0
- package/dist/internal/page-events.js +26 -0
- package/dist/internal/page-events.js.map +1 -0
- package/dist/internal/page-execution.d.ts +16 -1
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +432 -31
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/parity.d.ts +3 -0
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +28 -5
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/ppr/admission.d.ts +83 -0
- package/dist/internal/ppr/admission.d.ts.map +1 -0
- package/dist/internal/ppr/admission.js +221 -0
- package/dist/internal/ppr/admission.js.map +1 -0
- package/dist/internal/ppr/canary.d.ts +24 -0
- package/dist/internal/ppr/canary.d.ts.map +1 -0
- package/dist/internal/ppr/canary.js +102 -0
- package/dist/internal/ppr/canary.js.map +1 -0
- package/dist/internal/ppr/compile-shell.d.ts +39 -0
- package/dist/internal/ppr/compile-shell.d.ts.map +1 -0
- package/dist/internal/ppr/compile-shell.js +298 -0
- package/dist/internal/ppr/compile-shell.js.map +1 -0
- package/dist/internal/ppr/contract.d.ts +177 -0
- package/dist/internal/ppr/contract.d.ts.map +1 -0
- package/dist/internal/ppr/contract.js +80 -0
- package/dist/internal/ppr/contract.js.map +1 -0
- package/dist/internal/ppr/frames.d.ts +71 -0
- package/dist/internal/ppr/frames.d.ts.map +1 -0
- package/dist/internal/ppr/frames.js +119 -0
- package/dist/internal/ppr/frames.js.map +1 -0
- package/dist/internal/ppr/integration.d.ts +19 -0
- package/dist/internal/ppr/integration.d.ts.map +1 -0
- package/dist/internal/ppr/integration.js +106 -0
- package/dist/internal/ppr/integration.js.map +1 -0
- package/dist/internal/ppr/options.d.ts +96 -0
- package/dist/internal/ppr/options.d.ts.map +1 -0
- package/dist/internal/ppr/options.js +125 -0
- package/dist/internal/ppr/options.js.map +1 -0
- package/dist/internal/ppr/route-rules.d.ts +12 -0
- package/dist/internal/ppr/route-rules.d.ts.map +1 -0
- package/dist/internal/ppr/route-rules.js +53 -0
- package/dist/internal/ppr/route-rules.js.map +1 -0
- package/dist/internal/ppr/runtime.d.ts +29 -0
- package/dist/internal/ppr/runtime.d.ts.map +1 -0
- package/dist/internal/ppr/runtime.js +35 -0
- package/dist/internal/ppr/runtime.js.map +1 -0
- package/dist/internal/ppr/server.d.ts +29 -0
- package/dist/internal/ppr/server.d.ts.map +1 -0
- package/dist/internal/ppr/server.js +1409 -0
- package/dist/internal/ppr/server.js.map +1 -0
- package/dist/internal/ppr/shell-store.d.ts +39 -0
- package/dist/internal/ppr/shell-store.d.ts.map +1 -0
- package/dist/internal/ppr/shell-store.js +106 -0
- package/dist/internal/ppr/shell-store.js.map +1 -0
- package/dist/internal/prerendered-pages.d.ts +1 -1
- package/dist/internal/prerendered-pages.d.ts.map +1 -1
- package/dist/internal/prerendered-pages.js +7 -4
- package/dist/internal/prerendered-pages.js.map +1 -1
- package/dist/internal/present.d.ts +11 -0
- package/dist/internal/present.d.ts.map +1 -0
- package/dist/internal/present.js +13 -0
- package/dist/internal/present.js.map +1 -0
- package/dist/internal/query-digest.d.ts +9 -0
- package/dist/internal/query-digest.d.ts.map +1 -0
- package/dist/internal/query-digest.js +23 -0
- package/dist/internal/query-digest.js.map +1 -0
- package/dist/internal/query-runtime.d.ts +6 -1
- package/dist/internal/query-runtime.d.ts.map +1 -1
- package/dist/internal/query-runtime.js +9 -0
- package/dist/internal/query-runtime.js.map +1 -1
- package/dist/internal/query-seam.d.ts +29 -0
- package/dist/internal/query-seam.d.ts.map +1 -0
- package/dist/internal/query-seam.js +23 -0
- package/dist/internal/query-seam.js.map +1 -0
- package/dist/internal/render-document.d.ts +34 -64
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +82 -64
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/request-abort.d.ts +8 -0
- package/dist/internal/request-abort.d.ts.map +1 -0
- package/dist/internal/request-abort.js +22 -0
- package/dist/internal/request-abort.js.map +1 -0
- package/dist/internal/server-boundary.d.ts +2 -0
- package/dist/internal/server-boundary.d.ts.map +1 -1
- package/dist/internal/server-boundary.js +8 -1
- package/dist/internal/server-boundary.js.map +1 -1
- package/dist/internal/share-data.d.ts +10 -0
- package/dist/internal/share-data.d.ts.map +1 -0
- package/dist/internal/share-data.js +135 -0
- package/dist/internal/share-data.js.map +1 -0
- package/dist/internal/status-page.d.ts +68 -0
- package/dist/internal/status-page.d.ts.map +1 -0
- package/dist/internal/status-page.js +66 -0
- package/dist/internal/status-page.js.map +1 -0
- package/dist/internal/store-signature.d.ts +15 -0
- package/dist/internal/store-signature.d.ts.map +1 -0
- package/dist/internal/store-signature.js +114 -0
- package/dist/internal/store-signature.js.map +1 -0
- package/dist/internal/vite-plugin-compat.d.ts +2 -2
- package/dist/internal/vite-plugin-compat.js +2 -2
- package/dist/internal/web-app.d.ts +26 -0
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +79 -3
- package/dist/internal/web-app.js.map +1 -1
- package/dist/islands-swap.d.ts +41 -0
- package/dist/islands-swap.d.ts.map +1 -0
- package/dist/islands-swap.js +270 -0
- package/dist/islands-swap.js.map +1 -0
- package/dist/islands.d.ts +86 -24
- package/dist/islands.d.ts.map +1 -1
- package/dist/islands.js +138 -78
- package/dist/islands.js.map +1 -1
- package/dist/isr.d.ts +67 -13
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +247 -99
- package/dist/isr.js.map +1 -1
- package/dist/keep-alive.d.ts +147 -0
- package/dist/keep-alive.d.ts.map +1 -0
- package/dist/keep-alive.js +1102 -0
- package/dist/keep-alive.js.map +1 -0
- package/dist/live/bus.d.ts +70 -0
- package/dist/live/bus.d.ts.map +1 -0
- package/dist/live/bus.js +55 -0
- package/dist/live/bus.js.map +1 -0
- package/dist/live/core.d.ts +25 -0
- package/dist/live/core.d.ts.map +1 -0
- package/dist/live/core.js +29 -0
- package/dist/live/core.js.map +1 -0
- package/dist/live/history.d.ts +38 -0
- package/dist/live/history.d.ts.map +1 -0
- package/dist/live/history.js +104 -0
- package/dist/live/history.js.map +1 -0
- package/dist/live/hub-protocol.d.ts +46 -0
- package/dist/live/hub-protocol.d.ts.map +1 -0
- package/dist/live/hub-protocol.js +108 -0
- package/dist/live/hub-protocol.js.map +1 -0
- package/dist/live/hub.d.ts +185 -0
- package/dist/live/hub.d.ts.map +1 -0
- package/dist/live/hub.js +544 -0
- package/dist/live/hub.js.map +1 -0
- package/dist/live/keyring.d.ts +92 -0
- package/dist/live/keyring.d.ts.map +1 -0
- package/dist/live/keyring.js +265 -0
- package/dist/live/keyring.js.map +1 -0
- package/dist/live/read-capped.d.ts +6 -0
- package/dist/live/read-capped.d.ts.map +1 -0
- package/dist/live/read-capped.js +42 -0
- package/dist/live/read-capped.js.map +1 -0
- package/dist/live/revocations.d.ts +52 -0
- package/dist/live/revocations.d.ts.map +1 -0
- package/dist/live/revocations.js +116 -0
- package/dist/live/revocations.js.map +1 -0
- package/dist/live/settings.d.ts +14 -0
- package/dist/live/settings.d.ts.map +1 -0
- package/dist/live/settings.js +38 -0
- package/dist/live/settings.js.map +1 -0
- package/dist/live-client.d.ts +34 -0
- package/dist/live-client.d.ts.map +1 -0
- package/dist/live-client.js +878 -0
- package/dist/live-client.js.map +1 -0
- package/dist/live-hub.d.ts +130 -0
- package/dist/live-hub.d.ts.map +1 -0
- package/dist/live-hub.js +507 -0
- package/dist/live-hub.js.map +1 -0
- package/dist/live-redis.d.ts +60 -0
- package/dist/live-redis.d.ts.map +1 -0
- package/dist/live-redis.js +212 -0
- package/dist/live-redis.js.map +1 -0
- package/dist/live.d.ts +150 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +972 -0
- package/dist/live.js.map +1 -0
- package/dist/manifest.d.ts +113 -2
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +109 -5
- package/dist/manifest.js.map +1 -1
- package/dist/navigation.d.ts +9 -3
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +13 -3
- package/dist/navigation.js.map +1 -1
- package/dist/plugins/vite-zone-guard.d.ts +6 -0
- package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
- package/dist/plugins/vite-zone-guard.js +8 -2
- package/dist/plugins/vite-zone-guard.js.map +1 -1
- package/dist/ppr.d.ts +73 -0
- package/dist/ppr.d.ts.map +1 -0
- package/dist/ppr.js +76 -0
- package/dist/ppr.js.map +1 -0
- package/dist/query.d.ts +8 -13
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +112 -18
- package/dist/query.js.map +1 -1
- package/dist/render-seam.d.ts +32 -0
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +7 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/route-types.d.ts.map +1 -1
- package/dist/route-types.js +2 -2
- package/dist/route-types.js.map +1 -1
- package/dist/router.d.ts +72 -4
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +416 -129
- package/dist/router.js.map +1 -1
- package/dist/server-components/build.d.ts.map +1 -1
- package/dist/server-components/build.js +3 -3
- package/dist/server-components/build.js.map +1 -1
- package/dist/server-components/client.d.ts +23 -1
- package/dist/server-components/client.d.ts.map +1 -1
- package/dist/server-components/client.js +92 -5
- package/dist/server-components/client.js.map +1 -1
- package/dist/server-components/definition.d.ts.map +1 -1
- package/dist/server-components/definition.js +2 -1
- package/dist/server-components/definition.js.map +1 -1
- package/dist/server-components/hole.d.ts +8 -0
- package/dist/server-components/hole.d.ts.map +1 -0
- package/dist/server-components/hole.js +20 -0
- package/dist/server-components/hole.js.map +1 -0
- package/dist/server-components/html-policy.d.ts.map +1 -1
- package/dist/server-components/html-policy.js +62 -4
- package/dist/server-components/html-policy.js.map +1 -1
- package/dist/server-components/html-scan.d.ts +17 -0
- package/dist/server-components/html-scan.d.ts.map +1 -0
- package/dist/server-components/html-scan.js +180 -0
- package/dist/server-components/html-scan.js.map +1 -0
- package/dist/server-components/html.d.ts.map +1 -1
- package/dist/server-components/html.js +19 -4
- package/dist/server-components/html.js.map +1 -1
- package/dist/server-components/router-integration.d.ts +4 -1
- package/dist/server-components/router-integration.d.ts.map +1 -1
- package/dist/server-components/router-integration.js +23 -2
- package/dist/server-components/router-integration.js.map +1 -1
- package/dist/server-components/runtime.d.ts +6 -1
- package/dist/server-components/runtime.d.ts.map +1 -1
- package/dist/server-components/runtime.js +88 -29
- package/dist/server-components/runtime.js.map +1 -1
- package/dist/server-components/state.d.ts +3 -1
- package/dist/server-components/state.d.ts.map +1 -1
- package/dist/server-components/state.js +50 -38
- package/dist/server-components/state.js.map +1 -1
- package/dist/server-components/types.d.ts +9 -0
- package/dist/server-components/types.d.ts.map +1 -1
- package/dist/server-components/vite.d.ts.map +1 -1
- package/dist/server-components/vite.js +26 -9
- package/dist/server-components/vite.js.map +1 -1
- package/dist/vite.d.ts +5 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +58 -4
- package/dist/vite.js.map +1 -1
- package/package.json +48 -8
- package/src/boundary.ts +77 -5
- package/src/build-plan.ts +3 -0
- package/src/build-vite.ts +2 -0
- package/src/build.ts +20 -1
- package/src/client.ts +183 -23
- package/src/csp.ts +16 -2
- package/src/deferred.ts +11 -5
- package/src/dev-feed.ts +11 -2
- package/src/dev-session.ts +22 -11
- package/src/dev.ts +29 -8
- package/src/diagnostic-catalog.ts +53 -0
- package/src/diagnostic.ts +1 -0
- package/src/fn.ts +24 -7
- package/src/fs.ts +22 -8
- package/src/identity.ts +61 -0
- package/src/index.ts +20 -4
- package/src/internal/cdn/cloudflare.ts +2 -1
- package/src/internal/certified-components.ts +1 -1
- package/src/internal/codegen.ts +95 -9
- package/src/internal/dev-websocket.ts +230 -0
- package/src/internal/hole-runtime.ts +286 -0
- package/src/internal/identity-epoch.ts +239 -0
- package/src/internal/invalidation-contract.ts +35 -0
- package/src/internal/invalidation.ts +213 -0
- package/src/internal/island-bindings.ts +191 -0
- package/src/internal/island-list.ts +92 -0
- package/src/internal/island-mount.ts +68 -0
- package/src/internal/island-owner.ts +20 -0
- package/src/internal/island-resource.ts +76 -0
- package/src/internal/island-signals.ts +249 -0
- package/src/internal/keep-alive-table.ts +72 -0
- package/src/internal/live-contract.ts +111 -0
- package/src/internal/live-queries.ts +60 -0
- package/src/internal/page-events.ts +27 -0
- package/src/internal/page-execution.ts +605 -45
- package/src/internal/parity.ts +35 -6
- package/src/internal/ppr/admission.ts +300 -0
- package/src/internal/ppr/canary.ts +122 -0
- package/src/internal/ppr/compile-shell.ts +379 -0
- package/src/internal/ppr/contract.ts +265 -0
- package/src/internal/ppr/frames.ts +170 -0
- package/src/internal/ppr/integration.ts +137 -0
- package/src/internal/ppr/options.ts +221 -0
- package/src/internal/ppr/route-rules.ts +77 -0
- package/src/internal/ppr/runtime.ts +39 -0
- package/src/internal/ppr/server.ts +1766 -0
- package/src/internal/ppr/shell-store.ts +152 -0
- package/src/internal/prerendered-pages.ts +7 -4
- package/src/internal/present.ts +20 -0
- package/src/internal/query-digest.ts +34 -0
- package/src/internal/query-runtime.ts +14 -1
- package/src/internal/query-seam.ts +48 -0
- package/src/internal/render-document.ts +158 -113
- package/src/internal/request-abort.ts +26 -0
- package/src/internal/server-boundary.ts +10 -1
- package/src/internal/share-data.ts +136 -0
- package/src/internal/status-page.ts +95 -0
- package/src/internal/store-signature.ts +149 -0
- package/src/internal/vite-plugin-compat.ts +2 -2
- package/src/internal/web-app.ts +123 -3
- package/src/islands-swap.ts +283 -0
- package/src/islands.ts +169 -72
- package/src/isr.ts +348 -106
- package/src/keep-alive.ts +1354 -0
- package/src/live/bus.ts +104 -0
- package/src/live/core.ts +52 -0
- package/src/live/history.ts +109 -0
- package/src/live/hub-protocol.ts +133 -0
- package/src/live/hub.ts +620 -0
- package/src/live/keyring.ts +384 -0
- package/src/live/read-capped.ts +36 -0
- package/src/live/revocations.ts +155 -0
- package/src/live/settings.ts +46 -0
- package/src/live-client.ts +1011 -0
- package/src/live-hub.ts +658 -0
- package/src/live-redis.ts +284 -0
- package/src/live.ts +1257 -0
- package/src/manifest.ts +274 -6
- package/src/navigation.ts +14 -2
- package/src/plugins/vite-zone-guard.ts +15 -2
- package/src/ppr.ts +153 -0
- package/src/query.ts +147 -20
- package/src/render-seam.ts +37 -0
- package/src/route-types.ts +3 -1
- package/src/router.ts +534 -132
- package/src/server-components/build.ts +9 -3
- package/src/server-components/client.ts +89 -5
- package/src/server-components/definition.ts +2 -1
- package/src/server-components/hole.ts +23 -0
- package/src/server-components/html-policy.ts +63 -4
- package/src/server-components/html-scan.ts +159 -0
- package/src/server-components/html.ts +18 -4
- package/src/server-components/router-integration.ts +21 -2
- package/src/server-components/runtime.ts +114 -31
- package/src/server-components/state.ts +66 -44
- package/src/server-components/types.ts +16 -0
- package/src/server-components/vite.ts +33 -14
- package/src/vite.ts +75 -6
- package/dist/nano.d.ts +0 -125
- package/dist/nano.d.ts.map +0 -1
- package/dist/nano.js +0 -190
- package/dist/nano.js.map +0 -1
- package/src/nano.ts +0 -246
package/dist/router.js
CHANGED
|
@@ -7,14 +7,22 @@
|
|
|
7
7
|
import { decodeRouteParams } from "@nifrajs/core/pattern";
|
|
8
8
|
import { Router as CoreRouter } from "@nifrajs/core/router";
|
|
9
9
|
import { parseNdjsonData } from "./deferred.js";
|
|
10
|
+
import { epochOf, IDENTITY_CHANNEL, onIdentityChange } from "./identity.js";
|
|
11
|
+
import { present } from "./internal/present.js";
|
|
10
12
|
import { parsePageData } from "./internal/revive-dates.js";
|
|
11
13
|
import { appUrl, requestPattern } from "./internal/route-path.js";
|
|
14
|
+
import { shareData } from "./internal/share-data.js";
|
|
12
15
|
import { isClientOnlySearchChange } from "./search.js";
|
|
13
16
|
/**
|
|
14
17
|
* Request header that asks a nifra route's GET to return just the loader data as JSON (instead of
|
|
15
18
|
* the full HTML document). Set by client-side navigation; read by `createWebApp`'s GET handler.
|
|
16
19
|
*/
|
|
17
20
|
export const DATA_HEADER = "x-nifra-data";
|
|
21
|
+
/**
|
|
22
|
+
* Sent beside {@link DATA_HEADER} by a prefetch. A partially prerendered route answers it with its
|
|
23
|
+
* shell data and starts no hole: nothing could cancel that work if the link is never followed.
|
|
24
|
+
*/
|
|
25
|
+
export const PREFETCH_HEADER = "x-nifra-prefetch";
|
|
18
26
|
/**
|
|
19
27
|
* Global the server injects (`createWebApp({ prerenderedPaths })`) listing the SSG-prerendered paths.
|
|
20
28
|
* The client's default data fetch reads it: a soft-nav INTO a prerendered route fetches its static
|
|
@@ -71,6 +79,18 @@ export const RETAIN_HEADER = "x-nifra-retain";
|
|
|
71
79
|
* the full-chain case.
|
|
72
80
|
*/
|
|
73
81
|
export const NAV_FROM_HEADER = "x-nifra-from";
|
|
82
|
+
/**
|
|
83
|
+
* Sent on a data request that reloads the route instance on screen: the holes, comma separated, whose
|
|
84
|
+
* hosts the partially prerendered page holds. Each one's state then carries its fill (server HTML,
|
|
85
|
+
* plus props when it hydrates) instead of its data, so the client updates the host in place.
|
|
86
|
+
*/
|
|
87
|
+
export const HOLE_FILL_HEADER = "x-nifra-hole-fill";
|
|
88
|
+
/**
|
|
89
|
+
* Holes of a partially prerendered page the client keeps, comma separated: they are not loaded, and
|
|
90
|
+
* each state says so. Only the live load endpoint sets it, for holes whose keys did not change. A name
|
|
91
|
+
* the route does not declare is refused with a 400.
|
|
92
|
+
*/
|
|
93
|
+
export const KEEP_HOLES_HEADER = "x-nifra-keep-holes";
|
|
74
94
|
/** Recognise the envelope without mistaking a plain loader object that happens to have a `data` key. */
|
|
75
95
|
export function isRouteDataEnvelope(value) {
|
|
76
96
|
return (typeof value === "object" &&
|
|
@@ -112,9 +132,16 @@ export function createMatcher(patterns) {
|
|
|
112
132
|
/** Read a nifra data response: a deferred loader/action streams NDJSON (parse line 1 + settle
|
|
113
133
|
* `<Await>` markers as resolution lines arrive); a non-deferred one returns a single JSON. Shared by
|
|
114
134
|
* navigation fetches and action submits - both transports are identical. */
|
|
115
|
-
const readResponseData = (res, signal) => (res.headers.get("content-type") ?? "").includes("application/x-ndjson") && res.body !== null
|
|
135
|
+
const readResponseData = (res, signal, read) => (res.headers.get("content-type") ?? "").includes("application/x-ndjson") && res.body !== null
|
|
116
136
|
? parseNdjsonData(res.body, signal)
|
|
117
|
-
: res.text().then(
|
|
137
|
+
: res.text().then((text) => {
|
|
138
|
+
if (read === undefined)
|
|
139
|
+
return parsePageData(text);
|
|
140
|
+
if (text === read.previous)
|
|
141
|
+
return read;
|
|
142
|
+
read.text = text;
|
|
143
|
+
return parsePageData(text);
|
|
144
|
+
});
|
|
118
145
|
// Index scan, not `/\/+$/` - the unanchored-start trailing-run replace backtracks quadratically and
|
|
119
146
|
// the pathname is caller-controlled.
|
|
120
147
|
const trimTrailingSlashes = (path) => {
|
|
@@ -169,7 +196,24 @@ const targetUrl = (to) => {
|
|
|
169
196
|
}
|
|
170
197
|
};
|
|
171
198
|
const redirected = (to, cause) => Object.assign(new Error(`[nifra/web] redirected to ${to}`, { cause }), { redirectTo: to });
|
|
172
|
-
|
|
199
|
+
/**
|
|
200
|
+
* A route data response as `fetchData` resolves it: a loader's in-app redirect rejects with its
|
|
201
|
+
* target, a terminal status becomes an envelope carrying it, and any other failure rejects.
|
|
202
|
+
*/
|
|
203
|
+
export const routeDataOf = async (res, path, signal, read) => {
|
|
204
|
+
const redirectTo = res.headers.get(REDIRECT_HEADER);
|
|
205
|
+
if (redirectTo !== null)
|
|
206
|
+
throw redirected(redirectTo);
|
|
207
|
+
const status = Number(res.headers.get(STATUS_HEADER));
|
|
208
|
+
if (Number.isInteger(status) && status >= 400 && status <= 599) {
|
|
209
|
+
return { v: 1, data: null, status };
|
|
210
|
+
}
|
|
211
|
+
if (!res.ok)
|
|
212
|
+
throw new Error(`[nifra/web] navigation data fetch failed (${res.status}): ${path}`);
|
|
213
|
+
return readResponseData(res, signal, read);
|
|
214
|
+
};
|
|
215
|
+
/** The router's own transport, which a `fetchData` override may layer over. */
|
|
216
|
+
export const fetchRouteData = async (path, _match, signal, navigation, purpose, hosts, read) => {
|
|
173
217
|
// SSG fast path: if this path was prerendered, its loader data is a static file - fetch that (no
|
|
174
218
|
// worker). Falls through to the dynamic header-GET on any miss (file absent, e.g. a deferred route,
|
|
175
219
|
// or a stale set), so it's always safe. Non-SSG apps have no global → the dynamic path, unchanged.
|
|
@@ -184,25 +228,19 @@ const defaultFetchData = async (path, _match, signal, navigation) => {
|
|
|
184
228
|
if (prerenderedSetOf(prerendered).has(pathname)) {
|
|
185
229
|
const staticRes = await fetch(appUrl(dataUrlFor(pathname)), { signal: signal ?? null });
|
|
186
230
|
if (staticRes.ok)
|
|
187
|
-
return readResponseData(staticRes, signal);
|
|
231
|
+
return readResponseData(staticRes, signal, read);
|
|
188
232
|
}
|
|
189
233
|
}
|
|
190
234
|
const headers = new Headers({ [DATA_HEADER]: "1" });
|
|
235
|
+
if (purpose === "prefetch")
|
|
236
|
+
headers.set(PREFETCH_HEADER, "1");
|
|
237
|
+
if (hosts !== undefined && hosts.length > 0)
|
|
238
|
+
headers.set(HOLE_FILL_HEADER, hosts.join(","));
|
|
191
239
|
if (navigation !== undefined && navigation.retain.length > 0) {
|
|
192
240
|
headers.set(RETAIN_HEADER, navigation.retain.join(","));
|
|
193
241
|
headers.set(NAV_FROM_HEADER, navigation.from);
|
|
194
242
|
}
|
|
195
|
-
|
|
196
|
-
const redirectTo = res.headers.get(REDIRECT_HEADER);
|
|
197
|
-
if (redirectTo !== null)
|
|
198
|
-
throw redirected(redirectTo);
|
|
199
|
-
const status = Number(res.headers.get(STATUS_HEADER));
|
|
200
|
-
if (Number.isInteger(status) && status >= 400 && status <= 599) {
|
|
201
|
-
return { v: 1, data: null, status };
|
|
202
|
-
}
|
|
203
|
-
if (!res.ok)
|
|
204
|
-
throw new Error(`[nifra/web] navigation data fetch failed (${res.status}): ${path}`);
|
|
205
|
-
return readResponseData(res, signal);
|
|
243
|
+
return routeDataOf(await fetch(appUrl(path), { headers, signal: signal ?? null }), path, signal, read);
|
|
206
244
|
};
|
|
207
245
|
/**
|
|
208
246
|
* Create the agnostic router store. `navigate` is guarded by a monotonic generation so that when
|
|
@@ -224,9 +262,9 @@ const rawSearchOf = (path) => {
|
|
|
224
262
|
const isClientActionResult = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
225
263
|
export function createClientRouter(options) {
|
|
226
264
|
const match = createMatcher(options.patterns);
|
|
227
|
-
const fetchData = options.fetchData ??
|
|
265
|
+
const fetchData = options.fetchData ?? fetchRouteData;
|
|
228
266
|
// Start a handed-over list's fetch now, so the first navigation rarely waits for it.
|
|
229
|
-
if (fetchData ===
|
|
267
|
+
if (fetchData === fetchRouteData)
|
|
230
268
|
prerenderedOf();
|
|
231
269
|
// routeId → the route's client-only search keys. Read by reference (the generated entry keeps writing
|
|
232
270
|
// to it as routes load), so a same-route nav always sees the current route's keys.
|
|
@@ -273,18 +311,68 @@ export function createClientRouter(options) {
|
|
|
273
311
|
data: value.data,
|
|
274
312
|
layoutData: value.layoutData,
|
|
275
313
|
serverComponents: value.serverComponents,
|
|
314
|
+
live: value.live,
|
|
276
315
|
...(value.boundaries === undefined ? {} : { boundaries: value.boundaries }),
|
|
277
316
|
});
|
|
317
|
+
// The path the page on screen was loaded from. While a boundary holds an intercepted target, `path`
|
|
318
|
+
// is that target's URL, so anything that reloads or caches the page must read this instead.
|
|
319
|
+
const pagePathOf = (current) => current.intercepted?.pagePath ?? current.path;
|
|
320
|
+
// `boundaries` with `name` holding a target's outcome. The URL names one target, so every other
|
|
321
|
+
// intercept boundary goes back to unresolved. A target that answered with a status, or failed, is
|
|
322
|
+
// an error state: its data never reaches the boundary.
|
|
323
|
+
const holdTarget = (boundaries, descriptor, outcome) => {
|
|
324
|
+
const next = {};
|
|
325
|
+
for (const [name, boundary] of Object.entries(boundaries ?? {})) {
|
|
326
|
+
next[name] =
|
|
327
|
+
typeof boundary.mode === "string" || name === descriptor.name
|
|
328
|
+
? boundary
|
|
329
|
+
: {
|
|
330
|
+
name: boundary.name,
|
|
331
|
+
mode: boundary.mode,
|
|
332
|
+
status: "unresolved",
|
|
333
|
+
...(boundary.errorId === undefined ? {} : { errorId: boundary.errorId }),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const base = {
|
|
337
|
+
name: descriptor.name,
|
|
338
|
+
mode: descriptor.mode,
|
|
339
|
+
...(descriptor.errorId === undefined ? {} : { errorId: descriptor.errorId }),
|
|
340
|
+
};
|
|
341
|
+
next[descriptor.name] =
|
|
342
|
+
outcome === undefined || outcome.terminalRouteId !== undefined
|
|
343
|
+
? { ...base, status: "error", error: { name: "Error", message: "Boundary failed" } }
|
|
344
|
+
: { ...base, status: "ready", data: outcome.data, live: outcome.live };
|
|
345
|
+
return next;
|
|
346
|
+
};
|
|
347
|
+
// `boundaries` with `name`'s action result set (or cleared, for `undefined`). Only a boundary showing
|
|
348
|
+
// its target's data carries one.
|
|
349
|
+
const withActionData = (boundaries, name, actionData) => {
|
|
350
|
+
const held = boundaries?.[name];
|
|
351
|
+
if (held === undefined || held.status !== "ready")
|
|
352
|
+
return boundaries;
|
|
353
|
+
const { actionData: _previous, ...rest } = held;
|
|
354
|
+
return { ...boundaries, [name]: actionData === undefined ? rest : { ...rest, actionData } };
|
|
355
|
+
};
|
|
278
356
|
const loadRouteData = async (path, match, signal,
|
|
279
357
|
// Offer the server the layout data on screen: it re-runs whichever of those loaders it must.
|
|
280
|
-
keepLayouts
|
|
358
|
+
keepLayouts, purpose, hosts,
|
|
359
|
+
// What this path shows, when this is a reload of it: the same response text again is that, unparsed.
|
|
360
|
+
shown) => {
|
|
281
361
|
const retainFrom = keepLayouts
|
|
282
|
-
? state.layoutData && { path: state
|
|
362
|
+
? state.layoutData && { path: pagePathOf(state), layoutData: state.layoutData }
|
|
283
363
|
: undefined;
|
|
284
364
|
const retain = retainFrom?.layoutData.map((_, index) => index) ?? [];
|
|
285
|
-
const
|
|
365
|
+
const hooks = routeHooks[match.routeId];
|
|
366
|
+
// A client loader's output is not what the text says, and an opted-out route takes fresh data.
|
|
367
|
+
const read = {
|
|
368
|
+
previous: hooks?.clientLoader === undefined && hooks?.shareData !== false ? shown?.text : undefined,
|
|
369
|
+
};
|
|
370
|
+
const payload = await fetchData(path, match, signal, retainFrom === undefined ? undefined : { from: retainFrom.path, retain }, purpose, hosts, read);
|
|
371
|
+
if (shown !== undefined && payload === read)
|
|
372
|
+
return shown;
|
|
373
|
+
const text = read.text;
|
|
286
374
|
if (!isRouteDataEnvelope(payload))
|
|
287
|
-
return { data: payload };
|
|
375
|
+
return { data: payload, text };
|
|
288
376
|
if (payload.status !== undefined) {
|
|
289
377
|
const terminalRouteId = options.statusRoutes?.[payload.status] ?? options.statusRoutes?.[404];
|
|
290
378
|
if (terminalRouteId === undefined) {
|
|
@@ -301,7 +389,27 @@ export function createClientRouter(options) {
|
|
|
301
389
|
data: payload.data,
|
|
302
390
|
layoutData,
|
|
303
391
|
serverComponents: payload.serverComponents,
|
|
392
|
+
live: payload.live,
|
|
304
393
|
...(payload.boundaries !== undefined ? { boundaries: payload.boundaries } : {}),
|
|
394
|
+
text,
|
|
395
|
+
};
|
|
396
|
+
};
|
|
397
|
+
// A reload with every part equal to what its path shows replaced by that part, so what the reload
|
|
398
|
+
// did not change keeps its identity on screen (`shareData`), unless the route exports
|
|
399
|
+
// `shareData = false`.
|
|
400
|
+
const shareShown = (routeId, shown, loaded) => {
|
|
401
|
+
if (loaded.terminalRouteId !== undefined || routeHooks[routeId]?.shareData === false)
|
|
402
|
+
return loaded;
|
|
403
|
+
// Parsed JSON is a tree; a client loader/custom transport can return cyclic or aliased data.
|
|
404
|
+
const plainJson = loaded.text !== undefined && routeHooks[routeId]?.clientLoader === undefined;
|
|
405
|
+
return {
|
|
406
|
+
...loaded,
|
|
407
|
+
data: shareData(shown.data, loaded.data, plainJson),
|
|
408
|
+
layoutData: shareData(shown.layoutData, loaded.layoutData, plainJson),
|
|
409
|
+
serverComponents: shareData(shown.serverComponents, loaded.serverComponents, plainJson),
|
|
410
|
+
...(loaded.boundaries === undefined
|
|
411
|
+
? {}
|
|
412
|
+
: { boundaries: shareData(shown.boundaries, loaded.boundaries, plainJson) }),
|
|
305
413
|
};
|
|
306
414
|
};
|
|
307
415
|
/**
|
|
@@ -349,7 +457,10 @@ export function createClientRouter(options) {
|
|
|
349
457
|
params: target.params,
|
|
350
458
|
signal,
|
|
351
459
|
body,
|
|
352
|
-
|
|
460
|
+
// An action posted to the target an intercept boundary holds belongs to that target.
|
|
461
|
+
serverLoader: async () => state.intercepted !== undefined && pathnameOf(action) === pathnameOf(state.path)
|
|
462
|
+
? state.boundaries?.[state.intercepted.boundary]?.data
|
|
463
|
+
: state.data,
|
|
353
464
|
});
|
|
354
465
|
if (result === undefined)
|
|
355
466
|
return { body };
|
|
@@ -377,19 +488,15 @@ export function createClientRouter(options) {
|
|
|
377
488
|
const loaded = await applyClientLoader(state.path, route, ac.signal, () => Promise.resolve(initial), initial);
|
|
378
489
|
if (mine !== generation)
|
|
379
490
|
return;
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
384
|
-
else {
|
|
385
|
-
state = {
|
|
386
|
-
...state,
|
|
387
|
-
...dataFields(loaded),
|
|
388
|
-
};
|
|
389
|
-
}
|
|
491
|
+
// No terminal status can arrive here: the server already rendered this route (a status page
|
|
492
|
+
// included) and its data is the only server result the client loader sees.
|
|
493
|
+
state = { ...state, ...dataFields(loaded) };
|
|
390
494
|
emit();
|
|
391
495
|
};
|
|
392
496
|
let state = options.initial;
|
|
497
|
+
// The page as last published from a load, with the response text it was read from. Every publish of
|
|
498
|
+
// page data replaces it, and a reload reuses it only for the same text, which says the same.
|
|
499
|
+
let shownPage;
|
|
393
500
|
const listeners = new Set();
|
|
394
501
|
let generation = 0;
|
|
395
502
|
// Snapshot listeners to defend against accidental un/subscribe during notification. The adapters
|
|
@@ -406,12 +513,33 @@ export function createClientRouter(options) {
|
|
|
406
513
|
// fresh: a link warmed as it scrolled into view can be clicked minutes later.
|
|
407
514
|
const MAX_PREFETCH = 10;
|
|
408
515
|
const PREFETCH_MAX_AGE_MS = 30_000;
|
|
516
|
+
// path -> [data, when it arrived, the identity epoch it was loaded under]
|
|
409
517
|
const prefetched = new Map();
|
|
410
|
-
|
|
518
|
+
// path -> the token of the prefetch loading it; a prefetch stores what it read only while its
|
|
519
|
+
// token is still there, so dropping the token discards a load that started before the drop.
|
|
520
|
+
const inflight = new Map();
|
|
521
|
+
// Read without the DOM types: this module also builds in tsconfigs that leave the DOM lib out.
|
|
522
|
+
const readEpoch = () => {
|
|
523
|
+
const cookie = Reflect.get(Reflect.get(globalThis, "document") ?? {}, "cookie");
|
|
524
|
+
return typeof cookie === "string" ? epochOf(cookie) : "";
|
|
525
|
+
};
|
|
411
526
|
const freshPrefetch = (path) => {
|
|
412
527
|
const held = prefetched.get(path);
|
|
413
|
-
return held !== undefined &&
|
|
528
|
+
return held !== undefined &&
|
|
529
|
+
Date.now() - held[1] < PREFETCH_MAX_AGE_MS &&
|
|
530
|
+
held[2] === readEpoch()
|
|
531
|
+
? held[0]
|
|
532
|
+
: undefined;
|
|
414
533
|
};
|
|
534
|
+
// Warmed data was loaded before a mutation or for the previous visitor, so either one drops all of
|
|
535
|
+
// it, along with what a prefetch still in flight read.
|
|
536
|
+
const dropPrefetched = () => {
|
|
537
|
+
inflight.clear();
|
|
538
|
+
prefetched.clear();
|
|
539
|
+
};
|
|
540
|
+
onIdentityChange(dropPrefetched);
|
|
541
|
+
if (typeof BroadcastChannel !== "undefined")
|
|
542
|
+
new BroadcastChannel(IDENTITY_CHANNEL).addEventListener("message", dropPrefetched);
|
|
415
543
|
// Keyed data cache (path → latest loader data + freshness). Written on every published data
|
|
416
544
|
// (navigate/submit), read by `invalidate` (+ targeted revalidation and fetchers in later F16
|
|
417
545
|
// increments). Bounded - evict the oldest-inserted past the cap (route data is small; this just
|
|
@@ -442,6 +570,8 @@ export function createClientRouter(options) {
|
|
|
442
570
|
// `X-Nifra-Revalidate` handling in `submit`.
|
|
443
571
|
const markStale = (paths) => {
|
|
444
572
|
for (const p of paths) {
|
|
573
|
+
prefetched.delete(p);
|
|
574
|
+
inflight.delete(p);
|
|
445
575
|
const entry = cache.get(p);
|
|
446
576
|
if (entry !== undefined) {
|
|
447
577
|
cache.set(p, {
|
|
@@ -477,6 +607,14 @@ export function createClientRouter(options) {
|
|
|
477
607
|
const target = match(path);
|
|
478
608
|
return target === null ? undefined : [path, target];
|
|
479
609
|
};
|
|
610
|
+
// Load the status route a terminal answer for `at` renders. One inside layouts needs those layouts'
|
|
611
|
+
// loaders, which only a document request runs, so the caller loads `at` as a document instead.
|
|
612
|
+
const loadTerminal = async (routeId, at) => {
|
|
613
|
+
await loadModule?.(routeId, at);
|
|
614
|
+
if (options.layoutStatusRoutes?.[routeId] === true) {
|
|
615
|
+
throw new Error(`[nifra/web] ${routeId} renders inside layouts; loading ${at} as a document`);
|
|
616
|
+
}
|
|
617
|
+
};
|
|
480
618
|
// Load `path` for the navigation or submit `mine`, following the redirects its loaders answer with
|
|
481
619
|
// while they stay in the app (20 at most, as a browser allows).
|
|
482
620
|
const loadFollowing = async (mine, path, target, signal, keepLayouts, usePrefetch) => {
|
|
@@ -494,12 +632,8 @@ export function createClientRouter(options) {
|
|
|
494
632
|
prefetched.delete(at);
|
|
495
633
|
try {
|
|
496
634
|
const loaded = await applyClientLoader(at, route, signal, () => loadRouteData(at, route, signal, keepLayouts), prefetchedData);
|
|
497
|
-
if (loaded.terminalRouteId !== undefined)
|
|
498
|
-
await
|
|
499
|
-
if (options.layoutStatusRoutes?.[loaded.terminalRouteId] === true) {
|
|
500
|
-
throw new Error(`[nifra/web] ${loaded.terminalRouteId} renders inside layouts; loading ${at} as a document`);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
635
|
+
if (loaded.terminalRouteId !== undefined)
|
|
636
|
+
await loadTerminal(loaded.terminalRouteId, at);
|
|
503
637
|
return [at, route, loaded];
|
|
504
638
|
}
|
|
505
639
|
catch (error) {
|
|
@@ -510,9 +644,91 @@ export function createClientRouter(options) {
|
|
|
510
644
|
}
|
|
511
645
|
}
|
|
512
646
|
};
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
|
|
647
|
+
// Reload what is on screen: the page under its own path when `page`, and, while an intercept boundary
|
|
648
|
+
// holds a target, that target under its URL when `target`. The part not reloaded is kept as shown.
|
|
649
|
+
// Returns the fields to publish, both parts together. `route` is set when the page's reload names
|
|
650
|
+
// another route - the status route it answered with, or, on a status page, its URL's own route
|
|
651
|
+
// answering again - which then replaces the page and any boundary over it, so data is only ever
|
|
652
|
+
// rendered by the route it was loaded for. A target that fails becomes its
|
|
653
|
+
// boundary's error state rather than failing the page, as a dynamic boundary's load does. Nothing is
|
|
654
|
+
// cached once `mine` is superseded: a newer action's reload owns the cache.
|
|
655
|
+
const reloadActive = async (signal, page, target, mine) => {
|
|
656
|
+
const pagePath = pagePathOf(state);
|
|
657
|
+
const intercepted = state.intercepted;
|
|
658
|
+
const targetPath = state.path;
|
|
659
|
+
// The route the page's URL matches: the one on screen, except on a status page.
|
|
660
|
+
const pageRoute = match(pagePath) ?? { routeId: state.routeId, params: state.params };
|
|
661
|
+
const rematched = pageRoute.routeId !== state.routeId;
|
|
662
|
+
const targetRoute = intercepted !== undefined && target ? match(targetPath) : null;
|
|
663
|
+
// The page's own route shares with what the page shows; a status page answered by its URL's route
|
|
664
|
+
// has nothing of that route on screen.
|
|
665
|
+
const shown = rematched ? undefined : (shownPage ?? state);
|
|
666
|
+
const shownTarget = { data: intercepted && state.boundaries?.[intercepted.boundary]?.data };
|
|
667
|
+
// A hole on screen that is a host stays one: it reloads as a fill.
|
|
668
|
+
const hosts = rematched
|
|
669
|
+
? []
|
|
670
|
+
: Object.values(state.boundaries ?? {})
|
|
671
|
+
.filter((held) => held.ppr === true)
|
|
672
|
+
.map((held) => held.name);
|
|
673
|
+
const [reloaded, targetOutcome] = await Promise.all([
|
|
674
|
+
page
|
|
675
|
+
? (async () => {
|
|
676
|
+
if (rematched)
|
|
677
|
+
await loadModule?.(pageRoute.routeId, pagePath);
|
|
678
|
+
const loaded = await applyClientLoader(pagePath, pageRoute, signal, () => loadRouteData(pagePath, pageRoute, signal, false, undefined, hosts.length === 0 ? undefined : hosts, shown));
|
|
679
|
+
return shown === undefined ? loaded : shareShown(pageRoute.routeId, shown, loaded);
|
|
680
|
+
})()
|
|
681
|
+
: undefined,
|
|
682
|
+
targetRoute === null
|
|
683
|
+
? undefined
|
|
684
|
+
: (async () => {
|
|
685
|
+
try {
|
|
686
|
+
await loadModule?.(targetRoute.routeId, targetPath);
|
|
687
|
+
const outcome = shareShown(targetRoute.routeId, shownTarget, await applyClientLoader(targetPath, targetRoute, signal, () => loadRouteData(targetPath, targetRoute, signal)));
|
|
688
|
+
if (mine === generation)
|
|
689
|
+
cachePut(targetPath, outcome);
|
|
690
|
+
return outcome;
|
|
691
|
+
}
|
|
692
|
+
catch {
|
|
693
|
+
return undefined;
|
|
694
|
+
}
|
|
695
|
+
})(),
|
|
696
|
+
]);
|
|
697
|
+
if (reloaded?.terminalRouteId !== undefined)
|
|
698
|
+
await loadTerminal(reloaded.terminalRouteId, pagePath);
|
|
699
|
+
if (reloaded !== undefined && mine === generation) {
|
|
700
|
+
cachePut(pagePath, reloaded);
|
|
701
|
+
shownPage = reloaded;
|
|
702
|
+
}
|
|
703
|
+
const fields = dataFields(reloaded ?? state);
|
|
704
|
+
if (reloaded?.terminalRouteId !== undefined)
|
|
705
|
+
return { fields, route: { routeId: reloaded.terminalRouteId, params: {} } };
|
|
706
|
+
if (reloaded !== undefined && rematched)
|
|
707
|
+
return { fields, route: pageRoute };
|
|
708
|
+
if (intercepted === undefined)
|
|
709
|
+
return { fields };
|
|
710
|
+
const held = state.boundaries?.[intercepted.boundary];
|
|
711
|
+
if (held === undefined)
|
|
712
|
+
return { fields };
|
|
713
|
+
if (targetRoute === null)
|
|
714
|
+
return {
|
|
715
|
+
fields: {
|
|
716
|
+
...fields,
|
|
717
|
+
boundaries: { ...(fields.boundaries ?? {}), [intercepted.boundary]: held },
|
|
718
|
+
},
|
|
719
|
+
};
|
|
720
|
+
// A reload keeps the result of the last action posted to the target, as the page keeps its own.
|
|
721
|
+
const boundaries = withActionData(holdTarget(fields.boundaries, held, targetOutcome), intercepted.boundary, held.actionData);
|
|
722
|
+
return { fields: { ...fields, ...(boundaries === undefined ? {} : { boundaries }) } };
|
|
723
|
+
};
|
|
724
|
+
// Refetch what is on screen + republish, single-flight via the shared `generation`. Shared by
|
|
725
|
+
// `invalidate` and targeted revalidation: `inScope` says which paths to refresh, and a page or held
|
|
726
|
+
// target outside it is kept. Rejects if the page's fetch fails (clearing its own `pending` first).
|
|
727
|
+
const refetchActive = async (inScope) => {
|
|
728
|
+
const page = inScope === undefined || inScope(pagePathOf(state));
|
|
729
|
+
const target = state.intercepted !== undefined && (inScope === undefined || inScope(state.path));
|
|
730
|
+
if (!page && !target)
|
|
731
|
+
return;
|
|
516
732
|
const mine = ++generation;
|
|
517
733
|
navAbort?.abort();
|
|
518
734
|
const ac = new AbortController();
|
|
@@ -520,16 +736,22 @@ export function createClientRouter(options) {
|
|
|
520
736
|
state = { ...state, pending: true };
|
|
521
737
|
emit();
|
|
522
738
|
try {
|
|
523
|
-
const
|
|
524
|
-
const loaded = await applyClientLoader(state.path, active, ac.signal, () => loadRouteData(state.path, active, ac.signal));
|
|
739
|
+
const { fields, route } = await reloadActive(ac.signal, page, target, mine);
|
|
525
740
|
if (mine !== generation)
|
|
526
741
|
return; // superseded - drop the stale result
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
742
|
+
// A terminal answer for the page (the session expired, the record is gone) replaces it, and any
|
|
743
|
+
// boundary over it, with its status route, as a navigation to this URL would.
|
|
744
|
+
state =
|
|
745
|
+
route === undefined
|
|
746
|
+
? { ...state, ...fields, pending: false }
|
|
747
|
+
: {
|
|
748
|
+
routeId: route.routeId,
|
|
749
|
+
params: route.params,
|
|
750
|
+
path: state.path,
|
|
751
|
+
...fields,
|
|
752
|
+
actionData: undefined,
|
|
753
|
+
pending: false,
|
|
754
|
+
};
|
|
533
755
|
emit();
|
|
534
756
|
}
|
|
535
757
|
catch (err) {
|
|
@@ -558,8 +780,8 @@ export function createClientRouter(options) {
|
|
|
558
780
|
// swallowed - a failed targeted refresh must not fail the mutation that triggered it.
|
|
559
781
|
const refreshMounted = async (paths, skipActive) => {
|
|
560
782
|
const jobs = [];
|
|
561
|
-
if (!skipActive
|
|
562
|
-
jobs.push(refetchActive().catch(() => { }));
|
|
783
|
+
if (!skipActive)
|
|
784
|
+
jobs.push(refetchActive((path) => paths.includes(path)).catch(() => { }));
|
|
563
785
|
for (const entry of fetchers.values())
|
|
564
786
|
jobs.push(entry.refreshIfShowing(paths));
|
|
565
787
|
await Promise.all(jobs);
|
|
@@ -569,6 +791,8 @@ export function createClientRouter(options) {
|
|
|
569
791
|
let fGeneration = 0;
|
|
570
792
|
let fAbort;
|
|
571
793
|
let loadedPath;
|
|
794
|
+
// `data` as loaded, with the response text it was read from.
|
|
795
|
+
let shown;
|
|
572
796
|
const fListeners = new Set();
|
|
573
797
|
const fEmit = () => {
|
|
574
798
|
if (fListeners.size > 0)
|
|
@@ -587,12 +811,16 @@ export function createClientRouter(options) {
|
|
|
587
811
|
fState = { ...fState, pending: true };
|
|
588
812
|
fEmit();
|
|
589
813
|
try {
|
|
590
|
-
|
|
814
|
+
// Loading the path again is a reload of what this fetcher shows.
|
|
815
|
+
const before = path === loadedPath ? shown : undefined;
|
|
816
|
+
const [, fresh] = await Promise.all([
|
|
591
817
|
loadModule?.(matched.routeId),
|
|
592
|
-
loadRouteData(path, matched, ac.signal),
|
|
818
|
+
loadRouteData(path, matched, ac.signal, false, undefined, undefined, before),
|
|
593
819
|
]);
|
|
594
820
|
if (mine !== fGeneration)
|
|
595
821
|
return; // a newer load/submit on THIS fetcher superseded us
|
|
822
|
+
const loaded = before === undefined ? fresh : shareShown(matched.routeId, before, fresh);
|
|
823
|
+
shown = loaded;
|
|
596
824
|
// Record the loaded path only on SUCCESS - a thrown or superseded load must not
|
|
597
825
|
// leave `loadedPath` pointing at a path this fetcher never actually showed, or a later
|
|
598
826
|
// `X-Nifra-Revalidate` for it would spuriously refetch onto unexpected data.
|
|
@@ -640,6 +868,7 @@ export function createClientRouter(options) {
|
|
|
640
868
|
body,
|
|
641
869
|
headers: { [DATA_HEADER]: "1" },
|
|
642
870
|
});
|
|
871
|
+
dropPrefetched();
|
|
643
872
|
if (!res.ok)
|
|
644
873
|
throw new Error(`[nifra/web] fetcher action failed (${res.status}): ${action}`);
|
|
645
874
|
const actionData = res.status === 204 ? undefined : await readResponseData(res, ac.signal);
|
|
@@ -687,19 +916,34 @@ export function createClientRouter(options) {
|
|
|
687
916
|
const matched = match(path);
|
|
688
917
|
if (matched === null)
|
|
689
918
|
return;
|
|
919
|
+
// The target an open boundary already shows (a `#fragment` entry inside it, a link to itself):
|
|
920
|
+
// reloading it would rebuild the boundary and drop its action result. One in its error state
|
|
921
|
+
// loads again, so a link can retry it.
|
|
922
|
+
if (state.intercepted !== undefined &&
|
|
923
|
+
state.boundaries?.[state.intercepted.boundary]?.status === "ready" &&
|
|
924
|
+
!state.pending &&
|
|
925
|
+
path === state.path) {
|
|
926
|
+
emit();
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
690
929
|
// Client-only search fast path: same route + same pathname, and every changed search key is
|
|
691
930
|
// declared client-only. Publish the new URL (so `useSearch`/`useLocation` update) WITHOUT a data
|
|
692
931
|
// fetch - the loader's inputs did not change. Any loader-affecting key change falls through to the
|
|
693
932
|
// normal match → fetch → publish below, so data is never stale. Abort an in-flight fetch first: a
|
|
694
933
|
// rapid `?tab` after a real nav must not let the older fetch overwrite this synchronous update.
|
|
695
|
-
if (
|
|
934
|
+
if (state.intercepted === undefined &&
|
|
935
|
+
matched.routeId === state.routeId &&
|
|
696
936
|
pathnameOf(path) === pathnameOf(state.path) &&
|
|
697
937
|
isClientOnlySearchChange(rawSearchOf(state.path), rawSearchOf(path), searchClientKeys[state.routeId] ?? [])) {
|
|
698
|
-
++generation; // supersede any in-flight navigation so its late result is dropped
|
|
938
|
+
const mine = ++generation; // supersede any in-flight navigation so its late result is dropped
|
|
699
939
|
navAbort?.abort();
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
940
|
+
await present(() => {
|
|
941
|
+
if (mine !== generation)
|
|
942
|
+
return;
|
|
943
|
+
cachePut(path, dataFields(state));
|
|
944
|
+
state = { ...state, path, pending: false, pendingPath: undefined };
|
|
945
|
+
emit();
|
|
946
|
+
});
|
|
703
947
|
return;
|
|
704
948
|
}
|
|
705
949
|
const mine = ++generation;
|
|
@@ -713,57 +957,71 @@ export function createClientRouter(options) {
|
|
|
713
957
|
if (mine !== generation)
|
|
714
958
|
return; // a newer navigation superseded this one - drop the stale result
|
|
715
959
|
cachePut(to, loaded); // keep the keyed cache coherent with what we publish
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
// The
|
|
721
|
-
//
|
|
722
|
-
|
|
723
|
-
|
|
960
|
+
// The swap can wait a frame for the presenter, and a newer navigation started meanwhile owns it.
|
|
961
|
+
await present(() => {
|
|
962
|
+
if (mine !== generation)
|
|
963
|
+
return;
|
|
964
|
+
// The page's own URL is never intercepted: navigating there (Back out of a boundary, a link
|
|
965
|
+
// to itself) shows the page, even when a boundary's pattern matches it.
|
|
966
|
+
const pagePath = pagePathOf(state);
|
|
967
|
+
const interception = loaded.terminalRouteId === undefined && pathnameOf(to) !== pathnameOf(pagePath)
|
|
968
|
+
? interceptionFor(state.routeId, to, target)
|
|
969
|
+
: undefined;
|
|
970
|
+
if (interception !== undefined) {
|
|
971
|
+
// The target's ordinary data request has already completed (including its server authz and
|
|
972
|
+
// optional client wrapper). Only the presentation route is retained; the current page stays
|
|
973
|
+
// mounted underneath the named slot. A target that returned a terminal status never reaches
|
|
974
|
+
// this branch, so an unauthorized/missing target cannot become a modal.
|
|
975
|
+
state = {
|
|
976
|
+
...state,
|
|
977
|
+
path: to,
|
|
978
|
+
boundaries: holdTarget(state.boundaries, interception, loaded),
|
|
979
|
+
intercepted: { boundary: interception.name, pagePath },
|
|
980
|
+
pending: false,
|
|
981
|
+
pendingPath: undefined,
|
|
982
|
+
};
|
|
983
|
+
emit();
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
724
986
|
state = {
|
|
725
|
-
|
|
987
|
+
routeId: loaded.terminalRouteId ?? target.routeId,
|
|
988
|
+
params: loaded.terminalRouteId === undefined ? target.params : {},
|
|
726
989
|
path: to,
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
[interception.name]: {
|
|
730
|
-
name: interception.name,
|
|
731
|
-
mode: interception.mode,
|
|
732
|
-
status: "ready",
|
|
733
|
-
data: loaded.data,
|
|
734
|
-
...(interception.errorId === undefined ? {} : { errorId: interception.errorId }),
|
|
735
|
-
},
|
|
736
|
-
},
|
|
737
|
-
actionData: undefined,
|
|
990
|
+
...dataFields(loaded),
|
|
991
|
+
actionData: undefined, // a fresh navigation has no action result
|
|
738
992
|
pending: false,
|
|
739
|
-
pendingPath: undefined,
|
|
740
993
|
};
|
|
994
|
+
shownPage = loaded;
|
|
741
995
|
emit();
|
|
742
|
-
|
|
743
|
-
}
|
|
744
|
-
state = {
|
|
745
|
-
routeId: loaded.terminalRouteId ?? target.routeId,
|
|
746
|
-
params: loaded.terminalRouteId === undefined ? target.params : {},
|
|
747
|
-
path: to,
|
|
748
|
-
...dataFields(loaded),
|
|
749
|
-
actionData: undefined, // a fresh navigation has no action result
|
|
750
|
-
pending: false,
|
|
751
|
-
};
|
|
752
|
-
emit();
|
|
996
|
+
});
|
|
753
997
|
}
|
|
754
998
|
catch (err) {
|
|
755
|
-
//
|
|
756
|
-
//
|
|
757
|
-
if (mine
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
999
|
+
// A newer navigation or submit owns the page, and aborting this one's request is how it took
|
|
1000
|
+
// over: as with a submit, the superseded navigation settles quietly.
|
|
1001
|
+
if (mine !== generation)
|
|
1002
|
+
return;
|
|
1003
|
+
// Clear our pending flag + target and rethrow - the caller decides how to recover (the history
|
|
1004
|
+
// layer falls back to a full-page navigation).
|
|
1005
|
+
state = { ...state, pending: false, pendingPath: undefined };
|
|
1006
|
+
emit();
|
|
761
1007
|
throw err;
|
|
762
1008
|
}
|
|
763
1009
|
},
|
|
764
1010
|
submit: async (action, body, opts) => {
|
|
765
1011
|
const mine = ++generation;
|
|
766
1012
|
const previousActionData = state.actionData;
|
|
1013
|
+
// An action posted to the target an intercept boundary holds answers that boundary, not the page.
|
|
1014
|
+
const actionBoundary = state.intercepted !== undefined && pathnameOf(action) === pathnameOf(state.path)
|
|
1015
|
+
? state.intercepted.boundary
|
|
1016
|
+
: undefined;
|
|
1017
|
+
const previousBoundaryActionData = actionBoundary === undefined ? undefined : state.boundaries?.[actionBoundary]?.actionData;
|
|
1018
|
+
// The state with `value` as the action's result, on whichever owns the action.
|
|
1019
|
+
const answered = (current, value) => {
|
|
1020
|
+
if (actionBoundary === undefined)
|
|
1021
|
+
return { actionData: value };
|
|
1022
|
+
const boundaries = withActionData(current.boundaries, actionBoundary, value);
|
|
1023
|
+
return boundaries === undefined ? {} : { boundaries };
|
|
1024
|
+
};
|
|
767
1025
|
// A superseding navigation/submit aborts this submit's FOLLOW-UP reads (revalidation / redirect
|
|
768
1026
|
// fetch + their NDJSON drains) - not the mutation POST itself, which should complete.
|
|
769
1027
|
// Wire into `navAbort` like `navigate`/`refetchActive`, so a later nav cancels the in-flight read.
|
|
@@ -784,7 +1042,7 @@ export function createClientRouter(options) {
|
|
|
784
1042
|
try {
|
|
785
1043
|
const prepared = await prepareClientAction(action, body, ac.signal);
|
|
786
1044
|
if (Object.hasOwn(prepared, "optimisticData")) {
|
|
787
|
-
state = { ...state,
|
|
1045
|
+
state = { ...state, ...answered(state, prepared.optimisticData) };
|
|
788
1046
|
emit();
|
|
789
1047
|
}
|
|
790
1048
|
const res = await fetch(appUrl(action), {
|
|
@@ -794,6 +1052,7 @@ export function createClientRouter(options) {
|
|
|
794
1052
|
});
|
|
795
1053
|
// Whatever the status, the server took the POST: the action may have run before it failed.
|
|
796
1054
|
posted = true;
|
|
1055
|
+
dropPrefetched();
|
|
797
1056
|
if (!res.ok)
|
|
798
1057
|
throw new Error(`[nifra/web] action failed (${res.status}): ${action}`);
|
|
799
1058
|
const redirectTo = res.headers.get(REDIRECT_HEADER);
|
|
@@ -818,6 +1077,7 @@ export function createClientRouter(options) {
|
|
|
818
1077
|
actionData: undefined,
|
|
819
1078
|
pending: false,
|
|
820
1079
|
};
|
|
1080
|
+
shownPage = loaded;
|
|
821
1081
|
emit();
|
|
822
1082
|
return;
|
|
823
1083
|
}
|
|
@@ -828,31 +1088,43 @@ export function createClientRouter(options) {
|
|
|
828
1088
|
// Routes the action declared changed (via the `revalidate()` helper → `X-Nifra-Revalidate`),
|
|
829
1089
|
// validated against the manifest.
|
|
830
1090
|
const changed = parseRevalidate(res.headers.get(REVALIDATE_HEADER));
|
|
831
|
-
// Revalidate
|
|
832
|
-
// out (`revalidate: false`). A
|
|
833
|
-
//
|
|
834
|
-
|
|
835
|
-
const
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
: await applyClientLoader(state.path, active, ac.signal, () => loadRouteData(state.path, active, ac.signal));
|
|
1091
|
+
// Revalidate what is on screen so the mutation is reflected - the page, and a target an
|
|
1092
|
+
// intercept boundary holds - unless the caller opted out (`revalidate: false`). A
|
|
1093
|
+
// server-declared revalidate of a path on screen overrides the opt-out (the server says it
|
|
1094
|
+
// changed, so stale data would be wrong). Default is to revalidate.
|
|
1095
|
+
const pagePath = pagePathOf(state);
|
|
1096
|
+
const reloadPage = opts?.revalidate !== false || changed.includes(pagePath);
|
|
1097
|
+
const reloadTarget = state.intercepted !== undefined &&
|
|
1098
|
+
(opts?.revalidate !== false || changed.includes(state.path));
|
|
1099
|
+
const { fields, route } = await reloadActive(ac.signal, reloadPage, reloadTarget, mine);
|
|
841
1100
|
if (mine !== generation)
|
|
842
1101
|
return;
|
|
843
|
-
cachePut(state.path, loaded); // the revalidated (or kept) data is now the cache's truth
|
|
844
1102
|
// Mark the OTHER changed routes stale so the next access refetches.
|
|
845
|
-
markStale(changed.filter((p) => p !== state.path));
|
|
1103
|
+
markStale(changed.filter((p) => p !== pagePath && (!reloadTarget || p !== state.path)));
|
|
846
1104
|
// Reconcile: publish the revalidated data + actionData; omit `submission` (the optimistic
|
|
847
|
-
// window is over - the real data now drives the view).
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1105
|
+
// window is over - the real data now drives the view). A reload that names another route (a
|
|
1106
|
+
// terminal answer for the page) renders it in the page's place, closing any boundary, and the
|
|
1107
|
+
// action's result goes with the page.
|
|
1108
|
+
state =
|
|
1109
|
+
route === undefined
|
|
1110
|
+
? {
|
|
1111
|
+
routeId: state.routeId,
|
|
1112
|
+
params: state.params,
|
|
1113
|
+
path: state.path,
|
|
1114
|
+
...fields,
|
|
1115
|
+
actionData: previousActionData,
|
|
1116
|
+
...answered(fields, actionData),
|
|
1117
|
+
pending: false,
|
|
1118
|
+
...(state.intercepted === undefined ? {} : { intercepted: state.intercepted }),
|
|
1119
|
+
}
|
|
1120
|
+
: {
|
|
1121
|
+
routeId: route.routeId,
|
|
1122
|
+
params: route.params,
|
|
1123
|
+
path: state.path,
|
|
1124
|
+
...fields,
|
|
1125
|
+
actionData: undefined,
|
|
1126
|
+
pending: false,
|
|
1127
|
+
};
|
|
856
1128
|
emit();
|
|
857
1129
|
// Refresh any mounted fetcher showing one of the changed routes (the active route was just
|
|
858
1130
|
// revalidated inline above, so skip it here). Best-effort - never rejects the submit.
|
|
@@ -868,7 +1140,11 @@ export function createClientRouter(options) {
|
|
|
868
1140
|
path: state.path,
|
|
869
1141
|
...dataFields(state),
|
|
870
1142
|
actionData: previousActionData,
|
|
1143
|
+
...(actionBoundary === undefined
|
|
1144
|
+
? {}
|
|
1145
|
+
: answered(dataFields(state), previousBoundaryActionData)),
|
|
871
1146
|
pending: false,
|
|
1147
|
+
...(state.intercepted === undefined ? {} : { intercepted: state.intercepted }),
|
|
872
1148
|
};
|
|
873
1149
|
emit();
|
|
874
1150
|
}
|
|
@@ -876,21 +1152,24 @@ export function createClientRouter(options) {
|
|
|
876
1152
|
throw err;
|
|
877
1153
|
if (mine !== generation)
|
|
878
1154
|
return; // a newer navigation or submit owns the page
|
|
879
|
-
|
|
1155
|
+
// Otherwise show the route that owns the action, as a native post to it would.
|
|
1156
|
+
const owner = actionBoundary === undefined ? pagePathOf(state) : state.path;
|
|
1157
|
+
throw redirectOf(err) === undefined ? redirected(landing ?? owner, err) : err;
|
|
880
1158
|
}
|
|
881
1159
|
},
|
|
882
1160
|
hydrate,
|
|
883
1161
|
invalidate: async (paths) => {
|
|
884
1162
|
// Mark targeted cache entries stale (all entries when no `paths`) - unmounted ones refetch
|
|
885
|
-
// lazily on next access.
|
|
1163
|
+
// lazily on next access. A warmed prefetch of a target is dropped: it would show the old data.
|
|
886
1164
|
const targets = paths ?? [...cache.keys()];
|
|
887
1165
|
markStale(targets);
|
|
1166
|
+
if (paths === undefined)
|
|
1167
|
+
dropPrefetched();
|
|
888
1168
|
// Refresh mounted readers now: the active route (when in scope - an explicit list including it,
|
|
889
1169
|
// or an invalidate-all) + any fetcher showing a targeted path. The active refetch rejects on
|
|
890
1170
|
// failure (the caller asked to refresh); fetcher refreshes are best-effort (swallowed).
|
|
891
1171
|
const jobs = [];
|
|
892
|
-
|
|
893
|
-
jobs.push(refetchActive());
|
|
1172
|
+
jobs.push(refetchActive(paths === undefined ? undefined : (path) => paths.includes(path)));
|
|
894
1173
|
for (const entry of fetchers.values())
|
|
895
1174
|
jobs.push(entry.refreshIfShowing(targets));
|
|
896
1175
|
await Promise.all(jobs);
|
|
@@ -901,13 +1180,15 @@ export function createClientRouter(options) {
|
|
|
901
1180
|
const matched = match(path);
|
|
902
1181
|
if (matched === null)
|
|
903
1182
|
return;
|
|
904
|
-
|
|
1183
|
+
const token = {};
|
|
1184
|
+
inflight.set(path, token);
|
|
1185
|
+
const epoch = readEpoch();
|
|
905
1186
|
try {
|
|
906
1187
|
// Unwrapped here, so a prefetched entry is the same shape `navigate` builds state from -
|
|
907
1188
|
// otherwise a prefetch hit would publish an envelope where a miss publishes loader data.
|
|
908
1189
|
const [, loaded] = await Promise.all([
|
|
909
1190
|
loadModule?.(matched.routeId, path),
|
|
910
|
-
loadRouteData(path, matched),
|
|
1191
|
+
loadRouteData(path, matched, undefined, false, "prefetch"),
|
|
911
1192
|
]);
|
|
912
1193
|
prefetched.delete(path); // a stale entry re-enters as the newest
|
|
913
1194
|
if (prefetched.size >= MAX_PREFETCH) {
|
|
@@ -915,14 +1196,20 @@ export function createClientRouter(options) {
|
|
|
915
1196
|
if (oldest !== undefined)
|
|
916
1197
|
prefetched.delete(oldest);
|
|
917
1198
|
}
|
|
918
|
-
|
|
919
|
-
|
|
1199
|
+
// A prefetched hole is an empty host nothing fills; the navigation fetches it, cancellably.
|
|
1200
|
+
const holes = Object.values(loaded.boundaries ?? {}).some((state) => state.ppr === true);
|
|
1201
|
+
if (loaded.serverComponents === undefined &&
|
|
1202
|
+
!holes &&
|
|
1203
|
+
inflight.get(path) === token &&
|
|
1204
|
+
epoch === readEpoch())
|
|
1205
|
+
prefetched.set(path, [loaded, Date.now(), epoch]);
|
|
920
1206
|
}
|
|
921
1207
|
catch {
|
|
922
1208
|
// Best-effort: a failed prefetch just means the eventual navigate fetches normally.
|
|
923
1209
|
}
|
|
924
1210
|
finally {
|
|
925
|
-
inflight.
|
|
1211
|
+
if (inflight.get(path) === token)
|
|
1212
|
+
inflight.delete(path);
|
|
926
1213
|
}
|
|
927
1214
|
},
|
|
928
1215
|
fetcher: (key) => {
|