@nifrajs/web 5.5.1 → 5.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +243 -0
- package/README.md +21 -11
- package/dist/boundary.d.ts +67 -7
- package/dist/boundary.d.ts.map +1 -1
- package/dist/boundary.js +6 -4
- package/dist/boundary.js.map +1 -1
- package/dist/build-plan.d.ts +3 -0
- package/dist/build-plan.d.ts.map +1 -1
- package/dist/build-plan.js.map +1 -1
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +2 -0
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +9 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +10 -0
- package/dist/build.js.map +1 -1
- package/dist/client.d.ts +3 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +174 -23
- package/dist/client.js.map +1 -1
- package/dist/csp.d.ts +8 -0
- package/dist/csp.d.ts.map +1 -1
- package/dist/csp.js +11 -2
- package/dist/csp.js.map +1 -1
- package/dist/deferred.d.ts +6 -4
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +10 -6
- package/dist/deferred.js.map +1 -1
- package/dist/dev-feed.d.ts.map +1 -1
- package/dist/dev-feed.js +11 -2
- package/dist/dev-feed.js.map +1 -1
- package/dist/dev-session.d.ts.map +1 -1
- package/dist/dev-session.js +13 -6
- package/dist/dev-session.js.map +1 -1
- package/dist/dev.d.ts +4 -1
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +12 -5
- package/dist/dev.js.map +1 -1
- package/dist/diagnostic-catalog.d.ts +4 -0
- package/dist/diagnostic-catalog.d.ts.map +1 -1
- package/dist/diagnostic-catalog.js +48 -0
- package/dist/diagnostic-catalog.js.map +1 -1
- package/dist/diagnostic.d.ts +1 -1
- package/dist/diagnostic.d.ts.map +1 -1
- package/dist/diagnostic.js +1 -1
- package/dist/diagnostic.js.map +1 -1
- package/dist/fn.d.ts +5 -3
- package/dist/fn.d.ts.map +1 -1
- package/dist/fn.js +20 -5
- package/dist/fn.js.map +1 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +13 -3
- package/dist/fs.js.map +1 -1
- package/dist/identity.d.ts +22 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +60 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/internal/cdn/cloudflare.d.ts +2 -1
- package/dist/internal/cdn/cloudflare.d.ts.map +1 -1
- package/dist/internal/cdn/cloudflare.js +2 -1
- package/dist/internal/cdn/cloudflare.js.map +1 -1
- package/dist/internal/certified-components.d.ts +1 -1
- package/dist/internal/certified-components.js +1 -1
- package/dist/internal/codegen.d.ts +11 -10
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +73 -10
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/dev-websocket.d.ts +73 -0
- package/dist/internal/dev-websocket.d.ts.map +1 -0
- package/dist/internal/dev-websocket.js +145 -0
- package/dist/internal/dev-websocket.js.map +1 -0
- package/dist/internal/hole-runtime.d.ts +66 -0
- package/dist/internal/hole-runtime.d.ts.map +1 -0
- package/dist/internal/hole-runtime.js +211 -0
- package/dist/internal/hole-runtime.js.map +1 -0
- package/dist/internal/identity-epoch.d.ts +50 -0
- package/dist/internal/identity-epoch.d.ts.map +1 -0
- package/dist/internal/identity-epoch.js +180 -0
- package/dist/internal/identity-epoch.js.map +1 -0
- package/dist/internal/invalidation-contract.d.ts +23 -0
- package/dist/internal/invalidation-contract.d.ts.map +1 -0
- package/dist/internal/invalidation-contract.js +11 -0
- package/dist/internal/invalidation-contract.js.map +1 -0
- package/dist/internal/invalidation.d.ts +62 -0
- package/dist/internal/invalidation.d.ts.map +1 -0
- package/dist/internal/invalidation.js +149 -0
- package/dist/internal/invalidation.js.map +1 -0
- package/dist/internal/island-bindings.d.ts +51 -0
- package/dist/internal/island-bindings.d.ts.map +1 -0
- package/dist/internal/island-bindings.js +147 -0
- package/dist/internal/island-bindings.js.map +1 -0
- package/dist/internal/island-list.d.ts +26 -0
- package/dist/internal/island-list.d.ts.map +1 -0
- package/dist/internal/island-list.js +78 -0
- package/dist/internal/island-list.js.map +1 -0
- package/dist/internal/island-mount.d.ts +4 -0
- package/dist/internal/island-mount.d.ts.map +1 -0
- package/dist/internal/island-mount.js +66 -0
- package/dist/internal/island-mount.js.map +1 -0
- package/dist/internal/island-owner.d.ts +5 -0
- package/dist/internal/island-owner.d.ts.map +1 -0
- package/dist/internal/island-owner.js +20 -0
- package/dist/internal/island-owner.js.map +1 -0
- package/dist/internal/island-resource.d.ts +30 -0
- package/dist/internal/island-resource.d.ts.map +1 -0
- package/dist/internal/island-resource.js +57 -0
- package/dist/internal/island-resource.js.map +1 -0
- package/dist/internal/island-signals.d.ts +51 -0
- package/dist/internal/island-signals.d.ts.map +1 -0
- package/dist/internal/island-signals.js +220 -0
- package/dist/internal/island-signals.js.map +1 -0
- package/dist/internal/keep-alive-table.d.ts +23 -0
- package/dist/internal/keep-alive-table.d.ts.map +1 -0
- package/dist/internal/keep-alive-table.js +53 -0
- package/dist/internal/keep-alive-table.js.map +1 -0
- package/dist/internal/live-contract.d.ts +101 -0
- package/dist/internal/live-contract.d.ts.map +1 -0
- package/dist/internal/live-contract.js +5 -0
- package/dist/internal/live-contract.js.map +1 -0
- package/dist/internal/live-queries.d.ts +22 -0
- package/dist/internal/live-queries.d.ts.map +1 -0
- package/dist/internal/live-queries.js +54 -0
- package/dist/internal/live-queries.js.map +1 -0
- package/dist/internal/page-events.d.ts +10 -0
- package/dist/internal/page-events.d.ts.map +1 -0
- package/dist/internal/page-events.js +26 -0
- package/dist/internal/page-events.js.map +1 -0
- package/dist/internal/page-execution.d.ts +16 -1
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +432 -31
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/parity.d.ts +3 -0
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +28 -5
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/ppr/admission.d.ts +83 -0
- package/dist/internal/ppr/admission.d.ts.map +1 -0
- package/dist/internal/ppr/admission.js +221 -0
- package/dist/internal/ppr/admission.js.map +1 -0
- package/dist/internal/ppr/canary.d.ts +24 -0
- package/dist/internal/ppr/canary.d.ts.map +1 -0
- package/dist/internal/ppr/canary.js +102 -0
- package/dist/internal/ppr/canary.js.map +1 -0
- package/dist/internal/ppr/compile-shell.d.ts +39 -0
- package/dist/internal/ppr/compile-shell.d.ts.map +1 -0
- package/dist/internal/ppr/compile-shell.js +298 -0
- package/dist/internal/ppr/compile-shell.js.map +1 -0
- package/dist/internal/ppr/contract.d.ts +177 -0
- package/dist/internal/ppr/contract.d.ts.map +1 -0
- package/dist/internal/ppr/contract.js +80 -0
- package/dist/internal/ppr/contract.js.map +1 -0
- package/dist/internal/ppr/frames.d.ts +71 -0
- package/dist/internal/ppr/frames.d.ts.map +1 -0
- package/dist/internal/ppr/frames.js +119 -0
- package/dist/internal/ppr/frames.js.map +1 -0
- package/dist/internal/ppr/integration.d.ts +19 -0
- package/dist/internal/ppr/integration.d.ts.map +1 -0
- package/dist/internal/ppr/integration.js +106 -0
- package/dist/internal/ppr/integration.js.map +1 -0
- package/dist/internal/ppr/options.d.ts +96 -0
- package/dist/internal/ppr/options.d.ts.map +1 -0
- package/dist/internal/ppr/options.js +125 -0
- package/dist/internal/ppr/options.js.map +1 -0
- package/dist/internal/ppr/route-rules.d.ts +12 -0
- package/dist/internal/ppr/route-rules.d.ts.map +1 -0
- package/dist/internal/ppr/route-rules.js +53 -0
- package/dist/internal/ppr/route-rules.js.map +1 -0
- package/dist/internal/ppr/runtime.d.ts +29 -0
- package/dist/internal/ppr/runtime.d.ts.map +1 -0
- package/dist/internal/ppr/runtime.js +35 -0
- package/dist/internal/ppr/runtime.js.map +1 -0
- package/dist/internal/ppr/server.d.ts +29 -0
- package/dist/internal/ppr/server.d.ts.map +1 -0
- package/dist/internal/ppr/server.js +1409 -0
- package/dist/internal/ppr/server.js.map +1 -0
- package/dist/internal/ppr/shell-store.d.ts +39 -0
- package/dist/internal/ppr/shell-store.d.ts.map +1 -0
- package/dist/internal/ppr/shell-store.js +106 -0
- package/dist/internal/ppr/shell-store.js.map +1 -0
- package/dist/internal/prerendered-pages.d.ts +1 -1
- package/dist/internal/prerendered-pages.d.ts.map +1 -1
- package/dist/internal/prerendered-pages.js +7 -4
- package/dist/internal/prerendered-pages.js.map +1 -1
- package/dist/internal/present.d.ts +11 -0
- package/dist/internal/present.d.ts.map +1 -0
- package/dist/internal/present.js +13 -0
- package/dist/internal/present.js.map +1 -0
- package/dist/internal/query-digest.d.ts +9 -0
- package/dist/internal/query-digest.d.ts.map +1 -0
- package/dist/internal/query-digest.js +23 -0
- package/dist/internal/query-digest.js.map +1 -0
- package/dist/internal/query-runtime.d.ts +6 -1
- package/dist/internal/query-runtime.d.ts.map +1 -1
- package/dist/internal/query-runtime.js +9 -0
- package/dist/internal/query-runtime.js.map +1 -1
- package/dist/internal/query-seam.d.ts +29 -0
- package/dist/internal/query-seam.d.ts.map +1 -0
- package/dist/internal/query-seam.js +23 -0
- package/dist/internal/query-seam.js.map +1 -0
- package/dist/internal/render-document.d.ts +34 -64
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +82 -64
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/request-abort.d.ts +8 -0
- package/dist/internal/request-abort.d.ts.map +1 -0
- package/dist/internal/request-abort.js +22 -0
- package/dist/internal/request-abort.js.map +1 -0
- package/dist/internal/server-boundary.d.ts +2 -0
- package/dist/internal/server-boundary.d.ts.map +1 -1
- package/dist/internal/server-boundary.js +8 -1
- package/dist/internal/server-boundary.js.map +1 -1
- package/dist/internal/share-data.d.ts +10 -0
- package/dist/internal/share-data.d.ts.map +1 -0
- package/dist/internal/share-data.js +135 -0
- package/dist/internal/share-data.js.map +1 -0
- package/dist/internal/status-page.d.ts +68 -0
- package/dist/internal/status-page.d.ts.map +1 -0
- package/dist/internal/status-page.js +66 -0
- package/dist/internal/status-page.js.map +1 -0
- package/dist/internal/store-signature.d.ts +15 -0
- package/dist/internal/store-signature.d.ts.map +1 -0
- package/dist/internal/store-signature.js +114 -0
- package/dist/internal/store-signature.js.map +1 -0
- package/dist/internal/vite-plugin-compat.d.ts +2 -2
- package/dist/internal/vite-plugin-compat.js +2 -2
- package/dist/internal/web-app.d.ts +26 -0
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +79 -3
- package/dist/internal/web-app.js.map +1 -1
- package/dist/islands-swap.d.ts +41 -0
- package/dist/islands-swap.d.ts.map +1 -0
- package/dist/islands-swap.js +270 -0
- package/dist/islands-swap.js.map +1 -0
- package/dist/islands.d.ts +86 -24
- package/dist/islands.d.ts.map +1 -1
- package/dist/islands.js +138 -78
- package/dist/islands.js.map +1 -1
- package/dist/isr.d.ts +67 -13
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +247 -99
- package/dist/isr.js.map +1 -1
- package/dist/keep-alive.d.ts +147 -0
- package/dist/keep-alive.d.ts.map +1 -0
- package/dist/keep-alive.js +1102 -0
- package/dist/keep-alive.js.map +1 -0
- package/dist/live/bus.d.ts +70 -0
- package/dist/live/bus.d.ts.map +1 -0
- package/dist/live/bus.js +55 -0
- package/dist/live/bus.js.map +1 -0
- package/dist/live/core.d.ts +25 -0
- package/dist/live/core.d.ts.map +1 -0
- package/dist/live/core.js +29 -0
- package/dist/live/core.js.map +1 -0
- package/dist/live/history.d.ts +38 -0
- package/dist/live/history.d.ts.map +1 -0
- package/dist/live/history.js +104 -0
- package/dist/live/history.js.map +1 -0
- package/dist/live/hub-protocol.d.ts +46 -0
- package/dist/live/hub-protocol.d.ts.map +1 -0
- package/dist/live/hub-protocol.js +108 -0
- package/dist/live/hub-protocol.js.map +1 -0
- package/dist/live/hub.d.ts +185 -0
- package/dist/live/hub.d.ts.map +1 -0
- package/dist/live/hub.js +544 -0
- package/dist/live/hub.js.map +1 -0
- package/dist/live/keyring.d.ts +92 -0
- package/dist/live/keyring.d.ts.map +1 -0
- package/dist/live/keyring.js +265 -0
- package/dist/live/keyring.js.map +1 -0
- package/dist/live/read-capped.d.ts +6 -0
- package/dist/live/read-capped.d.ts.map +1 -0
- package/dist/live/read-capped.js +42 -0
- package/dist/live/read-capped.js.map +1 -0
- package/dist/live/revocations.d.ts +52 -0
- package/dist/live/revocations.d.ts.map +1 -0
- package/dist/live/revocations.js +116 -0
- package/dist/live/revocations.js.map +1 -0
- package/dist/live/settings.d.ts +14 -0
- package/dist/live/settings.d.ts.map +1 -0
- package/dist/live/settings.js +38 -0
- package/dist/live/settings.js.map +1 -0
- package/dist/live-client.d.ts +34 -0
- package/dist/live-client.d.ts.map +1 -0
- package/dist/live-client.js +878 -0
- package/dist/live-client.js.map +1 -0
- package/dist/live-hub.d.ts +130 -0
- package/dist/live-hub.d.ts.map +1 -0
- package/dist/live-hub.js +507 -0
- package/dist/live-hub.js.map +1 -0
- package/dist/live-redis.d.ts +60 -0
- package/dist/live-redis.d.ts.map +1 -0
- package/dist/live-redis.js +212 -0
- package/dist/live-redis.js.map +1 -0
- package/dist/live.d.ts +150 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +972 -0
- package/dist/live.js.map +1 -0
- package/dist/manifest.d.ts +113 -2
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +109 -5
- package/dist/manifest.js.map +1 -1
- package/dist/navigation.d.ts +9 -3
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +13 -3
- package/dist/navigation.js.map +1 -1
- package/dist/plugins/vite-zone-guard.d.ts +6 -0
- package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
- package/dist/plugins/vite-zone-guard.js +8 -2
- package/dist/plugins/vite-zone-guard.js.map +1 -1
- package/dist/ppr.d.ts +73 -0
- package/dist/ppr.d.ts.map +1 -0
- package/dist/ppr.js +76 -0
- package/dist/ppr.js.map +1 -0
- package/dist/query.d.ts +8 -13
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +112 -18
- package/dist/query.js.map +1 -1
- package/dist/render-seam.d.ts +32 -0
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +7 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/route-types.d.ts.map +1 -1
- package/dist/route-types.js +2 -2
- package/dist/route-types.js.map +1 -1
- package/dist/router.d.ts +72 -4
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +416 -129
- package/dist/router.js.map +1 -1
- package/dist/server-components/build.d.ts.map +1 -1
- package/dist/server-components/build.js +3 -3
- package/dist/server-components/build.js.map +1 -1
- package/dist/server-components/client.d.ts +23 -1
- package/dist/server-components/client.d.ts.map +1 -1
- package/dist/server-components/client.js +92 -5
- package/dist/server-components/client.js.map +1 -1
- package/dist/server-components/definition.d.ts.map +1 -1
- package/dist/server-components/definition.js +2 -1
- package/dist/server-components/definition.js.map +1 -1
- package/dist/server-components/hole.d.ts +8 -0
- package/dist/server-components/hole.d.ts.map +1 -0
- package/dist/server-components/hole.js +20 -0
- package/dist/server-components/hole.js.map +1 -0
- package/dist/server-components/html-policy.d.ts.map +1 -1
- package/dist/server-components/html-policy.js +62 -4
- package/dist/server-components/html-policy.js.map +1 -1
- package/dist/server-components/html-scan.d.ts +17 -0
- package/dist/server-components/html-scan.d.ts.map +1 -0
- package/dist/server-components/html-scan.js +180 -0
- package/dist/server-components/html-scan.js.map +1 -0
- package/dist/server-components/html.d.ts.map +1 -1
- package/dist/server-components/html.js +19 -4
- package/dist/server-components/html.js.map +1 -1
- package/dist/server-components/router-integration.d.ts +4 -1
- package/dist/server-components/router-integration.d.ts.map +1 -1
- package/dist/server-components/router-integration.js +23 -2
- package/dist/server-components/router-integration.js.map +1 -1
- package/dist/server-components/runtime.d.ts +6 -1
- package/dist/server-components/runtime.d.ts.map +1 -1
- package/dist/server-components/runtime.js +88 -29
- package/dist/server-components/runtime.js.map +1 -1
- package/dist/server-components/state.d.ts +3 -1
- package/dist/server-components/state.d.ts.map +1 -1
- package/dist/server-components/state.js +50 -38
- package/dist/server-components/state.js.map +1 -1
- package/dist/server-components/types.d.ts +9 -0
- package/dist/server-components/types.d.ts.map +1 -1
- package/dist/server-components/vite.d.ts.map +1 -1
- package/dist/server-components/vite.js +26 -9
- package/dist/server-components/vite.js.map +1 -1
- package/dist/vite.d.ts +5 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +58 -4
- package/dist/vite.js.map +1 -1
- package/package.json +48 -8
- package/src/boundary.ts +77 -5
- package/src/build-plan.ts +3 -0
- package/src/build-vite.ts +2 -0
- package/src/build.ts +20 -1
- package/src/client.ts +183 -23
- package/src/csp.ts +16 -2
- package/src/deferred.ts +11 -5
- package/src/dev-feed.ts +11 -2
- package/src/dev-session.ts +22 -11
- package/src/dev.ts +29 -8
- package/src/diagnostic-catalog.ts +53 -0
- package/src/diagnostic.ts +1 -0
- package/src/fn.ts +24 -7
- package/src/fs.ts +22 -8
- package/src/identity.ts +61 -0
- package/src/index.ts +20 -4
- package/src/internal/cdn/cloudflare.ts +2 -1
- package/src/internal/certified-components.ts +1 -1
- package/src/internal/codegen.ts +95 -9
- package/src/internal/dev-websocket.ts +230 -0
- package/src/internal/hole-runtime.ts +286 -0
- package/src/internal/identity-epoch.ts +239 -0
- package/src/internal/invalidation-contract.ts +35 -0
- package/src/internal/invalidation.ts +213 -0
- package/src/internal/island-bindings.ts +191 -0
- package/src/internal/island-list.ts +92 -0
- package/src/internal/island-mount.ts +68 -0
- package/src/internal/island-owner.ts +20 -0
- package/src/internal/island-resource.ts +76 -0
- package/src/internal/island-signals.ts +249 -0
- package/src/internal/keep-alive-table.ts +72 -0
- package/src/internal/live-contract.ts +111 -0
- package/src/internal/live-queries.ts +60 -0
- package/src/internal/page-events.ts +27 -0
- package/src/internal/page-execution.ts +605 -45
- package/src/internal/parity.ts +35 -6
- package/src/internal/ppr/admission.ts +300 -0
- package/src/internal/ppr/canary.ts +122 -0
- package/src/internal/ppr/compile-shell.ts +379 -0
- package/src/internal/ppr/contract.ts +265 -0
- package/src/internal/ppr/frames.ts +170 -0
- package/src/internal/ppr/integration.ts +137 -0
- package/src/internal/ppr/options.ts +221 -0
- package/src/internal/ppr/route-rules.ts +77 -0
- package/src/internal/ppr/runtime.ts +39 -0
- package/src/internal/ppr/server.ts +1766 -0
- package/src/internal/ppr/shell-store.ts +152 -0
- package/src/internal/prerendered-pages.ts +7 -4
- package/src/internal/present.ts +20 -0
- package/src/internal/query-digest.ts +34 -0
- package/src/internal/query-runtime.ts +14 -1
- package/src/internal/query-seam.ts +48 -0
- package/src/internal/render-document.ts +158 -113
- package/src/internal/request-abort.ts +26 -0
- package/src/internal/server-boundary.ts +10 -1
- package/src/internal/share-data.ts +136 -0
- package/src/internal/status-page.ts +95 -0
- package/src/internal/store-signature.ts +149 -0
- package/src/internal/vite-plugin-compat.ts +2 -2
- package/src/internal/web-app.ts +123 -3
- package/src/islands-swap.ts +283 -0
- package/src/islands.ts +169 -72
- package/src/isr.ts +348 -106
- package/src/keep-alive.ts +1354 -0
- package/src/live/bus.ts +104 -0
- package/src/live/core.ts +52 -0
- package/src/live/history.ts +109 -0
- package/src/live/hub-protocol.ts +133 -0
- package/src/live/hub.ts +620 -0
- package/src/live/keyring.ts +384 -0
- package/src/live/read-capped.ts +36 -0
- package/src/live/revocations.ts +155 -0
- package/src/live/settings.ts +46 -0
- package/src/live-client.ts +1011 -0
- package/src/live-hub.ts +658 -0
- package/src/live-redis.ts +284 -0
- package/src/live.ts +1257 -0
- package/src/manifest.ts +274 -6
- package/src/navigation.ts +14 -2
- package/src/plugins/vite-zone-guard.ts +15 -2
- package/src/ppr.ts +153 -0
- package/src/query.ts +147 -20
- package/src/render-seam.ts +37 -0
- package/src/route-types.ts +3 -1
- package/src/router.ts +534 -132
- package/src/server-components/build.ts +9 -3
- package/src/server-components/client.ts +89 -5
- package/src/server-components/definition.ts +2 -1
- package/src/server-components/hole.ts +23 -0
- package/src/server-components/html-policy.ts +63 -4
- package/src/server-components/html-scan.ts +159 -0
- package/src/server-components/html.ts +18 -4
- package/src/server-components/router-integration.ts +21 -2
- package/src/server-components/runtime.ts +114 -31
- package/src/server-components/state.ts +66 -44
- package/src/server-components/types.ts +16 -0
- package/src/server-components/vite.ts +33 -14
- package/src/vite.ts +75 -6
- package/dist/nano.d.ts +0 -125
- package/dist/nano.d.ts.map +0 -1
- package/dist/nano.js +0 -190
- package/dist/nano.js.map +0 -1
- package/src/nano.ts +0 -246
|
@@ -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,
|
|
@@ -33,15 +34,19 @@ import {
|
|
|
33
34
|
DATA_GLOBAL,
|
|
34
35
|
HANDOVER_ID,
|
|
35
36
|
LAYOUT_DATA_GLOBAL,
|
|
37
|
+
LIVE_GLOBAL,
|
|
36
38
|
LOCALES_GLOBAL,
|
|
37
39
|
ROOT_ATTRIBUTE,
|
|
38
40
|
ROUTE_GLOBAL,
|
|
41
|
+
SWAP_HEADER,
|
|
39
42
|
} from "../render-seam.ts"
|
|
40
43
|
import { PRERENDERED_GLOBAL } from "../router.ts"
|
|
41
44
|
import { trustedHeadAttributes } from "./head-attributes.ts"
|
|
42
45
|
import { isStaticMeta, mergeHeads } from "./head-merge.ts"
|
|
46
|
+
import { reportDeferredRejection } from "./request-abort.ts"
|
|
43
47
|
import { PRE_HYDRATION_GUARD } from "./runtime-contract.ts"
|
|
44
48
|
import { EXECUTABLE_SCRIPT_TYPES, INERT_SCRIPT_TYPES } from "./script-types.ts"
|
|
49
|
+
import type { HeadersLike } from "./status-page.ts"
|
|
45
50
|
|
|
46
51
|
const TEXT_ENCODER = new TextEncoder()
|
|
47
52
|
|
|
@@ -105,6 +110,11 @@ export interface RenderPageOptions {
|
|
|
105
110
|
/** An action's data return (POST only) - surfaced to the page as `actionData` + serialized
|
|
106
111
|
* for the client so post-POST hydration matches. Omit on GETs. */
|
|
107
112
|
readonly actionData?: unknown
|
|
113
|
+
/**
|
|
114
|
+
* A live runtime's seed for this page (`createWebApp({ live })` sets it): handed to the client
|
|
115
|
+
* entry, which exchanges it for a grant instead of loading the page again.
|
|
116
|
+
*/
|
|
117
|
+
readonly live?: unknown
|
|
108
118
|
/**
|
|
109
119
|
* URL of the built client entry (loaded as a module script).
|
|
110
120
|
*
|
|
@@ -171,6 +181,14 @@ export interface RenderPageOptions {
|
|
|
171
181
|
readonly matchChain?: MatchChain
|
|
172
182
|
/** HTTP status for the response (default 200; e.g. 404 for a not-found page). */
|
|
173
183
|
readonly status?: number
|
|
184
|
+
/**
|
|
185
|
+
* The slot an `x-nifra-swap` request names. When the adapter renders slots and nothing is
|
|
186
|
+
* deferred, the response is that slot's HTML alone; otherwise the whole document.
|
|
187
|
+
*/
|
|
188
|
+
readonly swapSlot?: string
|
|
189
|
+
/** Chain indices the adapter renders inside the slot wrappers of a kept page (`keepAlive`), so the
|
|
190
|
+
* browser can keep the page this document shows without remounting it. */
|
|
191
|
+
readonly slots?: readonly number[]
|
|
174
192
|
/** Extra response headers - e.g. the `cache-control` a terminal status page wants. `content-type`
|
|
175
193
|
* is ignored (the document is always HTML) and the ISR freshness header is applied after these,
|
|
176
194
|
* so neither can be overridden from here. */
|
|
@@ -205,6 +223,11 @@ export interface RenderPageOptions {
|
|
|
205
223
|
/** CSP nonce applied to every framework-owned executable script in this document. Nonce-bearing
|
|
206
224
|
* documents are marked `private, no-store` so a request-specific nonce is never replayed. */
|
|
207
225
|
readonly nonce?: string
|
|
226
|
+
/** The request's abort signal. A deferred value that rejects because it aborted is the client
|
|
227
|
+
* leaving, not a failure, so the server log skips it. Omit ⇒ every rejection is logged. */
|
|
228
|
+
readonly signal?: AbortSignal
|
|
229
|
+
/** Where a rejected deferred value is logged: the app's error log. Omit ⇒ the console. */
|
|
230
|
+
readonly logError?: ((error: unknown) => void) | undefined
|
|
208
231
|
/**
|
|
209
232
|
* A hash-based Content-Security-Policy from `createCspPolicy`. The document then carries `nonce`
|
|
210
233
|
* only when it needs one (it defers a value, or a head tag names the nonce); otherwise it renders
|
|
@@ -262,6 +285,18 @@ export function renderPage(options: RenderPageInput): MaybePromise<Response> {
|
|
|
262
285
|
return page instanceof Promise ? page.then((p) => p.toResponse()) : page.toResponse()
|
|
263
286
|
}
|
|
264
287
|
|
|
288
|
+
// Every option of an adapter call, for a render with slot wrappers (only a keep-alive app's).
|
|
289
|
+
const streamOptions = (
|
|
290
|
+
nonce: string | undefined,
|
|
291
|
+
parts: DocumentParts | undefined,
|
|
292
|
+
slots: readonly number[],
|
|
293
|
+
): RenderStreamOptions => {
|
|
294
|
+
const options: { -readonly [K in keyof RenderStreamOptions]: RenderStreamOptions[K] } = { slots }
|
|
295
|
+
if (nonce !== undefined) options.nonce = nonce
|
|
296
|
+
if (parts !== undefined) options.parts = parts
|
|
297
|
+
return options
|
|
298
|
+
}
|
|
299
|
+
|
|
265
300
|
export function renderPageResult(options: RenderPageInput): MaybePromise<RenderedPage> {
|
|
266
301
|
const { csp } = options
|
|
267
302
|
if (csp !== undefined) {
|
|
@@ -388,6 +423,8 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
388
423
|
options.serverComponents === undefined
|
|
389
424
|
? ""
|
|
390
425
|
: `,"__NIFRA_COMPONENTS__":${serializeData(options.serverComponents)}`
|
|
426
|
+
const liveTail =
|
|
427
|
+
options.live === undefined ? "" : `,"${LIVE_GLOBAL}":${serializeData(options.live)}`
|
|
391
428
|
const deferredRuntime =
|
|
392
429
|
allDeferred.length > 0 ? `<script${nonceAttr}>${DEFERRED_RUNTIME}</script>` : ""
|
|
393
430
|
// The regex only injects the CSP nonce into the (constant) hydration head; with no nonce it's a
|
|
@@ -500,7 +537,7 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
500
537
|
// Tail - the page-state handover + the client module. Module scripts defer (run after parse), so
|
|
501
538
|
// the handover + every streamed deferred resolution are in the document before the entry hydrates.
|
|
502
539
|
const tailHtml = hydrate
|
|
503
|
-
? `${slot.tailPre}${serializeData(forClient)}${action}${layoutTail}${boundaryTail}${componentTail}${slot.tailPost}`
|
|
540
|
+
? `${slot.tailPre}${serializeData(forClient)}${action}${layoutTail}${boundaryTail}${componentTail}${liveTail}${slot.tailPost}`
|
|
504
541
|
: (slot.tailPost as string)
|
|
505
542
|
const headers: Record<string, string> = { "content-type": "text/html; charset=utf-8" }
|
|
506
543
|
if (locale !== undefined) headers["content-language"] = lang
|
|
@@ -512,6 +549,8 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
512
549
|
if (name !== "content-type") headers[name] = value
|
|
513
550
|
}
|
|
514
551
|
}
|
|
552
|
+
if (adapter.renderSlot !== undefined)
|
|
553
|
+
headers.vary = headers.vary === undefined ? SWAP_HEADER : `${headers.vary}, ${SWAP_HEADER}`
|
|
515
554
|
// ISR: advertise the route's freshness so a `withISR` wrapper can set this page's cache TTL. A
|
|
516
555
|
// dedicated header (not the action-revalidation `x-nifra-revalidate`) so the TTL channel never aliases
|
|
517
556
|
// the client's path-list channel.
|
|
@@ -551,6 +590,17 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
551
590
|
...(locale === undefined ? {} : { locale }),
|
|
552
591
|
}
|
|
553
592
|
|
|
593
|
+
const { swapSlot, ...documentOptions } = options
|
|
594
|
+
const slots = options.slots
|
|
595
|
+
if (swapSlot !== undefined && allDeferred.length === 0 && adapter.renderSlot !== undefined) {
|
|
596
|
+
const slotPage = (slotHtml: string | undefined): MaybePromise<RenderedPage> =>
|
|
597
|
+
slotHtml === undefined
|
|
598
|
+
? renderPageResult(documentOptions)
|
|
599
|
+
: new BufferedRenderedPage(slotHtml, status, swapHeaders(headers, swapSlot))
|
|
600
|
+
const out = adapter.renderSlot(chain, renderProps, swapSlot)
|
|
601
|
+
return out instanceof Promise ? out.then(slotPage) : slotPage(out)
|
|
602
|
+
}
|
|
603
|
+
|
|
554
604
|
// Fast path: nothing `defer()`s and the adapter can render synchronously to a string → buffer the
|
|
555
605
|
// whole document in one pass. Progressive streaming only benefits pages with deferred boundaries
|
|
556
606
|
// (those take the streaming path below); for a plain page the streaming pipeline + the framework's
|
|
@@ -571,6 +621,9 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
571
621
|
headers,
|
|
572
622
|
streamNonce,
|
|
573
623
|
streamNonce === nonce ? nonceAttr : ` nonce="${escapeAttr(streamNonce ?? "")}"`,
|
|
624
|
+
options.signal,
|
|
625
|
+
options.slots,
|
|
626
|
+
options.logError,
|
|
574
627
|
).then((response) => new ResponseRenderedPage(response))
|
|
575
628
|
const last = chain[chain.length - 1]
|
|
576
629
|
const leaf = isWeakKey(last) ? last : undefined
|
|
@@ -596,15 +649,17 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
596
649
|
return streamed(renderNonce)
|
|
597
650
|
}
|
|
598
651
|
const out =
|
|
599
|
-
|
|
600
|
-
? adapter.renderToString(
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
:
|
|
652
|
+
slots !== undefined
|
|
653
|
+
? adapter.renderToString(chain, renderProps, streamOptions(renderNonce, parts, slots))
|
|
654
|
+
: parts !== undefined
|
|
655
|
+
? adapter.renderToString(
|
|
656
|
+
chain,
|
|
657
|
+
renderProps,
|
|
658
|
+
renderNonce === undefined ? { parts } : { nonce: renderNonce, parts },
|
|
659
|
+
)
|
|
660
|
+
: renderNonce === undefined
|
|
661
|
+
? adapter.renderToString(chain, renderProps)
|
|
662
|
+
: adapter.renderToString(chain, renderProps, { nonce: renderNonce })
|
|
608
663
|
const page = (bodyHtml: string | undefined): MaybePromise<RenderedPage> => {
|
|
609
664
|
if (bodyHtml === undefined) {
|
|
610
665
|
// The render suspended on something only the streaming renderer waits for (React's `use`
|
|
@@ -643,6 +698,9 @@ async function renderStreamedPage(
|
|
|
643
698
|
headers: Record<string, string>,
|
|
644
699
|
nonce: string | undefined,
|
|
645
700
|
nonceAttr: string,
|
|
701
|
+
signal: AbortSignal | undefined,
|
|
702
|
+
slots: readonly number[] | undefined,
|
|
703
|
+
logError: ((error: unknown) => void) | undefined,
|
|
646
704
|
): Promise<Response> {
|
|
647
705
|
// Streaming path - required for `defer()` (progressive `<Await>` resolution) and used by any adapter
|
|
648
706
|
// that doesn't implement `renderToString`. Awaiting `renderToStream` resolves on shell-readiness
|
|
@@ -652,21 +710,54 @@ async function renderStreamedPage(
|
|
|
652
710
|
const closeRoot = enc.encode(closeRootHtml)
|
|
653
711
|
const tail = enc.encode(tailHtml)
|
|
654
712
|
const appStream =
|
|
655
|
-
|
|
656
|
-
? await adapter.renderToStream(
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
:
|
|
713
|
+
slots !== undefined
|
|
714
|
+
? await adapter.renderToStream(chain, renderProps, streamOptions(nonce, parts, slots))
|
|
715
|
+
: parts !== undefined
|
|
716
|
+
? await adapter.renderToStream(
|
|
717
|
+
chain,
|
|
718
|
+
renderProps,
|
|
719
|
+
nonce === undefined ? { parts } : { nonce, parts },
|
|
720
|
+
)
|
|
721
|
+
: nonce === undefined
|
|
722
|
+
? await adapter.renderToStream(chain, renderProps)
|
|
723
|
+
: await adapter.renderToStream(chain, renderProps, { nonce })
|
|
664
724
|
// After the render: an adapter fills its document parts before handing back the stream.
|
|
665
725
|
const shell = enc.encode(shellOf())
|
|
666
|
-
const body = streamDocument(
|
|
726
|
+
const body = streamDocument(
|
|
727
|
+
shell,
|
|
728
|
+
appStream,
|
|
729
|
+
closeRoot,
|
|
730
|
+
allDeferred,
|
|
731
|
+
tail,
|
|
732
|
+
enc,
|
|
733
|
+
nonceAttr,
|
|
734
|
+
signal,
|
|
735
|
+
logError,
|
|
736
|
+
)
|
|
667
737
|
return new Response(body, { status, headers })
|
|
668
738
|
}
|
|
669
739
|
|
|
740
|
+
// A swap response shares its URL with the document, so like a data response it is never stored: a
|
|
741
|
+
// cache keyed on the URL alone would serve one slot in place of the page. A slot joins a page that
|
|
742
|
+
// already runs under its own policy, so the CSP header belongs to the document alone.
|
|
743
|
+
const DOCUMENT_ONLY_HEADERS = new Set([
|
|
744
|
+
ISR_REVALIDATE_HEADER,
|
|
745
|
+
ISR_REVALIDATE_TAGS_HEADER,
|
|
746
|
+
"content-security-policy",
|
|
747
|
+
])
|
|
748
|
+
|
|
749
|
+
function swapHeaders(
|
|
750
|
+
document: Readonly<Record<string, string>>,
|
|
751
|
+
slot: string,
|
|
752
|
+
): Record<string, string> {
|
|
753
|
+
const headers: Record<string, string> = {}
|
|
754
|
+
for (const [name, value] of Object.entries(document))
|
|
755
|
+
if (!DOCUMENT_ONLY_HEADERS.has(name)) headers[name] = value
|
|
756
|
+
headers["cache-control"] = "private, no-store"
|
|
757
|
+
headers[SWAP_HEADER] = slot
|
|
758
|
+
return headers
|
|
759
|
+
}
|
|
760
|
+
|
|
670
761
|
class BufferedRenderedPage implements RenderedPage {
|
|
671
762
|
readonly [RESPONSE_RESULT] = true
|
|
672
763
|
private readonly body: string
|
|
@@ -732,6 +823,8 @@ function streamDocument(
|
|
|
732
823
|
tail: Uint8Array,
|
|
733
824
|
enc: TextEncoder,
|
|
734
825
|
nonceAttr: string,
|
|
826
|
+
signal: AbortSignal | undefined,
|
|
827
|
+
logError: ((error: unknown) => void) | undefined,
|
|
735
828
|
): ReadableStream<Uint8Array> {
|
|
736
829
|
const reader = stream.getReader()
|
|
737
830
|
// A client that goes away cancels the body: the renderer stops, and nothing more is written.
|
|
@@ -756,7 +849,7 @@ function streamDocument(
|
|
|
756
849
|
// A rejected (or unserializable) deferred streams __nifraReject (the client `<Await>`
|
|
757
850
|
// surfaces it) - it must not break the whole body. Redact: stream a stable opaque code,
|
|
758
851
|
// never the raw error text; log the real reason server-side.
|
|
759
|
-
|
|
852
|
+
reportDeferredRejection(signal, err, logError)
|
|
760
853
|
return `<script${nonceAttr}>window.__nifraReject(${d.id},${serializeData(DEFERRED_ERROR_CODE)})</script>`
|
|
761
854
|
}),
|
|
762
855
|
)
|
|
@@ -857,24 +950,6 @@ export function redirect(location: string, options: RedirectOptions = {}): Respo
|
|
|
857
950
|
})
|
|
858
951
|
}
|
|
859
952
|
|
|
860
|
-
/**
|
|
861
|
-
* Brand marking a `Response` as a **terminal-status signal** rather than a response to serve verbatim.
|
|
862
|
-
*
|
|
863
|
-
* A loader that throws a bare `Response` has always been passed straight through - that is how
|
|
864
|
-
* `throw redirect(...)` works, and apps rely on it. So "render the `_404` boundary at 404" cannot be
|
|
865
|
-
* expressed by throwing a plain `Response`: the framework would have no way to tell it apart from a
|
|
866
|
-
* hand-rolled body the app wants served exactly as written. The brand is that distinction, and
|
|
867
|
-
* checking it *before* the pass-through is what keeps every existing throw working unchanged.
|
|
868
|
-
*
|
|
869
|
-
* `Symbol.for` matches `RESPONSE_RESULT` above: a registry symbol survives two copies of this module,
|
|
870
|
-
* which a `unique symbol` would not - and a duplicate-install turning a 404 into a raw 404 body would
|
|
871
|
-
* be a maddening bug to trace.
|
|
872
|
-
*/
|
|
873
|
-
/** Derived from the `Headers` constructor rather than the DOM lib's `HeadersInit`: `@nifrajs/web`
|
|
874
|
-
* is consumed from DOM-free programs (the root tsconfig runs `types: ["bun"]`), and naming the DOM
|
|
875
|
-
* type here would break their build. Same workaround as `SSEInit.headers` in core. */
|
|
876
|
-
export type HeadersLike = ConstructorParameters<typeof Headers>[0]
|
|
877
|
-
|
|
878
953
|
/**
|
|
879
954
|
* The signatures a **duplicate module instance** produces during SSR, across engines.
|
|
880
955
|
*
|
|
@@ -977,8 +1052,6 @@ export const layoutErrorId = (err: unknown): string | undefined =>
|
|
|
977
1052
|
? ((err as Record<symbol, unknown>)[LAYOUT_ERROR_ID] as string | undefined)
|
|
978
1053
|
: undefined
|
|
979
1054
|
|
|
980
|
-
export const STATUS_SIGNAL = Symbol.for("nifra.web.status-signal")
|
|
981
|
-
|
|
982
1055
|
/** Reason phrases for the plain-text fallback, used only when the app authored no `_<status>` and no
|
|
983
1056
|
* `_404` page. Deliberately not exhaustive - anything unlisted falls back to "Error", which is more
|
|
984
1057
|
* useful than shipping a table of every RFC status for a body almost nobody will see. */
|
|
@@ -993,84 +1066,49 @@ export const STATUS_TEXT: Readonly<Record<number, string>> = {
|
|
|
993
1066
|
500: "Internal Server Error",
|
|
994
1067
|
}
|
|
995
1068
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
export
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
`[nifra/web] statusPage(${JSON.stringify(status)}) must be an integer 4xx or 5xx status. Use redirect() for 3xx, and return data for a successful render.`,
|
|
1008
|
-
)
|
|
1069
|
+
/** The wrapper `revalidate()` returns: the action's `data` plus the paths it changed. A plain tagged
|
|
1070
|
+
* shape (not a class) so `@nifrajs/client`'s `ActionData` can unwrap it structurally without importing
|
|
1071
|
+
* from `@nifrajs/web`. `createWebApp` strips the wrapper - the client receives `data` as the body and
|
|
1072
|
+
* the paths via the `X-Nifra-Revalidate` header. */
|
|
1073
|
+
export interface RevalidateResult<T> {
|
|
1074
|
+
readonly __nifraRevalidate: readonly string[]
|
|
1075
|
+
readonly data: T
|
|
1076
|
+
/** What else the write made stale: cache tags and live keys. */
|
|
1077
|
+
readonly __nifraInvalidate?: {
|
|
1078
|
+
readonly tags: readonly string[]
|
|
1079
|
+
readonly keys: readonly string[]
|
|
1009
1080
|
}
|
|
1010
|
-
const response = new Response(null, { status })
|
|
1011
|
-
// Non-enumerable so the brand never lands in a structured clone or a JSON log of the response.
|
|
1012
|
-
Object.defineProperty(response, STATUS_SIGNAL, {
|
|
1013
|
-
value: { status, ...(options.headers !== undefined ? { headers: options.headers } : {}) },
|
|
1014
|
-
enumerable: false,
|
|
1015
|
-
})
|
|
1016
|
-
throw response
|
|
1017
1081
|
}
|
|
1018
1082
|
|
|
1019
|
-
/**
|
|
1020
|
-
export
|
|
1021
|
-
|
|
1022
|
-
* Extra headers for the rendered response - `cache-control` above all.
|
|
1023
|
-
*
|
|
1024
|
-
* The defaults differ by status for a reason: a 404 may be a race with publication, so it wants a
|
|
1025
|
-
* short TTL, while a 410 is a promise that the URL is permanently gone and can be cached hard.
|
|
1026
|
-
* Getting this wrong is the difference between a crawler dropping a dead URL and re-fetching it
|
|
1027
|
-
* for weeks.
|
|
1028
|
-
*/
|
|
1029
|
-
readonly headers?: HeadersLike
|
|
1083
|
+
/** `value` is what {@link revalidate} returned. */
|
|
1084
|
+
export function isRevalidateResult(value: unknown): value is RevalidateResult<unknown> {
|
|
1085
|
+
return typeof value === "object" && value !== null && "__nifraRevalidate" in value
|
|
1030
1086
|
}
|
|
1031
1087
|
|
|
1032
|
-
/**
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
* **200**. That looks correct in a browser and is invisible in review, which is why it ships and stays
|
|
1039
|
-
* shipped - while search engines penalise it and keep the dead URL indexed.
|
|
1040
|
-
*
|
|
1041
|
-
* Returns `never`: these throw, so a loader narrows without a redundant `return`, and the type states
|
|
1042
|
-
* the thing the "loaders `throw` redirect, actions `return` it" rule already trips people on.
|
|
1043
|
-
*/
|
|
1044
|
-
export function notFound(options: StatusPageOptions = {}): never {
|
|
1045
|
-
return statusSignal(404, options)
|
|
1088
|
+
/** Cache tags and live keys an action's write made stale, beside its paths. */
|
|
1089
|
+
export interface RevalidateOptions {
|
|
1090
|
+
/** Cache tags: data caches, the page store and the CDN drop entries carrying them; live pages watching a tag as a key reload. */
|
|
1091
|
+
readonly tags?: readonly string[]
|
|
1092
|
+
/** Live keys (`ctx.live`): every open page watching one reloads it. */
|
|
1093
|
+
readonly keys?: readonly string[]
|
|
1046
1094
|
}
|
|
1047
1095
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
* was deliberately removed - a withdrawn listing, a deleted post.
|
|
1051
|
-
*
|
|
1052
|
-
* 410 is not a pedantic 404: it tells a crawler to **drop** the URL rather than re-fetch it for weeks
|
|
1053
|
-
* on the assumption the 404 was transient. Uses `_410.tsx` if the app has one, otherwise `_404`.
|
|
1054
|
-
*/
|
|
1055
|
-
export function gone(options: StatusPageOptions = {}): never {
|
|
1056
|
-
return statusSignal(410, options)
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
/**
|
|
1060
|
-
* Render a terminal page at any 4xx/5xx status - the escape hatch behind {@link notFound} and
|
|
1061
|
-
* {@link gone} (402, 451, …). Uses `_<status>.tsx` if present, otherwise `_404`.
|
|
1062
|
-
*/
|
|
1063
|
-
export function statusPage(status: number, options: StatusPageOptions = {}): never {
|
|
1064
|
-
return statusSignal(status, options)
|
|
1065
|
-
}
|
|
1096
|
+
const INVALIDATION_NAME = /^[A-Za-z][A-Za-z0-9._:/-]{0,127}$/
|
|
1097
|
+
const MAX_INVALIDATION_NAMES = 64
|
|
1066
1098
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1099
|
+
function invalidationNames(names: readonly string[] | undefined, kind: string): readonly string[] {
|
|
1100
|
+
if (names === undefined) return []
|
|
1101
|
+
if (!Array.isArray(names) || names.length > MAX_INVALIDATION_NAMES) {
|
|
1102
|
+
throw new TypeError(`[nifra/web] revalidate() takes at most ${MAX_INVALIDATION_NAMES} ${kind}`)
|
|
1103
|
+
}
|
|
1104
|
+
for (const name of names) {
|
|
1105
|
+
if (typeof name !== "string" || !INVALIDATION_NAME.test(name)) {
|
|
1106
|
+
throw new TypeError(
|
|
1107
|
+
`[nifra/web] revalidate() ${kind} are a letter followed by up to 127 of A-Z a-z 0-9 . _ : / -`,
|
|
1108
|
+
)
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return [...new Set(names)]
|
|
1074
1112
|
}
|
|
1075
1113
|
|
|
1076
1114
|
/**
|
|
@@ -1080,8 +1118,15 @@ export interface RevalidateResult<T> {
|
|
|
1080
1118
|
* so a mutation can refresh views beyond the one that was submitted. `data` is still surfaced to the
|
|
1081
1119
|
* component as `actionData` (the wrapper is transparent to `ActionData<typeof action>`).
|
|
1082
1120
|
*/
|
|
1083
|
-
export function revalidate<T>(
|
|
1084
|
-
|
|
1121
|
+
export function revalidate<T>(
|
|
1122
|
+
paths: readonly string[],
|
|
1123
|
+
data: T,
|
|
1124
|
+
options?: RevalidateOptions,
|
|
1125
|
+
): RevalidateResult<T> {
|
|
1126
|
+
if (options === undefined) return { __nifraRevalidate: paths, data }
|
|
1127
|
+
const tags = invalidationNames(options.tags, "tags")
|
|
1128
|
+
const keys = invalidationNames(options.keys, "keys")
|
|
1129
|
+
return { __nifraRevalidate: paths, data, __nifraInvalidate: { tags, keys } }
|
|
1085
1130
|
}
|
|
1086
1131
|
|
|
1087
1132
|
/**
|
|
@@ -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. */
|
|
@@ -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
|
+
}
|