@nifrajs/web 5.5.1 → 5.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +154 -0
- package/dist/boundary.d.ts +67 -7
- package/dist/boundary.d.ts.map +1 -1
- package/dist/boundary.js +6 -4
- package/dist/boundary.js.map +1 -1
- package/dist/build-plan.d.ts +3 -0
- package/dist/build-plan.d.ts.map +1 -1
- package/dist/build-plan.js.map +1 -1
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +2 -0
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +9 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +9 -0
- package/dist/build.js.map +1 -1
- package/dist/client.d.ts +2 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +140 -12
- package/dist/client.js.map +1 -1
- package/dist/csp.d.ts +8 -0
- package/dist/csp.d.ts.map +1 -1
- package/dist/csp.js +11 -2
- package/dist/csp.js.map +1 -1
- package/dist/deferred.d.ts +6 -4
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +10 -6
- package/dist/deferred.js.map +1 -1
- package/dist/dev-feed.d.ts.map +1 -1
- package/dist/dev-feed.js +11 -2
- package/dist/dev-feed.js.map +1 -1
- package/dist/dev-session.d.ts.map +1 -1
- package/dist/dev-session.js +13 -6
- package/dist/dev-session.js.map +1 -1
- package/dist/dev.d.ts +4 -1
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +12 -5
- package/dist/dev.js.map +1 -1
- package/dist/diagnostic-catalog.d.ts +4 -0
- package/dist/diagnostic-catalog.d.ts.map +1 -1
- package/dist/diagnostic-catalog.js +48 -0
- package/dist/diagnostic-catalog.js.map +1 -1
- package/dist/diagnostic.d.ts +1 -1
- package/dist/diagnostic.d.ts.map +1 -1
- package/dist/diagnostic.js +1 -1
- package/dist/diagnostic.js.map +1 -1
- package/dist/fn.d.ts +5 -3
- package/dist/fn.d.ts.map +1 -1
- package/dist/fn.js +20 -5
- package/dist/fn.js.map +1 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +13 -3
- package/dist/fs.js.map +1 -1
- package/dist/identity.d.ts +22 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +60 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/cdn/cloudflare.d.ts +2 -1
- package/dist/internal/cdn/cloudflare.d.ts.map +1 -1
- package/dist/internal/cdn/cloudflare.js +2 -1
- package/dist/internal/cdn/cloudflare.js.map +1 -1
- package/dist/internal/certified-components.d.ts +1 -1
- package/dist/internal/certified-components.js +1 -1
- package/dist/internal/codegen.d.ts +11 -10
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +65 -8
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/dev-websocket.d.ts +73 -0
- package/dist/internal/dev-websocket.d.ts.map +1 -0
- package/dist/internal/dev-websocket.js +145 -0
- package/dist/internal/dev-websocket.js.map +1 -0
- package/dist/internal/hole-runtime.d.ts +66 -0
- package/dist/internal/hole-runtime.d.ts.map +1 -0
- package/dist/internal/hole-runtime.js +211 -0
- package/dist/internal/hole-runtime.js.map +1 -0
- package/dist/internal/identity-epoch.d.ts +50 -0
- package/dist/internal/identity-epoch.d.ts.map +1 -0
- package/dist/internal/identity-epoch.js +179 -0
- package/dist/internal/identity-epoch.js.map +1 -0
- package/dist/internal/invalidation-contract.d.ts +23 -0
- package/dist/internal/invalidation-contract.d.ts.map +1 -0
- package/dist/internal/invalidation-contract.js +11 -0
- package/dist/internal/invalidation-contract.js.map +1 -0
- package/dist/internal/invalidation.d.ts +62 -0
- package/dist/internal/invalidation.d.ts.map +1 -0
- package/dist/internal/invalidation.js +149 -0
- package/dist/internal/invalidation.js.map +1 -0
- package/dist/internal/island-bindings.d.ts +51 -0
- package/dist/internal/island-bindings.d.ts.map +1 -0
- package/dist/internal/island-bindings.js +147 -0
- package/dist/internal/island-bindings.js.map +1 -0
- package/dist/internal/island-list.d.ts +26 -0
- package/dist/internal/island-list.d.ts.map +1 -0
- package/dist/internal/island-list.js +78 -0
- package/dist/internal/island-list.js.map +1 -0
- package/dist/internal/island-mount.d.ts +4 -0
- package/dist/internal/island-mount.d.ts.map +1 -0
- package/dist/internal/island-mount.js +66 -0
- package/dist/internal/island-mount.js.map +1 -0
- package/dist/internal/island-owner.d.ts +5 -0
- package/dist/internal/island-owner.d.ts.map +1 -0
- package/dist/internal/island-owner.js +20 -0
- package/dist/internal/island-owner.js.map +1 -0
- package/dist/internal/island-resource.d.ts +30 -0
- package/dist/internal/island-resource.d.ts.map +1 -0
- package/dist/internal/island-resource.js +57 -0
- package/dist/internal/island-resource.js.map +1 -0
- package/dist/internal/island-signals.d.ts +51 -0
- package/dist/internal/island-signals.d.ts.map +1 -0
- package/dist/internal/island-signals.js +220 -0
- package/dist/internal/island-signals.js.map +1 -0
- package/dist/internal/keep-alive-table.d.ts +23 -0
- package/dist/internal/keep-alive-table.d.ts.map +1 -0
- package/dist/internal/keep-alive-table.js +53 -0
- package/dist/internal/keep-alive-table.js.map +1 -0
- package/dist/internal/live-contract.d.ts +96 -0
- package/dist/internal/live-contract.d.ts.map +1 -0
- package/dist/internal/live-contract.js +5 -0
- package/dist/internal/live-contract.js.map +1 -0
- package/dist/internal/live-queries.d.ts +22 -0
- package/dist/internal/live-queries.d.ts.map +1 -0
- package/dist/internal/live-queries.js +54 -0
- package/dist/internal/live-queries.js.map +1 -0
- package/dist/internal/page-events.d.ts +10 -0
- package/dist/internal/page-events.d.ts.map +1 -0
- package/dist/internal/page-events.js +26 -0
- package/dist/internal/page-events.js.map +1 -0
- package/dist/internal/page-execution.d.ts +14 -1
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +415 -31
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/ppr/admission.d.ts +83 -0
- package/dist/internal/ppr/admission.d.ts.map +1 -0
- package/dist/internal/ppr/admission.js +221 -0
- package/dist/internal/ppr/admission.js.map +1 -0
- package/dist/internal/ppr/canary.d.ts +24 -0
- package/dist/internal/ppr/canary.d.ts.map +1 -0
- package/dist/internal/ppr/canary.js +102 -0
- package/dist/internal/ppr/canary.js.map +1 -0
- package/dist/internal/ppr/compile-shell.d.ts +39 -0
- package/dist/internal/ppr/compile-shell.d.ts.map +1 -0
- package/dist/internal/ppr/compile-shell.js +298 -0
- package/dist/internal/ppr/compile-shell.js.map +1 -0
- package/dist/internal/ppr/contract.d.ts +177 -0
- package/dist/internal/ppr/contract.d.ts.map +1 -0
- package/dist/internal/ppr/contract.js +80 -0
- package/dist/internal/ppr/contract.js.map +1 -0
- package/dist/internal/ppr/frames.d.ts +71 -0
- package/dist/internal/ppr/frames.d.ts.map +1 -0
- package/dist/internal/ppr/frames.js +119 -0
- package/dist/internal/ppr/frames.js.map +1 -0
- package/dist/internal/ppr/integration.d.ts +19 -0
- package/dist/internal/ppr/integration.d.ts.map +1 -0
- package/dist/internal/ppr/integration.js +106 -0
- package/dist/internal/ppr/integration.js.map +1 -0
- package/dist/internal/ppr/options.d.ts +96 -0
- package/dist/internal/ppr/options.d.ts.map +1 -0
- package/dist/internal/ppr/options.js +125 -0
- package/dist/internal/ppr/options.js.map +1 -0
- package/dist/internal/ppr/route-rules.d.ts +12 -0
- package/dist/internal/ppr/route-rules.d.ts.map +1 -0
- package/dist/internal/ppr/route-rules.js +53 -0
- package/dist/internal/ppr/route-rules.js.map +1 -0
- package/dist/internal/ppr/runtime.d.ts +29 -0
- package/dist/internal/ppr/runtime.d.ts.map +1 -0
- package/dist/internal/ppr/runtime.js +32 -0
- package/dist/internal/ppr/runtime.js.map +1 -0
- package/dist/internal/ppr/server.d.ts +29 -0
- package/dist/internal/ppr/server.d.ts.map +1 -0
- package/dist/internal/ppr/server.js +1407 -0
- package/dist/internal/ppr/server.js.map +1 -0
- package/dist/internal/ppr/shell-store.d.ts +38 -0
- package/dist/internal/ppr/shell-store.d.ts.map +1 -0
- package/dist/internal/ppr/shell-store.js +90 -0
- package/dist/internal/ppr/shell-store.js.map +1 -0
- package/dist/internal/prerendered-pages.d.ts +1 -1
- package/dist/internal/prerendered-pages.d.ts.map +1 -1
- package/dist/internal/prerendered-pages.js +7 -4
- package/dist/internal/prerendered-pages.js.map +1 -1
- package/dist/internal/query-digest.d.ts +9 -0
- package/dist/internal/query-digest.d.ts.map +1 -0
- package/dist/internal/query-digest.js +23 -0
- package/dist/internal/query-digest.js.map +1 -0
- package/dist/internal/query-runtime.d.ts +6 -1
- package/dist/internal/query-runtime.d.ts.map +1 -1
- package/dist/internal/query-runtime.js +9 -0
- package/dist/internal/query-runtime.js.map +1 -1
- package/dist/internal/query-seam.d.ts +29 -0
- package/dist/internal/query-seam.d.ts.map +1 -0
- package/dist/internal/query-seam.js +23 -0
- package/dist/internal/query-seam.js.map +1 -0
- package/dist/internal/render-document.d.ts +28 -1
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +83 -18
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/request-abort.d.ts +8 -0
- package/dist/internal/request-abort.d.ts.map +1 -0
- package/dist/internal/request-abort.js +22 -0
- package/dist/internal/request-abort.js.map +1 -0
- package/dist/internal/server-boundary.d.ts +2 -0
- package/dist/internal/server-boundary.d.ts.map +1 -1
- package/dist/internal/server-boundary.js +8 -1
- package/dist/internal/server-boundary.js.map +1 -1
- package/dist/internal/share-data.d.ts +10 -0
- package/dist/internal/share-data.d.ts.map +1 -0
- package/dist/internal/share-data.js +135 -0
- package/dist/internal/share-data.js.map +1 -0
- package/dist/internal/store-signature.d.ts +15 -0
- package/dist/internal/store-signature.d.ts.map +1 -0
- package/dist/internal/store-signature.js +114 -0
- package/dist/internal/store-signature.js.map +1 -0
- package/dist/internal/vite-plugin-compat.d.ts +2 -2
- package/dist/internal/vite-plugin-compat.js +2 -2
- package/dist/internal/web-app.d.ts +25 -0
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +78 -3
- package/dist/internal/web-app.js.map +1 -1
- package/dist/islands-swap.d.ts +41 -0
- package/dist/islands-swap.d.ts.map +1 -0
- package/dist/islands-swap.js +270 -0
- package/dist/islands-swap.js.map +1 -0
- package/dist/islands.d.ts +86 -24
- package/dist/islands.d.ts.map +1 -1
- package/dist/islands.js +138 -78
- package/dist/islands.js.map +1 -1
- package/dist/isr.d.ts +67 -13
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +247 -99
- package/dist/isr.js.map +1 -1
- package/dist/keep-alive.d.ts +147 -0
- package/dist/keep-alive.d.ts.map +1 -0
- package/dist/keep-alive.js +1069 -0
- package/dist/keep-alive.js.map +1 -0
- package/dist/live/bus.d.ts +56 -0
- package/dist/live/bus.d.ts.map +1 -0
- package/dist/live/bus.js +32 -0
- package/dist/live/bus.js.map +1 -0
- package/dist/live/core.d.ts +25 -0
- package/dist/live/core.d.ts.map +1 -0
- package/dist/live/core.js +29 -0
- package/dist/live/core.js.map +1 -0
- package/dist/live/history.d.ts +38 -0
- package/dist/live/history.d.ts.map +1 -0
- package/dist/live/history.js +104 -0
- package/dist/live/history.js.map +1 -0
- package/dist/live/hub-protocol.d.ts +46 -0
- package/dist/live/hub-protocol.d.ts.map +1 -0
- package/dist/live/hub-protocol.js +108 -0
- package/dist/live/hub-protocol.js.map +1 -0
- package/dist/live/hub.d.ts +181 -0
- package/dist/live/hub.d.ts.map +1 -0
- package/dist/live/hub.js +539 -0
- package/dist/live/hub.js.map +1 -0
- package/dist/live/keyring.d.ts +87 -0
- package/dist/live/keyring.d.ts.map +1 -0
- package/dist/live/keyring.js +260 -0
- package/dist/live/keyring.js.map +1 -0
- package/dist/live/read-capped.d.ts +6 -0
- package/dist/live/read-capped.d.ts.map +1 -0
- package/dist/live/read-capped.js +42 -0
- package/dist/live/read-capped.js.map +1 -0
- package/dist/live/revocations.d.ts +52 -0
- package/dist/live/revocations.d.ts.map +1 -0
- package/dist/live/revocations.js +116 -0
- package/dist/live/revocations.js.map +1 -0
- package/dist/live/settings.d.ts +14 -0
- package/dist/live/settings.d.ts.map +1 -0
- package/dist/live/settings.js +38 -0
- package/dist/live/settings.js.map +1 -0
- package/dist/live-client.d.ts +34 -0
- package/dist/live-client.d.ts.map +1 -0
- package/dist/live-client.js +840 -0
- package/dist/live-client.js.map +1 -0
- package/dist/live-hub.d.ts +130 -0
- package/dist/live-hub.d.ts.map +1 -0
- package/dist/live-hub.js +507 -0
- package/dist/live-hub.js.map +1 -0
- package/dist/live-redis.d.ts +48 -0
- package/dist/live-redis.d.ts.map +1 -0
- package/dist/live-redis.js +201 -0
- package/dist/live-redis.js.map +1 -0
- package/dist/live.d.ts +94 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +869 -0
- package/dist/live.js.map +1 -0
- package/dist/manifest.d.ts +113 -2
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +109 -5
- package/dist/manifest.js.map +1 -1
- package/dist/navigation.d.ts +9 -3
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +13 -3
- package/dist/navigation.js.map +1 -1
- package/dist/plugins/vite-zone-guard.d.ts +6 -0
- package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
- package/dist/plugins/vite-zone-guard.js +8 -2
- package/dist/plugins/vite-zone-guard.js.map +1 -1
- package/dist/ppr.d.ts +73 -0
- package/dist/ppr.d.ts.map +1 -0
- package/dist/ppr.js +76 -0
- package/dist/ppr.js.map +1 -0
- package/dist/query.d.ts +8 -13
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +112 -18
- package/dist/query.js.map +1 -1
- package/dist/render-seam.d.ts +30 -0
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +5 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/route-types.d.ts.map +1 -1
- package/dist/route-types.js +2 -2
- package/dist/route-types.js.map +1 -1
- package/dist/router.d.ts +72 -4
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +374 -99
- package/dist/router.js.map +1 -1
- package/dist/server-components/build.d.ts.map +1 -1
- package/dist/server-components/build.js +3 -3
- package/dist/server-components/build.js.map +1 -1
- package/dist/server-components/client.d.ts +23 -1
- package/dist/server-components/client.d.ts.map +1 -1
- package/dist/server-components/client.js +92 -5
- package/dist/server-components/client.js.map +1 -1
- package/dist/server-components/hole.d.ts +8 -0
- package/dist/server-components/hole.d.ts.map +1 -0
- package/dist/server-components/hole.js +20 -0
- package/dist/server-components/hole.js.map +1 -0
- package/dist/server-components/html-policy.d.ts.map +1 -1
- package/dist/server-components/html-policy.js +62 -4
- package/dist/server-components/html-policy.js.map +1 -1
- package/dist/server-components/html-scan.d.ts +17 -0
- package/dist/server-components/html-scan.d.ts.map +1 -0
- package/dist/server-components/html-scan.js +180 -0
- package/dist/server-components/html-scan.js.map +1 -0
- package/dist/server-components/html.d.ts.map +1 -1
- package/dist/server-components/html.js +19 -4
- package/dist/server-components/html.js.map +1 -1
- package/dist/server-components/router-integration.d.ts +4 -1
- package/dist/server-components/router-integration.d.ts.map +1 -1
- package/dist/server-components/router-integration.js +23 -2
- package/dist/server-components/router-integration.js.map +1 -1
- package/dist/server-components/runtime.d.ts +6 -1
- package/dist/server-components/runtime.d.ts.map +1 -1
- package/dist/server-components/runtime.js +86 -28
- package/dist/server-components/runtime.js.map +1 -1
- package/dist/server-components/state.d.ts +3 -1
- package/dist/server-components/state.d.ts.map +1 -1
- package/dist/server-components/state.js +50 -38
- package/dist/server-components/state.js.map +1 -1
- package/dist/server-components/types.d.ts +9 -0
- package/dist/server-components/types.d.ts.map +1 -1
- package/dist/vite.d.ts +5 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +58 -4
- package/dist/vite.js.map +1 -1
- package/package.json +48 -8
- package/src/boundary.ts +77 -5
- package/src/build-plan.ts +3 -0
- package/src/build-vite.ts +2 -0
- package/src/build.ts +19 -1
- package/src/client.ts +147 -10
- package/src/csp.ts +16 -2
- package/src/deferred.ts +11 -5
- package/src/dev-feed.ts +11 -2
- package/src/dev-session.ts +22 -11
- package/src/dev.ts +29 -8
- package/src/diagnostic-catalog.ts +53 -0
- package/src/diagnostic.ts +1 -0
- package/src/fn.ts +24 -7
- package/src/fs.ts +22 -8
- package/src/identity.ts +61 -0
- package/src/index.ts +19 -0
- package/src/internal/cdn/cloudflare.ts +2 -1
- package/src/internal/certified-components.ts +1 -1
- package/src/internal/codegen.ts +87 -8
- package/src/internal/dev-websocket.ts +230 -0
- package/src/internal/hole-runtime.ts +286 -0
- package/src/internal/identity-epoch.ts +235 -0
- package/src/internal/invalidation-contract.ts +35 -0
- package/src/internal/invalidation.ts +213 -0
- package/src/internal/island-bindings.ts +191 -0
- package/src/internal/island-list.ts +92 -0
- package/src/internal/island-mount.ts +68 -0
- package/src/internal/island-owner.ts +20 -0
- package/src/internal/island-resource.ts +76 -0
- package/src/internal/island-signals.ts +249 -0
- package/src/internal/keep-alive-table.ts +72 -0
- package/src/internal/live-contract.ts +106 -0
- package/src/internal/live-queries.ts +60 -0
- package/src/internal/page-events.ts +27 -0
- package/src/internal/page-execution.ts +579 -40
- package/src/internal/ppr/admission.ts +300 -0
- package/src/internal/ppr/canary.ts +122 -0
- package/src/internal/ppr/compile-shell.ts +379 -0
- package/src/internal/ppr/contract.ts +265 -0
- package/src/internal/ppr/frames.ts +170 -0
- package/src/internal/ppr/integration.ts +137 -0
- package/src/internal/ppr/options.ts +221 -0
- package/src/internal/ppr/route-rules.ts +77 -0
- package/src/internal/ppr/runtime.ts +36 -0
- package/src/internal/ppr/server.ts +1764 -0
- package/src/internal/ppr/shell-store.ts +139 -0
- package/src/internal/prerendered-pages.ts +7 -4
- package/src/internal/query-digest.ts +34 -0
- package/src/internal/query-runtime.ts +14 -1
- package/src/internal/query-seam.ts +48 -0
- package/src/internal/render-document.ts +149 -22
- package/src/internal/request-abort.ts +26 -0
- package/src/internal/server-boundary.ts +10 -1
- package/src/internal/share-data.ts +136 -0
- package/src/internal/store-signature.ts +149 -0
- package/src/internal/vite-plugin-compat.ts +2 -2
- package/src/internal/web-app.ts +121 -3
- package/src/islands-swap.ts +283 -0
- package/src/islands.ts +169 -72
- package/src/isr.ts +348 -106
- package/src/keep-alive.ts +1315 -0
- package/src/live/bus.ts +77 -0
- package/src/live/core.ts +52 -0
- package/src/live/history.ts +109 -0
- package/src/live/hub-protocol.ts +133 -0
- package/src/live/hub.ts +615 -0
- package/src/live/keyring.ts +379 -0
- package/src/live/read-capped.ts +36 -0
- package/src/live/revocations.ts +155 -0
- package/src/live/settings.ts +46 -0
- package/src/live-client.ts +973 -0
- package/src/live-hub.ts +658 -0
- package/src/live-redis.ts +259 -0
- package/src/live.ts +1096 -0
- package/src/manifest.ts +274 -6
- package/src/navigation.ts +14 -2
- package/src/plugins/vite-zone-guard.ts +15 -2
- package/src/ppr.ts +153 -0
- package/src/query.ts +147 -20
- package/src/render-seam.ts +35 -0
- package/src/route-types.ts +3 -1
- package/src/router.ts +494 -101
- package/src/server-components/build.ts +9 -3
- package/src/server-components/client.ts +89 -5
- package/src/server-components/hole.ts +23 -0
- package/src/server-components/html-policy.ts +63 -4
- package/src/server-components/html-scan.ts +159 -0
- package/src/server-components/html.ts +18 -4
- package/src/server-components/router-integration.ts +21 -2
- package/src/server-components/runtime.ts +112 -30
- package/src/server-components/state.ts +66 -44
- package/src/server-components/types.ts +16 -0
- package/src/vite.ts +75 -6
- package/dist/nano.d.ts +0 -125
- package/dist/nano.d.ts.map +0 -1
- package/dist/nano.js +0 -190
- package/dist/nano.js.map +0 -1
- package/src/nano.ts +0 -246
|
@@ -0,0 +1,1315 @@
|
|
|
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.ts"
|
|
13
|
+
import type { KeepAliveTable } from "./internal/keep-alive-table.ts"
|
|
14
|
+
import { PAGE_HIDE_EVENT, PAGE_SHOW_EVENT, releasePage } from "./internal/page-events.ts"
|
|
15
|
+
import { shareData } from "./internal/share-data.ts"
|
|
16
|
+
import type { ClientRouteHooks, KeepAliveOptions } from "./manifest.ts"
|
|
17
|
+
import type { MatchChain } from "./render-seam.ts"
|
|
18
|
+
import {
|
|
19
|
+
type ClientRouter,
|
|
20
|
+
type Fetcher,
|
|
21
|
+
type FetchRouteData,
|
|
22
|
+
fetchRouteData,
|
|
23
|
+
isRouteDataEnvelope,
|
|
24
|
+
type KeptPage,
|
|
25
|
+
type RouterState,
|
|
26
|
+
} from "./router.ts"
|
|
27
|
+
|
|
28
|
+
export { EPOCH_COOKIE, IDENTITY_CHANNEL, identityChanged } from "./identity.ts"
|
|
29
|
+
export type { KeepAliveTable } from "./internal/keep-alive-table.ts"
|
|
30
|
+
export { PAGE_HIDE_EVENT, PAGE_SHOW_EVENT } from "./internal/page-events.ts"
|
|
31
|
+
|
|
32
|
+
/** Most pages kept per tab. */
|
|
33
|
+
export const KEEP_ALIVE_MAX = 3
|
|
34
|
+
/** Longest a page stays hidden unless its route sets `keepAlive: { maxAge }`, in milliseconds. */
|
|
35
|
+
export const KEEP_ALIVE_MAX_AGE_MS = 10 * 60 * 1000
|
|
36
|
+
|
|
37
|
+
/** Why kept pages were dropped. */
|
|
38
|
+
export type KeepAliveDropReason =
|
|
39
|
+
/** The visitor signed in or out (the epoch cookie changed, or another tab said so). */
|
|
40
|
+
| "identity"
|
|
41
|
+
/** The document entered the browser's own back/forward cache. */
|
|
42
|
+
| "pagehide"
|
|
43
|
+
/** A push cut off the history entries they belonged to. */
|
|
44
|
+
| "forward"
|
|
45
|
+
/** A fourth page was kept; the least recently shown went. */
|
|
46
|
+
| "lru"
|
|
47
|
+
/** Hidden longer than its route's `maxAge`. */
|
|
48
|
+
| "expired"
|
|
49
|
+
/** Its chain leaves the page on screen at a layout rendered without slot wrappers. */
|
|
50
|
+
| "layout"
|
|
51
|
+
/** A route module was replaced by a hot update. */
|
|
52
|
+
| "hot-update"
|
|
53
|
+
/** `dropKept` was called. */
|
|
54
|
+
| "manual"
|
|
55
|
+
|
|
56
|
+
/** Per-navigation options of {@link KeepAliveRouter.navigate}. */
|
|
57
|
+
export interface KeepAliveNavigateOptions {
|
|
58
|
+
/** The instance of the history entry a back/forward lands on. Read from `history.state` when absent. */
|
|
59
|
+
readonly traverse?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The router as {@link keepAliveClient} hands it to the history layer and the adapter. */
|
|
63
|
+
export interface KeepAliveRouter extends ClientRouter {
|
|
64
|
+
navigate: (path: string, options?: KeepAliveNavigateOptions) => Promise<void>
|
|
65
|
+
/** Drop every kept page. */
|
|
66
|
+
dropKept: (reason?: KeepAliveDropReason) => void
|
|
67
|
+
/** Run `listener` with each state before it is published: the DOM half reads what the next render
|
|
68
|
+
* hides or shows again while the page being left is still laid out. Returns its removal. */
|
|
69
|
+
beforePublish: (listener: (next: RouterState) => void) => () => void
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface KeepAliveClientOptions {
|
|
73
|
+
/** Where the slot wrappers go (generated from the route table). */
|
|
74
|
+
readonly table: KeepAliveTable
|
|
75
|
+
/** Route id → its client hooks, filled in as route modules load. A page is kept only while its
|
|
76
|
+
* module exports `keepAlive`. */
|
|
77
|
+
readonly routeHooks: Readonly<Record<string, ClientRouteHooks | undefined>>
|
|
78
|
+
/** Route id → its layout ids then its own id, filled in as route modules load. */
|
|
79
|
+
readonly matchChains: Readonly<Record<string, MatchChain | undefined>>
|
|
80
|
+
/** The transport to layer over (a live app's); the router's own by default. */
|
|
81
|
+
readonly fetchData?: FetchRouteData
|
|
82
|
+
/** Most pages kept (default {@link KEEP_ALIVE_MAX}). */
|
|
83
|
+
readonly max?: number
|
|
84
|
+
/** Called with the instances each drop removed. */
|
|
85
|
+
readonly onDrop?: (reason: KeepAliveDropReason, instances: readonly string[]) => void
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** What {@link keepAliveClient} returns: the transport to build the router with, then the layer. */
|
|
89
|
+
export interface KeepAliveClient {
|
|
90
|
+
/** Pass as `createClientRouter({ fetchData })`, so a restored page's reload keeps what it showed. */
|
|
91
|
+
readonly fetchData: FetchRouteData
|
|
92
|
+
/** Layer keep-alive over the router built with {@link fetchData}. Call once, before
|
|
93
|
+
* `installHistory`. */
|
|
94
|
+
attach(router: ClientRouter): KeepAliveRouter
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface Entry {
|
|
98
|
+
readonly index: number
|
|
99
|
+
readonly instance: string | undefined
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The server data a client loader built a page's data from, by the path it was loaded under.
|
|
103
|
+
type Sources = ReadonlyMap<string, unknown> | undefined
|
|
104
|
+
|
|
105
|
+
interface Kept {
|
|
106
|
+
readonly page: KeptPage
|
|
107
|
+
readonly index: number
|
|
108
|
+
readonly epoch: string
|
|
109
|
+
readonly sources: Sources
|
|
110
|
+
readonly hiddenAt: number
|
|
111
|
+
readonly maxAge: number
|
|
112
|
+
readonly revalidate: boolean
|
|
113
|
+
stale: boolean
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface Active {
|
|
117
|
+
readonly instance: string
|
|
118
|
+
readonly index: number
|
|
119
|
+
readonly settled: RouterState
|
|
120
|
+
readonly epoch: string
|
|
121
|
+
readonly sources: Sources
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface Restore {
|
|
125
|
+
/** The URL of the history entry restored: the page's, or the target its boundary holds. */
|
|
126
|
+
readonly path: string
|
|
127
|
+
/** The URL the page's own data reloads from. */
|
|
128
|
+
readonly pagePath: string
|
|
129
|
+
readonly kept: RouterState
|
|
130
|
+
readonly reload: boolean
|
|
131
|
+
readonly sources: Sources
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const pathnameOf = (path: string): string => {
|
|
135
|
+
const q = path.indexOf("?")
|
|
136
|
+
return q === -1 ? path : path.slice(0, q)
|
|
137
|
+
}
|
|
138
|
+
const pagePathOf = (state: RouterState): string => state.intercepted?.pagePath ?? state.path
|
|
139
|
+
|
|
140
|
+
// Unique per document, so an instance an entry carries from an earlier document never names one here.
|
|
141
|
+
const DOCUMENT = Math.random().toString(36).slice(2, 8)
|
|
142
|
+
let sequence = 0
|
|
143
|
+
const newInstance = (): string => `${++sequence}:${DOCUMENT}`
|
|
144
|
+
const ownInstance = (value: unknown): string | undefined =>
|
|
145
|
+
typeof value === "string" && value.endsWith(`:${DOCUMENT}`) ? value : undefined
|
|
146
|
+
|
|
147
|
+
const readEntry = (): Entry => {
|
|
148
|
+
const state: unknown = typeof history === "undefined" ? null : history.state
|
|
149
|
+
const fields = typeof state === "object" && state !== null ? state : {}
|
|
150
|
+
const index = Reflect.get(fields, "nifraIndex")
|
|
151
|
+
return {
|
|
152
|
+
index: typeof index === "number" ? index : 0,
|
|
153
|
+
instance: ownInstance(Reflect.get(fields, "nifraInstance")),
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// The instance rides on the entry beside the history layer's own fields, which every write it makes
|
|
157
|
+
// to an existing entry keeps; a fresh push or replace carries none.
|
|
158
|
+
const stampEntry = (instance: string): void => {
|
|
159
|
+
if (typeof history === "undefined") return
|
|
160
|
+
const state: unknown = history.state
|
|
161
|
+
if (
|
|
162
|
+
typeof state === "object" &&
|
|
163
|
+
state !== null &&
|
|
164
|
+
Reflect.get(state, "nifraInstance") === instance
|
|
165
|
+
)
|
|
166
|
+
return
|
|
167
|
+
try {
|
|
168
|
+
history.replaceState(
|
|
169
|
+
Object.assign({}, typeof state === "object" ? state : null, { nifraInstance: instance }),
|
|
170
|
+
"",
|
|
171
|
+
)
|
|
172
|
+
} catch {}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const readEpoch = (): string => (typeof document === "undefined" ? "" : epochOf(document.cookie))
|
|
176
|
+
const now = (): number => Date.now()
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Keep-alive for a router. See the module docs; the generated client entry wires it:
|
|
180
|
+
*
|
|
181
|
+
* ```ts
|
|
182
|
+
* const keep = keepAliveClient({ table, routeHooks, matchChains })
|
|
183
|
+
* const router = keep.attach(createClientRouter({ ..., fetchData: keep.fetchData }))
|
|
184
|
+
* installHistory(router)
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
export function keepAliveClient(options: KeepAliveClientOptions): KeepAliveClient {
|
|
188
|
+
const { table, routeHooks, matchChains } = options
|
|
189
|
+
const base = options.fetchData ?? fetchRouteData
|
|
190
|
+
const max = options.max ?? KEEP_ALIVE_MAX
|
|
191
|
+
const keepRoutes = new Set(table.routes)
|
|
192
|
+
const wrappedLayouts = new Set(table.layouts)
|
|
193
|
+
// The restore whose reload the router is running, which the transport answers.
|
|
194
|
+
let reloading: Restore | undefined
|
|
195
|
+
const ownsLoader = (routeId: string | undefined): boolean =>
|
|
196
|
+
routeId !== undefined && routeHooks[routeId]?.clientLoader !== undefined
|
|
197
|
+
// What client-loader routes loaded since the page on screen settled. A restore that loads nothing
|
|
198
|
+
// runs the client loader over the kept page's own, as a navigation over prefetched data does.
|
|
199
|
+
const fresh = new Map<string, unknown>()
|
|
200
|
+
const noteSource = (path: string, payload: unknown): void => {
|
|
201
|
+
if (!isRouteDataEnvelope(payload)) fresh.set(path, payload)
|
|
202
|
+
else if (payload.status === undefined) fresh.set(path, payload.data)
|
|
203
|
+
}
|
|
204
|
+
const sourcesOf = (state: RouterState): Sources => {
|
|
205
|
+
const sources = new Map<string, unknown>()
|
|
206
|
+
for (const path of [pagePathOf(state), state.path])
|
|
207
|
+
if (fresh.has(path)) sources.set(path, fresh.get(path))
|
|
208
|
+
return sources.size === 0 ? undefined : sources
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// A restored page's reload: its kept data, unrequested, when it needs no reload; otherwise the
|
|
212
|
+
// response shared onto the kept data, so what did not change keeps the identity it has on screen.
|
|
213
|
+
// A reload that fails shows the kept data instead of loading the page as a document.
|
|
214
|
+
const fetchData: FetchRouteData = async (
|
|
215
|
+
path,
|
|
216
|
+
match,
|
|
217
|
+
signal,
|
|
218
|
+
navigation,
|
|
219
|
+
purpose,
|
|
220
|
+
hosts,
|
|
221
|
+
read,
|
|
222
|
+
) => {
|
|
223
|
+
const restore = reloading
|
|
224
|
+
const held = restore?.kept.intercepted
|
|
225
|
+
// A page kept with a boundary open reloads twice: its own data, then the target the boundary holds.
|
|
226
|
+
const target =
|
|
227
|
+
held !== undefined && path === restore?.kept.path && path !== restore.pagePath
|
|
228
|
+
? restore.kept.boundaries?.[held.boundary]
|
|
229
|
+
: undefined
|
|
230
|
+
if (restore === undefined || (path !== restore.pagePath && target === undefined)) {
|
|
231
|
+
if (purpose === "prefetch" || !ownsLoader(match.routeId))
|
|
232
|
+
return base(path, match, signal, navigation, purpose, hosts, read)
|
|
233
|
+
const payload = await base(path, match, signal, navigation, purpose, hosts, read)
|
|
234
|
+
noteSource(path, payload)
|
|
235
|
+
return payload
|
|
236
|
+
}
|
|
237
|
+
const sourced = restore.sources?.has(path) === true
|
|
238
|
+
const kept = (): Record<string, unknown> => {
|
|
239
|
+
const data = sourced ? restore.sources?.get(path) : target?.data
|
|
240
|
+
if (target !== undefined) return { v: 1, data, live: target.live }
|
|
241
|
+
const envelope = envelopeOf(restore.kept)
|
|
242
|
+
if (sourced) envelope.data = data
|
|
243
|
+
return envelope
|
|
244
|
+
}
|
|
245
|
+
// The page's hole hosts reload as fills, applied in place on the roots kept with it, even when
|
|
246
|
+
// the route keeps its own data.
|
|
247
|
+
const holes = target === undefined ? hostsOf(restore.kept) : []
|
|
248
|
+
if (!restore.reload && holes.length === 0) return kept()
|
|
249
|
+
let payload: unknown
|
|
250
|
+
try {
|
|
251
|
+
payload = await base(
|
|
252
|
+
path,
|
|
253
|
+
match,
|
|
254
|
+
signal,
|
|
255
|
+
navigation,
|
|
256
|
+
purpose,
|
|
257
|
+
holes.length === 0 ? hosts : holes,
|
|
258
|
+
read,
|
|
259
|
+
)
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (signal?.aborted === true || hasRedirect(error)) throw error
|
|
262
|
+
return kept()
|
|
263
|
+
}
|
|
264
|
+
if (!restore.reload) return fillsOnto(kept(), restore.kept, payload, holes)
|
|
265
|
+
if (ownsLoader(match.routeId)) noteSource(path, payload)
|
|
266
|
+
const hooks = routeHooks[target === undefined ? restore.kept.routeId : match.routeId]
|
|
267
|
+
// `read` itself answers a reload whose response text is what the path shows, which the router
|
|
268
|
+
// keeps unparsed.
|
|
269
|
+
if (payload === read || hooks?.shareData === false) return payload
|
|
270
|
+
// The router's transport records the text of a plain JSON response, a tree with no symbol keys.
|
|
271
|
+
const plainJson = read?.text !== undefined && hooks?.clientLoader === undefined
|
|
272
|
+
return target === undefined
|
|
273
|
+
? sharedOnto(restore.kept, payload, plainJson)
|
|
274
|
+
: targetOnto(target.data, payload, plainJson)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const attach = (inner: ClientRouter): KeepAliveRouter => {
|
|
278
|
+
const records = new Map<string, Kept>()
|
|
279
|
+
const listeners = new Set<() => void>()
|
|
280
|
+
const before = new Set<(next: RouterState) => void>()
|
|
281
|
+
const wrappedFetchers = new Map<string, Fetcher>()
|
|
282
|
+
let keptPages: readonly KeptPage[] | undefined
|
|
283
|
+
let lastEpoch = readEpoch()
|
|
284
|
+
|
|
285
|
+
const first = readEntry()
|
|
286
|
+
const initial = inner.snapshot()
|
|
287
|
+
// A client loader runs over the document's own data as the router hydrates.
|
|
288
|
+
if (ownsLoader(initial.routeId)) fresh.set(initial.path, initial.data)
|
|
289
|
+
// Where the page's data came from: `epoch` is the one it was loaded under (a page left after a
|
|
290
|
+
// sign-in or sign-out shows the previous visitor's data, so it is not kept), and `sources` what
|
|
291
|
+
// a client loader built it from.
|
|
292
|
+
let active: Active = {
|
|
293
|
+
instance: first.instance ?? newInstance(),
|
|
294
|
+
index: first.index,
|
|
295
|
+
settled: initial,
|
|
296
|
+
epoch: lastEpoch,
|
|
297
|
+
sources: sourcesOf(initial),
|
|
298
|
+
}
|
|
299
|
+
stampEntry(active.instance)
|
|
300
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production")
|
|
301
|
+
Reflect.set(globalThis, KEPT_PAGES_GLOBAL, {
|
|
302
|
+
pages: () => describeKept(records.values()),
|
|
303
|
+
})
|
|
304
|
+
// What is on screen: the router's state, or a restored page's kept state until its reload lands.
|
|
305
|
+
let shown = active.settled
|
|
306
|
+
let flags: Partial<RouterState> | undefined
|
|
307
|
+
// The page being left stays mounted, hidden, while a `_loading` page takes its place.
|
|
308
|
+
let leaving = false
|
|
309
|
+
// The page being left is gone (dropped, or unmountable) before its navigation settled.
|
|
310
|
+
let leftGone = false
|
|
311
|
+
|
|
312
|
+
const keepAliveOf = (routeId: string): true | KeepAliveOptions | undefined =>
|
|
313
|
+
keepRoutes.has(routeId) ? routeHooks[routeId]?.keepAlive : undefined
|
|
314
|
+
// A boundary open over the page is kept with it (its target is the page's to show again).
|
|
315
|
+
const keepable = (state: RouterState): boolean =>
|
|
316
|
+
!state.pending && keepAliveOf(state.routeId) !== undefined
|
|
317
|
+
// One page instance per route, and per pathname on a route that keeps alive, whose pages would
|
|
318
|
+
// otherwise have nothing to keep.
|
|
319
|
+
const sameInstance = (a: RouterState, b: RouterState): boolean =>
|
|
320
|
+
a.routeId === b.routeId &&
|
|
321
|
+
(!keepRoutes.has(b.routeId) || pathnameOf(pagePathOf(a)) === pathnameOf(pagePathOf(b)))
|
|
322
|
+
|
|
323
|
+
const compose = (): RouterState => {
|
|
324
|
+
keptPages ??= [...records.values()].map((record) => record.page)
|
|
325
|
+
const kept = leaving
|
|
326
|
+
? [...keptPages, { instance: active.instance, state: active.settled }]
|
|
327
|
+
: keptPages
|
|
328
|
+
return {
|
|
329
|
+
...shown,
|
|
330
|
+
instance: leaving ? undefined : active.instance,
|
|
331
|
+
kept: kept.length === 0 ? undefined : kept,
|
|
332
|
+
...flags,
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
let published = compose()
|
|
336
|
+
|
|
337
|
+
const drop = (instances: readonly string[], reason: KeepAliveDropReason): void => {
|
|
338
|
+
const gone = instances.filter((instance) => records.delete(instance))
|
|
339
|
+
if (gone.length === 0) return
|
|
340
|
+
keptPages = undefined
|
|
341
|
+
options.onDrop?.(reason, gone)
|
|
342
|
+
}
|
|
343
|
+
const refresh = (): void => {
|
|
344
|
+
let next = compose()
|
|
345
|
+
// A kept page whose chain leaves the page on screen at an unwrapped layout cannot be hidden.
|
|
346
|
+
const stuck = unplaceable(next, matchChains, keepRoutes, wrappedLayouts)
|
|
347
|
+
if (stuck.length > 0) {
|
|
348
|
+
if (leaving && stuck.includes(active.instance)) {
|
|
349
|
+
leaving = false
|
|
350
|
+
leftGone = true
|
|
351
|
+
}
|
|
352
|
+
drop(stuck, "layout")
|
|
353
|
+
next = compose()
|
|
354
|
+
}
|
|
355
|
+
for (const listener of [...before]) listener(next)
|
|
356
|
+
published = next
|
|
357
|
+
for (const listener of [...listeners]) listener()
|
|
358
|
+
}
|
|
359
|
+
const dropKept = (reason: KeepAliveDropReason = "manual"): void => {
|
|
360
|
+
const left = leaving || inner.snapshot().pending
|
|
361
|
+
if (records.size === 0 && !leaving) {
|
|
362
|
+
if (left) leftGone = true
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
if (left) {
|
|
366
|
+
leaving = false
|
|
367
|
+
leftGone = true
|
|
368
|
+
}
|
|
369
|
+
drop([...records.keys()], reason)
|
|
370
|
+
refresh()
|
|
371
|
+
}
|
|
372
|
+
const sweep = (): void => {
|
|
373
|
+
const at = now()
|
|
374
|
+
const expired: string[] = []
|
|
375
|
+
for (const [instance, record] of records)
|
|
376
|
+
if (at - record.hiddenAt > record.maxAge) expired.push(instance)
|
|
377
|
+
drop(expired, "expired")
|
|
378
|
+
}
|
|
379
|
+
// A response or another tab may carry a new epoch: what was kept belonged to the previous visitor.
|
|
380
|
+
const checkEpoch = (): void => {
|
|
381
|
+
const epoch = readEpoch()
|
|
382
|
+
if (epoch === lastEpoch) return
|
|
383
|
+
lastEpoch = epoch
|
|
384
|
+
channel?.postMessage("changed")
|
|
385
|
+
dropKept("identity")
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const keep = ({ instance, index, settled: state, epoch, sources }: Active): void => {
|
|
389
|
+
if (!keepable(state) || epoch !== readEpoch()) return
|
|
390
|
+
const setting = keepAliveOf(state.routeId)
|
|
391
|
+
const settings = typeof setting === "object" ? setting : {}
|
|
392
|
+
records.delete(instance)
|
|
393
|
+
records.set(instance, {
|
|
394
|
+
page: { instance, state },
|
|
395
|
+
index,
|
|
396
|
+
epoch,
|
|
397
|
+
sources,
|
|
398
|
+
hiddenAt: now(),
|
|
399
|
+
maxAge: settings.maxAge ?? KEEP_ALIVE_MAX_AGE_MS,
|
|
400
|
+
revalidate: settings.revalidate !== false,
|
|
401
|
+
stale: false,
|
|
402
|
+
})
|
|
403
|
+
keptPages = undefined
|
|
404
|
+
if (records.size > max) drop([...records.keys()].slice(0, records.size - max), "lru")
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const settle = (next: RouterState): void => {
|
|
408
|
+
const entry = readEntry()
|
|
409
|
+
if (sameInstance(active.settled, next)) {
|
|
410
|
+
// Data the router did not load again is still the data it was loaded with.
|
|
411
|
+
const same = next.data === active.settled.data
|
|
412
|
+
active = {
|
|
413
|
+
instance: active.instance,
|
|
414
|
+
index: entry.index,
|
|
415
|
+
settled: next,
|
|
416
|
+
epoch: same ? active.epoch : readEpoch(),
|
|
417
|
+
sources: same ? active.sources : sourcesOf(next),
|
|
418
|
+
}
|
|
419
|
+
} else {
|
|
420
|
+
// The page on screen was left: by a push (a fresh entry above it), a traversal (an entry that
|
|
421
|
+
// already carries an instance), or a replace (its own entry rewritten), which discards it.
|
|
422
|
+
if (entry.index > active.index && entry.instance === undefined) {
|
|
423
|
+
drop(
|
|
424
|
+
[...records].filter(([, record]) => record.index >= entry.index).map(([id]) => id),
|
|
425
|
+
"forward",
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
if (entry.index !== active.index && !leftGone) keep(active)
|
|
429
|
+
const reuse =
|
|
430
|
+
entry.index !== active.index &&
|
|
431
|
+
entry.instance !== undefined &&
|
|
432
|
+
entry.instance !== active.instance &&
|
|
433
|
+
!records.has(entry.instance)
|
|
434
|
+
? entry.instance
|
|
435
|
+
: undefined
|
|
436
|
+
active = {
|
|
437
|
+
instance: reuse ?? newInstance(),
|
|
438
|
+
index: entry.index,
|
|
439
|
+
settled: next,
|
|
440
|
+
epoch: readEpoch(),
|
|
441
|
+
sources: sourcesOf(next),
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
fresh.clear()
|
|
445
|
+
stampEntry(active.instance)
|
|
446
|
+
sweep()
|
|
447
|
+
shown = next
|
|
448
|
+
flags = undefined
|
|
449
|
+
leaving = false
|
|
450
|
+
leftGone = false
|
|
451
|
+
refresh()
|
|
452
|
+
checkEpoch()
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
let restoring:
|
|
456
|
+
| (Restore & { readonly done: () => void; readonly ended: Promise<void> })
|
|
457
|
+
| undefined
|
|
458
|
+
const endRestore = (): void => {
|
|
459
|
+
const restore = restoring
|
|
460
|
+
restoring = undefined
|
|
461
|
+
reloading = undefined
|
|
462
|
+
restore?.done()
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
inner.subscribe(() => {
|
|
466
|
+
const next = inner.snapshot()
|
|
467
|
+
const restore = restoring
|
|
468
|
+
if (restore !== undefined) {
|
|
469
|
+
// The restored page stays on screen while its reload runs: the router's own pending tick for
|
|
470
|
+
// it is not a navigation the visitor made. A redirect it follows is.
|
|
471
|
+
if (
|
|
472
|
+
next.pending &&
|
|
473
|
+
(next.pendingPath === undefined ||
|
|
474
|
+
next.pendingPath === restore.path ||
|
|
475
|
+
next.pendingPath === restore.pagePath)
|
|
476
|
+
)
|
|
477
|
+
return
|
|
478
|
+
const onPage =
|
|
479
|
+
!next.pending &&
|
|
480
|
+
pagePathOf(next) === restore.pagePath &&
|
|
481
|
+
next.routeId === restore.kept.routeId
|
|
482
|
+
// The page alone, on the way to opening its boundary on the target again.
|
|
483
|
+
if (onPage && restore.kept.intercepted !== undefined && next.intercepted === undefined)
|
|
484
|
+
return
|
|
485
|
+
endRestore()
|
|
486
|
+
if (onPage) {
|
|
487
|
+
active = {
|
|
488
|
+
instance: active.instance,
|
|
489
|
+
index: active.index,
|
|
490
|
+
settled: next,
|
|
491
|
+
epoch: restore.reload ? readEpoch() : active.epoch,
|
|
492
|
+
// A reload that failed shows the kept data, from the kept sources.
|
|
493
|
+
sources: sourcesOf(next) ?? active.sources,
|
|
494
|
+
}
|
|
495
|
+
fresh.clear()
|
|
496
|
+
shown = { ...next, params: shareData(restore.kept.params, next.params) }
|
|
497
|
+
flags = undefined
|
|
498
|
+
refresh()
|
|
499
|
+
checkEpoch()
|
|
500
|
+
return
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (!next.pending) {
|
|
504
|
+
settle(next)
|
|
505
|
+
return
|
|
506
|
+
}
|
|
507
|
+
shown = next
|
|
508
|
+
flags = undefined
|
|
509
|
+
leaving = next.routeId !== active.settled.routeId && !leftGone && keepable(active.settled)
|
|
510
|
+
refresh()
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
const restore = (path: string, instance: string, record: Kept): Promise<void> => {
|
|
514
|
+
const entry = readEntry()
|
|
515
|
+
records.delete(instance)
|
|
516
|
+
keptPages = undefined
|
|
517
|
+
if (!leftGone) keep(active)
|
|
518
|
+
const kept = record.page.state
|
|
519
|
+
const { sources } = record
|
|
520
|
+
active = { instance, index: entry.index, settled: kept, epoch: record.epoch, sources }
|
|
521
|
+
fresh.clear()
|
|
522
|
+
// Data a client loader built is shown again only from what it was built from.
|
|
523
|
+
const unsourced =
|
|
524
|
+
(ownsLoader(kept.routeId) && sources?.has(pagePathOf(kept)) !== true) ||
|
|
525
|
+
(kept.intercepted !== undefined &&
|
|
526
|
+
ownsLoader(inner.match(kept.path)?.routeId) &&
|
|
527
|
+
sources?.has(kept.path) !== true)
|
|
528
|
+
const reload = record.revalidate || record.stale || kept.path !== path || unsourced
|
|
529
|
+
const modal = kept.intercepted
|
|
530
|
+
const pagePath = modal === undefined ? path : modal.pagePath
|
|
531
|
+
let done = (): void => {}
|
|
532
|
+
const ended = new Promise<void>((resolve) => {
|
|
533
|
+
done = resolve
|
|
534
|
+
})
|
|
535
|
+
endRestore()
|
|
536
|
+
const mine = { path, pagePath, kept, reload, sources, done, ended }
|
|
537
|
+
restoring = mine
|
|
538
|
+
reloading = { path, pagePath, kept, reload, sources }
|
|
539
|
+
stampEntry(instance)
|
|
540
|
+
shown = kept
|
|
541
|
+
flags = { restored: true, revalidating: reload }
|
|
542
|
+
leaving = false
|
|
543
|
+
leftGone = false
|
|
544
|
+
refresh()
|
|
545
|
+
// The router shows a boundary's target only over the page it opened on: it loads the page, then
|
|
546
|
+
// opens the boundary on the target again, as the link that first opened it did.
|
|
547
|
+
const reloaded =
|
|
548
|
+
modal === undefined
|
|
549
|
+
? inner.navigate(path)
|
|
550
|
+
: inner.navigate(pagePath).then(() => {
|
|
551
|
+
const now = inner.snapshot()
|
|
552
|
+
const onPage =
|
|
553
|
+
restoring === mine &&
|
|
554
|
+
!now.pending &&
|
|
555
|
+
now.routeId === kept.routeId &&
|
|
556
|
+
pagePathOf(now) === pagePath
|
|
557
|
+
return onPage ? inner.navigate(path) : undefined
|
|
558
|
+
})
|
|
559
|
+
return reloaded.finally(() => {
|
|
560
|
+
if (restoring === mine) endRestore()
|
|
561
|
+
})
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const navigate = (path: string, navOptions?: KeepAliveNavigateOptions): Promise<void> => {
|
|
565
|
+
sweep()
|
|
566
|
+
const instance = navOptions?.traverse ?? readEntry().instance
|
|
567
|
+
const record =
|
|
568
|
+
instance === undefined || instance === active.instance ? undefined : records.get(instance)
|
|
569
|
+
if (instance !== undefined && record !== undefined) {
|
|
570
|
+
if (record.epoch !== readEpoch()) dropKept("identity")
|
|
571
|
+
else if (pathnameOf(record.page.state.path) === pathnameOf(path))
|
|
572
|
+
return restore(path, instance, record)
|
|
573
|
+
}
|
|
574
|
+
endRestore()
|
|
575
|
+
return inner.navigate(path)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// A submit or an invalidation may change what a hidden page shows; the response that says which
|
|
579
|
+
// paths (`X-Nifra-Revalidate`) is read inside the router, so every kept page reloads when shown.
|
|
580
|
+
const markStale = (paths?: readonly string[]): void => {
|
|
581
|
+
for (const record of records.values()) {
|
|
582
|
+
const { state } = record.page
|
|
583
|
+
if (paths === undefined || paths.includes(pagePathOf(state)) || paths.includes(state.path))
|
|
584
|
+
record.stale = true
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
// A submit or an invalidation acts on the page on screen, which a restore hands to the router only
|
|
588
|
+
// once its reload settles.
|
|
589
|
+
const afterRestore = async (): Promise<void> => {
|
|
590
|
+
while (restoring !== undefined) await restoring.ended
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const channel =
|
|
594
|
+
typeof BroadcastChannel === "undefined" ? undefined : new BroadcastChannel(IDENTITY_CHANNEL)
|
|
595
|
+
const identityDrop = (): void => {
|
|
596
|
+
lastEpoch = readEpoch()
|
|
597
|
+
dropKept("identity")
|
|
598
|
+
}
|
|
599
|
+
channel?.addEventListener("message", identityDrop)
|
|
600
|
+
onIdentityChange(identityDrop)
|
|
601
|
+
if (typeof addEventListener === "function") {
|
|
602
|
+
addEventListener("pagehide", (event) => {
|
|
603
|
+
if ("persisted" in event && event.persisted === true) dropKept("pagehide")
|
|
604
|
+
})
|
|
605
|
+
addEventListener("visibilitychange", () => {
|
|
606
|
+
if (document.visibilityState === "visible") checkEpoch()
|
|
607
|
+
})
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return {
|
|
611
|
+
snapshot: () => published,
|
|
612
|
+
subscribe: (listener) => {
|
|
613
|
+
listeners.add(listener)
|
|
614
|
+
return () => {
|
|
615
|
+
listeners.delete(listener)
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
navigate,
|
|
619
|
+
submit: async (action, body, submitOptions) => {
|
|
620
|
+
await afterRestore()
|
|
621
|
+
try {
|
|
622
|
+
await inner.submit(action, body, submitOptions)
|
|
623
|
+
} finally {
|
|
624
|
+
markStale()
|
|
625
|
+
checkEpoch()
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
hydrate: () => inner.hydrate(),
|
|
629
|
+
invalidate: (paths) => {
|
|
630
|
+
markStale(paths)
|
|
631
|
+
// At once when nothing is restoring: the live client names its load around this call.
|
|
632
|
+
return restoring === undefined
|
|
633
|
+
? inner.invalidate(paths)
|
|
634
|
+
: afterRestore().then(() => inner.invalidate(paths))
|
|
635
|
+
},
|
|
636
|
+
prefetch: (path) => inner.prefetch(path),
|
|
637
|
+
fetcher: (key) => {
|
|
638
|
+
let fetcher = wrappedFetchers.get(key)
|
|
639
|
+
if (fetcher === undefined) {
|
|
640
|
+
const own = inner.fetcher(key)
|
|
641
|
+
fetcher = {
|
|
642
|
+
snapshot: own.snapshot,
|
|
643
|
+
subscribe: own.subscribe,
|
|
644
|
+
load: own.load,
|
|
645
|
+
submit: async (action, body) => {
|
|
646
|
+
try {
|
|
647
|
+
await own.submit(action, body)
|
|
648
|
+
} finally {
|
|
649
|
+
markStale()
|
|
650
|
+
checkEpoch()
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
}
|
|
654
|
+
wrappedFetchers.set(key, fetcher)
|
|
655
|
+
}
|
|
656
|
+
return fetcher
|
|
657
|
+
},
|
|
658
|
+
fetchers: () => inner.fetchers(),
|
|
659
|
+
subscribeFetchers: (listener) => inner.subscribeFetchers(listener),
|
|
660
|
+
match: (path) => inner.match(path),
|
|
661
|
+
dropKept,
|
|
662
|
+
beforePublish: (listener) => {
|
|
663
|
+
before.add(listener)
|
|
664
|
+
return () => {
|
|
665
|
+
before.delete(listener)
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return { fetchData, attach }
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const hasRedirect = (error: unknown): boolean =>
|
|
675
|
+
typeof error === "object" &&
|
|
676
|
+
error !== null &&
|
|
677
|
+
typeof Reflect.get(error, "redirectTo") === "string"
|
|
678
|
+
|
|
679
|
+
/** A held target's reload, its data shared onto what the boundary shows. */
|
|
680
|
+
const targetOnto = (shown: unknown, payload: unknown, plainJson: boolean): unknown => {
|
|
681
|
+
if (!isRouteDataEnvelope(payload)) return shareData(shown, payload, plainJson)
|
|
682
|
+
if (payload.status !== undefined) return payload
|
|
683
|
+
return { ...payload, data: shareData(shown, payload.data, plainJson) }
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/** The holes of `state`'s page that are hosts a partially prerendered document left open. */
|
|
687
|
+
const hostsOf = (state: RouterState): string[] =>
|
|
688
|
+
Object.values(state.boundaries ?? {})
|
|
689
|
+
.filter((held) => held.ppr === true)
|
|
690
|
+
.map((held) => held.name)
|
|
691
|
+
|
|
692
|
+
/** `envelope`, what `kept` shows, with the fills `payload` carries for its hole hosts `holes` and
|
|
693
|
+
* nothing else of it. */
|
|
694
|
+
const fillsOnto = (
|
|
695
|
+
envelope: Record<string, unknown>,
|
|
696
|
+
kept: RouterState,
|
|
697
|
+
payload: unknown,
|
|
698
|
+
holes: readonly string[],
|
|
699
|
+
): unknown => {
|
|
700
|
+
if (!isRouteDataEnvelope(payload) || payload.status !== undefined) return envelope
|
|
701
|
+
const boundaries = { ...kept.boundaries }
|
|
702
|
+
for (const name of holes) {
|
|
703
|
+
const fill = payload.boundaries?.[name]
|
|
704
|
+
if (fill?.ppr === true) boundaries[name] = fill
|
|
705
|
+
}
|
|
706
|
+
return { ...envelope, boundaries }
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** A route data envelope that publishes exactly `state`'s data. */
|
|
710
|
+
const envelopeOf = (state: RouterState): Record<string, unknown> => {
|
|
711
|
+
const envelope: Record<string, unknown> = {
|
|
712
|
+
v: 1,
|
|
713
|
+
data: state.data,
|
|
714
|
+
layoutData: state.layoutData,
|
|
715
|
+
serverComponents: state.serverComponents,
|
|
716
|
+
live: state.live,
|
|
717
|
+
}
|
|
718
|
+
if (state.boundaries !== undefined) envelope.boundaries = state.boundaries
|
|
719
|
+
return envelope
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/** `payload` with every part equal to what `kept` shows replaced by that part. A layout the server
|
|
723
|
+
* skipped (`retained`) takes the value on screen, as on any navigation. */
|
|
724
|
+
const sharedOnto = (kept: RouterState, payload: unknown, plainJson: boolean): unknown => {
|
|
725
|
+
if (!isRouteDataEnvelope(payload)) return shareData(kept.data, payload, plainJson)
|
|
726
|
+
if (payload.status !== undefined) return payload
|
|
727
|
+
const retained = new Set(payload.retained ?? [])
|
|
728
|
+
return {
|
|
729
|
+
...payload,
|
|
730
|
+
data: shareData(kept.data, payload.data, plainJson),
|
|
731
|
+
layoutData: payload.layoutData?.map((value, index) =>
|
|
732
|
+
retained.has(index) ? value : shareData(kept.layoutData?.[index], value, plainJson),
|
|
733
|
+
),
|
|
734
|
+
...(payload.boundaries === undefined
|
|
735
|
+
? {}
|
|
736
|
+
: { boundaries: shareData(kept.boundaries, payload.boundaries, plainJson) }),
|
|
737
|
+
serverComponents: shareData(kept.serverComponents, payload.serverComponents, plainJson),
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/** One element of the view: a layout and the slots under it, or a page. */
|
|
742
|
+
export interface SlotNode {
|
|
743
|
+
/** Unique in the tree: a layout's id, or `\0` and a page instance. */
|
|
744
|
+
readonly key: string
|
|
745
|
+
/** The chain index of the element it renders: a layout's depth, or the page's (its route's layout
|
|
746
|
+
* count), from where the page's own chain (an `_error` boundary, then the page) continues. */
|
|
747
|
+
readonly depth: number
|
|
748
|
+
/** The state it renders from: the page on screen's while it is on the path to it, else the most
|
|
749
|
+
* recently shown kept page under it. */
|
|
750
|
+
readonly state: RouterState
|
|
751
|
+
/** Not on the path to the page on screen. */
|
|
752
|
+
readonly hidden: boolean
|
|
753
|
+
/** Rendered inside the two slot wrappers, so it can be taken out of the document. */
|
|
754
|
+
readonly wrapped: boolean
|
|
755
|
+
/** The slots under a layout, in the order their pages were first shown. Absent on a page. */
|
|
756
|
+
readonly children?: readonly SlotNode[]
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
interface Member {
|
|
760
|
+
readonly key: string
|
|
761
|
+
readonly order: number
|
|
762
|
+
readonly recency: number
|
|
763
|
+
readonly state: RouterState
|
|
764
|
+
readonly hidden: boolean
|
|
765
|
+
readonly layouts: readonly string[]
|
|
766
|
+
readonly leaf: string
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const NO_LAYOUTS: readonly string[] = []
|
|
770
|
+
// The `_loading` page shown while a navigation runs has no instance of its own.
|
|
771
|
+
const LOADING = "\0loading"
|
|
772
|
+
|
|
773
|
+
const memberOf = (
|
|
774
|
+
instance: string | undefined,
|
|
775
|
+
state: RouterState,
|
|
776
|
+
hidden: boolean,
|
|
777
|
+
recency: number,
|
|
778
|
+
chains: Readonly<Record<string, MatchChain | undefined>>,
|
|
779
|
+
): Member => {
|
|
780
|
+
const ids = chains[state.routeId]?.ids
|
|
781
|
+
const order = instance === undefined ? Number.MAX_SAFE_INTEGER : Number.parseInt(instance, 10)
|
|
782
|
+
return {
|
|
783
|
+
key: instance === undefined ? LOADING : `\0${instance}`,
|
|
784
|
+
order: Number.isNaN(order) ? Number.MAX_SAFE_INTEGER : order,
|
|
785
|
+
recency,
|
|
786
|
+
state,
|
|
787
|
+
hidden,
|
|
788
|
+
layouts: ids === undefined ? NO_LAYOUTS : ids.slice(0, -1),
|
|
789
|
+
leaf: ids?.at(-1) ?? state.routeId,
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const membersOf = (
|
|
794
|
+
state: RouterState,
|
|
795
|
+
chains: Readonly<Record<string, MatchChain | undefined>>,
|
|
796
|
+
): Member[] => {
|
|
797
|
+
const kept = state.kept ?? []
|
|
798
|
+
const members = kept.map((page, i) => memberOf(page.instance, page.state, true, i, chains))
|
|
799
|
+
members.push(memberOf(state.instance, state, false, kept.length, chains))
|
|
800
|
+
return members.sort((a, b) => a.order - b.order || (a.key < b.key ? -1 : 1))
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const levelOf = (
|
|
804
|
+
members: readonly Member[],
|
|
805
|
+
depth: number,
|
|
806
|
+
pages: ReadonlySet<string>,
|
|
807
|
+
layouts: ReadonlySet<string>,
|
|
808
|
+
previous: ReadonlyMap<string, SlotNode>,
|
|
809
|
+
): SlotNode[] => {
|
|
810
|
+
const groups = new Map<string, Member[]>()
|
|
811
|
+
for (const member of members) {
|
|
812
|
+
const key = member.layouts[depth] ?? member.key
|
|
813
|
+
const group = groups.get(key)
|
|
814
|
+
if (group === undefined) groups.set(key, [member])
|
|
815
|
+
else group.push(member)
|
|
816
|
+
}
|
|
817
|
+
const nodes: SlotNode[] = []
|
|
818
|
+
for (const [key, group] of groups) {
|
|
819
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: a group is created with its first member
|
|
820
|
+
const head = group[0] as Member
|
|
821
|
+
if (key === head.key) {
|
|
822
|
+
nodes.push(
|
|
823
|
+
reuse(previous, {
|
|
824
|
+
key,
|
|
825
|
+
depth,
|
|
826
|
+
state: head.state,
|
|
827
|
+
hidden: head.hidden,
|
|
828
|
+
wrapped: pages.has(head.leaf),
|
|
829
|
+
}),
|
|
830
|
+
)
|
|
831
|
+
continue
|
|
832
|
+
}
|
|
833
|
+
let shown = head
|
|
834
|
+
for (const member of group) {
|
|
835
|
+
if (!member.hidden) {
|
|
836
|
+
shown = member
|
|
837
|
+
break
|
|
838
|
+
}
|
|
839
|
+
if (member.recency > shown.recency) shown = member
|
|
840
|
+
}
|
|
841
|
+
nodes.push(
|
|
842
|
+
reuse(previous, {
|
|
843
|
+
key,
|
|
844
|
+
depth,
|
|
845
|
+
state: shown.state,
|
|
846
|
+
hidden: shown.hidden,
|
|
847
|
+
wrapped: layouts.has(key),
|
|
848
|
+
children: levelOf(group, depth + 1, pages, layouts, previous),
|
|
849
|
+
}),
|
|
850
|
+
)
|
|
851
|
+
}
|
|
852
|
+
return nodes
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// The previous tree's node when nothing about it changed, so a renderer that memoizes on the node
|
|
856
|
+
// skips it: a hidden page never re-renders while it is hidden.
|
|
857
|
+
const reuse = (previous: ReadonlyMap<string, SlotNode>, node: SlotNode): SlotNode => {
|
|
858
|
+
const old = previous.get(node.key)
|
|
859
|
+
if (
|
|
860
|
+
old === undefined ||
|
|
861
|
+
old.depth !== node.depth ||
|
|
862
|
+
old.state !== node.state ||
|
|
863
|
+
old.hidden !== node.hidden ||
|
|
864
|
+
old.wrapped !== node.wrapped
|
|
865
|
+
)
|
|
866
|
+
return node
|
|
867
|
+
const a = old.children
|
|
868
|
+
const b = node.children
|
|
869
|
+
if (a === b) return old
|
|
870
|
+
if (a === undefined || b === undefined || a.length !== b.length) return node
|
|
871
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return node
|
|
872
|
+
return old
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const indexTree = (nodes: readonly SlotNode[], into: Map<string, SlotNode>): void => {
|
|
876
|
+
for (const node of nodes) {
|
|
877
|
+
into.set(node.key, node)
|
|
878
|
+
if (node.children !== undefined) indexTree(node.children, into)
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* The view of `state`: the page on screen and every kept page, grouped by the layouts they share. A
|
|
884
|
+
* layout every page under it shares renders once; where two chains part, each part is a slot. Pass
|
|
885
|
+
* the previous tree to keep the identity of every node that did not change.
|
|
886
|
+
*/
|
|
887
|
+
export function slotTree(
|
|
888
|
+
state: RouterState,
|
|
889
|
+
chains: Readonly<Record<string, MatchChain | undefined>>,
|
|
890
|
+
table: KeepAliveTable,
|
|
891
|
+
previous?: readonly SlotNode[],
|
|
892
|
+
): readonly SlotNode[] {
|
|
893
|
+
const before = new Map<string, SlotNode>()
|
|
894
|
+
if (previous !== undefined) indexTree(previous, before)
|
|
895
|
+
return levelOf(membersOf(state, chains), 0, new Set(table.routes), new Set(table.layouts), before)
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// The kept pages a render of `state` could not hide: their chain leaves the page on screen at a
|
|
899
|
+
// layout rendered without slot wrappers.
|
|
900
|
+
const unplaceable = (
|
|
901
|
+
state: RouterState,
|
|
902
|
+
chains: Readonly<Record<string, MatchChain | undefined>>,
|
|
903
|
+
pages: ReadonlySet<string>,
|
|
904
|
+
layouts: ReadonlySet<string>,
|
|
905
|
+
): string[] => {
|
|
906
|
+
if (state.kept === undefined) return []
|
|
907
|
+
const stuck: string[] = []
|
|
908
|
+
const visit = (nodes: readonly SlotNode[], parentHidden: boolean): void => {
|
|
909
|
+
for (const node of nodes) {
|
|
910
|
+
if (node.hidden && !parentHidden && !node.wrapped) collect(node, stuck)
|
|
911
|
+
else if (node.children !== undefined) visit(node.children, node.hidden)
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
visit(levelOf(membersOf(state, chains), 0, pages, layouts, new Map()), false)
|
|
915
|
+
return stuck
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const collect = (node: SlotNode, into: string[]): void => {
|
|
919
|
+
if (node.children === undefined) {
|
|
920
|
+
if (node.key !== LOADING) into.push(node.key.slice(1))
|
|
921
|
+
return
|
|
922
|
+
}
|
|
923
|
+
for (const child of node.children) collect(child, into)
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/** The DOM half of keep-alive: takes hidden slots out of the document and puts them back. */
|
|
927
|
+
export interface SlotParking {
|
|
928
|
+
/** The ref of a slot's outer wrapper (`data-nifra-entry`). Stable per key. */
|
|
929
|
+
entry(key: string): (element: Element | null) => void
|
|
930
|
+
/** The ref of a slot's inner wrapper (`data-nifra-page`), the element that is parked. Stable per key. */
|
|
931
|
+
page(key: string): (element: Element | null) => void
|
|
932
|
+
/** Before the render of `next`: put back what it shows again, and note the inner scroll offsets and
|
|
933
|
+
* focus of what it hides while that is still laid out (React hides it during the commit). */
|
|
934
|
+
before(next: readonly SlotNode[]): void
|
|
935
|
+
/** After the render of `tree` committed: park what it hides, restore what it shows again. */
|
|
936
|
+
after(tree: readonly SlotNode[]): void
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
interface Parked {
|
|
940
|
+
entry?: Element | undefined
|
|
941
|
+
page?: Element | undefined
|
|
942
|
+
parked: boolean
|
|
943
|
+
shown: boolean
|
|
944
|
+
showing: boolean
|
|
945
|
+
/** The hide event went out and no show event since. */
|
|
946
|
+
hid: boolean
|
|
947
|
+
next: Node | null
|
|
948
|
+
offsets: (readonly [Element, number, number])[] | undefined
|
|
949
|
+
focus: Element | undefined
|
|
950
|
+
checked: boolean
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const scrollOffsets = (page: Element): (readonly [Element, number, number])[] => {
|
|
954
|
+
const offsets: (readonly [Element, number, number])[] = []
|
|
955
|
+
if (page.scrollTop !== 0 || page.scrollLeft !== 0)
|
|
956
|
+
offsets.push([page, page.scrollTop, page.scrollLeft])
|
|
957
|
+
for (const element of page.querySelectorAll("*")) {
|
|
958
|
+
if (element.scrollTop !== 0 || element.scrollLeft !== 0)
|
|
959
|
+
offsets.push([element, element.scrollTop, element.scrollLeft])
|
|
960
|
+
}
|
|
961
|
+
return offsets
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/** What a page showed as it was hidden, put back by {@link PageView.restore}. */
|
|
965
|
+
export interface PageView {
|
|
966
|
+
/** Put back the inner scroll offsets and the focus, once the page is in the document again. */
|
|
967
|
+
restore(): void
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Note the inner scroll offsets of a page's top-level `elements` and the element inside them that has
|
|
972
|
+
* focus, while they are still laid out. For an adapter whose framework takes a hidden page out of the
|
|
973
|
+
* document itself (Vue's `KeepAlive`), in place of {@link createSlotParking}.
|
|
974
|
+
*/
|
|
975
|
+
export function rememberView(elements: readonly Element[]): PageView {
|
|
976
|
+
const offsets = elements.flatMap(scrollOffsets)
|
|
977
|
+
const focused = typeof document === "undefined" ? null : document.activeElement
|
|
978
|
+
const focus =
|
|
979
|
+
focused !== null && elements.some((element) => element.contains(focused)) ? focused : undefined
|
|
980
|
+
return {
|
|
981
|
+
restore() {
|
|
982
|
+
for (const [element, top, left] of offsets) {
|
|
983
|
+
if (!element.isConnected) continue
|
|
984
|
+
element.scrollTop = top
|
|
985
|
+
element.scrollLeft = left
|
|
986
|
+
}
|
|
987
|
+
if (focus?.isConnected)
|
|
988
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: it held focus, so it is focusable; the call stays optional
|
|
989
|
+
(focus as HTMLElement).focus?.({ preventScroll: true })
|
|
990
|
+
},
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const wrappedNodes = (nodes: readonly SlotNode[], into: SlotNode[] = []): SlotNode[] => {
|
|
995
|
+
for (const node of nodes) {
|
|
996
|
+
if (node.wrapped) into.push(node)
|
|
997
|
+
if (node.children !== undefined) wrappedNodes(node.children, into)
|
|
998
|
+
}
|
|
999
|
+
return into
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Development only: what each slot renders, by slot key, for the devtools panel's node counts.
|
|
1003
|
+
const keptViews = new Map<string, () => readonly Element[]>()
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* For an adapter, in development: the elements slot `key` renders, read when the devtools panel
|
|
1007
|
+
* counts a kept page's nodes. `undefined` once the slot has left the view.
|
|
1008
|
+
*/
|
|
1009
|
+
export function noteKeptView(key: string, elements: (() => readonly Element[]) | undefined): void {
|
|
1010
|
+
if (elements === undefined) keptViews.delete(key)
|
|
1011
|
+
else keptViews.set(key, elements)
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/** Where a development build lists its kept pages, for `@nifrajs/devtools`. */
|
|
1015
|
+
export const KEPT_PAGES_GLOBAL = "__nifraKeepAlive"
|
|
1016
|
+
|
|
1017
|
+
/** One kept page, as the devtools panel lists it. */
|
|
1018
|
+
export interface KeptPageInfo {
|
|
1019
|
+
readonly instance: string
|
|
1020
|
+
readonly path: string
|
|
1021
|
+
readonly routeId: string
|
|
1022
|
+
/** How long it has been hidden, in milliseconds. */
|
|
1023
|
+
readonly hiddenFor: number
|
|
1024
|
+
/** The elements it renders, while its view has them. */
|
|
1025
|
+
readonly nodes: number | undefined
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const describeKept = (records: Iterable<Kept>): KeptPageInfo[] => {
|
|
1029
|
+
const at = now()
|
|
1030
|
+
return [...records].map(({ page, hiddenAt }) => {
|
|
1031
|
+
const elements = keptViews.get(`\0${page.instance}`)?.()
|
|
1032
|
+
return {
|
|
1033
|
+
instance: page.instance,
|
|
1034
|
+
path: page.state.path,
|
|
1035
|
+
routeId: page.state.routeId,
|
|
1036
|
+
hiddenFor: at - hiddenAt,
|
|
1037
|
+
nodes: elements?.reduce(
|
|
1038
|
+
(count, element) => count + 1 + element.querySelectorAll("*").length,
|
|
1039
|
+
0,
|
|
1040
|
+
),
|
|
1041
|
+
}
|
|
1042
|
+
})
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/** One per mounted view. */
|
|
1046
|
+
export function createSlotParking(): SlotParking {
|
|
1047
|
+
const slots = new Map<string, Parked>()
|
|
1048
|
+
const entryRefs = new Map<string, (element: Element | null) => void>()
|
|
1049
|
+
const pageRefs = new Map<string, (element: Element | null) => void>()
|
|
1050
|
+
const slotOf = (key: string): Parked => {
|
|
1051
|
+
let slot = slots.get(key)
|
|
1052
|
+
if (slot === undefined) {
|
|
1053
|
+
slot = {
|
|
1054
|
+
parked: false,
|
|
1055
|
+
shown: false,
|
|
1056
|
+
showing: false,
|
|
1057
|
+
hid: false,
|
|
1058
|
+
next: null,
|
|
1059
|
+
offsets: undefined,
|
|
1060
|
+
focus: undefined,
|
|
1061
|
+
checked: false,
|
|
1062
|
+
}
|
|
1063
|
+
slots.set(key, slot)
|
|
1064
|
+
}
|
|
1065
|
+
return slot
|
|
1066
|
+
}
|
|
1067
|
+
const remember = (slot: Parked): void => {
|
|
1068
|
+
if (slot.page === undefined || slot.offsets !== undefined) return
|
|
1069
|
+
slot.offsets = scrollOffsets(slot.page)
|
|
1070
|
+
const focused = typeof document === "undefined" ? null : document.activeElement
|
|
1071
|
+
slot.focus =
|
|
1072
|
+
focused !== null && focused !== slot.page && slot.page.contains(focused) ? focused : undefined
|
|
1073
|
+
}
|
|
1074
|
+
// Sent while the page is still in the document, before the router's subscribers see the state that
|
|
1075
|
+
// hides it, so what holds the page's content (server components) sets it aside first.
|
|
1076
|
+
const fire = (slot: Parked, hide: boolean): void => {
|
|
1077
|
+
slot.hid = hide
|
|
1078
|
+
slot.page?.dispatchEvent(
|
|
1079
|
+
new CustomEvent(hide ? PAGE_HIDE_EVENT : PAGE_SHOW_EVENT, { bubbles: true }),
|
|
1080
|
+
)
|
|
1081
|
+
}
|
|
1082
|
+
const unpark = (slot: Parked): void => {
|
|
1083
|
+
if (!slot.parked || slot.page === undefined || slot.entry === undefined) return
|
|
1084
|
+
slot.entry.insertBefore(slot.page, slot.next?.parentNode === slot.entry ? slot.next : null)
|
|
1085
|
+
slot.parked = false
|
|
1086
|
+
slot.showing = true
|
|
1087
|
+
}
|
|
1088
|
+
return {
|
|
1089
|
+
entry(key) {
|
|
1090
|
+
let ref = entryRefs.get(key)
|
|
1091
|
+
if (ref === undefined) {
|
|
1092
|
+
ref = (element) => {
|
|
1093
|
+
slotOf(key).entry = element ?? undefined
|
|
1094
|
+
}
|
|
1095
|
+
entryRefs.set(key, ref)
|
|
1096
|
+
}
|
|
1097
|
+
return ref
|
|
1098
|
+
},
|
|
1099
|
+
page(key) {
|
|
1100
|
+
let ref = pageRefs.get(key)
|
|
1101
|
+
if (ref === undefined) {
|
|
1102
|
+
// A null never clears the page: React detaches refs inside a hidden `Activity`, and a parked
|
|
1103
|
+
// page is still the framework's element. The slot leaving the tree (`after`) drops it.
|
|
1104
|
+
ref = (element) => {
|
|
1105
|
+
if (element === null) return
|
|
1106
|
+
slotOf(key).page = element
|
|
1107
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production")
|
|
1108
|
+
noteKeptView(key, () => [element])
|
|
1109
|
+
}
|
|
1110
|
+
pageRefs.set(key, ref)
|
|
1111
|
+
}
|
|
1112
|
+
return ref
|
|
1113
|
+
},
|
|
1114
|
+
before(next) {
|
|
1115
|
+
for (const node of wrappedNodes(next)) {
|
|
1116
|
+
const slot = slots.get(node.key)
|
|
1117
|
+
if (slot === undefined) continue
|
|
1118
|
+
if (node.hidden) {
|
|
1119
|
+
if (!slot.shown) continue
|
|
1120
|
+
remember(slot)
|
|
1121
|
+
if (!slot.hid && slot.page !== undefined) fire(slot, true)
|
|
1122
|
+
} else {
|
|
1123
|
+
// Shown again before it was parked: it never left the document.
|
|
1124
|
+
if (slot.hid && !slot.parked) fire(slot, false)
|
|
1125
|
+
unpark(slot)
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
},
|
|
1129
|
+
after(tree) {
|
|
1130
|
+
const live = new Set<string>()
|
|
1131
|
+
for (const node of wrappedNodes(tree)) {
|
|
1132
|
+
live.add(node.key)
|
|
1133
|
+
const slot = slots.get(node.key)
|
|
1134
|
+
const page = slot?.page
|
|
1135
|
+
if (slot === undefined || page === undefined || slot.entry === undefined) continue
|
|
1136
|
+
if (node.hidden) {
|
|
1137
|
+
if (!slot.parked) {
|
|
1138
|
+
if (slot.shown) remember(slot)
|
|
1139
|
+
if (!slot.hid) fire(slot, true)
|
|
1140
|
+
slot.next = page.nextSibling
|
|
1141
|
+
page.remove()
|
|
1142
|
+
slot.parked = true
|
|
1143
|
+
}
|
|
1144
|
+
slot.shown = false
|
|
1145
|
+
continue
|
|
1146
|
+
}
|
|
1147
|
+
unpark(slot)
|
|
1148
|
+
if (slot.showing) {
|
|
1149
|
+
slot.showing = false
|
|
1150
|
+
for (const [element, top, left] of slot.offsets ?? []) {
|
|
1151
|
+
if (!page.contains(element)) continue
|
|
1152
|
+
element.scrollTop = top
|
|
1153
|
+
element.scrollLeft = left
|
|
1154
|
+
}
|
|
1155
|
+
const focus = slot.focus
|
|
1156
|
+
if (focus?.isConnected && page.contains(focus))
|
|
1157
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: it held focus, so it is focusable; the call stays optional
|
|
1158
|
+
(focus as HTMLElement).focus?.({ preventScroll: true })
|
|
1159
|
+
fire(slot, false)
|
|
1160
|
+
}
|
|
1161
|
+
slot.offsets = undefined
|
|
1162
|
+
slot.focus = undefined
|
|
1163
|
+
if (!slot.shown && !slot.checked) {
|
|
1164
|
+
slot.checked = true
|
|
1165
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production")
|
|
1166
|
+
checkSlotCss(slot.entry, page)
|
|
1167
|
+
}
|
|
1168
|
+
slot.shown = true
|
|
1169
|
+
}
|
|
1170
|
+
for (const key of [...slots.keys()]) {
|
|
1171
|
+
if (live.has(key)) continue
|
|
1172
|
+
const page = slots.get(key)?.page
|
|
1173
|
+
if (page !== undefined) releasePage(page)
|
|
1174
|
+
slots.delete(key)
|
|
1175
|
+
entryRefs.delete(key)
|
|
1176
|
+
pageRefs.delete(key)
|
|
1177
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production")
|
|
1178
|
+
noteKeptView(key, undefined)
|
|
1179
|
+
}
|
|
1180
|
+
},
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// The selectors already reported, once per document.
|
|
1185
|
+
const reportedCss = new Set<string>()
|
|
1186
|
+
// Pseudo-classes that read an element's position among its siblings, which the wrappers change.
|
|
1187
|
+
const POSITIONAL =
|
|
1188
|
+
/:(?:first-child|last-child|only-child|nth-child\([^)]*\)|nth-last-child\([^)]*\))/g
|
|
1189
|
+
const DIRECT_HAS = /:has\(\s*>\s*([^)]*)\)/
|
|
1190
|
+
|
|
1191
|
+
/** `selector`'s compounds and the combinators between them, split outside parentheses and brackets. */
|
|
1192
|
+
const compoundsOf = (
|
|
1193
|
+
selector: string,
|
|
1194
|
+
): { readonly compound: string; readonly before: string }[] => {
|
|
1195
|
+
const parts: { compound: string; before: string }[] = []
|
|
1196
|
+
let depth = 0
|
|
1197
|
+
let start = 0
|
|
1198
|
+
let combinator = ""
|
|
1199
|
+
const flush = (end: number): void => {
|
|
1200
|
+
const compound = selector.slice(start, end).trim()
|
|
1201
|
+
if (compound !== "") {
|
|
1202
|
+
parts.push({ compound, before: combinator })
|
|
1203
|
+
combinator = ""
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
for (let i = 0; i < selector.length; i++) {
|
|
1207
|
+
const char = selector[i]
|
|
1208
|
+
if (char === "(" || char === "[") depth++
|
|
1209
|
+
else if (char === ")" || char === "]") depth--
|
|
1210
|
+
if (depth > 0 || (char !== " " && char !== ">" && char !== "+" && char !== "~")) continue
|
|
1211
|
+
flush(i)
|
|
1212
|
+
if (char !== " ") combinator = char
|
|
1213
|
+
else if (combinator === "") combinator = " "
|
|
1214
|
+
start = i + 1
|
|
1215
|
+
}
|
|
1216
|
+
if (depth <= 0) flush(selector.length)
|
|
1217
|
+
return parts
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
const matches = (element: Element, selector: string): boolean => {
|
|
1221
|
+
try {
|
|
1222
|
+
return element.matches(selector)
|
|
1223
|
+
} catch {
|
|
1224
|
+
return false
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// Whether `selector` targets a top-level node of the page as a child of the element the slot sits in,
|
|
1229
|
+
// which the two wrappers between them now break.
|
|
1230
|
+
const breaksAround = (selector: string, parent: Element, tops: readonly Element[]): boolean => {
|
|
1231
|
+
const parts = compoundsOf(selector)
|
|
1232
|
+
for (const [i, { compound, before }] of parts.entries()) {
|
|
1233
|
+
const prefix = parts
|
|
1234
|
+
.slice(0, i)
|
|
1235
|
+
.map((part, j) => (j === 0 ? part.compound : `${part.before} ${part.compound}`))
|
|
1236
|
+
.join(" ")
|
|
1237
|
+
if (before === ">" && matches(parent, prefix) && tops.some((top) => matches(top, compound)))
|
|
1238
|
+
return true
|
|
1239
|
+
if (POSITIONAL.test(compound)) {
|
|
1240
|
+
POSITIONAL.lastIndex = 0
|
|
1241
|
+
const rest = compound.replace(POSITIONAL, "") || "*"
|
|
1242
|
+
if (
|
|
1243
|
+
tops.some((top) => matches(top, rest)) &&
|
|
1244
|
+
(prefix === "" || matches(parent, prefix) || parent.closest(prefix) !== null)
|
|
1245
|
+
)
|
|
1246
|
+
return true
|
|
1247
|
+
}
|
|
1248
|
+
POSITIONAL.lastIndex = 0
|
|
1249
|
+
const has = DIRECT_HAS.exec(compound)
|
|
1250
|
+
if (has !== null) {
|
|
1251
|
+
const owner = compound.replace(DIRECT_HAS, "") || "*"
|
|
1252
|
+
if (matches(parent, owner) && tops.some((top) => matches(top, has[1] ?? ""))) return true
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return false
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
const scanRules = (
|
|
1259
|
+
rules: CSSRuleList,
|
|
1260
|
+
parent: Element,
|
|
1261
|
+
tops: readonly Element[],
|
|
1262
|
+
source: string,
|
|
1263
|
+
): void => {
|
|
1264
|
+
for (const rule of rules) {
|
|
1265
|
+
if (rule instanceof CSSStyleRule) {
|
|
1266
|
+
for (const selector of splitSelectors(rule.selectorText)) {
|
|
1267
|
+
if (reportedCss.has(selector) || !breaksAround(selector, parent, tops)) continue
|
|
1268
|
+
reportedCss.add(selector)
|
|
1269
|
+
console.warn(
|
|
1270
|
+
`[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.`,
|
|
1271
|
+
)
|
|
1272
|
+
}
|
|
1273
|
+
} else if ("cssRules" in rule) {
|
|
1274
|
+
// @media, @supports, @layer and @container rules hold style rules of their own.
|
|
1275
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: a rule with cssRules is a grouping rule
|
|
1276
|
+
scanRules((rule as CSSGroupingRule).cssRules, parent, tops, source)
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
const splitSelectors = (list: string): string[] => {
|
|
1282
|
+
const out: string[] = []
|
|
1283
|
+
let depth = 0
|
|
1284
|
+
let start = 0
|
|
1285
|
+
for (let i = 0; i < list.length; i++) {
|
|
1286
|
+
const char = list[i]
|
|
1287
|
+
if (char === "(" || char === "[") depth++
|
|
1288
|
+
else if (char === ")" || char === "]") depth--
|
|
1289
|
+
else if (char === "," && depth === 0) {
|
|
1290
|
+
out.push(list.slice(start, i).trim())
|
|
1291
|
+
start = i + 1
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
out.push(list.slice(start).trim())
|
|
1295
|
+
return out.filter((selector) => selector !== "")
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/**
|
|
1299
|
+
* Dev only: report each same-origin style rule whose selector the slot wrappers around `page` stop
|
|
1300
|
+
* matching (`NIFRA_KEEPALIVE_DIRECT_CHILD_CSS`). A stylesheet the page cannot read is skipped.
|
|
1301
|
+
*/
|
|
1302
|
+
export function checkSlotCss(entry: Element, page: Element): void {
|
|
1303
|
+
const parent = entry.parentElement
|
|
1304
|
+
const tops = [...page.children]
|
|
1305
|
+
if (parent === null || tops.length === 0 || typeof document === "undefined") return
|
|
1306
|
+
for (const sheet of document.styleSheets) {
|
|
1307
|
+
let rules: CSSRuleList
|
|
1308
|
+
try {
|
|
1309
|
+
rules = sheet.cssRules
|
|
1310
|
+
} catch {
|
|
1311
|
+
continue
|
|
1312
|
+
}
|
|
1313
|
+
scanRules(rules, parent, tops, sheet.href === null ? "" : ` (${sheet.href})`)
|
|
1314
|
+
}
|
|
1315
|
+
}
|