@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,136 @@
|
|
|
1
|
+
const isPlain = (value: unknown): value is Record<string, unknown> =>
|
|
2
|
+
typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype
|
|
3
|
+
|
|
4
|
+
const TOO_DEEP = Symbol("data too deep")
|
|
5
|
+
|
|
6
|
+
// Client loaders may return graphs, unlike JSON. Check the whole graph before comparing: a cycle
|
|
7
|
+
// inside a new subtree or beyond the array sample must not acquire copied parents. Shared references
|
|
8
|
+
// and accessors also keep the fresh result intact, rather than changing aliases or invoking getters.
|
|
9
|
+
function isTree(value: unknown, seen: Set<object>, depth: number): boolean {
|
|
10
|
+
if (!Array.isArray(value) && !isPlain(value)) return true
|
|
11
|
+
if (depth >= 128 || seen.has(value)) return false
|
|
12
|
+
seen.add(value)
|
|
13
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
14
|
+
const field = Object.getOwnPropertyDescriptor(value, key)
|
|
15
|
+
if (
|
|
16
|
+
field === undefined ||
|
|
17
|
+
!("value" in field) ||
|
|
18
|
+
(!field.enumerable && !(Array.isArray(value) && key === "length")) ||
|
|
19
|
+
!isTree(field.value, seen, depth + 1)
|
|
20
|
+
)
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Structural sharing for reloaded route data. Plain objects and arrays compare by content and Dates
|
|
28
|
+
* by time; other values and symbol-bearing objects come from `next`. Non-tree client data stays
|
|
29
|
+
* intact. `plainJson` is only for freshly parsed JSON, before any client-loader transformation.
|
|
30
|
+
*
|
|
31
|
+
* Array elements with an `id` pair by it before position, preserving unchanged rows on a prepend or
|
|
32
|
+
* reorder. An array whose first 32 elements keep nothing is taken whole, without a full old-row index.
|
|
33
|
+
*/
|
|
34
|
+
export function shareData<T>(prev: unknown, next: T, plainJson?: boolean): T
|
|
35
|
+
export function shareData(prev: unknown, next: unknown, plainJson = false): unknown {
|
|
36
|
+
if (Object.is(prev, next)) return prev
|
|
37
|
+
if (!plainJson && !isTree(next, new Set(), 0)) return next
|
|
38
|
+
try {
|
|
39
|
+
return share(prev, next, 0, plainJson)
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error === TOO_DEEP) return next
|
|
42
|
+
throw error
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Client loaders can attach metadata that an enumerable-string-key copy would lose. Parsed JSON has
|
|
47
|
+
// no symbol keys, and the check would cost a third of a reload that changed one row.
|
|
48
|
+
const hasSymbols = (a: object, b: object): boolean =>
|
|
49
|
+
Object.getOwnPropertySymbols(a).length > 0 || Object.getOwnPropertySymbols(b).length > 0
|
|
50
|
+
|
|
51
|
+
function share(prev: unknown, next: unknown, depth: number, json: boolean): unknown {
|
|
52
|
+
if (Object.is(prev, next)) return prev
|
|
53
|
+
if (prev instanceof Date)
|
|
54
|
+
return next instanceof Date && prev.getTime() === next.getTime() ? prev : next
|
|
55
|
+
if (Array.isArray(next)) {
|
|
56
|
+
if (!Array.isArray(prev) || (!json && hasSymbols(prev, next))) return next
|
|
57
|
+
if (depth >= 128) throw TOO_DEEP
|
|
58
|
+
const out: unknown[] = []
|
|
59
|
+
let same = prev.length === next.length
|
|
60
|
+
let byId: Map<unknown, number> | undefined
|
|
61
|
+
let completeIndex = false
|
|
62
|
+
// Which old elements a new one took, from the first id lookup on: each pairs at most once, so
|
|
63
|
+
// duplicate ids never put one old object at two places. Every element before that lookup took
|
|
64
|
+
// its own position.
|
|
65
|
+
let taken: Uint8Array | undefined
|
|
66
|
+
let kept = 0
|
|
67
|
+
let i = 0
|
|
68
|
+
for (const item of next) {
|
|
69
|
+
let old: unknown = taken?.[i] === 1 ? undefined : prev[i]
|
|
70
|
+
const id = isPlain(item) ? item.id : undefined
|
|
71
|
+
if (id !== undefined && !(isPlain(old) && old.id === id)) {
|
|
72
|
+
if (taken === undefined) {
|
|
73
|
+
taken = new Uint8Array(prev.length)
|
|
74
|
+
taken.fill(1, 0, i)
|
|
75
|
+
}
|
|
76
|
+
if (byId === undefined || (i >= 32 && !completeIndex)) {
|
|
77
|
+
// Index only IDs needed by the sample until a kept row makes a full index worthwhile.
|
|
78
|
+
const wanted =
|
|
79
|
+
i < 32
|
|
80
|
+
? new Set(next.slice(0, 32).map((value) => (isPlain(value) ? value.id : undefined)))
|
|
81
|
+
: undefined
|
|
82
|
+
completeIndex = wanted === undefined
|
|
83
|
+
let min = Number.POSITIVE_INFINITY
|
|
84
|
+
let max = Number.NEGATIVE_INFINITY
|
|
85
|
+
for (const key of wanted ?? []) {
|
|
86
|
+
if (typeof key !== "number" || Number.isNaN(key)) {
|
|
87
|
+
min = Number.NEGATIVE_INFINITY
|
|
88
|
+
max = Number.POSITIVE_INFINITY
|
|
89
|
+
break
|
|
90
|
+
}
|
|
91
|
+
min = Math.min(min, key)
|
|
92
|
+
max = Math.max(max, key)
|
|
93
|
+
}
|
|
94
|
+
byId = new Map()
|
|
95
|
+
for (let at = 0; at < prev.length; at++) {
|
|
96
|
+
const entry = prev[at]
|
|
97
|
+
if (!isPlain(entry)) continue
|
|
98
|
+
const entryId = entry.id
|
|
99
|
+
if (
|
|
100
|
+
wanted === undefined ||
|
|
101
|
+
((typeof entryId !== "number" || !(entryId < min || entryId > max)) &&
|
|
102
|
+
wanted.has(entryId))
|
|
103
|
+
)
|
|
104
|
+
byId.set(entryId, at)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const at = byId.get(id)
|
|
108
|
+
old = at === undefined || taken[at] === 1 ? undefined : prev[at]
|
|
109
|
+
if (at !== undefined) taken[at] = 1
|
|
110
|
+
} else if (taken !== undefined && i < taken.length && old !== undefined) {
|
|
111
|
+
taken[i] = 1
|
|
112
|
+
}
|
|
113
|
+
const value = share(old, item, depth + 1, json)
|
|
114
|
+
out.push(value)
|
|
115
|
+
if (value === old) kept++
|
|
116
|
+
else if (i === 31 && kept === 0) return next
|
|
117
|
+
if (value !== prev[i]) same = false
|
|
118
|
+
i++
|
|
119
|
+
}
|
|
120
|
+
return same ? prev : out
|
|
121
|
+
}
|
|
122
|
+
if (!isPlain(prev) || !isPlain(next) || (!json && hasSymbols(prev, next))) return next
|
|
123
|
+
if (depth >= 128) throw TOO_DEEP
|
|
124
|
+
const keys = Object.keys(next)
|
|
125
|
+
let same = keys.length === Object.keys(prev).length
|
|
126
|
+
const out: Record<string, unknown> = {}
|
|
127
|
+
for (const key of keys) {
|
|
128
|
+
// A parsed payload can own a `__proto__` key, which a copy would turn into its prototype.
|
|
129
|
+
if (key === "__proto__") return next
|
|
130
|
+
const old = prev[key]
|
|
131
|
+
const value = share(old, next[key], depth + 1, json)
|
|
132
|
+
if (value !== old || (old === undefined && !Object.hasOwn(prev, key))) same = false
|
|
133
|
+
out[key] = value
|
|
134
|
+
}
|
|
135
|
+
return same ? prev : out
|
|
136
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signatures on entries kept in a store other processes share (Workers KV, Redis). Whoever can write
|
|
3
|
+
* such a store could hand every visitor a page this app never built, so each entry carries an
|
|
4
|
+
* HMAC-SHA-256 under the app's secret, and an entry that fails the check reads as a miss.
|
|
5
|
+
*/
|
|
6
|
+
import type * as NodeCrypto from "node:crypto"
|
|
7
|
+
import { timingSafeEqual } from "./timing-safe-equal.ts"
|
|
8
|
+
|
|
9
|
+
const TEXT = new TextEncoder()
|
|
10
|
+
const MAC_HEX = /^[0-9a-f]{64}$/
|
|
11
|
+
// Code units of signed text a signer remembers as proven. A hot page then costs a comparison per
|
|
12
|
+
// hit, not a hash: about 1.5 ns a byte to hash against a fraction of that to compare.
|
|
13
|
+
const MAX_PROVEN_CHARS = 2 * 1024 * 1024
|
|
14
|
+
const MAX_SIGNERS = 4
|
|
15
|
+
|
|
16
|
+
/** Below this, a secret is too short to sign with. */
|
|
17
|
+
export const MIN_STORE_SECRET_LENGTH = 32
|
|
18
|
+
|
|
19
|
+
export const isStoreSecret = (value: unknown): value is string =>
|
|
20
|
+
typeof value === "string" && value.length >= MIN_STORE_SECRET_LENGTH
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Signs a list of strings. Each part is hashed behind its length, so no two lists hash the same
|
|
24
|
+
* bytes, whatever the parts hold.
|
|
25
|
+
*/
|
|
26
|
+
export interface StoreSigner {
|
|
27
|
+
/** The parts' MAC, as lowercase hex. */
|
|
28
|
+
sign(parts: readonly string[]): Promise<string>
|
|
29
|
+
/** Whether `mac` is the parts' MAC under this secret. Anything but 64 lowercase hex digits is not. */
|
|
30
|
+
verify(parts: readonly string[], mac: unknown): Promise<boolean>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type CreateHmac = typeof NodeCrypto.createHmac
|
|
34
|
+
|
|
35
|
+
interface Hasher {
|
|
36
|
+
sign(parts: readonly string[]): Promise<string>
|
|
37
|
+
/** `mac` is already 64 lowercase hex digits. */
|
|
38
|
+
matches(parts: readonly string[], mac: string): Promise<boolean>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Looked up, never imported: a Worker without nodejs_compat cannot load node:crypto. Where it exists
|
|
42
|
+
// it is the cheaper path: Web Crypto answers on another thread, about 20 us a call on Bun, Node and
|
|
43
|
+
// Deno, more than hashing a 16 KB page.
|
|
44
|
+
function nodeCreateHmac(): CreateHmac | undefined {
|
|
45
|
+
const createHmac = globalThis.process?.getBuiltinModule?.("node:crypto")?.createHmac
|
|
46
|
+
return typeof createHmac === "function" ? createHmac : undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const sameParts = (a: readonly string[], b: readonly string[]): boolean =>
|
|
50
|
+
a.length === b.length && a.every((part, at) => part === b[at])
|
|
51
|
+
|
|
52
|
+
const lengthOf = (parts: readonly string[]): number =>
|
|
53
|
+
parts.reduce((sum, part) => sum + part.length, 0)
|
|
54
|
+
|
|
55
|
+
// One signer per secret for the process, so a handler built per request (a Worker reads its bindings
|
|
56
|
+
// in `fetch`) still finds the pages earlier requests proved.
|
|
57
|
+
const signers = new Map<string, StoreSigner>()
|
|
58
|
+
|
|
59
|
+
export function storeSigner(secret: string): StoreSigner {
|
|
60
|
+
const existing = signers.get(secret)
|
|
61
|
+
if (existing !== undefined) return existing
|
|
62
|
+
const signer = provingSigner(hasher(secret))
|
|
63
|
+
for (const oldest of signers.keys()) {
|
|
64
|
+
if (signers.size < MAX_SIGNERS) break
|
|
65
|
+
signers.delete(oldest)
|
|
66
|
+
}
|
|
67
|
+
signers.set(secret, signer)
|
|
68
|
+
return signer
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function provingSigner(hashing: Hasher): StoreSigner {
|
|
72
|
+
// MAC to the parts this signer signed or verified, oldest first.
|
|
73
|
+
const proven = new Map<string, readonly string[]>()
|
|
74
|
+
let provenChars = 0
|
|
75
|
+
const remember = (mac: string, parts: readonly string[]): void => {
|
|
76
|
+
const length = lengthOf(parts)
|
|
77
|
+
if (length > MAX_PROVEN_CHARS / 8) return
|
|
78
|
+
const had = proven.get(mac)
|
|
79
|
+
if (had !== undefined) {
|
|
80
|
+
proven.delete(mac)
|
|
81
|
+
provenChars -= lengthOf(had)
|
|
82
|
+
}
|
|
83
|
+
proven.set(mac, parts)
|
|
84
|
+
provenChars += length
|
|
85
|
+
for (const [oldest, kept] of proven) {
|
|
86
|
+
if (provenChars <= MAX_PROVEN_CHARS) break
|
|
87
|
+
proven.delete(oldest)
|
|
88
|
+
provenChars -= lengthOf(kept)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
async sign(parts) {
|
|
93
|
+
const mac = await hashing.sign(parts)
|
|
94
|
+
remember(mac, parts)
|
|
95
|
+
return mac
|
|
96
|
+
},
|
|
97
|
+
async verify(parts, mac) {
|
|
98
|
+
if (typeof mac !== "string" || !MAC_HEX.test(mac)) return false
|
|
99
|
+
const known = proven.get(mac)
|
|
100
|
+
if (known !== undefined && sameParts(known, parts)) return true
|
|
101
|
+
if (!(await hashing.matches(parts, mac))) return false
|
|
102
|
+
remember(mac, parts)
|
|
103
|
+
return true
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function hasher(secret: string): Hasher {
|
|
109
|
+
const createHmac = nodeCreateHmac()
|
|
110
|
+
if (createHmac !== undefined) {
|
|
111
|
+
const hex = (parts: readonly string[]): string => {
|
|
112
|
+
const hmac = createHmac("sha256", secret)
|
|
113
|
+
for (const part of parts) hmac.update(`${part.length}:`).update(part)
|
|
114
|
+
return hmac.digest("hex")
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
sign: async (parts) => hex(parts),
|
|
118
|
+
matches: async (parts, mac) => timingSafeEqual(hex(parts), mac),
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
let signingKey: Promise<CryptoKey> | undefined
|
|
122
|
+
const key = (): Promise<CryptoKey> => {
|
|
123
|
+
signingKey ??= crypto.subtle.importKey(
|
|
124
|
+
"raw",
|
|
125
|
+
TEXT.encode(secret),
|
|
126
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
127
|
+
false,
|
|
128
|
+
["sign", "verify"],
|
|
129
|
+
)
|
|
130
|
+
return signingKey
|
|
131
|
+
}
|
|
132
|
+
const bytes = (parts: readonly string[]): Uint8Array<ArrayBuffer> =>
|
|
133
|
+
TEXT.encode(parts.map((part) => `${part.length}:${part}`).join(""))
|
|
134
|
+
return {
|
|
135
|
+
async sign(parts) {
|
|
136
|
+
const mac = new Uint8Array(await crypto.subtle.sign("HMAC", await key(), bytes(parts)))
|
|
137
|
+
let hex = ""
|
|
138
|
+
for (const byte of mac) hex += byte.toString(16).padStart(2, "0")
|
|
139
|
+
return hex
|
|
140
|
+
},
|
|
141
|
+
async matches(parts, mac) {
|
|
142
|
+
const expected = new Uint8Array(mac.length / 2)
|
|
143
|
+
for (let at = 0; at < expected.length; at++) {
|
|
144
|
+
expected[at] = Number.parseInt(mac.slice(at * 2, at * 2 + 2), 16)
|
|
145
|
+
}
|
|
146
|
+
return crypto.subtle.verify("HMAC", await key(), expected, bytes(parts))
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -23,8 +23,8 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
|
23
23
|
* Only a plugin's `config` hook output is rewritten, never mutated, and only under rolldown-vite.
|
|
24
24
|
* Non-rolldown Vite is passed through verbatim.
|
|
25
25
|
*
|
|
26
|
-
* FLATTEN FIRST: a
|
|
27
|
-
* returns `[vite:react-babel, vite:react-refresh]`, and it's `
|
|
26
|
+
* FLATTEN FIRST: a plugin factory may return an ARRAY of plugins - `@vitejs/plugin-react@4.x`'s `react()`
|
|
27
|
+
* returns `[vite:react-babel, vite:react-refresh]`, and it's `vite:react-babel`'s `config` hook that emits
|
|
28
28
|
* the offending `optimizeDeps.rollupOptions.jsx`. `nifra.config.ts` writes `vitePlugins = [react()]`, so the
|
|
29
29
|
* plugin list arrives NESTED (`[[babel, refresh]]`). Without flattening, `.map` sees the inner array (which
|
|
30
30
|
* has no `config`), leaves it untouched, and Vite - which flattens plugin arrays itself before running them
|
package/src/internal/web-app.ts
CHANGED
|
@@ -10,8 +10,9 @@ import {
|
|
|
10
10
|
NIFRA_BACKEND_WS_RUNTIME,
|
|
11
11
|
preRouteMountPaths,
|
|
12
12
|
} from "@nifrajs/core/mount"
|
|
13
|
+
import { withResponseObserver } from "@nifrajs/core/response-observer"
|
|
13
14
|
import type { MountableApp, MountOptions } from "@nifrajs/core/server"
|
|
14
|
-
import { type ServerOptions, server } from "@nifrajs/core/server"
|
|
15
|
+
import { replacedRequestOf, type ServerOptions, server } from "@nifrajs/core/server"
|
|
15
16
|
import { type CspPolicy, cspNonceResolver, DOCUMENT_POLICY, isCspPolicy } from "../csp.ts"
|
|
16
17
|
import type { CssLoadingMode } from "../css-contract.ts"
|
|
17
18
|
import {
|
|
@@ -26,9 +27,14 @@ import type { Manifest } from "../manifest.ts"
|
|
|
26
27
|
import type { RenderAdapter } from "../render-seam.ts"
|
|
27
28
|
import { createMatcher, PRERENDERED_LIST_PATH } from "../router.ts"
|
|
28
29
|
import type { EvidenceAncestry } from "./evidence-traversal.ts"
|
|
30
|
+
import { identityEpochHook, type KeepAliveServerOptions } from "./identity-epoch.ts"
|
|
31
|
+
import { INVALIDATION, type InvalidationOp, invalidationOf } from "./invalidation-contract.ts"
|
|
32
|
+
import { LIVE_ATTACH, type LiveAttachment, type LiveRuntimeHandle } from "./live-contract.ts"
|
|
29
33
|
import { formatShadowedPages, shadowedPages } from "./mount-shadow.ts"
|
|
30
34
|
import { createPageRequestExecutor, type NonceResolver, type PageI18n } from "./page-execution.ts"
|
|
35
|
+
import { PPR_SERVER, PPR_SETUP, type PprSetup } from "./ppr/contract.ts"
|
|
31
36
|
import { type PrerenderedPages, servePrerenderedPages } from "./prerendered-pages.ts"
|
|
37
|
+
import type { RevalidateResult } from "./render-document.ts"
|
|
32
38
|
import { requestPattern } from "./route-path.ts"
|
|
33
39
|
|
|
34
40
|
export type { NonceResolver } from "./page-execution.ts"
|
|
@@ -101,6 +107,15 @@ export interface CreateWebAppOptions<Env = unknown> {
|
|
|
101
107
|
* and stays cacheable by `withISR` or a CDN. `meta` still receives a nonce to name.
|
|
102
108
|
*/
|
|
103
109
|
readonly csp?: CspPolicy
|
|
110
|
+
/** Experimental and unstable: features whose API may change in any release. */
|
|
111
|
+
readonly experimental?: {
|
|
112
|
+
/**
|
|
113
|
+
* Partial prerendering, from `partialPrerendering()` in `@nifrajs/web/ppr`. A route whose backend
|
|
114
|
+
* half exports `shellLoader` streams a shell shared by every visitor, then each `dynamic` boundary
|
|
115
|
+
* as it settles. Needs `csp` (hashes), not `nonce`.
|
|
116
|
+
*/
|
|
117
|
+
readonly ppr?: PprSetup
|
|
118
|
+
}
|
|
104
119
|
/**
|
|
105
120
|
* Options for the underlying `server()` - `requestTimeoutMs`, `admission`, `gracefulSignals`, and
|
|
106
121
|
* the rest of {@link ServerOptions}.
|
|
@@ -221,6 +236,11 @@ export interface CreateWebAppOptions<Env = unknown> {
|
|
|
221
236
|
* signed `__nifra_draft` cookie gets `ctx.draft === true` in loaders/actions (else always `false`).
|
|
222
237
|
* Pair with `withISR({ draftSecret })` so editors bypass the cache. Omit to disable draft mode. */
|
|
223
238
|
readonly draftSecret?: string
|
|
239
|
+
/**
|
|
240
|
+
* Live data refresh, from `live()` in `@nifrajs/web/live`. Loaders declare keys with `ctx.live()`;
|
|
241
|
+
* the app answers live loads at the runtime's load endpoint. Omit and nothing live is loaded.
|
|
242
|
+
*/
|
|
243
|
+
readonly live?: LiveRuntimeHandle
|
|
224
244
|
/** Per-route chunk URLs (`buildClient`'s `BuildManifest.routes`) - `routeId → [layout chunks…, own
|
|
225
245
|
* chunk]`. When present, each page `modulepreload`s its matched route's chunks alongside the entry,
|
|
226
246
|
* so the route code downloads in parallel (no entry→route-chunk waterfall). Omit ⇒ entry-only. */
|
|
@@ -276,6 +296,14 @@ export interface CreateWebAppOptions<Env = unknown> {
|
|
|
276
296
|
* server entry, before its static files too. A refused path answers the policy's refusal.
|
|
277
297
|
*/
|
|
278
298
|
readonly hostPolicy?: HostPolicy
|
|
299
|
+
/**
|
|
300
|
+
* Which cookies say who is signed in, for an app with a `keepAlive` route. A response that changes
|
|
301
|
+
* or deletes one rotates the readable identity epoch cookie, and every tab drops the pages it kept
|
|
302
|
+
* for the previous visitor. The session cookies of `@nifrajs/auth`, Better Auth and Auth.js count
|
|
303
|
+
* under their default names; name any other in `identityCookies`. Development reports, once per
|
|
304
|
+
* name, an `HttpOnly` cookie that changes and is in neither list (`NIFRA_KEEPALIVE_IDENTITY_COOKIE`).
|
|
305
|
+
*/
|
|
306
|
+
readonly keepAlive?: KeepAliveServerOptions
|
|
279
307
|
/**
|
|
280
308
|
* The files the build emitted, by request path: `buildAssets` from the generated server manifest.
|
|
281
309
|
* A {@link hostPolicy} rule's `buildAssets` token matches exactly these. `nifra dev` serves the
|
|
@@ -330,6 +358,16 @@ export interface CreateWebAppOptions<Env = unknown> {
|
|
|
330
358
|
* The `Request` is rebuilt rather than mutated (`url` is read-only), preserving method, headers, body,
|
|
331
359
|
* and duplex streaming - the body is passed through unread, so a large or streamed upload is untouched.
|
|
332
360
|
*/
|
|
361
|
+
/** Publish what a write named, for an app with `live` and no cache wrapper to run the pipeline. */
|
|
362
|
+
async function publishOwn(
|
|
363
|
+
op: InvalidationOp,
|
|
364
|
+
request: Request,
|
|
365
|
+
publish: (keys: readonly string[]) => Promise<void>,
|
|
366
|
+
): Promise<void> {
|
|
367
|
+
const { settleInvalidation } = await import("./invalidation.ts")
|
|
368
|
+
await settleInvalidation(op, { publish }, request, undefined)
|
|
369
|
+
}
|
|
370
|
+
|
|
333
371
|
/**
|
|
334
372
|
* Resolve the explicit symbol-keyed backend mount interface. The symbol seam forwards platform
|
|
335
373
|
* context without making web depend on client.
|
|
@@ -421,15 +459,23 @@ export function createWebApp<Env = unknown>(
|
|
|
421
459
|
}
|
|
422
460
|
app.use(hostPolicyGate(options.hostPolicy, options.buildAssets))
|
|
423
461
|
}
|
|
462
|
+
// Before every app hook: a live load becomes the target page's GET here, so each hook, middleware,
|
|
463
|
+
// gate and loader sees that path once, as a navigation would.
|
|
464
|
+
const liveAttachment: LiveAttachment | undefined = options.live?.[LIVE_ATTACH]({
|
|
465
|
+
matchRoute: (path) => pageExecutor.matchRoute(path),
|
|
466
|
+
app,
|
|
467
|
+
})
|
|
468
|
+
if (liveAttachment !== undefined) app.onRequest(liveAttachment.onRequest)
|
|
424
469
|
// Before the mount hooks and before every page route, so the order-scoped hooks (`derive`,
|
|
425
470
|
// `beforeHandle`, assurance evidence) actually bind to them - see the `use` option. A caller
|
|
426
471
|
// cannot do this after the fact: by the time this function returns, the routes are declared.
|
|
427
472
|
options.use?.(app)
|
|
428
473
|
if (options.serverComponents !== undefined) {
|
|
429
474
|
const runtime = options.serverComponents
|
|
475
|
+
// A route whose holes show server components is one visitor's too.
|
|
430
476
|
const match = createMatcher(
|
|
431
477
|
manifest.routes
|
|
432
|
-
.filter((route) => runtime.owns(route.id))
|
|
478
|
+
.filter((route) => runtime.owns(route.id) || runtime.hosts?.(route.id) === true)
|
|
433
479
|
.map((route) => ({ routeId: route.id, pattern: route.pattern })),
|
|
434
480
|
)
|
|
435
481
|
const privateRoute = (request: Request): boolean =>
|
|
@@ -473,6 +519,44 @@ export function createWebApp<Env = unknown>(
|
|
|
473
519
|
cacheChannel.enabled = true
|
|
474
520
|
},
|
|
475
521
|
})
|
|
522
|
+
// One owner per write. A cache wrapper that opened the channel runs the whole pipeline and takes
|
|
523
|
+
// the operation here, by the request it passed in; without one, the app publishes the write's keys
|
|
524
|
+
// and tags itself, and a write with nothing to publish needs nothing at all.
|
|
525
|
+
const handedOver = new WeakMap<Request, InvalidationOp>()
|
|
526
|
+
const live = options.live
|
|
527
|
+
const publish =
|
|
528
|
+
live === undefined ? undefined : (keys: readonly string[]) => live.publish(...keys)
|
|
529
|
+
Object.defineProperty(app, INVALIDATION, {
|
|
530
|
+
value: {
|
|
531
|
+
take: (request: Request): InvalidationOp | undefined => {
|
|
532
|
+
const op = handedOver.get(request)
|
|
533
|
+
handedOver.delete(request)
|
|
534
|
+
return op
|
|
535
|
+
},
|
|
536
|
+
publish,
|
|
537
|
+
},
|
|
538
|
+
})
|
|
539
|
+
const invalidate = (
|
|
540
|
+
revalidation: RevalidateResult<unknown>,
|
|
541
|
+
request: Request,
|
|
542
|
+
): Promise<void> | undefined => {
|
|
543
|
+
if (cacheChannel.enabled) {
|
|
544
|
+
const op = invalidationOf(revalidation)
|
|
545
|
+
// Every request in a hook's replacement chain: the wrapper holds the first one.
|
|
546
|
+
for (
|
|
547
|
+
let held: Request | undefined = request;
|
|
548
|
+
held !== undefined;
|
|
549
|
+
held = replacedRequestOf(held)
|
|
550
|
+
)
|
|
551
|
+
handedOver.set(held, op)
|
|
552
|
+
return undefined
|
|
553
|
+
}
|
|
554
|
+
// Paths alone refresh only the submitting page, which the response header already does.
|
|
555
|
+
const named = revalidation.__nifraInvalidate
|
|
556
|
+
if (publish === undefined || named === undefined || named.keys.length + named.tags.length === 0)
|
|
557
|
+
return undefined
|
|
558
|
+
return publishOwn(invalidationOf(revalidation), request, publish)
|
|
559
|
+
}
|
|
476
560
|
// Mount the in-process backend over HTTP at `apiPrefix`, when one is given, BEFORE page routing.
|
|
477
561
|
// Core's pre-route mount seam runs before the page wildcard `/*`; parent request hooks run first,
|
|
478
562
|
// child request hooks run inside the mount, and parent response hooks still wrap the result.
|
|
@@ -511,6 +595,14 @@ export function createWebApp<Env = unknown>(
|
|
|
511
595
|
const prerenderedList = ownPatterns.has(PRERENDERED_LIST_PATH)
|
|
512
596
|
? undefined
|
|
513
597
|
: servedPrerenderedList(options.prerenderedPaths)
|
|
598
|
+
const ppr = options.experimental?.ppr?.[PPR_SETUP]({
|
|
599
|
+
adapter,
|
|
600
|
+
// biome-ignore lint/plugin/noConditionalEmptyObjectSpread: partial prerendering lists its scripts by hash only under an app's own policy.
|
|
601
|
+
...(options.csp === undefined ? {} : { csp: options.csp }),
|
|
602
|
+
// The content-hashed entry changes with every client build; the store is per process.
|
|
603
|
+
deployment: clientEntry,
|
|
604
|
+
perRequestNonce: options.nonce !== undefined,
|
|
605
|
+
})
|
|
514
606
|
const pageExecutor = createPageRequestExecutor<Env>({
|
|
515
607
|
...(options.cacheRouteModules === undefined
|
|
516
608
|
? {}
|
|
@@ -558,7 +650,17 @@ export function createWebApp<Env = unknown>(
|
|
|
558
650
|
...(options.onLoaderError === undefined ? {} : { onLoaderError: options.onLoaderError }),
|
|
559
651
|
logBoundaryError: boundaryErrorLog(options.server),
|
|
560
652
|
cacheChannel,
|
|
653
|
+
// biome-ignore lint/plugin/noConditionalEmptyObjectSpread: the executor takes a route's shell path only when partial prerendering is set up.
|
|
654
|
+
...(ppr === undefined ? {} : { ppr }),
|
|
655
|
+
invalidate,
|
|
656
|
+
liveLoadOf: liveAttachment?.loadOf,
|
|
561
657
|
})
|
|
658
|
+
if (ppr !== undefined) {
|
|
659
|
+
Object.defineProperty(app, PPR_SERVER, { value: ppr.server })
|
|
660
|
+
app.register("POST", ppr.cancelPath, undefined, (c: { readonly req: Request }) =>
|
|
661
|
+
ppr.cancel(c.req),
|
|
662
|
+
)
|
|
663
|
+
}
|
|
562
664
|
|
|
563
665
|
// A nonce app's documents authorize their scripts per request, which a file rendered once cannot.
|
|
564
666
|
const prerendered =
|
|
@@ -573,7 +675,8 @@ export function createWebApp<Env = unknown>(
|
|
|
573
675
|
app.register(
|
|
574
676
|
"GET",
|
|
575
677
|
pattern,
|
|
576
|
-
options.serverComponents?.owns(route.id)
|
|
678
|
+
options.serverComponents?.owns(route.id) ||
|
|
679
|
+
options.serverComponents?.hosts?.(route.id) === true
|
|
577
680
|
? { capabilities: options.serverComponents.capabilities(route.id) }
|
|
578
681
|
: undefined,
|
|
579
682
|
prerendered === undefined ? render : prerendered(render),
|
|
@@ -632,6 +735,21 @@ export function createWebApp<Env = unknown>(
|
|
|
632
735
|
// and the router refuses a second `/*` under another param name.
|
|
633
736
|
if (!ownsRootCatchAll) app.register("GET", "/*", undefined, pageExecutor.fallback)
|
|
634
737
|
|
|
738
|
+
// Last, after every hook the caller's `use` added: a cookie any of them sets is seen here.
|
|
739
|
+
if (manifest.routes.some((route) => route.keepAlive === true)) {
|
|
740
|
+
// As for error details: only a run that says it is development or test reports, and an unset
|
|
741
|
+
// NODE_ENV (an unbuilt `bun app.ts`, a Worker) counts as production.
|
|
742
|
+
const nodeEnv = typeof process === "undefined" ? undefined : process.env?.NODE_ENV
|
|
743
|
+
app.use(
|
|
744
|
+
withResponseObserver({
|
|
745
|
+
onResponseHeaders: identityEpochHook(
|
|
746
|
+
options.keepAlive ?? {},
|
|
747
|
+
nodeEnv === "development" || nodeEnv === "test",
|
|
748
|
+
),
|
|
749
|
+
}),
|
|
750
|
+
)
|
|
751
|
+
}
|
|
752
|
+
|
|
635
753
|
const evidenceProvider: ComposedEvidenceProvider = async (passed) => {
|
|
636
754
|
const { traverseEvidence } = await import("./evidence-traversal.ts")
|
|
637
755
|
return traverseEvidence(evidenceProvider, passed, async (ancestry) => {
|