@nifrajs/web 5.5.0 → 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 +162 -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
package/dist/isr.js
CHANGED
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
import { isAuthenticatedRequest, writableResponse } from "@nifrajs/core/server";
|
|
10
10
|
import { readBoundedBytes, TransportCodecError } from "@nifrajs/core/transport-codec";
|
|
11
11
|
import { isDraftEnabled } from "./draft.js";
|
|
12
|
+
import { runInvalidation, settleInvalidation, } from "./internal/invalidation.js";
|
|
13
|
+
import { INVALIDATION } from "./internal/invalidation-contract.js";
|
|
14
|
+
import { isStoreSecret, MIN_STORE_SECRET_LENGTH, storeSigner, } from "./internal/store-signature.js";
|
|
12
15
|
import { assertTokenSecret, timingSafeEqual } from "./internal/timing-safe-equal.js";
|
|
16
|
+
import { SWAP_HEADER } from "./render-seam.js";
|
|
13
17
|
const ISR_TAG_PATTERN = /^[A-Za-z][A-Za-z0-9._:/-]{0,127}$/;
|
|
14
18
|
const MAX_ISR_TAGS = 32;
|
|
15
19
|
function normalizeTag(tag) {
|
|
@@ -73,6 +77,8 @@ export function routeTags(declared, input, routeId) {
|
|
|
73
77
|
}
|
|
74
78
|
return [...kept];
|
|
75
79
|
}
|
|
80
|
+
// Nothing outside this process can write these, so `withISR` keeps their pages unsigned.
|
|
81
|
+
const inProcessStores = new WeakSet();
|
|
76
82
|
/**
|
|
77
83
|
* In-process ISR cache. Refuses to run in production unless explicitly allowed (mirrors the
|
|
78
84
|
* rate-limit `MemoryStore` - a per-instance cache is unsafe across instances). Bounded **LRU**: a
|
|
@@ -100,6 +106,7 @@ export class MemoryCacheStore {
|
|
|
100
106
|
if (!Number.isSafeInteger(this.max) || this.max <= 0) {
|
|
101
107
|
throw new RangeError("[nifra/web] MemoryCacheStore max must be a positive safe integer");
|
|
102
108
|
}
|
|
109
|
+
inProcessStores.add(this);
|
|
103
110
|
}
|
|
104
111
|
get(key) {
|
|
105
112
|
const row = this.cache.get(key);
|
|
@@ -243,26 +250,14 @@ const isCachedResponse = (value) => {
|
|
|
243
250
|
return true;
|
|
244
251
|
};
|
|
245
252
|
const KV_TAG_EPOCH_PREFIX = "__nifra_isr_tag_v1:";
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const at = parsed[KV_PURGED_AT];
|
|
255
|
-
if (typeof at === "number")
|
|
256
|
-
return at;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
catch {
|
|
260
|
-
// Not a marker.
|
|
261
|
-
}
|
|
262
|
-
return Number.NEGATIVE_INFINITY;
|
|
263
|
-
}
|
|
264
|
-
/** When a tag epoch was written (`<time>:<uuid>`); an epoch from before epochs carried a time is older
|
|
265
|
-
* than any render. */
|
|
253
|
+
const KV_PATH_EPOCH_PREFIX = "__nifra_isr_path_v1:";
|
|
254
|
+
const KV_PATH_DIGEST_PREFIX = "__nifra_isr_path_v1#";
|
|
255
|
+
/** Cloudflare KV's key limit, in UTF-8 bytes. */
|
|
256
|
+
const KV_KEY_LIMIT = 512;
|
|
257
|
+
/** How long (seconds) a path epoch outlives the store's `expirationTtl`. */
|
|
258
|
+
const PATH_EPOCH_SLACK = 86_400;
|
|
259
|
+
/** When a tag or path epoch was written (`<time>:<uuid>`); an epoch from before epochs carried a time
|
|
260
|
+
* is older than any render. */
|
|
266
261
|
function kvEpochTime(epoch) {
|
|
267
262
|
const at = epoch.indexOf(":");
|
|
268
263
|
const time = at === -1 ? Number.NaN : Number(epoch.slice(0, at));
|
|
@@ -282,7 +277,8 @@ const kvClock = () => {
|
|
|
282
277
|
* binding) - the production-grade shared/durable store ISR wants: cached pages and on-demand purges
|
|
283
278
|
* hold *across* worker instances (unlike the per-instance {@link MemoryCacheStore}). Entries serialize
|
|
284
279
|
* to JSON; every read is validated before it's trusted (a malformed/version-skewed entry is treated as
|
|
285
|
-
* a miss). Construct it in your Workers `fetch` from the binding: `new KVCacheStore(env.ISR_CACHE)
|
|
280
|
+
* a miss). Construct it in your Workers `fetch` from the binding: `new KVCacheStore(env.ISR_CACHE)`,
|
|
281
|
+
* and give `withISR` the `storeSecret` it signs these pages with.
|
|
286
282
|
*
|
|
287
283
|
* Cloudflare is the default, not a requirement: {@link KVNamespaceLike} is three structural methods,
|
|
288
284
|
* so a Redis, Deno KV, or Upstash binding satisfies it (pass `minExpirationTtl` for their TTL floor).
|
|
@@ -292,6 +288,7 @@ const kvClock = () => {
|
|
|
292
288
|
export class KVCacheStore {
|
|
293
289
|
kv;
|
|
294
290
|
putOptions;
|
|
291
|
+
pathEpochOptions;
|
|
295
292
|
constructor(kv, options = {}) {
|
|
296
293
|
const floor = options.minExpirationTtl ?? 60;
|
|
297
294
|
if (!Number.isSafeInteger(floor) || floor < 0) {
|
|
@@ -312,9 +309,16 @@ export class KVCacheStore {
|
|
|
312
309
|
// Precompute the put options object once: omit it entirely when no TTL (KV then never expires).
|
|
313
310
|
this.putOptions =
|
|
314
311
|
options.expirationTtl !== undefined ? { expirationTtl: options.expirationTtl } : undefined;
|
|
312
|
+
// An expired path epoch would make pages stored under the one it replaced read as current again.
|
|
313
|
+
// Those were stored before the purge reached their location, so they expire within the TTL plus
|
|
314
|
+
// that delay; without a TTL they never expire, and neither may the epoch.
|
|
315
|
+
this.pathEpochOptions =
|
|
316
|
+
options.expirationTtl !== undefined
|
|
317
|
+
? { expirationTtl: options.expirationTtl + PATH_EPOCH_SLACK }
|
|
318
|
+
: undefined;
|
|
315
319
|
}
|
|
316
320
|
async get(key) {
|
|
317
|
-
const raw = await this.kv.get(key);
|
|
321
|
+
const [raw, pathEpoch] = await Promise.all([this.kv.get(key), this.pathEpoch(key)]);
|
|
318
322
|
if (raw === null)
|
|
319
323
|
return undefined;
|
|
320
324
|
let parsed;
|
|
@@ -327,47 +331,54 @@ export class KVCacheStore {
|
|
|
327
331
|
}
|
|
328
332
|
if (!isCachedResponse(parsed))
|
|
329
333
|
return undefined;
|
|
330
|
-
const
|
|
334
|
+
const { __nifraTagEpochs: epochs, __nifraPathEpoch: storedPathEpoch, ...value } = parsed;
|
|
335
|
+
// A page another location stored before this path's purge reached it keeps the older epoch.
|
|
336
|
+
if ((storedPathEpoch ?? "0") !== pathEpoch)
|
|
337
|
+
return undefined;
|
|
338
|
+
const tags = normalizeTags(value.tags);
|
|
331
339
|
if (tags.length === 0)
|
|
332
|
-
return
|
|
333
|
-
const epochs = parsed.__nifraTagEpochs;
|
|
340
|
+
return value;
|
|
334
341
|
if (epochs === undefined)
|
|
335
342
|
return undefined;
|
|
336
343
|
const current = await Promise.all(tags.map(async (tag) => [tag, (await this.kv.get(this.tagKey(tag))) ?? "0"]));
|
|
337
344
|
if (current.some(([tag, epoch]) => epochs[tag] !== epoch))
|
|
338
345
|
return undefined;
|
|
339
|
-
const { __nifraTagEpochs: _ignored, ...value } = parsed;
|
|
340
346
|
return value;
|
|
341
347
|
}
|
|
342
|
-
/** Wall-clock time, so purges on any instance order against it. KV is eventually consistent, so
|
|
343
|
-
*
|
|
348
|
+
/** Wall-clock time, so purges on any instance order against it. KV is eventually consistent, so
|
|
349
|
+
* `set` refuses only purges this location has already seen; a page stored before a purge reached
|
|
350
|
+
* its location keeps the older epoch and reads as a miss once the purge arrives. */
|
|
344
351
|
fence() {
|
|
345
352
|
return kvClock();
|
|
346
353
|
}
|
|
347
354
|
async set(key, value) {
|
|
348
355
|
const { fence, ...entry } = value;
|
|
349
|
-
if (fence !== undefined && kvPurgedAt(await this.kv.get(key)) > fence)
|
|
350
|
-
return;
|
|
351
356
|
const tags = normalizeTags(entry.tags);
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
357
|
+
const [pathEpoch, ...tagEpochs] = await Promise.all([
|
|
358
|
+
this.pathEpoch(key),
|
|
359
|
+
...tags.map(async (tag) => (await this.kv.get(this.tagKey(tag))) ?? "0"),
|
|
360
|
+
]);
|
|
361
|
+
if (fence !== undefined &&
|
|
362
|
+
[pathEpoch, ...tagEpochs].some((epoch) => kvEpochTime(epoch) > fence)) {
|
|
358
363
|
return;
|
|
359
364
|
}
|
|
360
365
|
const stored = {
|
|
361
366
|
...entry,
|
|
362
|
-
tags
|
|
363
|
-
|
|
367
|
+
...(tags.length === 0
|
|
368
|
+
? {}
|
|
369
|
+
: {
|
|
370
|
+
tags,
|
|
371
|
+
__nifraTagEpochs: Object.fromEntries(tags.map((tag, at) => [tag, tagEpochs[at]])),
|
|
372
|
+
}),
|
|
373
|
+
...(pathEpoch === "0" ? {} : { __nifraPathEpoch: pathEpoch }),
|
|
364
374
|
};
|
|
365
375
|
await this.kv.put(key, JSON.stringify(stored), this.putOptions);
|
|
366
376
|
}
|
|
367
|
-
/**
|
|
368
|
-
*
|
|
377
|
+
/** Moves the path's epoch and leaves the page: a page stored under the old epoch reads as a miss,
|
|
378
|
+
* even one another location stored before this purge reached it, and `set` refuses a render begun
|
|
379
|
+
* before the purge. */
|
|
369
380
|
async delete(key) {
|
|
370
|
-
await this.kv.put(key,
|
|
381
|
+
await this.kv.put(await this.pathKey(key), `${kvClock()}:${crypto.randomUUID()}`, this.pathEpochOptions);
|
|
371
382
|
}
|
|
372
383
|
async invalidateTag(tag) {
|
|
373
384
|
normalizeTag(tag);
|
|
@@ -379,6 +390,21 @@ export class KVCacheStore {
|
|
|
379
390
|
tagKey(tag) {
|
|
380
391
|
return `${KV_TAG_EPOCH_PREFIX}${encodeURIComponent(tag)}`;
|
|
381
392
|
}
|
|
393
|
+
/** The page key under a prefix, or its digest when that would pass KV's key limit. */
|
|
394
|
+
async pathKey(key) {
|
|
395
|
+
const named = `${KV_PATH_EPOCH_PREFIX}${key}`;
|
|
396
|
+
// A UTF-16 unit is at most three UTF-8 bytes, so most keys skip the encode.
|
|
397
|
+
if (named.length * 3 <= KV_KEY_LIMIT)
|
|
398
|
+
return named;
|
|
399
|
+
const encoder = new TextEncoder();
|
|
400
|
+
if (encoder.encode(named).byteLength <= KV_KEY_LIMIT)
|
|
401
|
+
return named;
|
|
402
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(key)));
|
|
403
|
+
return `${KV_PATH_DIGEST_PREFIX}${Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
404
|
+
}
|
|
405
|
+
async pathEpoch(key) {
|
|
406
|
+
return (await this.kv.get(await this.pathKey(key))) ?? "0";
|
|
407
|
+
}
|
|
382
408
|
}
|
|
383
409
|
/** Response header marking how an ISR response was served: a cache `hit` (fresh), `stale` (served +
|
|
384
410
|
* regenerating behind it), or `miss` (rendered now + stored). Useful for debugging + tests. */
|
|
@@ -412,6 +438,7 @@ export function withoutCacheChannel(res) {
|
|
|
412
438
|
return target;
|
|
413
439
|
}
|
|
414
440
|
const STORE_FAILED = Symbol("nifra.isr.store-failed");
|
|
441
|
+
const MAC_HEADER = "x-nifra-isr-mac";
|
|
415
442
|
const STORE_REPORT_INTERVAL_MS = 60_000;
|
|
416
443
|
const STORE_REPORT_DETAIL_CHARS = 200;
|
|
417
444
|
/** The error's own words with the cache key cut out, since a store may name the key it failed on. */
|
|
@@ -431,6 +458,68 @@ function storeErrorDetail(error, key) {
|
|
|
431
458
|
? `${redacted.slice(0, STORE_REPORT_DETAIL_CHARS)}...`
|
|
432
459
|
: redacted;
|
|
433
460
|
}
|
|
461
|
+
/**
|
|
462
|
+
* What a page's MAC covers: everything a replay serves, plus the key, so a page copied to another key
|
|
463
|
+
* never verifies there. Header names and tags are sorted, since a store need not keep their order.
|
|
464
|
+
*/
|
|
465
|
+
function signedParts(key, entry) {
|
|
466
|
+
const names = Object.keys(entry.headers)
|
|
467
|
+
.filter((name) => name !== MAC_HEADER)
|
|
468
|
+
.sort();
|
|
469
|
+
const headers = names.map((name) => [name, entry.headers[name]]);
|
|
470
|
+
const tags = entry.tags === undefined ? [] : [...entry.tags].sort();
|
|
471
|
+
const meta = JSON.stringify([
|
|
472
|
+
1,
|
|
473
|
+
key,
|
|
474
|
+
entry.status,
|
|
475
|
+
entry.storedAt,
|
|
476
|
+
entry.revalidate,
|
|
477
|
+
headers,
|
|
478
|
+
tags,
|
|
479
|
+
]);
|
|
480
|
+
return [meta, entry.body];
|
|
481
|
+
}
|
|
482
|
+
async function signed(signer, key, entry) {
|
|
483
|
+
const mac = await signer.sign(signedParts(key, entry));
|
|
484
|
+
return { ...entry, headers: { ...entry.headers, [MAC_HEADER]: mac } };
|
|
485
|
+
}
|
|
486
|
+
async function verified(signer, key, entry) {
|
|
487
|
+
try {
|
|
488
|
+
return await signer.verify(signedParts(key, entry), entry.headers[MAC_HEADER]);
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
// A store's own malformed record.
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
function signerFor(store, secret) {
|
|
496
|
+
if (secret !== undefined && !isStoreSecret(secret)) {
|
|
497
|
+
throw new TypeError(`[nifra/web] ISR storeSecret must be a string of at least ${MIN_STORE_SECRET_LENGTH} characters`);
|
|
498
|
+
}
|
|
499
|
+
if (inProcessStores.has(store))
|
|
500
|
+
return undefined;
|
|
501
|
+
if (secret === undefined) {
|
|
502
|
+
throw new TypeError(`[nifra/web] withISR needs storeSecret, at least ${MIN_STORE_SECRET_LENGTH} characters, to sign the pages it keeps in a store other processes can write. Only a MemoryCacheStore runs without one.`);
|
|
503
|
+
}
|
|
504
|
+
return storeSigner(secret);
|
|
505
|
+
}
|
|
506
|
+
// Headers only an action or a data request carries; a stored page is always a plain document GET.
|
|
507
|
+
const ACTION_ONLY_HEADERS = [
|
|
508
|
+
"content-type",
|
|
509
|
+
"content-length",
|
|
510
|
+
"content-encoding",
|
|
511
|
+
"transfer-encoding",
|
|
512
|
+
"x-nifra-data",
|
|
513
|
+
"x-nifra-component",
|
|
514
|
+
SWAP_HEADER,
|
|
515
|
+
];
|
|
516
|
+
/** The document GET for `path` from the client that sent `req`: what a custom key sees on a read. */
|
|
517
|
+
function documentGet(path, req) {
|
|
518
|
+
const headers = new Headers(req.headers);
|
|
519
|
+
for (const name of ACTION_ONLY_HEADERS)
|
|
520
|
+
headers.delete(name);
|
|
521
|
+
return new Request(originOf(req) + path, { headers });
|
|
522
|
+
}
|
|
434
523
|
/** `createWebApp`'s marker for how its documents meet a CSP (see `DOCUMENT_POLICY` in ./csp.ts). Read
|
|
435
524
|
* through the global symbol registry so a cache wrapper never imports the CSP module. */
|
|
436
525
|
const DOCUMENT_POLICY = Symbol.for("nifra.web.documentPolicy");
|
|
@@ -466,6 +555,27 @@ export const urlKeyOf = (query = "bypass") => {
|
|
|
466
555
|
};
|
|
467
556
|
};
|
|
468
557
|
const requestKeyOf = (urlKey) => (req) => urlKey(new URL(req.url));
|
|
558
|
+
/**
|
|
559
|
+
* The key of a request, and of a path on the request's origin. Under the default URL key a path needs
|
|
560
|
+
* no `Request`; a custom `key` is handed one.
|
|
561
|
+
*/
|
|
562
|
+
function cacheKeysOf(options) {
|
|
563
|
+
const custom = options.key;
|
|
564
|
+
if (custom !== undefined) {
|
|
565
|
+
return { request: custom, path: (path, req) => custom(documentGet(path, req)) };
|
|
566
|
+
}
|
|
567
|
+
const urlKey = urlKeyOf(options.query);
|
|
568
|
+
return {
|
|
569
|
+
request: requestKeyOf(urlKey),
|
|
570
|
+
path: (path, req) => urlKey(new URL(originOf(req) + path)),
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
/** `req.url` up to its path, without a parse: the path joined to it is parsed as one URL anyway. */
|
|
574
|
+
const originOf = (req) => {
|
|
575
|
+
const url = req.url;
|
|
576
|
+
const end = url.indexOf("/", url.indexOf("//") + 2);
|
|
577
|
+
return end === -1 ? url : url.slice(0, end);
|
|
578
|
+
};
|
|
469
579
|
function assertRevalidate(value, name) {
|
|
470
580
|
if (!Number.isFinite(value) || value < 0) {
|
|
471
581
|
throw new RangeError(`[nifra/web] ${name} must be a finite non-negative number`);
|
|
@@ -497,15 +607,14 @@ const cookieNamesWithin = (header, names) => {
|
|
|
497
607
|
// the credential. The mark is set on the request the app was handed, which is why this runs after it.
|
|
498
608
|
const requestCarriesPrivateState = (req) => req.headers.has("authorization") || req.headers.has("cookie") || isAuthenticatedRequest(req);
|
|
499
609
|
const responseIsExplicitlyPublic = (res) => cacheControlHas(res.headers, ["public"]);
|
|
500
|
-
//
|
|
501
|
-
// or writes the store, so every stored entry is the document.
|
|
610
|
+
// Data and swap requests bypass both cache reads and writes, so these headers cannot vary stored HTML.
|
|
502
611
|
const responseDeclaresVary = (res) => {
|
|
503
612
|
const value = res.headers.get("vary");
|
|
504
613
|
if (value === null)
|
|
505
614
|
return false;
|
|
506
615
|
for (const part of value.split(",")) {
|
|
507
616
|
const name = part.trim().toLowerCase();
|
|
508
|
-
if (name !== "" && name !== "x-nifra-data")
|
|
617
|
+
if (name !== "" && name !== "x-nifra-data" && name !== SWAP_HEADER)
|
|
509
618
|
return true;
|
|
510
619
|
}
|
|
511
620
|
return false;
|
|
@@ -513,14 +622,14 @@ const responseDeclaresVary = (res) => {
|
|
|
513
622
|
/**
|
|
514
623
|
* Whether `res` may be stored by a shared cache and served to anyone requesting `req`'s URL: a
|
|
515
624
|
* full-document `GET` 200 `text/html` with no Set-Cookie, no `private`/`no-store`, no `Vary` beyond
|
|
516
|
-
* the data
|
|
625
|
+
* the data/swap headers, and, for a request carrying a cookie or Authorization or accepted by an
|
|
517
626
|
* authentication stage, an explicit `public`.
|
|
518
627
|
* The one rule both `withISR` and `@nifrajs/web/cdn` cache by.
|
|
519
628
|
*/
|
|
520
629
|
export const isCacheablePage = (req, res) => {
|
|
521
630
|
if (req.method !== "GET")
|
|
522
631
|
return false;
|
|
523
|
-
if (req.headers.get("x-nifra-data") !== null)
|
|
632
|
+
if (req.headers.get("x-nifra-data") !== null || req.headers.has(SWAP_HEADER))
|
|
524
633
|
return false;
|
|
525
634
|
if (res.status !== 200)
|
|
526
635
|
return false;
|
|
@@ -620,6 +729,10 @@ const isInvalidationResponse = (req, res) => {
|
|
|
620
729
|
export function withISR(app, options) {
|
|
621
730
|
const { store, now } = options;
|
|
622
731
|
assertRevalidate(options.revalidate, "revalidate");
|
|
732
|
+
const signer = signerFor(store, options.storeSecret);
|
|
733
|
+
const write = (key, entry) => signer === undefined
|
|
734
|
+
? store.set(key, entry)
|
|
735
|
+
: signed(signer, key, entry).then((value) => store.set(key, value));
|
|
623
736
|
const storeTimeoutMs = options.storeTimeoutMs;
|
|
624
737
|
if (storeTimeoutMs !== undefined && !(Number.isFinite(storeTimeoutMs) && storeTimeoutMs > 0)) {
|
|
625
738
|
throw new RangeError("[nifra/web] ISR storeTimeoutMs must be a positive finite number");
|
|
@@ -643,29 +756,32 @@ export function withISR(app, options) {
|
|
|
643
756
|
}
|
|
644
757
|
};
|
|
645
758
|
// Runs one store call; a throw, a rejection or a timeout is reported and answered with STORE_FAILED.
|
|
646
|
-
const
|
|
759
|
+
const withinTimeout = async (op, pending) => {
|
|
760
|
+
if (storeTimeoutMs === undefined)
|
|
761
|
+
return pending;
|
|
647
762
|
let timer;
|
|
763
|
+
const timeout = new Promise((_, reject) => {
|
|
764
|
+
timer = setTimeout(() => {
|
|
765
|
+
const error = new Error(`ISR store ${op} exceeded storeTimeoutMs (${storeTimeoutMs} ms)`);
|
|
766
|
+
error.name = "TimeoutError";
|
|
767
|
+
reject(error);
|
|
768
|
+
}, storeTimeoutMs);
|
|
769
|
+
});
|
|
648
770
|
try {
|
|
649
|
-
const pending = call();
|
|
650
|
-
if (storeTimeoutMs === undefined)
|
|
651
|
-
return await pending;
|
|
652
|
-
const timeout = new Promise((_, reject) => {
|
|
653
|
-
timer = setTimeout(() => {
|
|
654
|
-
const error = new Error(`ISR store ${op} exceeded storeTimeoutMs (${storeTimeoutMs} ms)`);
|
|
655
|
-
error.name = "TimeoutError";
|
|
656
|
-
reject(error);
|
|
657
|
-
}, storeTimeoutMs);
|
|
658
|
-
});
|
|
659
771
|
return await Promise.race([pending, timeout]);
|
|
660
772
|
}
|
|
773
|
+
finally {
|
|
774
|
+
clearTimeout(timer);
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
const attempt = async (op, key, call) => {
|
|
778
|
+
try {
|
|
779
|
+
return await withinTimeout(op, call());
|
|
780
|
+
}
|
|
661
781
|
catch (error) {
|
|
662
782
|
reportStoreError(op, key, error);
|
|
663
783
|
return STORE_FAILED;
|
|
664
784
|
}
|
|
665
|
-
finally {
|
|
666
|
-
if (timer !== undefined)
|
|
667
|
-
clearTimeout(timer);
|
|
668
|
-
}
|
|
669
785
|
};
|
|
670
786
|
// A store without purge order, or one whose fence failed, stores the render unfenced.
|
|
671
787
|
const fenceFor = async (key) => {
|
|
@@ -675,7 +791,8 @@ export function withISR(app, options) {
|
|
|
675
791
|
const taken = await attempt("fence", key, async () => fence.call(store));
|
|
676
792
|
return taken === STORE_FAILED ? undefined : taken;
|
|
677
793
|
};
|
|
678
|
-
const
|
|
794
|
+
const keys = cacheKeysOf(options);
|
|
795
|
+
const keyOf = keys.request;
|
|
679
796
|
const draftSecret = options.draftSecret;
|
|
680
797
|
const anonymousCookies = new Set(options.anonymousCookies ?? []);
|
|
681
798
|
// A request whose cookies are all anonymous is judged for storage as if it carried none.
|
|
@@ -693,6 +810,34 @@ export function withISR(app, options) {
|
|
|
693
810
|
};
|
|
694
811
|
const regenerating = new Set();
|
|
695
812
|
openCacheChannel(app);
|
|
813
|
+
// This wrapper owns the app's invalidation pipeline from here on: an action's operation comes back
|
|
814
|
+
// by its request, and every stage runs in order before the action's answer leaves.
|
|
815
|
+
const handoff = Reflect.get(app, INVALIDATION);
|
|
816
|
+
const taken = isHandoff(handoff) ? handoff : undefined;
|
|
817
|
+
// The pipeline's purges are bounded like every other store call; their failures reach its own report.
|
|
818
|
+
const invalidateTag = store.invalidateTag;
|
|
819
|
+
const targets = {
|
|
820
|
+
dataCaches: options.dataCaches,
|
|
821
|
+
store: {
|
|
822
|
+
delete: (key) => withinTimeout("delete", store.delete(key)),
|
|
823
|
+
invalidateTag: invalidateTag === undefined
|
|
824
|
+
? undefined
|
|
825
|
+
: (tag) => withinTimeout("invalidateTag", invalidateTag.call(store, tag)),
|
|
826
|
+
},
|
|
827
|
+
keyOf: keys.path,
|
|
828
|
+
publish: taken?.publish,
|
|
829
|
+
cdn: options.cdn,
|
|
830
|
+
};
|
|
831
|
+
const passThrough = async (req, platform) => {
|
|
832
|
+
const res = withoutCacheChannel(await app.fetch(req, platform));
|
|
833
|
+
const op = taken?.take(req);
|
|
834
|
+
if (op !== undefined) {
|
|
835
|
+
await settleInvalidation(op, targets, req, platform, {
|
|
836
|
+
onError: options.onInvalidationError,
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
return res;
|
|
840
|
+
};
|
|
696
841
|
// An outer cache layer (`withCdn`) opens this wrapper's own channel: cached responses then carry how
|
|
697
842
|
// much freshness they have left and their tags, so the CDN never holds a page longer than ISR would.
|
|
698
843
|
let channelOpen = false;
|
|
@@ -756,7 +901,7 @@ export function withISR(app, options) {
|
|
|
756
901
|
};
|
|
757
902
|
// A failed write still answers with the page, and still hands it to concurrent misses: whether it
|
|
758
903
|
// may be shared was decided above, not by the store.
|
|
759
|
-
await attempt("set", key, () =>
|
|
904
|
+
await attempt("set", key, () => write(key, entry));
|
|
760
905
|
stored?.(entry);
|
|
761
906
|
return fresh(entry);
|
|
762
907
|
};
|
|
@@ -811,7 +956,7 @@ export function withISR(app, options) {
|
|
|
811
956
|
...(tags.length === 0 ? {} : { tags }),
|
|
812
957
|
fence,
|
|
813
958
|
};
|
|
814
|
-
await attempt("set", key, () =>
|
|
959
|
+
await attempt("set", key, () => write(key, entry));
|
|
815
960
|
}
|
|
816
961
|
else if (isInvalidationResponse(judged(req), res)) {
|
|
817
962
|
// A withdrawal is authoritative. Keeping the old entry would make a withdrawn page stay
|
|
@@ -832,23 +977,24 @@ export function withISR(app, options) {
|
|
|
832
977
|
}
|
|
833
978
|
};
|
|
834
979
|
const handler = async (req, platform) => {
|
|
835
|
-
// A data-mode soft-nav GET bypasses the cache entirely: entries are full HTML
|
|
836
|
-
// by URL, and serving one to
|
|
837
|
-
// the loader payload. (The write path already refuses
|
|
980
|
+
// A data-mode soft-nav GET or a slot swap bypasses the cache entirely: entries are full HTML
|
|
981
|
+
// documents keyed by URL, and serving one to either would hand the client a document where it
|
|
982
|
+
// expects the loader payload or one slot. (The write path already refuses both.)
|
|
838
983
|
const privateRoute = Reflect.get(app, Symbol.for("nifra.web.private-route"));
|
|
839
984
|
const ownsPrivate = typeof privateRoute === "function" && privateRoute(req) === true;
|
|
840
985
|
const key = req.method === "GET" &&
|
|
841
986
|
req.headers.get("x-nifra-data") === null &&
|
|
842
987
|
!req.headers.has("x-nifra-component") &&
|
|
988
|
+
!req.headers.has(SWAP_HEADER) &&
|
|
843
989
|
!ownsPrivate
|
|
844
990
|
? keyOf(req)
|
|
845
991
|
: null;
|
|
846
992
|
if (key === null)
|
|
847
|
-
return
|
|
993
|
+
return passThrough(req, platform);
|
|
848
994
|
// Draft/preview: an editor (valid signed cookie) always renders fresh and is never cached, so
|
|
849
995
|
// unpublished content can't leak into the public cache (and the editor isn't served a stale page).
|
|
850
996
|
if (draftSecret !== undefined && (await isDraftEnabled(req, draftSecret))) {
|
|
851
|
-
return
|
|
997
|
+
return passThrough(req, platform);
|
|
852
998
|
}
|
|
853
999
|
// A cookie or Authorization request may be personalized: under the default key it never reads
|
|
854
1000
|
// the store, and its private render never marks the URL uncacheable for everyone else.
|
|
@@ -860,7 +1006,11 @@ export function withISR(app, options) {
|
|
|
860
1006
|
uncacheable.delete(key);
|
|
861
1007
|
}
|
|
862
1008
|
const read = personal ? undefined : await attempt("get", key, () => store.get(key));
|
|
863
|
-
|
|
1009
|
+
let hit = read === STORE_FAILED ? undefined : read;
|
|
1010
|
+
if (hit !== undefined && signer !== undefined && !(await verified(signer, key, hit))) {
|
|
1011
|
+
reportStoreError("verify", key, new Error("a stored page failed its signature check"));
|
|
1012
|
+
hit = undefined;
|
|
1013
|
+
}
|
|
864
1014
|
if (hit !== undefined) {
|
|
865
1015
|
if (now() - hit.storedAt < hit.revalidate)
|
|
866
1016
|
return served(hit, "hit");
|
|
@@ -882,6 +1032,7 @@ export function withISR(app, options) {
|
|
|
882
1032
|
return handler;
|
|
883
1033
|
}
|
|
884
1034
|
const jsonError = (status, error) => Response.json({ ok: false, error }, { status });
|
|
1035
|
+
const isHandoff = (value) => typeof value === "object" && value !== null && typeof Reflect.get(value, "take") === "function";
|
|
885
1036
|
/** Most paths and tags one revalidation request may name, so a leaked token cannot fan out. */
|
|
886
1037
|
export const MAX_REVALIDATE_PATHS = 100;
|
|
887
1038
|
export const MAX_REVALIDATE_TAGS = MAX_ISR_TAGS;
|
|
@@ -985,47 +1136,44 @@ export function revalidateEndpoint(options) {
|
|
|
985
1136
|
if (tags.length > 0 && invalidateTag === undefined) {
|
|
986
1137
|
return jsonError(501, "tag_invalidation_not_supported");
|
|
987
1138
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
await invalidateTag?.call(options.store, candidate);
|
|
1139
|
+
// The same pipeline an action's `revalidate()` runs: data caches, this store, live, then the CDN.
|
|
1140
|
+
const live = options.live;
|
|
1141
|
+
const report = await runInvalidation({ id: crypto.randomUUID(), paths, tags, keys: [] }, {
|
|
1142
|
+
dataCaches: options.dataCaches,
|
|
1143
|
+
store: options.store,
|
|
1144
|
+
keyOf: (candidate) => keys[paths.indexOf(candidate)] ?? null,
|
|
1145
|
+
publish: live === undefined ? undefined : (names) => live.publish(...names),
|
|
1146
|
+
cdn: options.cdn,
|
|
1147
|
+
}, req, platform);
|
|
1148
|
+
const stageOf = (stage) => report.stages.find((each) => each.stage === stage);
|
|
1149
|
+
if (stageOf("data")?.ok === false)
|
|
1150
|
+
return jsonError(500, "data_cache_invalidation_failed");
|
|
1151
|
+
if (stageOf("pages")?.ok === false)
|
|
1152
|
+
return jsonError(500, "page_store_invalidation_failed");
|
|
1003
1153
|
const done = path !== null
|
|
1004
1154
|
? { revalidated: path }
|
|
1005
1155
|
: tag !== null
|
|
1006
1156
|
? { revalidatedTag: tag }
|
|
1007
1157
|
: { revalidated: paths, revalidatedTags: tags };
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
try {
|
|
1012
|
-
outcome = await options.cdn.purge({ tags, paths }, platform);
|
|
1013
|
-
}
|
|
1014
|
-
catch {
|
|
1015
|
-
outcome = { cdn: "failed", retryable: true, error: "cdn_purge_threw" };
|
|
1016
|
-
}
|
|
1017
|
-
if (outcome.cdn === "failed") {
|
|
1158
|
+
const published = stageOf("publish");
|
|
1159
|
+
const cdn = stageOf("cdn");
|
|
1160
|
+
if (cdn?.ok === false) {
|
|
1018
1161
|
const failure = {
|
|
1019
1162
|
ok: false,
|
|
1020
1163
|
error: "cdn_purge_failed",
|
|
1021
1164
|
origin: "done",
|
|
1022
|
-
retryable:
|
|
1165
|
+
retryable: cdn.retryable,
|
|
1023
1166
|
};
|
|
1024
|
-
if (
|
|
1025
|
-
failure.reason =
|
|
1167
|
+
if (cdn.error !== undefined && cdn.error !== "cdn_failed")
|
|
1168
|
+
failure.reason = cdn.error;
|
|
1026
1169
|
return Response.json(failure, { status: 502 });
|
|
1027
1170
|
}
|
|
1028
|
-
|
|
1171
|
+
if (published?.ok === false) {
|
|
1172
|
+
return Response.json({ ok: false, error: "live_publish_failed", origin: "done", retryable: true }, { status: 502 });
|
|
1173
|
+
}
|
|
1174
|
+
if (cdn === undefined)
|
|
1175
|
+
return Response.json(done);
|
|
1176
|
+
return Response.json({ ...done, cdn: cdn.queued === true ? "queued" : "accepted" }, { status: cdn.queued === true ? 202 : 200 });
|
|
1029
1177
|
};
|
|
1030
1178
|
}
|
|
1031
1179
|
//# sourceMappingURL=isr.js.map
|