@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
|
@@ -0,0 +1,1069 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nifrajs/web/keep-alive - pages that stay mounted after the visitor leaves them, so back and forward
|
|
3
|
+
* show them again as they were left. The generated client entry imports this only when a route
|
|
4
|
+
* exports `keepAlive`, so no other app's client chunks contain it.
|
|
5
|
+
*
|
|
6
|
+
* {@link keepAliveClient} layers over the router. The page on screen is kept when a push or a
|
|
7
|
+
* back/forward traversal leaves it (at most three, each for at most ten minutes, all dropped when the
|
|
8
|
+
* visitor signs in or out); a traversal back to a kept page's own history entry publishes that page at
|
|
9
|
+
* once, then reloads its data in place. Adapters render the active page and every kept one from
|
|
10
|
+
* {@link slotTree}, and {@link createSlotParking} takes the hidden ones out of the document.
|
|
11
|
+
*/
|
|
12
|
+
import { epochOf, IDENTITY_CHANNEL, onIdentityChange } from "./identity.js";
|
|
13
|
+
import { PAGE_HIDE_EVENT, PAGE_SHOW_EVENT, releasePage } from "./internal/page-events.js";
|
|
14
|
+
import { shareData } from "./internal/share-data.js";
|
|
15
|
+
import { fetchRouteData, isRouteDataEnvelope, } from "./router.js";
|
|
16
|
+
export { EPOCH_COOKIE, IDENTITY_CHANNEL, identityChanged } from "./identity.js";
|
|
17
|
+
export { PAGE_HIDE_EVENT, PAGE_SHOW_EVENT } from "./internal/page-events.js";
|
|
18
|
+
/** Most pages kept per tab. */
|
|
19
|
+
export const KEEP_ALIVE_MAX = 3;
|
|
20
|
+
/** Longest a page stays hidden unless its route sets `keepAlive: { maxAge }`, in milliseconds. */
|
|
21
|
+
export const KEEP_ALIVE_MAX_AGE_MS = 10 * 60 * 1000;
|
|
22
|
+
const pathnameOf = (path) => {
|
|
23
|
+
const q = path.indexOf("?");
|
|
24
|
+
return q === -1 ? path : path.slice(0, q);
|
|
25
|
+
};
|
|
26
|
+
const pagePathOf = (state) => state.intercepted?.pagePath ?? state.path;
|
|
27
|
+
// Unique per document, so an instance an entry carries from an earlier document never names one here.
|
|
28
|
+
const DOCUMENT = Math.random().toString(36).slice(2, 8);
|
|
29
|
+
let sequence = 0;
|
|
30
|
+
const newInstance = () => `${++sequence}:${DOCUMENT}`;
|
|
31
|
+
const ownInstance = (value) => typeof value === "string" && value.endsWith(`:${DOCUMENT}`) ? value : undefined;
|
|
32
|
+
const readEntry = () => {
|
|
33
|
+
const state = typeof history === "undefined" ? null : history.state;
|
|
34
|
+
const fields = typeof state === "object" && state !== null ? state : {};
|
|
35
|
+
const index = Reflect.get(fields, "nifraIndex");
|
|
36
|
+
return {
|
|
37
|
+
index: typeof index === "number" ? index : 0,
|
|
38
|
+
instance: ownInstance(Reflect.get(fields, "nifraInstance")),
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
// The instance rides on the entry beside the history layer's own fields, which every write it makes
|
|
42
|
+
// to an existing entry keeps; a fresh push or replace carries none.
|
|
43
|
+
const stampEntry = (instance) => {
|
|
44
|
+
if (typeof history === "undefined")
|
|
45
|
+
return;
|
|
46
|
+
const state = history.state;
|
|
47
|
+
if (typeof state === "object" &&
|
|
48
|
+
state !== null &&
|
|
49
|
+
Reflect.get(state, "nifraInstance") === instance)
|
|
50
|
+
return;
|
|
51
|
+
try {
|
|
52
|
+
history.replaceState(Object.assign({}, typeof state === "object" ? state : null, { nifraInstance: instance }), "");
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
55
|
+
};
|
|
56
|
+
const readEpoch = () => (typeof document === "undefined" ? "" : epochOf(document.cookie));
|
|
57
|
+
const now = () => Date.now();
|
|
58
|
+
/**
|
|
59
|
+
* Keep-alive for a router. See the module docs; the generated client entry wires it:
|
|
60
|
+
*
|
|
61
|
+
* ```ts
|
|
62
|
+
* const keep = keepAliveClient({ table, routeHooks, matchChains })
|
|
63
|
+
* const router = keep.attach(createClientRouter({ ..., fetchData: keep.fetchData }))
|
|
64
|
+
* installHistory(router)
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function keepAliveClient(options) {
|
|
68
|
+
const { table, routeHooks, matchChains } = options;
|
|
69
|
+
const base = options.fetchData ?? fetchRouteData;
|
|
70
|
+
const max = options.max ?? KEEP_ALIVE_MAX;
|
|
71
|
+
const keepRoutes = new Set(table.routes);
|
|
72
|
+
const wrappedLayouts = new Set(table.layouts);
|
|
73
|
+
// The restore whose reload the router is running, which the transport answers.
|
|
74
|
+
let reloading;
|
|
75
|
+
const ownsLoader = (routeId) => routeId !== undefined && routeHooks[routeId]?.clientLoader !== undefined;
|
|
76
|
+
// What client-loader routes loaded since the page on screen settled. A restore that loads nothing
|
|
77
|
+
// runs the client loader over the kept page's own, as a navigation over prefetched data does.
|
|
78
|
+
const fresh = new Map();
|
|
79
|
+
const noteSource = (path, payload) => {
|
|
80
|
+
if (!isRouteDataEnvelope(payload))
|
|
81
|
+
fresh.set(path, payload);
|
|
82
|
+
else if (payload.status === undefined)
|
|
83
|
+
fresh.set(path, payload.data);
|
|
84
|
+
};
|
|
85
|
+
const sourcesOf = (state) => {
|
|
86
|
+
const sources = new Map();
|
|
87
|
+
for (const path of [pagePathOf(state), state.path])
|
|
88
|
+
if (fresh.has(path))
|
|
89
|
+
sources.set(path, fresh.get(path));
|
|
90
|
+
return sources.size === 0 ? undefined : sources;
|
|
91
|
+
};
|
|
92
|
+
// A restored page's reload: its kept data, unrequested, when it needs no reload; otherwise the
|
|
93
|
+
// response shared onto the kept data, so what did not change keeps the identity it has on screen.
|
|
94
|
+
// A reload that fails shows the kept data instead of loading the page as a document.
|
|
95
|
+
const fetchData = async (path, match, signal, navigation, purpose, hosts, read) => {
|
|
96
|
+
const restore = reloading;
|
|
97
|
+
const held = restore?.kept.intercepted;
|
|
98
|
+
// A page kept with a boundary open reloads twice: its own data, then the target the boundary holds.
|
|
99
|
+
const target = held !== undefined && path === restore?.kept.path && path !== restore.pagePath
|
|
100
|
+
? restore.kept.boundaries?.[held.boundary]
|
|
101
|
+
: undefined;
|
|
102
|
+
if (restore === undefined || (path !== restore.pagePath && target === undefined)) {
|
|
103
|
+
if (purpose === "prefetch" || !ownsLoader(match.routeId))
|
|
104
|
+
return base(path, match, signal, navigation, purpose, hosts, read);
|
|
105
|
+
const payload = await base(path, match, signal, navigation, purpose, hosts, read);
|
|
106
|
+
noteSource(path, payload);
|
|
107
|
+
return payload;
|
|
108
|
+
}
|
|
109
|
+
const sourced = restore.sources?.has(path) === true;
|
|
110
|
+
const kept = () => {
|
|
111
|
+
const data = sourced ? restore.sources?.get(path) : target?.data;
|
|
112
|
+
if (target !== undefined)
|
|
113
|
+
return { v: 1, data, live: target.live };
|
|
114
|
+
const envelope = envelopeOf(restore.kept);
|
|
115
|
+
if (sourced)
|
|
116
|
+
envelope.data = data;
|
|
117
|
+
return envelope;
|
|
118
|
+
};
|
|
119
|
+
// The page's hole hosts reload as fills, applied in place on the roots kept with it, even when
|
|
120
|
+
// the route keeps its own data.
|
|
121
|
+
const holes = target === undefined ? hostsOf(restore.kept) : [];
|
|
122
|
+
if (!restore.reload && holes.length === 0)
|
|
123
|
+
return kept();
|
|
124
|
+
let payload;
|
|
125
|
+
try {
|
|
126
|
+
payload = await base(path, match, signal, navigation, purpose, holes.length === 0 ? hosts : holes, read);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (signal?.aborted === true || hasRedirect(error))
|
|
130
|
+
throw error;
|
|
131
|
+
return kept();
|
|
132
|
+
}
|
|
133
|
+
if (!restore.reload)
|
|
134
|
+
return fillsOnto(kept(), restore.kept, payload, holes);
|
|
135
|
+
if (ownsLoader(match.routeId))
|
|
136
|
+
noteSource(path, payload);
|
|
137
|
+
const hooks = routeHooks[target === undefined ? restore.kept.routeId : match.routeId];
|
|
138
|
+
// `read` itself answers a reload whose response text is what the path shows, which the router
|
|
139
|
+
// keeps unparsed.
|
|
140
|
+
if (payload === read || hooks?.shareData === false)
|
|
141
|
+
return payload;
|
|
142
|
+
// The router's transport records the text of a plain JSON response, a tree with no symbol keys.
|
|
143
|
+
const plainJson = read?.text !== undefined && hooks?.clientLoader === undefined;
|
|
144
|
+
return target === undefined
|
|
145
|
+
? sharedOnto(restore.kept, payload, plainJson)
|
|
146
|
+
: targetOnto(target.data, payload, plainJson);
|
|
147
|
+
};
|
|
148
|
+
const attach = (inner) => {
|
|
149
|
+
const records = new Map();
|
|
150
|
+
const listeners = new Set();
|
|
151
|
+
const before = new Set();
|
|
152
|
+
const wrappedFetchers = new Map();
|
|
153
|
+
let keptPages;
|
|
154
|
+
let lastEpoch = readEpoch();
|
|
155
|
+
const first = readEntry();
|
|
156
|
+
const initial = inner.snapshot();
|
|
157
|
+
// A client loader runs over the document's own data as the router hydrates.
|
|
158
|
+
if (ownsLoader(initial.routeId))
|
|
159
|
+
fresh.set(initial.path, initial.data);
|
|
160
|
+
// Where the page's data came from: `epoch` is the one it was loaded under (a page left after a
|
|
161
|
+
// sign-in or sign-out shows the previous visitor's data, so it is not kept), and `sources` what
|
|
162
|
+
// a client loader built it from.
|
|
163
|
+
let active = {
|
|
164
|
+
instance: first.instance ?? newInstance(),
|
|
165
|
+
index: first.index,
|
|
166
|
+
settled: initial,
|
|
167
|
+
epoch: lastEpoch,
|
|
168
|
+
sources: sourcesOf(initial),
|
|
169
|
+
};
|
|
170
|
+
stampEntry(active.instance);
|
|
171
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production")
|
|
172
|
+
Reflect.set(globalThis, KEPT_PAGES_GLOBAL, {
|
|
173
|
+
pages: () => describeKept(records.values()),
|
|
174
|
+
});
|
|
175
|
+
// What is on screen: the router's state, or a restored page's kept state until its reload lands.
|
|
176
|
+
let shown = active.settled;
|
|
177
|
+
let flags;
|
|
178
|
+
// The page being left stays mounted, hidden, while a `_loading` page takes its place.
|
|
179
|
+
let leaving = false;
|
|
180
|
+
// The page being left is gone (dropped, or unmountable) before its navigation settled.
|
|
181
|
+
let leftGone = false;
|
|
182
|
+
const keepAliveOf = (routeId) => keepRoutes.has(routeId) ? routeHooks[routeId]?.keepAlive : undefined;
|
|
183
|
+
// A boundary open over the page is kept with it (its target is the page's to show again).
|
|
184
|
+
const keepable = (state) => !state.pending && keepAliveOf(state.routeId) !== undefined;
|
|
185
|
+
// One page instance per route, and per pathname on a route that keeps alive, whose pages would
|
|
186
|
+
// otherwise have nothing to keep.
|
|
187
|
+
const sameInstance = (a, b) => a.routeId === b.routeId &&
|
|
188
|
+
(!keepRoutes.has(b.routeId) || pathnameOf(pagePathOf(a)) === pathnameOf(pagePathOf(b)));
|
|
189
|
+
const compose = () => {
|
|
190
|
+
keptPages ??= [...records.values()].map((record) => record.page);
|
|
191
|
+
const kept = leaving
|
|
192
|
+
? [...keptPages, { instance: active.instance, state: active.settled }]
|
|
193
|
+
: keptPages;
|
|
194
|
+
return {
|
|
195
|
+
...shown,
|
|
196
|
+
instance: leaving ? undefined : active.instance,
|
|
197
|
+
kept: kept.length === 0 ? undefined : kept,
|
|
198
|
+
...flags,
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
let published = compose();
|
|
202
|
+
const drop = (instances, reason) => {
|
|
203
|
+
const gone = instances.filter((instance) => records.delete(instance));
|
|
204
|
+
if (gone.length === 0)
|
|
205
|
+
return;
|
|
206
|
+
keptPages = undefined;
|
|
207
|
+
options.onDrop?.(reason, gone);
|
|
208
|
+
};
|
|
209
|
+
const refresh = () => {
|
|
210
|
+
let next = compose();
|
|
211
|
+
// A kept page whose chain leaves the page on screen at an unwrapped layout cannot be hidden.
|
|
212
|
+
const stuck = unplaceable(next, matchChains, keepRoutes, wrappedLayouts);
|
|
213
|
+
if (stuck.length > 0) {
|
|
214
|
+
if (leaving && stuck.includes(active.instance)) {
|
|
215
|
+
leaving = false;
|
|
216
|
+
leftGone = true;
|
|
217
|
+
}
|
|
218
|
+
drop(stuck, "layout");
|
|
219
|
+
next = compose();
|
|
220
|
+
}
|
|
221
|
+
for (const listener of [...before])
|
|
222
|
+
listener(next);
|
|
223
|
+
published = next;
|
|
224
|
+
for (const listener of [...listeners])
|
|
225
|
+
listener();
|
|
226
|
+
};
|
|
227
|
+
const dropKept = (reason = "manual") => {
|
|
228
|
+
const left = leaving || inner.snapshot().pending;
|
|
229
|
+
if (records.size === 0 && !leaving) {
|
|
230
|
+
if (left)
|
|
231
|
+
leftGone = true;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (left) {
|
|
235
|
+
leaving = false;
|
|
236
|
+
leftGone = true;
|
|
237
|
+
}
|
|
238
|
+
drop([...records.keys()], reason);
|
|
239
|
+
refresh();
|
|
240
|
+
};
|
|
241
|
+
const sweep = () => {
|
|
242
|
+
const at = now();
|
|
243
|
+
const expired = [];
|
|
244
|
+
for (const [instance, record] of records)
|
|
245
|
+
if (at - record.hiddenAt > record.maxAge)
|
|
246
|
+
expired.push(instance);
|
|
247
|
+
drop(expired, "expired");
|
|
248
|
+
};
|
|
249
|
+
// A response or another tab may carry a new epoch: what was kept belonged to the previous visitor.
|
|
250
|
+
const checkEpoch = () => {
|
|
251
|
+
const epoch = readEpoch();
|
|
252
|
+
if (epoch === lastEpoch)
|
|
253
|
+
return;
|
|
254
|
+
lastEpoch = epoch;
|
|
255
|
+
channel?.postMessage("changed");
|
|
256
|
+
dropKept("identity");
|
|
257
|
+
};
|
|
258
|
+
const keep = ({ instance, index, settled: state, epoch, sources }) => {
|
|
259
|
+
if (!keepable(state) || epoch !== readEpoch())
|
|
260
|
+
return;
|
|
261
|
+
const setting = keepAliveOf(state.routeId);
|
|
262
|
+
const settings = typeof setting === "object" ? setting : {};
|
|
263
|
+
records.delete(instance);
|
|
264
|
+
records.set(instance, {
|
|
265
|
+
page: { instance, state },
|
|
266
|
+
index,
|
|
267
|
+
epoch,
|
|
268
|
+
sources,
|
|
269
|
+
hiddenAt: now(),
|
|
270
|
+
maxAge: settings.maxAge ?? KEEP_ALIVE_MAX_AGE_MS,
|
|
271
|
+
revalidate: settings.revalidate !== false,
|
|
272
|
+
stale: false,
|
|
273
|
+
});
|
|
274
|
+
keptPages = undefined;
|
|
275
|
+
if (records.size > max)
|
|
276
|
+
drop([...records.keys()].slice(0, records.size - max), "lru");
|
|
277
|
+
};
|
|
278
|
+
const settle = (next) => {
|
|
279
|
+
const entry = readEntry();
|
|
280
|
+
if (sameInstance(active.settled, next)) {
|
|
281
|
+
// Data the router did not load again is still the data it was loaded with.
|
|
282
|
+
const same = next.data === active.settled.data;
|
|
283
|
+
active = {
|
|
284
|
+
instance: active.instance,
|
|
285
|
+
index: entry.index,
|
|
286
|
+
settled: next,
|
|
287
|
+
epoch: same ? active.epoch : readEpoch(),
|
|
288
|
+
sources: same ? active.sources : sourcesOf(next),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
// The page on screen was left: by a push (a fresh entry above it), a traversal (an entry that
|
|
293
|
+
// already carries an instance), or a replace (its own entry rewritten), which discards it.
|
|
294
|
+
if (entry.index > active.index && entry.instance === undefined) {
|
|
295
|
+
drop([...records].filter(([, record]) => record.index >= entry.index).map(([id]) => id), "forward");
|
|
296
|
+
}
|
|
297
|
+
if (entry.index !== active.index && !leftGone)
|
|
298
|
+
keep(active);
|
|
299
|
+
const reuse = entry.index !== active.index &&
|
|
300
|
+
entry.instance !== undefined &&
|
|
301
|
+
entry.instance !== active.instance &&
|
|
302
|
+
!records.has(entry.instance)
|
|
303
|
+
? entry.instance
|
|
304
|
+
: undefined;
|
|
305
|
+
active = {
|
|
306
|
+
instance: reuse ?? newInstance(),
|
|
307
|
+
index: entry.index,
|
|
308
|
+
settled: next,
|
|
309
|
+
epoch: readEpoch(),
|
|
310
|
+
sources: sourcesOf(next),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
fresh.clear();
|
|
314
|
+
stampEntry(active.instance);
|
|
315
|
+
sweep();
|
|
316
|
+
shown = next;
|
|
317
|
+
flags = undefined;
|
|
318
|
+
leaving = false;
|
|
319
|
+
leftGone = false;
|
|
320
|
+
refresh();
|
|
321
|
+
checkEpoch();
|
|
322
|
+
};
|
|
323
|
+
let restoring;
|
|
324
|
+
const endRestore = () => {
|
|
325
|
+
const restore = restoring;
|
|
326
|
+
restoring = undefined;
|
|
327
|
+
reloading = undefined;
|
|
328
|
+
restore?.done();
|
|
329
|
+
};
|
|
330
|
+
inner.subscribe(() => {
|
|
331
|
+
const next = inner.snapshot();
|
|
332
|
+
const restore = restoring;
|
|
333
|
+
if (restore !== undefined) {
|
|
334
|
+
// The restored page stays on screen while its reload runs: the router's own pending tick for
|
|
335
|
+
// it is not a navigation the visitor made. A redirect it follows is.
|
|
336
|
+
if (next.pending &&
|
|
337
|
+
(next.pendingPath === undefined ||
|
|
338
|
+
next.pendingPath === restore.path ||
|
|
339
|
+
next.pendingPath === restore.pagePath))
|
|
340
|
+
return;
|
|
341
|
+
const onPage = !next.pending &&
|
|
342
|
+
pagePathOf(next) === restore.pagePath &&
|
|
343
|
+
next.routeId === restore.kept.routeId;
|
|
344
|
+
// The page alone, on the way to opening its boundary on the target again.
|
|
345
|
+
if (onPage && restore.kept.intercepted !== undefined && next.intercepted === undefined)
|
|
346
|
+
return;
|
|
347
|
+
endRestore();
|
|
348
|
+
if (onPage) {
|
|
349
|
+
active = {
|
|
350
|
+
instance: active.instance,
|
|
351
|
+
index: active.index,
|
|
352
|
+
settled: next,
|
|
353
|
+
epoch: restore.reload ? readEpoch() : active.epoch,
|
|
354
|
+
// A reload that failed shows the kept data, from the kept sources.
|
|
355
|
+
sources: sourcesOf(next) ?? active.sources,
|
|
356
|
+
};
|
|
357
|
+
fresh.clear();
|
|
358
|
+
shown = { ...next, params: shareData(restore.kept.params, next.params) };
|
|
359
|
+
flags = undefined;
|
|
360
|
+
refresh();
|
|
361
|
+
checkEpoch();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (!next.pending) {
|
|
366
|
+
settle(next);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
shown = next;
|
|
370
|
+
flags = undefined;
|
|
371
|
+
leaving = next.routeId !== active.settled.routeId && !leftGone && keepable(active.settled);
|
|
372
|
+
refresh();
|
|
373
|
+
});
|
|
374
|
+
const restore = (path, instance, record) => {
|
|
375
|
+
const entry = readEntry();
|
|
376
|
+
records.delete(instance);
|
|
377
|
+
keptPages = undefined;
|
|
378
|
+
if (!leftGone)
|
|
379
|
+
keep(active);
|
|
380
|
+
const kept = record.page.state;
|
|
381
|
+
const { sources } = record;
|
|
382
|
+
active = { instance, index: entry.index, settled: kept, epoch: record.epoch, sources };
|
|
383
|
+
fresh.clear();
|
|
384
|
+
// Data a client loader built is shown again only from what it was built from.
|
|
385
|
+
const unsourced = (ownsLoader(kept.routeId) && sources?.has(pagePathOf(kept)) !== true) ||
|
|
386
|
+
(kept.intercepted !== undefined &&
|
|
387
|
+
ownsLoader(inner.match(kept.path)?.routeId) &&
|
|
388
|
+
sources?.has(kept.path) !== true);
|
|
389
|
+
const reload = record.revalidate || record.stale || kept.path !== path || unsourced;
|
|
390
|
+
const modal = kept.intercepted;
|
|
391
|
+
const pagePath = modal === undefined ? path : modal.pagePath;
|
|
392
|
+
let done = () => { };
|
|
393
|
+
const ended = new Promise((resolve) => {
|
|
394
|
+
done = resolve;
|
|
395
|
+
});
|
|
396
|
+
endRestore();
|
|
397
|
+
const mine = { path, pagePath, kept, reload, sources, done, ended };
|
|
398
|
+
restoring = mine;
|
|
399
|
+
reloading = { path, pagePath, kept, reload, sources };
|
|
400
|
+
stampEntry(instance);
|
|
401
|
+
shown = kept;
|
|
402
|
+
flags = { restored: true, revalidating: reload };
|
|
403
|
+
leaving = false;
|
|
404
|
+
leftGone = false;
|
|
405
|
+
refresh();
|
|
406
|
+
// The router shows a boundary's target only over the page it opened on: it loads the page, then
|
|
407
|
+
// opens the boundary on the target again, as the link that first opened it did.
|
|
408
|
+
const reloaded = modal === undefined
|
|
409
|
+
? inner.navigate(path)
|
|
410
|
+
: inner.navigate(pagePath).then(() => {
|
|
411
|
+
const now = inner.snapshot();
|
|
412
|
+
const onPage = restoring === mine &&
|
|
413
|
+
!now.pending &&
|
|
414
|
+
now.routeId === kept.routeId &&
|
|
415
|
+
pagePathOf(now) === pagePath;
|
|
416
|
+
return onPage ? inner.navigate(path) : undefined;
|
|
417
|
+
});
|
|
418
|
+
return reloaded.finally(() => {
|
|
419
|
+
if (restoring === mine)
|
|
420
|
+
endRestore();
|
|
421
|
+
});
|
|
422
|
+
};
|
|
423
|
+
const navigate = (path, navOptions) => {
|
|
424
|
+
sweep();
|
|
425
|
+
const instance = navOptions?.traverse ?? readEntry().instance;
|
|
426
|
+
const record = instance === undefined || instance === active.instance ? undefined : records.get(instance);
|
|
427
|
+
if (instance !== undefined && record !== undefined) {
|
|
428
|
+
if (record.epoch !== readEpoch())
|
|
429
|
+
dropKept("identity");
|
|
430
|
+
else if (pathnameOf(record.page.state.path) === pathnameOf(path))
|
|
431
|
+
return restore(path, instance, record);
|
|
432
|
+
}
|
|
433
|
+
endRestore();
|
|
434
|
+
return inner.navigate(path);
|
|
435
|
+
};
|
|
436
|
+
// A submit or an invalidation may change what a hidden page shows; the response that says which
|
|
437
|
+
// paths (`X-Nifra-Revalidate`) is read inside the router, so every kept page reloads when shown.
|
|
438
|
+
const markStale = (paths) => {
|
|
439
|
+
for (const record of records.values()) {
|
|
440
|
+
const { state } = record.page;
|
|
441
|
+
if (paths === undefined || paths.includes(pagePathOf(state)) || paths.includes(state.path))
|
|
442
|
+
record.stale = true;
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
// A submit or an invalidation acts on the page on screen, which a restore hands to the router only
|
|
446
|
+
// once its reload settles.
|
|
447
|
+
const afterRestore = async () => {
|
|
448
|
+
while (restoring !== undefined)
|
|
449
|
+
await restoring.ended;
|
|
450
|
+
};
|
|
451
|
+
const channel = typeof BroadcastChannel === "undefined" ? undefined : new BroadcastChannel(IDENTITY_CHANNEL);
|
|
452
|
+
const identityDrop = () => {
|
|
453
|
+
lastEpoch = readEpoch();
|
|
454
|
+
dropKept("identity");
|
|
455
|
+
};
|
|
456
|
+
channel?.addEventListener("message", identityDrop);
|
|
457
|
+
onIdentityChange(identityDrop);
|
|
458
|
+
if (typeof addEventListener === "function") {
|
|
459
|
+
addEventListener("pagehide", (event) => {
|
|
460
|
+
if ("persisted" in event && event.persisted === true)
|
|
461
|
+
dropKept("pagehide");
|
|
462
|
+
});
|
|
463
|
+
addEventListener("visibilitychange", () => {
|
|
464
|
+
if (document.visibilityState === "visible")
|
|
465
|
+
checkEpoch();
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
snapshot: () => published,
|
|
470
|
+
subscribe: (listener) => {
|
|
471
|
+
listeners.add(listener);
|
|
472
|
+
return () => {
|
|
473
|
+
listeners.delete(listener);
|
|
474
|
+
};
|
|
475
|
+
},
|
|
476
|
+
navigate,
|
|
477
|
+
submit: async (action, body, submitOptions) => {
|
|
478
|
+
await afterRestore();
|
|
479
|
+
try {
|
|
480
|
+
await inner.submit(action, body, submitOptions);
|
|
481
|
+
}
|
|
482
|
+
finally {
|
|
483
|
+
markStale();
|
|
484
|
+
checkEpoch();
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
hydrate: () => inner.hydrate(),
|
|
488
|
+
invalidate: (paths) => {
|
|
489
|
+
markStale(paths);
|
|
490
|
+
// At once when nothing is restoring: the live client names its load around this call.
|
|
491
|
+
return restoring === undefined
|
|
492
|
+
? inner.invalidate(paths)
|
|
493
|
+
: afterRestore().then(() => inner.invalidate(paths));
|
|
494
|
+
},
|
|
495
|
+
prefetch: (path) => inner.prefetch(path),
|
|
496
|
+
fetcher: (key) => {
|
|
497
|
+
let fetcher = wrappedFetchers.get(key);
|
|
498
|
+
if (fetcher === undefined) {
|
|
499
|
+
const own = inner.fetcher(key);
|
|
500
|
+
fetcher = {
|
|
501
|
+
snapshot: own.snapshot,
|
|
502
|
+
subscribe: own.subscribe,
|
|
503
|
+
load: own.load,
|
|
504
|
+
submit: async (action, body) => {
|
|
505
|
+
try {
|
|
506
|
+
await own.submit(action, body);
|
|
507
|
+
}
|
|
508
|
+
finally {
|
|
509
|
+
markStale();
|
|
510
|
+
checkEpoch();
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
};
|
|
514
|
+
wrappedFetchers.set(key, fetcher);
|
|
515
|
+
}
|
|
516
|
+
return fetcher;
|
|
517
|
+
},
|
|
518
|
+
fetchers: () => inner.fetchers(),
|
|
519
|
+
subscribeFetchers: (listener) => inner.subscribeFetchers(listener),
|
|
520
|
+
match: (path) => inner.match(path),
|
|
521
|
+
dropKept,
|
|
522
|
+
beforePublish: (listener) => {
|
|
523
|
+
before.add(listener);
|
|
524
|
+
return () => {
|
|
525
|
+
before.delete(listener);
|
|
526
|
+
};
|
|
527
|
+
},
|
|
528
|
+
};
|
|
529
|
+
};
|
|
530
|
+
return { fetchData, attach };
|
|
531
|
+
}
|
|
532
|
+
const hasRedirect = (error) => typeof error === "object" &&
|
|
533
|
+
error !== null &&
|
|
534
|
+
typeof Reflect.get(error, "redirectTo") === "string";
|
|
535
|
+
/** A held target's reload, its data shared onto what the boundary shows. */
|
|
536
|
+
const targetOnto = (shown, payload, plainJson) => {
|
|
537
|
+
if (!isRouteDataEnvelope(payload))
|
|
538
|
+
return shareData(shown, payload, plainJson);
|
|
539
|
+
if (payload.status !== undefined)
|
|
540
|
+
return payload;
|
|
541
|
+
return { ...payload, data: shareData(shown, payload.data, plainJson) };
|
|
542
|
+
};
|
|
543
|
+
/** The holes of `state`'s page that are hosts a partially prerendered document left open. */
|
|
544
|
+
const hostsOf = (state) => Object.values(state.boundaries ?? {})
|
|
545
|
+
.filter((held) => held.ppr === true)
|
|
546
|
+
.map((held) => held.name);
|
|
547
|
+
/** `envelope`, what `kept` shows, with the fills `payload` carries for its hole hosts `holes` and
|
|
548
|
+
* nothing else of it. */
|
|
549
|
+
const fillsOnto = (envelope, kept, payload, holes) => {
|
|
550
|
+
if (!isRouteDataEnvelope(payload) || payload.status !== undefined)
|
|
551
|
+
return envelope;
|
|
552
|
+
const boundaries = { ...kept.boundaries };
|
|
553
|
+
for (const name of holes) {
|
|
554
|
+
const fill = payload.boundaries?.[name];
|
|
555
|
+
if (fill?.ppr === true)
|
|
556
|
+
boundaries[name] = fill;
|
|
557
|
+
}
|
|
558
|
+
return { ...envelope, boundaries };
|
|
559
|
+
};
|
|
560
|
+
/** A route data envelope that publishes exactly `state`'s data. */
|
|
561
|
+
const envelopeOf = (state) => {
|
|
562
|
+
const envelope = {
|
|
563
|
+
v: 1,
|
|
564
|
+
data: state.data,
|
|
565
|
+
layoutData: state.layoutData,
|
|
566
|
+
serverComponents: state.serverComponents,
|
|
567
|
+
live: state.live,
|
|
568
|
+
};
|
|
569
|
+
if (state.boundaries !== undefined)
|
|
570
|
+
envelope.boundaries = state.boundaries;
|
|
571
|
+
return envelope;
|
|
572
|
+
};
|
|
573
|
+
/** `payload` with every part equal to what `kept` shows replaced by that part. A layout the server
|
|
574
|
+
* skipped (`retained`) takes the value on screen, as on any navigation. */
|
|
575
|
+
const sharedOnto = (kept, payload, plainJson) => {
|
|
576
|
+
if (!isRouteDataEnvelope(payload))
|
|
577
|
+
return shareData(kept.data, payload, plainJson);
|
|
578
|
+
if (payload.status !== undefined)
|
|
579
|
+
return payload;
|
|
580
|
+
const retained = new Set(payload.retained ?? []);
|
|
581
|
+
return {
|
|
582
|
+
...payload,
|
|
583
|
+
data: shareData(kept.data, payload.data, plainJson),
|
|
584
|
+
layoutData: payload.layoutData?.map((value, index) => retained.has(index) ? value : shareData(kept.layoutData?.[index], value, plainJson)),
|
|
585
|
+
...(payload.boundaries === undefined
|
|
586
|
+
? {}
|
|
587
|
+
: { boundaries: shareData(kept.boundaries, payload.boundaries, plainJson) }),
|
|
588
|
+
serverComponents: shareData(kept.serverComponents, payload.serverComponents, plainJson),
|
|
589
|
+
};
|
|
590
|
+
};
|
|
591
|
+
const NO_LAYOUTS = [];
|
|
592
|
+
// The `_loading` page shown while a navigation runs has no instance of its own.
|
|
593
|
+
const LOADING = "\0loading";
|
|
594
|
+
const memberOf = (instance, state, hidden, recency, chains) => {
|
|
595
|
+
const ids = chains[state.routeId]?.ids;
|
|
596
|
+
const order = instance === undefined ? Number.MAX_SAFE_INTEGER : Number.parseInt(instance, 10);
|
|
597
|
+
return {
|
|
598
|
+
key: instance === undefined ? LOADING : `\0${instance}`,
|
|
599
|
+
order: Number.isNaN(order) ? Number.MAX_SAFE_INTEGER : order,
|
|
600
|
+
recency,
|
|
601
|
+
state,
|
|
602
|
+
hidden,
|
|
603
|
+
layouts: ids === undefined ? NO_LAYOUTS : ids.slice(0, -1),
|
|
604
|
+
leaf: ids?.at(-1) ?? state.routeId,
|
|
605
|
+
};
|
|
606
|
+
};
|
|
607
|
+
const membersOf = (state, chains) => {
|
|
608
|
+
const kept = state.kept ?? [];
|
|
609
|
+
const members = kept.map((page, i) => memberOf(page.instance, page.state, true, i, chains));
|
|
610
|
+
members.push(memberOf(state.instance, state, false, kept.length, chains));
|
|
611
|
+
return members.sort((a, b) => a.order - b.order || (a.key < b.key ? -1 : 1));
|
|
612
|
+
};
|
|
613
|
+
const levelOf = (members, depth, pages, layouts, previous) => {
|
|
614
|
+
const groups = new Map();
|
|
615
|
+
for (const member of members) {
|
|
616
|
+
const key = member.layouts[depth] ?? member.key;
|
|
617
|
+
const group = groups.get(key);
|
|
618
|
+
if (group === undefined)
|
|
619
|
+
groups.set(key, [member]);
|
|
620
|
+
else
|
|
621
|
+
group.push(member);
|
|
622
|
+
}
|
|
623
|
+
const nodes = [];
|
|
624
|
+
for (const [key, group] of groups) {
|
|
625
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: a group is created with its first member
|
|
626
|
+
const head = group[0];
|
|
627
|
+
if (key === head.key) {
|
|
628
|
+
nodes.push(reuse(previous, {
|
|
629
|
+
key,
|
|
630
|
+
depth,
|
|
631
|
+
state: head.state,
|
|
632
|
+
hidden: head.hidden,
|
|
633
|
+
wrapped: pages.has(head.leaf),
|
|
634
|
+
}));
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
let shown = head;
|
|
638
|
+
for (const member of group) {
|
|
639
|
+
if (!member.hidden) {
|
|
640
|
+
shown = member;
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
if (member.recency > shown.recency)
|
|
644
|
+
shown = member;
|
|
645
|
+
}
|
|
646
|
+
nodes.push(reuse(previous, {
|
|
647
|
+
key,
|
|
648
|
+
depth,
|
|
649
|
+
state: shown.state,
|
|
650
|
+
hidden: shown.hidden,
|
|
651
|
+
wrapped: layouts.has(key),
|
|
652
|
+
children: levelOf(group, depth + 1, pages, layouts, previous),
|
|
653
|
+
}));
|
|
654
|
+
}
|
|
655
|
+
return nodes;
|
|
656
|
+
};
|
|
657
|
+
// The previous tree's node when nothing about it changed, so a renderer that memoizes on the node
|
|
658
|
+
// skips it: a hidden page never re-renders while it is hidden.
|
|
659
|
+
const reuse = (previous, node) => {
|
|
660
|
+
const old = previous.get(node.key);
|
|
661
|
+
if (old === undefined ||
|
|
662
|
+
old.depth !== node.depth ||
|
|
663
|
+
old.state !== node.state ||
|
|
664
|
+
old.hidden !== node.hidden ||
|
|
665
|
+
old.wrapped !== node.wrapped)
|
|
666
|
+
return node;
|
|
667
|
+
const a = old.children;
|
|
668
|
+
const b = node.children;
|
|
669
|
+
if (a === b)
|
|
670
|
+
return old;
|
|
671
|
+
if (a === undefined || b === undefined || a.length !== b.length)
|
|
672
|
+
return node;
|
|
673
|
+
for (let i = 0; i < a.length; i++)
|
|
674
|
+
if (a[i] !== b[i])
|
|
675
|
+
return node;
|
|
676
|
+
return old;
|
|
677
|
+
};
|
|
678
|
+
const indexTree = (nodes, into) => {
|
|
679
|
+
for (const node of nodes) {
|
|
680
|
+
into.set(node.key, node);
|
|
681
|
+
if (node.children !== undefined)
|
|
682
|
+
indexTree(node.children, into);
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
/**
|
|
686
|
+
* The view of `state`: the page on screen and every kept page, grouped by the layouts they share. A
|
|
687
|
+
* layout every page under it shares renders once; where two chains part, each part is a slot. Pass
|
|
688
|
+
* the previous tree to keep the identity of every node that did not change.
|
|
689
|
+
*/
|
|
690
|
+
export function slotTree(state, chains, table, previous) {
|
|
691
|
+
const before = new Map();
|
|
692
|
+
if (previous !== undefined)
|
|
693
|
+
indexTree(previous, before);
|
|
694
|
+
return levelOf(membersOf(state, chains), 0, new Set(table.routes), new Set(table.layouts), before);
|
|
695
|
+
}
|
|
696
|
+
// The kept pages a render of `state` could not hide: their chain leaves the page on screen at a
|
|
697
|
+
// layout rendered without slot wrappers.
|
|
698
|
+
const unplaceable = (state, chains, pages, layouts) => {
|
|
699
|
+
if (state.kept === undefined)
|
|
700
|
+
return [];
|
|
701
|
+
const stuck = [];
|
|
702
|
+
const visit = (nodes, parentHidden) => {
|
|
703
|
+
for (const node of nodes) {
|
|
704
|
+
if (node.hidden && !parentHidden && !node.wrapped)
|
|
705
|
+
collect(node, stuck);
|
|
706
|
+
else if (node.children !== undefined)
|
|
707
|
+
visit(node.children, node.hidden);
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
visit(levelOf(membersOf(state, chains), 0, pages, layouts, new Map()), false);
|
|
711
|
+
return stuck;
|
|
712
|
+
};
|
|
713
|
+
const collect = (node, into) => {
|
|
714
|
+
if (node.children === undefined) {
|
|
715
|
+
if (node.key !== LOADING)
|
|
716
|
+
into.push(node.key.slice(1));
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
for (const child of node.children)
|
|
720
|
+
collect(child, into);
|
|
721
|
+
};
|
|
722
|
+
const scrollOffsets = (page) => {
|
|
723
|
+
const offsets = [];
|
|
724
|
+
if (page.scrollTop !== 0 || page.scrollLeft !== 0)
|
|
725
|
+
offsets.push([page, page.scrollTop, page.scrollLeft]);
|
|
726
|
+
for (const element of page.querySelectorAll("*")) {
|
|
727
|
+
if (element.scrollTop !== 0 || element.scrollLeft !== 0)
|
|
728
|
+
offsets.push([element, element.scrollTop, element.scrollLeft]);
|
|
729
|
+
}
|
|
730
|
+
return offsets;
|
|
731
|
+
};
|
|
732
|
+
/**
|
|
733
|
+
* Note the inner scroll offsets of a page's top-level `elements` and the element inside them that has
|
|
734
|
+
* focus, while they are still laid out. For an adapter whose framework takes a hidden page out of the
|
|
735
|
+
* document itself (Vue's `KeepAlive`), in place of {@link createSlotParking}.
|
|
736
|
+
*/
|
|
737
|
+
export function rememberView(elements) {
|
|
738
|
+
const offsets = elements.flatMap(scrollOffsets);
|
|
739
|
+
const focused = typeof document === "undefined" ? null : document.activeElement;
|
|
740
|
+
const focus = focused !== null && elements.some((element) => element.contains(focused)) ? focused : undefined;
|
|
741
|
+
return {
|
|
742
|
+
restore() {
|
|
743
|
+
for (const [element, top, left] of offsets) {
|
|
744
|
+
if (!element.isConnected)
|
|
745
|
+
continue;
|
|
746
|
+
element.scrollTop = top;
|
|
747
|
+
element.scrollLeft = left;
|
|
748
|
+
}
|
|
749
|
+
if (focus?.isConnected)
|
|
750
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: it held focus, so it is focusable; the call stays optional
|
|
751
|
+
focus.focus?.({ preventScroll: true });
|
|
752
|
+
},
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
const wrappedNodes = (nodes, into = []) => {
|
|
756
|
+
for (const node of nodes) {
|
|
757
|
+
if (node.wrapped)
|
|
758
|
+
into.push(node);
|
|
759
|
+
if (node.children !== undefined)
|
|
760
|
+
wrappedNodes(node.children, into);
|
|
761
|
+
}
|
|
762
|
+
return into;
|
|
763
|
+
};
|
|
764
|
+
// Development only: what each slot renders, by slot key, for the devtools panel's node counts.
|
|
765
|
+
const keptViews = new Map();
|
|
766
|
+
/**
|
|
767
|
+
* For an adapter, in development: the elements slot `key` renders, read when the devtools panel
|
|
768
|
+
* counts a kept page's nodes. `undefined` once the slot has left the view.
|
|
769
|
+
*/
|
|
770
|
+
export function noteKeptView(key, elements) {
|
|
771
|
+
if (elements === undefined)
|
|
772
|
+
keptViews.delete(key);
|
|
773
|
+
else
|
|
774
|
+
keptViews.set(key, elements);
|
|
775
|
+
}
|
|
776
|
+
/** Where a development build lists its kept pages, for `@nifrajs/devtools`. */
|
|
777
|
+
export const KEPT_PAGES_GLOBAL = "__nifraKeepAlive";
|
|
778
|
+
const describeKept = (records) => {
|
|
779
|
+
const at = now();
|
|
780
|
+
return [...records].map(({ page, hiddenAt }) => {
|
|
781
|
+
const elements = keptViews.get(`\0${page.instance}`)?.();
|
|
782
|
+
return {
|
|
783
|
+
instance: page.instance,
|
|
784
|
+
path: page.state.path,
|
|
785
|
+
routeId: page.state.routeId,
|
|
786
|
+
hiddenFor: at - hiddenAt,
|
|
787
|
+
nodes: elements?.reduce((count, element) => count + 1 + element.querySelectorAll("*").length, 0),
|
|
788
|
+
};
|
|
789
|
+
});
|
|
790
|
+
};
|
|
791
|
+
/** One per mounted view. */
|
|
792
|
+
export function createSlotParking() {
|
|
793
|
+
const slots = new Map();
|
|
794
|
+
const entryRefs = new Map();
|
|
795
|
+
const pageRefs = new Map();
|
|
796
|
+
const slotOf = (key) => {
|
|
797
|
+
let slot = slots.get(key);
|
|
798
|
+
if (slot === undefined) {
|
|
799
|
+
slot = {
|
|
800
|
+
parked: false,
|
|
801
|
+
shown: false,
|
|
802
|
+
showing: false,
|
|
803
|
+
hid: false,
|
|
804
|
+
next: null,
|
|
805
|
+
offsets: undefined,
|
|
806
|
+
focus: undefined,
|
|
807
|
+
checked: false,
|
|
808
|
+
};
|
|
809
|
+
slots.set(key, slot);
|
|
810
|
+
}
|
|
811
|
+
return slot;
|
|
812
|
+
};
|
|
813
|
+
const remember = (slot) => {
|
|
814
|
+
if (slot.page === undefined || slot.offsets !== undefined)
|
|
815
|
+
return;
|
|
816
|
+
slot.offsets = scrollOffsets(slot.page);
|
|
817
|
+
const focused = typeof document === "undefined" ? null : document.activeElement;
|
|
818
|
+
slot.focus =
|
|
819
|
+
focused !== null && focused !== slot.page && slot.page.contains(focused) ? focused : undefined;
|
|
820
|
+
};
|
|
821
|
+
// Sent while the page is still in the document, before the router's subscribers see the state that
|
|
822
|
+
// hides it, so what holds the page's content (server components) sets it aside first.
|
|
823
|
+
const fire = (slot, hide) => {
|
|
824
|
+
slot.hid = hide;
|
|
825
|
+
slot.page?.dispatchEvent(new CustomEvent(hide ? PAGE_HIDE_EVENT : PAGE_SHOW_EVENT, { bubbles: true }));
|
|
826
|
+
};
|
|
827
|
+
const unpark = (slot) => {
|
|
828
|
+
if (!slot.parked || slot.page === undefined || slot.entry === undefined)
|
|
829
|
+
return;
|
|
830
|
+
slot.entry.insertBefore(slot.page, slot.next?.parentNode === slot.entry ? slot.next : null);
|
|
831
|
+
slot.parked = false;
|
|
832
|
+
slot.showing = true;
|
|
833
|
+
};
|
|
834
|
+
return {
|
|
835
|
+
entry(key) {
|
|
836
|
+
let ref = entryRefs.get(key);
|
|
837
|
+
if (ref === undefined) {
|
|
838
|
+
ref = (element) => {
|
|
839
|
+
slotOf(key).entry = element ?? undefined;
|
|
840
|
+
};
|
|
841
|
+
entryRefs.set(key, ref);
|
|
842
|
+
}
|
|
843
|
+
return ref;
|
|
844
|
+
},
|
|
845
|
+
page(key) {
|
|
846
|
+
let ref = pageRefs.get(key);
|
|
847
|
+
if (ref === undefined) {
|
|
848
|
+
// A null never clears the page: React detaches refs inside a hidden `Activity`, and a parked
|
|
849
|
+
// page is still the framework's element. The slot leaving the tree (`after`) drops it.
|
|
850
|
+
ref = (element) => {
|
|
851
|
+
if (element === null)
|
|
852
|
+
return;
|
|
853
|
+
slotOf(key).page = element;
|
|
854
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production")
|
|
855
|
+
noteKeptView(key, () => [element]);
|
|
856
|
+
};
|
|
857
|
+
pageRefs.set(key, ref);
|
|
858
|
+
}
|
|
859
|
+
return ref;
|
|
860
|
+
},
|
|
861
|
+
before(next) {
|
|
862
|
+
for (const node of wrappedNodes(next)) {
|
|
863
|
+
const slot = slots.get(node.key);
|
|
864
|
+
if (slot === undefined)
|
|
865
|
+
continue;
|
|
866
|
+
if (node.hidden) {
|
|
867
|
+
if (!slot.shown)
|
|
868
|
+
continue;
|
|
869
|
+
remember(slot);
|
|
870
|
+
if (!slot.hid && slot.page !== undefined)
|
|
871
|
+
fire(slot, true);
|
|
872
|
+
}
|
|
873
|
+
else {
|
|
874
|
+
// Shown again before it was parked: it never left the document.
|
|
875
|
+
if (slot.hid && !slot.parked)
|
|
876
|
+
fire(slot, false);
|
|
877
|
+
unpark(slot);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
},
|
|
881
|
+
after(tree) {
|
|
882
|
+
const live = new Set();
|
|
883
|
+
for (const node of wrappedNodes(tree)) {
|
|
884
|
+
live.add(node.key);
|
|
885
|
+
const slot = slots.get(node.key);
|
|
886
|
+
const page = slot?.page;
|
|
887
|
+
if (slot === undefined || page === undefined || slot.entry === undefined)
|
|
888
|
+
continue;
|
|
889
|
+
if (node.hidden) {
|
|
890
|
+
if (!slot.parked) {
|
|
891
|
+
if (slot.shown)
|
|
892
|
+
remember(slot);
|
|
893
|
+
if (!slot.hid)
|
|
894
|
+
fire(slot, true);
|
|
895
|
+
slot.next = page.nextSibling;
|
|
896
|
+
page.remove();
|
|
897
|
+
slot.parked = true;
|
|
898
|
+
}
|
|
899
|
+
slot.shown = false;
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
unpark(slot);
|
|
903
|
+
if (slot.showing) {
|
|
904
|
+
slot.showing = false;
|
|
905
|
+
for (const [element, top, left] of slot.offsets ?? []) {
|
|
906
|
+
if (!page.contains(element))
|
|
907
|
+
continue;
|
|
908
|
+
element.scrollTop = top;
|
|
909
|
+
element.scrollLeft = left;
|
|
910
|
+
}
|
|
911
|
+
const focus = slot.focus;
|
|
912
|
+
if (focus?.isConnected && page.contains(focus))
|
|
913
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: it held focus, so it is focusable; the call stays optional
|
|
914
|
+
focus.focus?.({ preventScroll: true });
|
|
915
|
+
fire(slot, false);
|
|
916
|
+
}
|
|
917
|
+
slot.offsets = undefined;
|
|
918
|
+
slot.focus = undefined;
|
|
919
|
+
if (!slot.shown && !slot.checked) {
|
|
920
|
+
slot.checked = true;
|
|
921
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production")
|
|
922
|
+
checkSlotCss(slot.entry, page);
|
|
923
|
+
}
|
|
924
|
+
slot.shown = true;
|
|
925
|
+
}
|
|
926
|
+
for (const key of [...slots.keys()]) {
|
|
927
|
+
if (live.has(key))
|
|
928
|
+
continue;
|
|
929
|
+
const page = slots.get(key)?.page;
|
|
930
|
+
if (page !== undefined)
|
|
931
|
+
releasePage(page);
|
|
932
|
+
slots.delete(key);
|
|
933
|
+
entryRefs.delete(key);
|
|
934
|
+
pageRefs.delete(key);
|
|
935
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production")
|
|
936
|
+
noteKeptView(key, undefined);
|
|
937
|
+
}
|
|
938
|
+
},
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
// The selectors already reported, once per document.
|
|
942
|
+
const reportedCss = new Set();
|
|
943
|
+
// Pseudo-classes that read an element's position among its siblings, which the wrappers change.
|
|
944
|
+
const POSITIONAL = /:(?:first-child|last-child|only-child|nth-child\([^)]*\)|nth-last-child\([^)]*\))/g;
|
|
945
|
+
const DIRECT_HAS = /:has\(\s*>\s*([^)]*)\)/;
|
|
946
|
+
/** `selector`'s compounds and the combinators between them, split outside parentheses and brackets. */
|
|
947
|
+
const compoundsOf = (selector) => {
|
|
948
|
+
const parts = [];
|
|
949
|
+
let depth = 0;
|
|
950
|
+
let start = 0;
|
|
951
|
+
let combinator = "";
|
|
952
|
+
const flush = (end) => {
|
|
953
|
+
const compound = selector.slice(start, end).trim();
|
|
954
|
+
if (compound !== "") {
|
|
955
|
+
parts.push({ compound, before: combinator });
|
|
956
|
+
combinator = "";
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
for (let i = 0; i < selector.length; i++) {
|
|
960
|
+
const char = selector[i];
|
|
961
|
+
if (char === "(" || char === "[")
|
|
962
|
+
depth++;
|
|
963
|
+
else if (char === ")" || char === "]")
|
|
964
|
+
depth--;
|
|
965
|
+
if (depth > 0 || (char !== " " && char !== ">" && char !== "+" && char !== "~"))
|
|
966
|
+
continue;
|
|
967
|
+
flush(i);
|
|
968
|
+
if (char !== " ")
|
|
969
|
+
combinator = char;
|
|
970
|
+
else if (combinator === "")
|
|
971
|
+
combinator = " ";
|
|
972
|
+
start = i + 1;
|
|
973
|
+
}
|
|
974
|
+
if (depth <= 0)
|
|
975
|
+
flush(selector.length);
|
|
976
|
+
return parts;
|
|
977
|
+
};
|
|
978
|
+
const matches = (element, selector) => {
|
|
979
|
+
try {
|
|
980
|
+
return element.matches(selector);
|
|
981
|
+
}
|
|
982
|
+
catch {
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
// Whether `selector` targets a top-level node of the page as a child of the element the slot sits in,
|
|
987
|
+
// which the two wrappers between them now break.
|
|
988
|
+
const breaksAround = (selector, parent, tops) => {
|
|
989
|
+
const parts = compoundsOf(selector);
|
|
990
|
+
for (const [i, { compound, before }] of parts.entries()) {
|
|
991
|
+
const prefix = parts
|
|
992
|
+
.slice(0, i)
|
|
993
|
+
.map((part, j) => (j === 0 ? part.compound : `${part.before} ${part.compound}`))
|
|
994
|
+
.join(" ");
|
|
995
|
+
if (before === ">" && matches(parent, prefix) && tops.some((top) => matches(top, compound)))
|
|
996
|
+
return true;
|
|
997
|
+
if (POSITIONAL.test(compound)) {
|
|
998
|
+
POSITIONAL.lastIndex = 0;
|
|
999
|
+
const rest = compound.replace(POSITIONAL, "") || "*";
|
|
1000
|
+
if (tops.some((top) => matches(top, rest)) &&
|
|
1001
|
+
(prefix === "" || matches(parent, prefix) || parent.closest(prefix) !== null))
|
|
1002
|
+
return true;
|
|
1003
|
+
}
|
|
1004
|
+
POSITIONAL.lastIndex = 0;
|
|
1005
|
+
const has = DIRECT_HAS.exec(compound);
|
|
1006
|
+
if (has !== null) {
|
|
1007
|
+
const owner = compound.replace(DIRECT_HAS, "") || "*";
|
|
1008
|
+
if (matches(parent, owner) && tops.some((top) => matches(top, has[1] ?? "")))
|
|
1009
|
+
return true;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return false;
|
|
1013
|
+
};
|
|
1014
|
+
const scanRules = (rules, parent, tops, source) => {
|
|
1015
|
+
for (const rule of rules) {
|
|
1016
|
+
if (rule instanceof CSSStyleRule) {
|
|
1017
|
+
for (const selector of splitSelectors(rule.selectorText)) {
|
|
1018
|
+
if (reportedCss.has(selector) || !breaksAround(selector, parent, tops))
|
|
1019
|
+
continue;
|
|
1020
|
+
reportedCss.add(selector);
|
|
1021
|
+
console.warn(`[nifra/web] NIFRA_KEEPALIVE_DIRECT_CHILD_CSS: the selector "${selector}"${source} reaches a kept page's top-level element as a direct child of <${parent.tagName.toLowerCase()}>, but a keepAlive page renders inside two display:contents wrappers, so it no longer matches. Use a descendant combinator, target the wrapper ([data-nifra-page] > ...), or put a class on the page's root element.`);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
else if ("cssRules" in rule) {
|
|
1025
|
+
// @media, @supports, @layer and @container rules hold style rules of their own.
|
|
1026
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: a rule with cssRules is a grouping rule
|
|
1027
|
+
scanRules(rule.cssRules, parent, tops, source);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
const splitSelectors = (list) => {
|
|
1032
|
+
const out = [];
|
|
1033
|
+
let depth = 0;
|
|
1034
|
+
let start = 0;
|
|
1035
|
+
for (let i = 0; i < list.length; i++) {
|
|
1036
|
+
const char = list[i];
|
|
1037
|
+
if (char === "(" || char === "[")
|
|
1038
|
+
depth++;
|
|
1039
|
+
else if (char === ")" || char === "]")
|
|
1040
|
+
depth--;
|
|
1041
|
+
else if (char === "," && depth === 0) {
|
|
1042
|
+
out.push(list.slice(start, i).trim());
|
|
1043
|
+
start = i + 1;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
out.push(list.slice(start).trim());
|
|
1047
|
+
return out.filter((selector) => selector !== "");
|
|
1048
|
+
};
|
|
1049
|
+
/**
|
|
1050
|
+
* Dev only: report each same-origin style rule whose selector the slot wrappers around `page` stop
|
|
1051
|
+
* matching (`NIFRA_KEEPALIVE_DIRECT_CHILD_CSS`). A stylesheet the page cannot read is skipped.
|
|
1052
|
+
*/
|
|
1053
|
+
export function checkSlotCss(entry, page) {
|
|
1054
|
+
const parent = entry.parentElement;
|
|
1055
|
+
const tops = [...page.children];
|
|
1056
|
+
if (parent === null || tops.length === 0 || typeof document === "undefined")
|
|
1057
|
+
return;
|
|
1058
|
+
for (const sheet of document.styleSheets) {
|
|
1059
|
+
let rules;
|
|
1060
|
+
try {
|
|
1061
|
+
rules = sheet.cssRules;
|
|
1062
|
+
}
|
|
1063
|
+
catch {
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
scanRules(rules, parent, tops, sheet.href === null ? "" : ` (${sheet.href})`);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
//# sourceMappingURL=keep-alive.js.map
|