@nifrajs/web 5.5.1 → 5.6.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 +154 -0
- 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 +9 -0
- package/dist/build.js.map +1 -1
- package/dist/client.d.ts +2 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +140 -12
- 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 +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- 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 +65 -8
- 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 +179 -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 +96 -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 +14 -1
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +415 -31
- package/dist/internal/page-execution.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 +32 -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 +1407 -0
- package/dist/internal/ppr/server.js.map +1 -0
- package/dist/internal/ppr/shell-store.d.ts +38 -0
- package/dist/internal/ppr/shell-store.d.ts.map +1 -0
- package/dist/internal/ppr/shell-store.js +90 -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/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 +28 -1
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +83 -18
- 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/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 +25 -0
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +78 -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 +1069 -0
- package/dist/keep-alive.js.map +1 -0
- package/dist/live/bus.d.ts +56 -0
- package/dist/live/bus.d.ts.map +1 -0
- package/dist/live/bus.js +32 -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 +181 -0
- package/dist/live/hub.d.ts.map +1 -0
- package/dist/live/hub.js +539 -0
- package/dist/live/hub.js.map +1 -0
- package/dist/live/keyring.d.ts +87 -0
- package/dist/live/keyring.d.ts.map +1 -0
- package/dist/live/keyring.js +260 -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 +840 -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 +48 -0
- package/dist/live-redis.d.ts.map +1 -0
- package/dist/live-redis.js +201 -0
- package/dist/live-redis.js.map +1 -0
- package/dist/live.d.ts +94 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +869 -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 +30 -0
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +5 -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 +374 -99
- 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/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 +86 -28
- 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/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 +19 -1
- package/src/client.ts +147 -10
- 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 +19 -0
- package/src/internal/cdn/cloudflare.ts +2 -1
- package/src/internal/certified-components.ts +1 -1
- package/src/internal/codegen.ts +87 -8
- package/src/internal/dev-websocket.ts +230 -0
- package/src/internal/hole-runtime.ts +286 -0
- package/src/internal/identity-epoch.ts +235 -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 +106 -0
- package/src/internal/live-queries.ts +60 -0
- package/src/internal/page-events.ts +27 -0
- package/src/internal/page-execution.ts +579 -40
- 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 +36 -0
- package/src/internal/ppr/server.ts +1764 -0
- package/src/internal/ppr/shell-store.ts +139 -0
- package/src/internal/prerendered-pages.ts +7 -4
- 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 +149 -22
- 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/store-signature.ts +149 -0
- package/src/internal/vite-plugin-compat.ts +2 -2
- package/src/internal/web-app.ts +121 -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 +1315 -0
- package/src/live/bus.ts +77 -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 +615 -0
- package/src/live/keyring.ts +379 -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 +973 -0
- package/src/live-hub.ts +658 -0
- package/src/live-redis.ts +259 -0
- package/src/live.ts +1096 -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 +35 -0
- package/src/route-types.ts +3 -1
- package/src/router.ts +494 -101
- package/src/server-components/build.ts +9 -3
- package/src/server-components/client.ts +89 -5
- 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 +112 -30
- package/src/server-components/state.ts +66 -44
- package/src/server-components/types.ts +16 -0
- 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
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shells and shared hole frames in a store shared by processes (Workers KV, Redis): a second level
|
|
3
|
+
* behind the in-process map. Whoever can write the store could put script into every visitor's page,
|
|
4
|
+
* so each entry is signed with the app's secret over its key, deployment and contents; anything that
|
|
5
|
+
* fails the check is a miss.
|
|
6
|
+
*/
|
|
7
|
+
import type { CachedResponse, CacheStore } from "../../isr.ts"
|
|
8
|
+
import { storeSigner } from "../store-signature.ts"
|
|
9
|
+
|
|
10
|
+
/** A shell or a shared hole's frame as the shared store holds it: everything a process needs to
|
|
11
|
+
* serve it. */
|
|
12
|
+
export interface StoredEntry {
|
|
13
|
+
readonly routeId: string
|
|
14
|
+
/** A shell's document up to the request script's end, with room for the token at `tokenAt`
|
|
15
|
+
* (bytes); or a hole's frame. */
|
|
16
|
+
readonly html: string
|
|
17
|
+
readonly tokenAt: number
|
|
18
|
+
readonly data: unknown
|
|
19
|
+
readonly builtAt: number
|
|
20
|
+
readonly tags: readonly string[]
|
|
21
|
+
/** A shared hole's live keys. */
|
|
22
|
+
readonly keys?: readonly string[]
|
|
23
|
+
/** The shell loader's `notFound()`. */
|
|
24
|
+
readonly notFound: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SharedShellStore {
|
|
28
|
+
get(key: string): Promise<StoredEntry | undefined>
|
|
29
|
+
/** Skipped by the store when the key or one of its tags was purged after `fence`. */
|
|
30
|
+
set(
|
|
31
|
+
key: string,
|
|
32
|
+
entry: StoredEntry,
|
|
33
|
+
keepSeconds: number,
|
|
34
|
+
fence: number | undefined,
|
|
35
|
+
): Promise<void>
|
|
36
|
+
fence(): Promise<number | undefined>
|
|
37
|
+
/** Absent when the store cannot drop entries by tag. */
|
|
38
|
+
readonly invalidateTag: ((tag: string) => Promise<void>) | undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const MAC_HEADER = "x-nifra-ppr-mac"
|
|
42
|
+
|
|
43
|
+
const isStrings = (value: unknown): value is readonly string[] =>
|
|
44
|
+
Array.isArray(value) && value.every((item) => typeof item === "string")
|
|
45
|
+
|
|
46
|
+
const field = (value: object, name: string): unknown => Reflect.get(value, name)
|
|
47
|
+
|
|
48
|
+
const isStoredEntry = (value: unknown): value is StoredEntry => {
|
|
49
|
+
if (typeof value !== "object" || value === null) return false
|
|
50
|
+
const tokenAt = field(value, "tokenAt")
|
|
51
|
+
const keys = field(value, "keys")
|
|
52
|
+
return (
|
|
53
|
+
typeof field(value, "routeId") === "string" &&
|
|
54
|
+
typeof field(value, "html") === "string" &&
|
|
55
|
+
typeof tokenAt === "number" &&
|
|
56
|
+
Number.isSafeInteger(tokenAt) &&
|
|
57
|
+
typeof field(value, "builtAt") === "number" &&
|
|
58
|
+
isStrings(field(value, "tags")) &&
|
|
59
|
+
(keys === undefined || isStrings(keys)) &&
|
|
60
|
+
typeof field(value, "notFound") === "boolean"
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function sharedShellStore(
|
|
65
|
+
store: CacheStore,
|
|
66
|
+
secret: string,
|
|
67
|
+
deployment: string,
|
|
68
|
+
): SharedShellStore {
|
|
69
|
+
const signer = storeSigner(secret)
|
|
70
|
+
const invalidateTag = store.invalidateTag
|
|
71
|
+
return {
|
|
72
|
+
async get(entryKey) {
|
|
73
|
+
const cached = await store.get(entryKey)
|
|
74
|
+
if (cached === undefined) return undefined
|
|
75
|
+
// One part, where ISR signs two: under a shared secret neither entry verifies as the other.
|
|
76
|
+
if (!(await signer.verify([cached.body], cached.headers[MAC_HEADER]))) {
|
|
77
|
+
console.error("[nifra/web] ppr: a shared store entry failed its signature check", {
|
|
78
|
+
key: entryKey,
|
|
79
|
+
})
|
|
80
|
+
return undefined
|
|
81
|
+
}
|
|
82
|
+
let payload: unknown
|
|
83
|
+
try {
|
|
84
|
+
payload = JSON.parse(cached.body)
|
|
85
|
+
} catch {
|
|
86
|
+
return undefined
|
|
87
|
+
}
|
|
88
|
+
if (typeof payload !== "object" || payload === null) return undefined
|
|
89
|
+
// The key and deployment are signed too, so an entry copied to another key or kept from
|
|
90
|
+
// another deployment never verifies as this one.
|
|
91
|
+
if (
|
|
92
|
+
field(payload, "v") !== 1 ||
|
|
93
|
+
field(payload, "key") !== entryKey ||
|
|
94
|
+
field(payload, "deployment") !== deployment
|
|
95
|
+
) {
|
|
96
|
+
return undefined
|
|
97
|
+
}
|
|
98
|
+
const entry = field(payload, "entry")
|
|
99
|
+
return isStoredEntry(entry) ? entry : undefined
|
|
100
|
+
},
|
|
101
|
+
async set(entryKey, entry, keepSeconds, fence) {
|
|
102
|
+
const body = JSON.stringify({ v: 1, key: entryKey, deployment, entry })
|
|
103
|
+
const mac = await signer.sign([body])
|
|
104
|
+
const value: CachedResponse = {
|
|
105
|
+
body,
|
|
106
|
+
status: entry.notFound ? 404 : 200,
|
|
107
|
+
headers: { [MAC_HEADER]: mac },
|
|
108
|
+
storedAt: entry.builtAt,
|
|
109
|
+
revalidate: keepSeconds * 1000,
|
|
110
|
+
tags: entry.tags,
|
|
111
|
+
fence,
|
|
112
|
+
}
|
|
113
|
+
await store.set(entryKey, value)
|
|
114
|
+
},
|
|
115
|
+
async fence() {
|
|
116
|
+
return store.fence === undefined ? undefined : await store.fence()
|
|
117
|
+
},
|
|
118
|
+
invalidateTag:
|
|
119
|
+
invalidateTag === undefined ? undefined : (tag) => invalidateTag.call(store, tag),
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A short, stable digest of `text` (FNV-1a, 64 bits as hex). It names a deployment inside cache keys;
|
|
125
|
+
* nothing an attacker controls goes into it, so it needs no collision resistance.
|
|
126
|
+
*/
|
|
127
|
+
export function digest(text: string): string {
|
|
128
|
+
let high = 0xcbf29ce4
|
|
129
|
+
let low = 0x84222325
|
|
130
|
+
for (let at = 0; at < text.length; at++) {
|
|
131
|
+
low = (low ^ text.charCodeAt(at)) >>> 0
|
|
132
|
+
// Multiply the 64-bit value by the FNV prime 0x100000001b3 in two 32-bit halves.
|
|
133
|
+
const lowProduct = low * 0x1b3
|
|
134
|
+
const carry = Math.floor(lowProduct / 0x100000000)
|
|
135
|
+
high = (high * 0x1b3 + low * 0x100 + carry) >>> 0
|
|
136
|
+
low = lowProduct >>> 0
|
|
137
|
+
}
|
|
138
|
+
return high.toString(16).padStart(8, "0") + low.toString(16).padStart(8, "0")
|
|
139
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { pathnameOf } from "@nifrajs/core/server"
|
|
2
2
|
import { isDraftEnabled } from "../draft.ts"
|
|
3
|
+
import { SWAP_HEADER } from "../render-seam.ts"
|
|
3
4
|
import { DATA_HEADER } from "../router.ts"
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -21,10 +22,11 @@ const REPLAYED = [
|
|
|
21
22
|
"content-language",
|
|
22
23
|
"content-security-policy",
|
|
23
24
|
"content-security-policy-report-only",
|
|
25
|
+
"vary",
|
|
24
26
|
]
|
|
25
|
-
// Vary values a file still answers correctly: compression is negotiated by the host, and a data
|
|
26
|
-
// never reaches the file.
|
|
27
|
-
const STATIC_VARY = new Set(["accept-encoding", DATA_HEADER])
|
|
27
|
+
// Vary values a file still answers correctly: compression is negotiated by the host, and a data or
|
|
28
|
+
// slot-swap request never reaches the file.
|
|
29
|
+
const STATIC_VARY = new Set(["accept-encoding", DATA_HEADER, SWAP_HEADER])
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
32
|
* The headers a self-hosted server replays with a prerendered page, or `undefined` when its build-time
|
|
@@ -84,7 +86,7 @@ export function servablePrerenderedPages(
|
|
|
84
86
|
/**
|
|
85
87
|
* Wrap a page route's GET handler so a plain document request for a prerendered path is answered from
|
|
86
88
|
* its file. Anything a file cannot answer the way a render would renders on demand: a query string, a
|
|
87
|
-
* navigation data or
|
|
89
|
+
* navigation data, component or slot-swap request, a draft-mode visitor, or a file the reader cannot return.
|
|
88
90
|
*/
|
|
89
91
|
export function servePrerenderedPages(
|
|
90
92
|
prerendered: PrerenderedPages,
|
|
@@ -102,6 +104,7 @@ export function servePrerenderedPages(
|
|
|
102
104
|
req.url.includes("?") ||
|
|
103
105
|
req.headers.has(DATA_HEADER) ||
|
|
104
106
|
req.headers.has("x-nifra-component") ||
|
|
107
|
+
req.headers.has(SWAP_HEADER) ||
|
|
105
108
|
(draftSecret !== undefined && (await isDraftEnabled(req, draftSecret)))
|
|
106
109
|
) {
|
|
107
110
|
return render(c)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The digest a live load gives each granted key for query hooks: HMAC-SHA-256 under a salt fresh per
|
|
3
|
+
* load, the first 16 bytes in base64url. A page's queries name the keys they read and find the load's
|
|
4
|
+
* tag for each one without the load ever sending key text. Shared by the server and the browser.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const TEXT = new TextEncoder()
|
|
8
|
+
const DIGEST_BYTES = 16
|
|
9
|
+
const SALT_BYTES = 16
|
|
10
|
+
|
|
11
|
+
export function querySalt(): string {
|
|
12
|
+
return base64url(crypto.getRandomValues(new Uint8Array(SALT_BYTES)))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Digests keys under one salt, importing the salt once. */
|
|
16
|
+
export async function queryDigester(salt: string): Promise<(key: string) => Promise<string>> {
|
|
17
|
+
const secret = await crypto.subtle.importKey(
|
|
18
|
+
"raw",
|
|
19
|
+
TEXT.encode(salt),
|
|
20
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
21
|
+
false,
|
|
22
|
+
["sign"],
|
|
23
|
+
)
|
|
24
|
+
return async (key) =>
|
|
25
|
+
base64url(
|
|
26
|
+
new Uint8Array(await crypto.subtle.sign("HMAC", secret, TEXT.encode(key)), 0, DIGEST_BYTES),
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function base64url(bytes: Uint8Array): string {
|
|
31
|
+
let binary = ""
|
|
32
|
+
for (const byte of bytes) binary += String.fromCharCode(byte)
|
|
33
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
|
|
34
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { QueryClient, QueryState } from "../query.ts"
|
|
1
|
+
import type { QueryClient, QueryHandle, QueryState } from "../query.ts"
|
|
2
2
|
import { createQueryClient } from "../query.ts"
|
|
3
3
|
|
|
4
4
|
/** Stable idle state shared by every framework query binding. */
|
|
@@ -23,6 +23,19 @@ export function getQueryClientSingleton(): QueryClient | undefined {
|
|
|
23
23
|
return singleton
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export const noop = (): void => {}
|
|
27
|
+
|
|
28
|
+
/** Stable no-op subscription for a binding with no client (server, pre-hydration). */
|
|
29
|
+
export const noopSubscribe = (): (() => void) => noop
|
|
30
|
+
|
|
31
|
+
/** A binding's fetch on mount. A failed fetch shows as the query's `error`, so its promise is let go. */
|
|
32
|
+
export const fetchOnMount = (handle: QueryHandle<unknown> | undefined): (() => void) =>
|
|
33
|
+
handle === undefined
|
|
34
|
+
? noop
|
|
35
|
+
: () => {
|
|
36
|
+
handle.fetch().catch(noop)
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
/** Create the refetch fallback used when a binding has no client-side query cache. */
|
|
27
40
|
export function createNoClientRefetch<T>(tag: string, operation: string): () => Promise<T> {
|
|
28
41
|
return async () => {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a query client tells the live client. `@nifrajs/web/live-client` installs the seam; a page
|
|
3
|
+
* without live refresh never does, so its queries keep no live state and ship none of it.
|
|
4
|
+
*/
|
|
5
|
+
import type { QueryClient } from "../query.ts"
|
|
6
|
+
|
|
7
|
+
export interface LiveQuery {
|
|
8
|
+
/** The live keys its data depends on, as the page's loaders declare them. */
|
|
9
|
+
keys(): readonly string[]
|
|
10
|
+
/** The {@link QuerySeam.tick} its data was asked for at: the start of the fetch in flight, else of
|
|
11
|
+
* the fetch that produced it. */
|
|
12
|
+
tick(): number
|
|
13
|
+
/** Drop its data's freshness and refetch, superseding a fetch in flight. */
|
|
14
|
+
refresh(): void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface QuerySeam {
|
|
18
|
+
/** The live tick now, read as each fetch starts. */
|
|
19
|
+
tick(): number
|
|
20
|
+
/** A query was mounted or unmounted; only a mounted one with live keys is watched. */
|
|
21
|
+
track(query: LiveQuery, mounted: boolean): void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// On the global, so a query engine and a live client from two copies of the package still meet.
|
|
25
|
+
const SEAM = Symbol.for("nifra.web.querySeam")
|
|
26
|
+
const CLIENTS = Symbol.for("nifra.web.queryClients")
|
|
27
|
+
|
|
28
|
+
export const querySeam = (): QuerySeam | undefined => Reflect.get(globalThis, SEAM)
|
|
29
|
+
|
|
30
|
+
export function installQuerySeam(seam: QuerySeam): void {
|
|
31
|
+
if (querySeam() === undefined) Reflect.set(globalThis, SEAM, seam)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The browser query clients the page made, held weakly: a viewer switch clears each one still alive,
|
|
36
|
+
* whether it was made before or after the live client loaded.
|
|
37
|
+
*/
|
|
38
|
+
export const queryClients = (): readonly WeakRef<QueryClient>[] =>
|
|
39
|
+
Reflect.get(globalThis, CLIENTS) ?? []
|
|
40
|
+
|
|
41
|
+
export function adoptClient(client: QueryClient): void {
|
|
42
|
+
// A server builds a client per request; none of them outlives it or sees a viewer switch.
|
|
43
|
+
if (typeof Reflect.get(globalThis, "window") === "undefined") return
|
|
44
|
+
// Each new client sheds the ones the page let go, so the list never outgrows the live clients.
|
|
45
|
+
const clients = queryClients().filter((ref) => ref.deref() !== undefined)
|
|
46
|
+
clients.push(new WeakRef(client))
|
|
47
|
+
Reflect.set(globalThis, CLIENTS, clients)
|
|
48
|
+
}
|
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
PageLocales,
|
|
25
25
|
RenderAdapter,
|
|
26
26
|
RenderProps,
|
|
27
|
+
RenderStreamOptions,
|
|
27
28
|
} from "../render-seam.ts"
|
|
28
29
|
import {
|
|
29
30
|
ACTION_GLOBAL,
|
|
@@ -36,10 +37,12 @@ import {
|
|
|
36
37
|
LOCALES_GLOBAL,
|
|
37
38
|
ROOT_ATTRIBUTE,
|
|
38
39
|
ROUTE_GLOBAL,
|
|
40
|
+
SWAP_HEADER,
|
|
39
41
|
} from "../render-seam.ts"
|
|
40
42
|
import { PRERENDERED_GLOBAL } from "../router.ts"
|
|
41
43
|
import { trustedHeadAttributes } from "./head-attributes.ts"
|
|
42
44
|
import { isStaticMeta, mergeHeads } from "./head-merge.ts"
|
|
45
|
+
import { reportDeferredRejection } from "./request-abort.ts"
|
|
43
46
|
import { PRE_HYDRATION_GUARD } from "./runtime-contract.ts"
|
|
44
47
|
import { EXECUTABLE_SCRIPT_TYPES, INERT_SCRIPT_TYPES } from "./script-types.ts"
|
|
45
48
|
|
|
@@ -171,6 +174,14 @@ export interface RenderPageOptions {
|
|
|
171
174
|
readonly matchChain?: MatchChain
|
|
172
175
|
/** HTTP status for the response (default 200; e.g. 404 for a not-found page). */
|
|
173
176
|
readonly status?: number
|
|
177
|
+
/**
|
|
178
|
+
* The slot an `x-nifra-swap` request names. When the adapter renders slots and nothing is
|
|
179
|
+
* deferred, the response is that slot's HTML alone; otherwise the whole document.
|
|
180
|
+
*/
|
|
181
|
+
readonly swapSlot?: string
|
|
182
|
+
/** Chain indices the adapter renders inside the slot wrappers of a kept page (`keepAlive`), so the
|
|
183
|
+
* browser can keep the page this document shows without remounting it. */
|
|
184
|
+
readonly slots?: readonly number[]
|
|
174
185
|
/** Extra response headers - e.g. the `cache-control` a terminal status page wants. `content-type`
|
|
175
186
|
* is ignored (the document is always HTML) and the ISR freshness header is applied after these,
|
|
176
187
|
* so neither can be overridden from here. */
|
|
@@ -205,6 +216,11 @@ export interface RenderPageOptions {
|
|
|
205
216
|
/** CSP nonce applied to every framework-owned executable script in this document. Nonce-bearing
|
|
206
217
|
* documents are marked `private, no-store` so a request-specific nonce is never replayed. */
|
|
207
218
|
readonly nonce?: string
|
|
219
|
+
/** The request's abort signal. A deferred value that rejects because it aborted is the client
|
|
220
|
+
* leaving, not a failure, so the server log skips it. Omit ⇒ every rejection is logged. */
|
|
221
|
+
readonly signal?: AbortSignal
|
|
222
|
+
/** Where a rejected deferred value is logged: the app's error log. Omit ⇒ the console. */
|
|
223
|
+
readonly logError?: ((error: unknown) => void) | undefined
|
|
208
224
|
/**
|
|
209
225
|
* A hash-based Content-Security-Policy from `createCspPolicy`. The document then carries `nonce`
|
|
210
226
|
* only when it needs one (it defers a value, or a head tag names the nonce); otherwise it renders
|
|
@@ -262,6 +278,18 @@ export function renderPage(options: RenderPageInput): MaybePromise<Response> {
|
|
|
262
278
|
return page instanceof Promise ? page.then((p) => p.toResponse()) : page.toResponse()
|
|
263
279
|
}
|
|
264
280
|
|
|
281
|
+
// Every option of an adapter call, for a render with slot wrappers (only a keep-alive app's).
|
|
282
|
+
const streamOptions = (
|
|
283
|
+
nonce: string | undefined,
|
|
284
|
+
parts: DocumentParts | undefined,
|
|
285
|
+
slots: readonly number[],
|
|
286
|
+
): RenderStreamOptions => {
|
|
287
|
+
const options: { -readonly [K in keyof RenderStreamOptions]: RenderStreamOptions[K] } = { slots }
|
|
288
|
+
if (nonce !== undefined) options.nonce = nonce
|
|
289
|
+
if (parts !== undefined) options.parts = parts
|
|
290
|
+
return options
|
|
291
|
+
}
|
|
292
|
+
|
|
265
293
|
export function renderPageResult(options: RenderPageInput): MaybePromise<RenderedPage> {
|
|
266
294
|
const { csp } = options
|
|
267
295
|
if (csp !== undefined) {
|
|
@@ -512,6 +540,8 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
512
540
|
if (name !== "content-type") headers[name] = value
|
|
513
541
|
}
|
|
514
542
|
}
|
|
543
|
+
if (adapter.renderSlot !== undefined)
|
|
544
|
+
headers.vary = headers.vary === undefined ? SWAP_HEADER : `${headers.vary}, ${SWAP_HEADER}`
|
|
515
545
|
// ISR: advertise the route's freshness so a `withISR` wrapper can set this page's cache TTL. A
|
|
516
546
|
// dedicated header (not the action-revalidation `x-nifra-revalidate`) so the TTL channel never aliases
|
|
517
547
|
// the client's path-list channel.
|
|
@@ -551,6 +581,17 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
551
581
|
...(locale === undefined ? {} : { locale }),
|
|
552
582
|
}
|
|
553
583
|
|
|
584
|
+
const { swapSlot, ...documentOptions } = options
|
|
585
|
+
const slots = options.slots
|
|
586
|
+
if (swapSlot !== undefined && allDeferred.length === 0 && adapter.renderSlot !== undefined) {
|
|
587
|
+
const slotPage = (slotHtml: string | undefined): MaybePromise<RenderedPage> =>
|
|
588
|
+
slotHtml === undefined
|
|
589
|
+
? renderPageResult(documentOptions)
|
|
590
|
+
: new BufferedRenderedPage(slotHtml, status, swapHeaders(headers, swapSlot))
|
|
591
|
+
const out = adapter.renderSlot(chain, renderProps, swapSlot)
|
|
592
|
+
return out instanceof Promise ? out.then(slotPage) : slotPage(out)
|
|
593
|
+
}
|
|
594
|
+
|
|
554
595
|
// Fast path: nothing `defer()`s and the adapter can render synchronously to a string → buffer the
|
|
555
596
|
// whole document in one pass. Progressive streaming only benefits pages with deferred boundaries
|
|
556
597
|
// (those take the streaming path below); for a plain page the streaming pipeline + the framework's
|
|
@@ -571,6 +612,9 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
571
612
|
headers,
|
|
572
613
|
streamNonce,
|
|
573
614
|
streamNonce === nonce ? nonceAttr : ` nonce="${escapeAttr(streamNonce ?? "")}"`,
|
|
615
|
+
options.signal,
|
|
616
|
+
options.slots,
|
|
617
|
+
options.logError,
|
|
574
618
|
).then((response) => new ResponseRenderedPage(response))
|
|
575
619
|
const last = chain[chain.length - 1]
|
|
576
620
|
const leaf = isWeakKey(last) ? last : undefined
|
|
@@ -596,15 +640,17 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
596
640
|
return streamed(renderNonce)
|
|
597
641
|
}
|
|
598
642
|
const out =
|
|
599
|
-
|
|
600
|
-
? adapter.renderToString(
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
:
|
|
643
|
+
slots !== undefined
|
|
644
|
+
? adapter.renderToString(chain, renderProps, streamOptions(renderNonce, parts, slots))
|
|
645
|
+
: parts !== undefined
|
|
646
|
+
? adapter.renderToString(
|
|
647
|
+
chain,
|
|
648
|
+
renderProps,
|
|
649
|
+
renderNonce === undefined ? { parts } : { nonce: renderNonce, parts },
|
|
650
|
+
)
|
|
651
|
+
: renderNonce === undefined
|
|
652
|
+
? adapter.renderToString(chain, renderProps)
|
|
653
|
+
: adapter.renderToString(chain, renderProps, { nonce: renderNonce })
|
|
608
654
|
const page = (bodyHtml: string | undefined): MaybePromise<RenderedPage> => {
|
|
609
655
|
if (bodyHtml === undefined) {
|
|
610
656
|
// The render suspended on something only the streaming renderer waits for (React's `use`
|
|
@@ -643,6 +689,9 @@ async function renderStreamedPage(
|
|
|
643
689
|
headers: Record<string, string>,
|
|
644
690
|
nonce: string | undefined,
|
|
645
691
|
nonceAttr: string,
|
|
692
|
+
signal: AbortSignal | undefined,
|
|
693
|
+
slots: readonly number[] | undefined,
|
|
694
|
+
logError: ((error: unknown) => void) | undefined,
|
|
646
695
|
): Promise<Response> {
|
|
647
696
|
// Streaming path - required for `defer()` (progressive `<Await>` resolution) and used by any adapter
|
|
648
697
|
// that doesn't implement `renderToString`. Awaiting `renderToStream` resolves on shell-readiness
|
|
@@ -652,21 +701,54 @@ async function renderStreamedPage(
|
|
|
652
701
|
const closeRoot = enc.encode(closeRootHtml)
|
|
653
702
|
const tail = enc.encode(tailHtml)
|
|
654
703
|
const appStream =
|
|
655
|
-
|
|
656
|
-
? await adapter.renderToStream(
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
:
|
|
704
|
+
slots !== undefined
|
|
705
|
+
? await adapter.renderToStream(chain, renderProps, streamOptions(nonce, parts, slots))
|
|
706
|
+
: parts !== undefined
|
|
707
|
+
? await adapter.renderToStream(
|
|
708
|
+
chain,
|
|
709
|
+
renderProps,
|
|
710
|
+
nonce === undefined ? { parts } : { nonce, parts },
|
|
711
|
+
)
|
|
712
|
+
: nonce === undefined
|
|
713
|
+
? await adapter.renderToStream(chain, renderProps)
|
|
714
|
+
: await adapter.renderToStream(chain, renderProps, { nonce })
|
|
664
715
|
// After the render: an adapter fills its document parts before handing back the stream.
|
|
665
716
|
const shell = enc.encode(shellOf())
|
|
666
|
-
const body = streamDocument(
|
|
717
|
+
const body = streamDocument(
|
|
718
|
+
shell,
|
|
719
|
+
appStream,
|
|
720
|
+
closeRoot,
|
|
721
|
+
allDeferred,
|
|
722
|
+
tail,
|
|
723
|
+
enc,
|
|
724
|
+
nonceAttr,
|
|
725
|
+
signal,
|
|
726
|
+
logError,
|
|
727
|
+
)
|
|
667
728
|
return new Response(body, { status, headers })
|
|
668
729
|
}
|
|
669
730
|
|
|
731
|
+
// A swap response shares its URL with the document, so like a data response it is never stored: a
|
|
732
|
+
// cache keyed on the URL alone would serve one slot in place of the page. A slot joins a page that
|
|
733
|
+
// already runs under its own policy, so the CSP header belongs to the document alone.
|
|
734
|
+
const DOCUMENT_ONLY_HEADERS = new Set([
|
|
735
|
+
ISR_REVALIDATE_HEADER,
|
|
736
|
+
ISR_REVALIDATE_TAGS_HEADER,
|
|
737
|
+
"content-security-policy",
|
|
738
|
+
])
|
|
739
|
+
|
|
740
|
+
function swapHeaders(
|
|
741
|
+
document: Readonly<Record<string, string>>,
|
|
742
|
+
slot: string,
|
|
743
|
+
): Record<string, string> {
|
|
744
|
+
const headers: Record<string, string> = {}
|
|
745
|
+
for (const [name, value] of Object.entries(document))
|
|
746
|
+
if (!DOCUMENT_ONLY_HEADERS.has(name)) headers[name] = value
|
|
747
|
+
headers["cache-control"] = "private, no-store"
|
|
748
|
+
headers[SWAP_HEADER] = slot
|
|
749
|
+
return headers
|
|
750
|
+
}
|
|
751
|
+
|
|
670
752
|
class BufferedRenderedPage implements RenderedPage {
|
|
671
753
|
readonly [RESPONSE_RESULT] = true
|
|
672
754
|
private readonly body: string
|
|
@@ -732,6 +814,8 @@ function streamDocument(
|
|
|
732
814
|
tail: Uint8Array,
|
|
733
815
|
enc: TextEncoder,
|
|
734
816
|
nonceAttr: string,
|
|
817
|
+
signal: AbortSignal | undefined,
|
|
818
|
+
logError: ((error: unknown) => void) | undefined,
|
|
735
819
|
): ReadableStream<Uint8Array> {
|
|
736
820
|
const reader = stream.getReader()
|
|
737
821
|
// A client that goes away cancels the body: the renderer stops, and nothing more is written.
|
|
@@ -756,7 +840,7 @@ function streamDocument(
|
|
|
756
840
|
// A rejected (or unserializable) deferred streams __nifraReject (the client `<Await>`
|
|
757
841
|
// surfaces it) - it must not break the whole body. Redact: stream a stable opaque code,
|
|
758
842
|
// never the raw error text; log the real reason server-side.
|
|
759
|
-
|
|
843
|
+
reportDeferredRejection(signal, err, logError)
|
|
760
844
|
return `<script${nonceAttr}>window.__nifraReject(${d.id},${serializeData(DEFERRED_ERROR_CODE)})</script>`
|
|
761
845
|
}),
|
|
762
846
|
)
|
|
@@ -1071,6 +1155,42 @@ export function statusPage(status: number, options: StatusPageOptions = {}): nev
|
|
|
1071
1155
|
export interface RevalidateResult<T> {
|
|
1072
1156
|
readonly __nifraRevalidate: readonly string[]
|
|
1073
1157
|
readonly data: T
|
|
1158
|
+
/** What else the write made stale: cache tags and live keys. */
|
|
1159
|
+
readonly __nifraInvalidate?: {
|
|
1160
|
+
readonly tags: readonly string[]
|
|
1161
|
+
readonly keys: readonly string[]
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/** `value` is what {@link revalidate} returned. */
|
|
1166
|
+
export function isRevalidateResult(value: unknown): value is RevalidateResult<unknown> {
|
|
1167
|
+
return typeof value === "object" && value !== null && "__nifraRevalidate" in value
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/** Cache tags and live keys an action's write made stale, beside its paths. */
|
|
1171
|
+
export interface RevalidateOptions {
|
|
1172
|
+
/** Cache tags: data caches, the page store and the CDN drop entries carrying them; live pages watching a tag as a key reload. */
|
|
1173
|
+
readonly tags?: readonly string[]
|
|
1174
|
+
/** Live keys (`ctx.live`): every open page watching one reloads it. */
|
|
1175
|
+
readonly keys?: readonly string[]
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
const INVALIDATION_NAME = /^[A-Za-z][A-Za-z0-9._:/-]{0,127}$/
|
|
1179
|
+
const MAX_INVALIDATION_NAMES = 64
|
|
1180
|
+
|
|
1181
|
+
function invalidationNames(names: readonly string[] | undefined, kind: string): readonly string[] {
|
|
1182
|
+
if (names === undefined) return []
|
|
1183
|
+
if (!Array.isArray(names) || names.length > MAX_INVALIDATION_NAMES) {
|
|
1184
|
+
throw new TypeError(`[nifra/web] revalidate() takes at most ${MAX_INVALIDATION_NAMES} ${kind}`)
|
|
1185
|
+
}
|
|
1186
|
+
for (const name of names) {
|
|
1187
|
+
if (typeof name !== "string" || !INVALIDATION_NAME.test(name)) {
|
|
1188
|
+
throw new TypeError(
|
|
1189
|
+
`[nifra/web] revalidate() ${kind} are a letter followed by up to 127 of A-Z a-z 0-9 . _ : / -`,
|
|
1190
|
+
)
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return [...new Set(names)]
|
|
1074
1194
|
}
|
|
1075
1195
|
|
|
1076
1196
|
/**
|
|
@@ -1080,8 +1200,15 @@ export interface RevalidateResult<T> {
|
|
|
1080
1200
|
* so a mutation can refresh views beyond the one that was submitted. `data` is still surfaced to the
|
|
1081
1201
|
* component as `actionData` (the wrapper is transparent to `ActionData<typeof action>`).
|
|
1082
1202
|
*/
|
|
1083
|
-
export function revalidate<T>(
|
|
1084
|
-
|
|
1203
|
+
export function revalidate<T>(
|
|
1204
|
+
paths: readonly string[],
|
|
1205
|
+
data: T,
|
|
1206
|
+
options?: RevalidateOptions,
|
|
1207
|
+
): RevalidateResult<T> {
|
|
1208
|
+
if (options === undefined) return { __nifraRevalidate: paths, data }
|
|
1209
|
+
const tags = invalidationNames(options.tags, "tags")
|
|
1210
|
+
const keys = invalidationNames(options.keys, "keys")
|
|
1211
|
+
return { __nifraRevalidate: paths, data, __nifraInvalidate: { tags, keys } }
|
|
1085
1212
|
}
|
|
1086
1213
|
|
|
1087
1214
|
/**
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Whether `error` is a load giving up because its request was aborted - the client went away - rather
|
|
2
|
+
* than a failure: nobody reads the outcome, so there is nothing to report. */
|
|
3
|
+
export function isRequestAbort(signal: AbortSignal | undefined, error: unknown): boolean {
|
|
4
|
+
return (
|
|
5
|
+
signal?.aborted === true &&
|
|
6
|
+
(error === signal.reason || (error instanceof Error && error.name === "AbortError"))
|
|
7
|
+
)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Report a deferred value's rejection to `log`, the app's error log, or to the console without
|
|
11
|
+
* one or when it throws: both callers stream an opaque error code after this, which a failing
|
|
12
|
+
* logger must not cut off. A rejection the client's leaving caused is not reported. */
|
|
13
|
+
export function reportDeferredRejection(
|
|
14
|
+
signal: AbortSignal | undefined,
|
|
15
|
+
error: unknown,
|
|
16
|
+
log: ((error: unknown) => void) | undefined,
|
|
17
|
+
): void {
|
|
18
|
+
if (isRequestAbort(signal, error)) return
|
|
19
|
+
if (log !== undefined) {
|
|
20
|
+
try {
|
|
21
|
+
log(error)
|
|
22
|
+
return
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
25
|
+
console.error("[nifra/web] deferred value rejected:", error)
|
|
26
|
+
}
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
/** Modules whose exports become client stubs: `todos.fn.ts`, `x.fn.mjs`, or the extensionless `foo.fn`. */
|
|
14
14
|
export const SERVER_FN_MODULE = /\.fn(\.[cm]?[jt]sx?)?$/
|
|
15
|
+
/** A server-function namespace: lowercase, dot/dash separated, so a mount cannot invent a path. */
|
|
16
|
+
export const SERVER_FN_NAMESPACE = /^[a-z0-9]+(?:[-.][a-z0-9]+)*$/
|
|
15
17
|
|
|
16
18
|
import { jsStringLiteral } from "./js-string.ts"
|
|
17
19
|
import { DATE_TAG, REVIVE_DATES_SOURCE } from "./revive-dates.ts"
|
|
@@ -105,7 +107,14 @@ function stringEnd(source: string, start: number): number {
|
|
|
105
107
|
*/
|
|
106
108
|
export function serverFnNamespace(filePath: string): string {
|
|
107
109
|
const base = filePath.replaceAll("\\", "/").split("/").pop() ?? ""
|
|
108
|
-
|
|
110
|
+
const namespace = base.replace(SERVER_FN_MODULE, "")
|
|
111
|
+
// `serverFunctions()` refuses this namespace, so a stub calling it could never reach a function.
|
|
112
|
+
if (!SERVER_FN_NAMESPACE.test(namespace)) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`[nifra/fn] ${base} names the server-function namespace ${JSON.stringify(namespace)}, which serverFunctions() refuses - name the file in lowercase dot/dash segments (todo-items.fn.ts).`,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
return namespace
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
/** The server functions a module declares, in source order. */
|