@nifrajs/web 5.5.1 → 5.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +243 -0
- package/README.md +21 -11
- package/dist/boundary.d.ts +67 -7
- package/dist/boundary.d.ts.map +1 -1
- package/dist/boundary.js +6 -4
- package/dist/boundary.js.map +1 -1
- package/dist/build-plan.d.ts +3 -0
- package/dist/build-plan.d.ts.map +1 -1
- package/dist/build-plan.js.map +1 -1
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +2 -0
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +9 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +10 -0
- package/dist/build.js.map +1 -1
- package/dist/client.d.ts +3 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +174 -23
- package/dist/client.js.map +1 -1
- package/dist/csp.d.ts +8 -0
- package/dist/csp.d.ts.map +1 -1
- package/dist/csp.js +11 -2
- package/dist/csp.js.map +1 -1
- package/dist/deferred.d.ts +6 -4
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +10 -6
- package/dist/deferred.js.map +1 -1
- package/dist/dev-feed.d.ts.map +1 -1
- package/dist/dev-feed.js +11 -2
- package/dist/dev-feed.js.map +1 -1
- package/dist/dev-session.d.ts.map +1 -1
- package/dist/dev-session.js +13 -6
- package/dist/dev-session.js.map +1 -1
- package/dist/dev.d.ts +4 -1
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +12 -5
- package/dist/dev.js.map +1 -1
- package/dist/diagnostic-catalog.d.ts +4 -0
- package/dist/diagnostic-catalog.d.ts.map +1 -1
- package/dist/diagnostic-catalog.js +48 -0
- package/dist/diagnostic-catalog.js.map +1 -1
- package/dist/diagnostic.d.ts +1 -1
- package/dist/diagnostic.d.ts.map +1 -1
- package/dist/diagnostic.js +1 -1
- package/dist/diagnostic.js.map +1 -1
- package/dist/fn.d.ts +5 -3
- package/dist/fn.d.ts.map +1 -1
- package/dist/fn.js +20 -5
- package/dist/fn.js.map +1 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +13 -3
- package/dist/fs.js.map +1 -1
- package/dist/identity.d.ts +22 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +60 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/internal/cdn/cloudflare.d.ts +2 -1
- package/dist/internal/cdn/cloudflare.d.ts.map +1 -1
- package/dist/internal/cdn/cloudflare.js +2 -1
- package/dist/internal/cdn/cloudflare.js.map +1 -1
- package/dist/internal/certified-components.d.ts +1 -1
- package/dist/internal/certified-components.js +1 -1
- package/dist/internal/codegen.d.ts +11 -10
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +73 -10
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/dev-websocket.d.ts +73 -0
- package/dist/internal/dev-websocket.d.ts.map +1 -0
- package/dist/internal/dev-websocket.js +145 -0
- package/dist/internal/dev-websocket.js.map +1 -0
- package/dist/internal/hole-runtime.d.ts +66 -0
- package/dist/internal/hole-runtime.d.ts.map +1 -0
- package/dist/internal/hole-runtime.js +211 -0
- package/dist/internal/hole-runtime.js.map +1 -0
- package/dist/internal/identity-epoch.d.ts +50 -0
- package/dist/internal/identity-epoch.d.ts.map +1 -0
- package/dist/internal/identity-epoch.js +180 -0
- package/dist/internal/identity-epoch.js.map +1 -0
- package/dist/internal/invalidation-contract.d.ts +23 -0
- package/dist/internal/invalidation-contract.d.ts.map +1 -0
- package/dist/internal/invalidation-contract.js +11 -0
- package/dist/internal/invalidation-contract.js.map +1 -0
- package/dist/internal/invalidation.d.ts +62 -0
- package/dist/internal/invalidation.d.ts.map +1 -0
- package/dist/internal/invalidation.js +149 -0
- package/dist/internal/invalidation.js.map +1 -0
- package/dist/internal/island-bindings.d.ts +51 -0
- package/dist/internal/island-bindings.d.ts.map +1 -0
- package/dist/internal/island-bindings.js +147 -0
- package/dist/internal/island-bindings.js.map +1 -0
- package/dist/internal/island-list.d.ts +26 -0
- package/dist/internal/island-list.d.ts.map +1 -0
- package/dist/internal/island-list.js +78 -0
- package/dist/internal/island-list.js.map +1 -0
- package/dist/internal/island-mount.d.ts +4 -0
- package/dist/internal/island-mount.d.ts.map +1 -0
- package/dist/internal/island-mount.js +66 -0
- package/dist/internal/island-mount.js.map +1 -0
- package/dist/internal/island-owner.d.ts +5 -0
- package/dist/internal/island-owner.d.ts.map +1 -0
- package/dist/internal/island-owner.js +20 -0
- package/dist/internal/island-owner.js.map +1 -0
- package/dist/internal/island-resource.d.ts +30 -0
- package/dist/internal/island-resource.d.ts.map +1 -0
- package/dist/internal/island-resource.js +57 -0
- package/dist/internal/island-resource.js.map +1 -0
- package/dist/internal/island-signals.d.ts +51 -0
- package/dist/internal/island-signals.d.ts.map +1 -0
- package/dist/internal/island-signals.js +220 -0
- package/dist/internal/island-signals.js.map +1 -0
- package/dist/internal/keep-alive-table.d.ts +23 -0
- package/dist/internal/keep-alive-table.d.ts.map +1 -0
- package/dist/internal/keep-alive-table.js +53 -0
- package/dist/internal/keep-alive-table.js.map +1 -0
- package/dist/internal/live-contract.d.ts +101 -0
- package/dist/internal/live-contract.d.ts.map +1 -0
- package/dist/internal/live-contract.js +5 -0
- package/dist/internal/live-contract.js.map +1 -0
- package/dist/internal/live-queries.d.ts +22 -0
- package/dist/internal/live-queries.d.ts.map +1 -0
- package/dist/internal/live-queries.js +54 -0
- package/dist/internal/live-queries.js.map +1 -0
- package/dist/internal/page-events.d.ts +10 -0
- package/dist/internal/page-events.d.ts.map +1 -0
- package/dist/internal/page-events.js +26 -0
- package/dist/internal/page-events.js.map +1 -0
- package/dist/internal/page-execution.d.ts +16 -1
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +432 -31
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/parity.d.ts +3 -0
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +28 -5
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/ppr/admission.d.ts +83 -0
- package/dist/internal/ppr/admission.d.ts.map +1 -0
- package/dist/internal/ppr/admission.js +221 -0
- package/dist/internal/ppr/admission.js.map +1 -0
- package/dist/internal/ppr/canary.d.ts +24 -0
- package/dist/internal/ppr/canary.d.ts.map +1 -0
- package/dist/internal/ppr/canary.js +102 -0
- package/dist/internal/ppr/canary.js.map +1 -0
- package/dist/internal/ppr/compile-shell.d.ts +39 -0
- package/dist/internal/ppr/compile-shell.d.ts.map +1 -0
- package/dist/internal/ppr/compile-shell.js +298 -0
- package/dist/internal/ppr/compile-shell.js.map +1 -0
- package/dist/internal/ppr/contract.d.ts +177 -0
- package/dist/internal/ppr/contract.d.ts.map +1 -0
- package/dist/internal/ppr/contract.js +80 -0
- package/dist/internal/ppr/contract.js.map +1 -0
- package/dist/internal/ppr/frames.d.ts +71 -0
- package/dist/internal/ppr/frames.d.ts.map +1 -0
- package/dist/internal/ppr/frames.js +119 -0
- package/dist/internal/ppr/frames.js.map +1 -0
- package/dist/internal/ppr/integration.d.ts +19 -0
- package/dist/internal/ppr/integration.d.ts.map +1 -0
- package/dist/internal/ppr/integration.js +106 -0
- package/dist/internal/ppr/integration.js.map +1 -0
- package/dist/internal/ppr/options.d.ts +96 -0
- package/dist/internal/ppr/options.d.ts.map +1 -0
- package/dist/internal/ppr/options.js +125 -0
- package/dist/internal/ppr/options.js.map +1 -0
- package/dist/internal/ppr/route-rules.d.ts +12 -0
- package/dist/internal/ppr/route-rules.d.ts.map +1 -0
- package/dist/internal/ppr/route-rules.js +53 -0
- package/dist/internal/ppr/route-rules.js.map +1 -0
- package/dist/internal/ppr/runtime.d.ts +29 -0
- package/dist/internal/ppr/runtime.d.ts.map +1 -0
- package/dist/internal/ppr/runtime.js +35 -0
- package/dist/internal/ppr/runtime.js.map +1 -0
- package/dist/internal/ppr/server.d.ts +29 -0
- package/dist/internal/ppr/server.d.ts.map +1 -0
- package/dist/internal/ppr/server.js +1409 -0
- package/dist/internal/ppr/server.js.map +1 -0
- package/dist/internal/ppr/shell-store.d.ts +39 -0
- package/dist/internal/ppr/shell-store.d.ts.map +1 -0
- package/dist/internal/ppr/shell-store.js +106 -0
- package/dist/internal/ppr/shell-store.js.map +1 -0
- package/dist/internal/prerendered-pages.d.ts +1 -1
- package/dist/internal/prerendered-pages.d.ts.map +1 -1
- package/dist/internal/prerendered-pages.js +7 -4
- package/dist/internal/prerendered-pages.js.map +1 -1
- package/dist/internal/present.d.ts +11 -0
- package/dist/internal/present.d.ts.map +1 -0
- package/dist/internal/present.js +13 -0
- package/dist/internal/present.js.map +1 -0
- package/dist/internal/query-digest.d.ts +9 -0
- package/dist/internal/query-digest.d.ts.map +1 -0
- package/dist/internal/query-digest.js +23 -0
- package/dist/internal/query-digest.js.map +1 -0
- package/dist/internal/query-runtime.d.ts +6 -1
- package/dist/internal/query-runtime.d.ts.map +1 -1
- package/dist/internal/query-runtime.js +9 -0
- package/dist/internal/query-runtime.js.map +1 -1
- package/dist/internal/query-seam.d.ts +29 -0
- package/dist/internal/query-seam.d.ts.map +1 -0
- package/dist/internal/query-seam.js +23 -0
- package/dist/internal/query-seam.js.map +1 -0
- package/dist/internal/render-document.d.ts +34 -64
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +82 -64
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/request-abort.d.ts +8 -0
- package/dist/internal/request-abort.d.ts.map +1 -0
- package/dist/internal/request-abort.js +22 -0
- package/dist/internal/request-abort.js.map +1 -0
- package/dist/internal/server-boundary.d.ts +2 -0
- package/dist/internal/server-boundary.d.ts.map +1 -1
- package/dist/internal/server-boundary.js +8 -1
- package/dist/internal/server-boundary.js.map +1 -1
- package/dist/internal/share-data.d.ts +10 -0
- package/dist/internal/share-data.d.ts.map +1 -0
- package/dist/internal/share-data.js +135 -0
- package/dist/internal/share-data.js.map +1 -0
- package/dist/internal/status-page.d.ts +68 -0
- package/dist/internal/status-page.d.ts.map +1 -0
- package/dist/internal/status-page.js +66 -0
- package/dist/internal/status-page.js.map +1 -0
- package/dist/internal/store-signature.d.ts +15 -0
- package/dist/internal/store-signature.d.ts.map +1 -0
- package/dist/internal/store-signature.js +114 -0
- package/dist/internal/store-signature.js.map +1 -0
- package/dist/internal/vite-plugin-compat.d.ts +2 -2
- package/dist/internal/vite-plugin-compat.js +2 -2
- package/dist/internal/web-app.d.ts +26 -0
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +79 -3
- package/dist/internal/web-app.js.map +1 -1
- package/dist/islands-swap.d.ts +41 -0
- package/dist/islands-swap.d.ts.map +1 -0
- package/dist/islands-swap.js +270 -0
- package/dist/islands-swap.js.map +1 -0
- package/dist/islands.d.ts +86 -24
- package/dist/islands.d.ts.map +1 -1
- package/dist/islands.js +138 -78
- package/dist/islands.js.map +1 -1
- package/dist/isr.d.ts +67 -13
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +247 -99
- package/dist/isr.js.map +1 -1
- package/dist/keep-alive.d.ts +147 -0
- package/dist/keep-alive.d.ts.map +1 -0
- package/dist/keep-alive.js +1102 -0
- package/dist/keep-alive.js.map +1 -0
- package/dist/live/bus.d.ts +70 -0
- package/dist/live/bus.d.ts.map +1 -0
- package/dist/live/bus.js +55 -0
- package/dist/live/bus.js.map +1 -0
- package/dist/live/core.d.ts +25 -0
- package/dist/live/core.d.ts.map +1 -0
- package/dist/live/core.js +29 -0
- package/dist/live/core.js.map +1 -0
- package/dist/live/history.d.ts +38 -0
- package/dist/live/history.d.ts.map +1 -0
- package/dist/live/history.js +104 -0
- package/dist/live/history.js.map +1 -0
- package/dist/live/hub-protocol.d.ts +46 -0
- package/dist/live/hub-protocol.d.ts.map +1 -0
- package/dist/live/hub-protocol.js +108 -0
- package/dist/live/hub-protocol.js.map +1 -0
- package/dist/live/hub.d.ts +185 -0
- package/dist/live/hub.d.ts.map +1 -0
- package/dist/live/hub.js +544 -0
- package/dist/live/hub.js.map +1 -0
- package/dist/live/keyring.d.ts +92 -0
- package/dist/live/keyring.d.ts.map +1 -0
- package/dist/live/keyring.js +265 -0
- package/dist/live/keyring.js.map +1 -0
- package/dist/live/read-capped.d.ts +6 -0
- package/dist/live/read-capped.d.ts.map +1 -0
- package/dist/live/read-capped.js +42 -0
- package/dist/live/read-capped.js.map +1 -0
- package/dist/live/revocations.d.ts +52 -0
- package/dist/live/revocations.d.ts.map +1 -0
- package/dist/live/revocations.js +116 -0
- package/dist/live/revocations.js.map +1 -0
- package/dist/live/settings.d.ts +14 -0
- package/dist/live/settings.d.ts.map +1 -0
- package/dist/live/settings.js +38 -0
- package/dist/live/settings.js.map +1 -0
- package/dist/live-client.d.ts +34 -0
- package/dist/live-client.d.ts.map +1 -0
- package/dist/live-client.js +878 -0
- package/dist/live-client.js.map +1 -0
- package/dist/live-hub.d.ts +130 -0
- package/dist/live-hub.d.ts.map +1 -0
- package/dist/live-hub.js +507 -0
- package/dist/live-hub.js.map +1 -0
- package/dist/live-redis.d.ts +60 -0
- package/dist/live-redis.d.ts.map +1 -0
- package/dist/live-redis.js +212 -0
- package/dist/live-redis.js.map +1 -0
- package/dist/live.d.ts +150 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +972 -0
- package/dist/live.js.map +1 -0
- package/dist/manifest.d.ts +113 -2
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +109 -5
- package/dist/manifest.js.map +1 -1
- package/dist/navigation.d.ts +9 -3
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +13 -3
- package/dist/navigation.js.map +1 -1
- package/dist/plugins/vite-zone-guard.d.ts +6 -0
- package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
- package/dist/plugins/vite-zone-guard.js +8 -2
- package/dist/plugins/vite-zone-guard.js.map +1 -1
- package/dist/ppr.d.ts +73 -0
- package/dist/ppr.d.ts.map +1 -0
- package/dist/ppr.js +76 -0
- package/dist/ppr.js.map +1 -0
- package/dist/query.d.ts +8 -13
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +112 -18
- package/dist/query.js.map +1 -1
- package/dist/render-seam.d.ts +32 -0
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +7 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/route-types.d.ts.map +1 -1
- package/dist/route-types.js +2 -2
- package/dist/route-types.js.map +1 -1
- package/dist/router.d.ts +72 -4
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +416 -129
- package/dist/router.js.map +1 -1
- package/dist/server-components/build.d.ts.map +1 -1
- package/dist/server-components/build.js +3 -3
- package/dist/server-components/build.js.map +1 -1
- package/dist/server-components/client.d.ts +23 -1
- package/dist/server-components/client.d.ts.map +1 -1
- package/dist/server-components/client.js +92 -5
- package/dist/server-components/client.js.map +1 -1
- package/dist/server-components/definition.d.ts.map +1 -1
- package/dist/server-components/definition.js +2 -1
- package/dist/server-components/definition.js.map +1 -1
- package/dist/server-components/hole.d.ts +8 -0
- package/dist/server-components/hole.d.ts.map +1 -0
- package/dist/server-components/hole.js +20 -0
- package/dist/server-components/hole.js.map +1 -0
- package/dist/server-components/html-policy.d.ts.map +1 -1
- package/dist/server-components/html-policy.js +62 -4
- package/dist/server-components/html-policy.js.map +1 -1
- package/dist/server-components/html-scan.d.ts +17 -0
- package/dist/server-components/html-scan.d.ts.map +1 -0
- package/dist/server-components/html-scan.js +180 -0
- package/dist/server-components/html-scan.js.map +1 -0
- package/dist/server-components/html.d.ts.map +1 -1
- package/dist/server-components/html.js +19 -4
- package/dist/server-components/html.js.map +1 -1
- package/dist/server-components/router-integration.d.ts +4 -1
- package/dist/server-components/router-integration.d.ts.map +1 -1
- package/dist/server-components/router-integration.js +23 -2
- package/dist/server-components/router-integration.js.map +1 -1
- package/dist/server-components/runtime.d.ts +6 -1
- package/dist/server-components/runtime.d.ts.map +1 -1
- package/dist/server-components/runtime.js +88 -29
- package/dist/server-components/runtime.js.map +1 -1
- package/dist/server-components/state.d.ts +3 -1
- package/dist/server-components/state.d.ts.map +1 -1
- package/dist/server-components/state.js +50 -38
- package/dist/server-components/state.js.map +1 -1
- package/dist/server-components/types.d.ts +9 -0
- package/dist/server-components/types.d.ts.map +1 -1
- package/dist/server-components/vite.d.ts.map +1 -1
- package/dist/server-components/vite.js +26 -9
- package/dist/server-components/vite.js.map +1 -1
- package/dist/vite.d.ts +5 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +58 -4
- package/dist/vite.js.map +1 -1
- package/package.json +48 -8
- package/src/boundary.ts +77 -5
- package/src/build-plan.ts +3 -0
- package/src/build-vite.ts +2 -0
- package/src/build.ts +20 -1
- package/src/client.ts +183 -23
- package/src/csp.ts +16 -2
- package/src/deferred.ts +11 -5
- package/src/dev-feed.ts +11 -2
- package/src/dev-session.ts +22 -11
- package/src/dev.ts +29 -8
- package/src/diagnostic-catalog.ts +53 -0
- package/src/diagnostic.ts +1 -0
- package/src/fn.ts +24 -7
- package/src/fs.ts +22 -8
- package/src/identity.ts +61 -0
- package/src/index.ts +20 -4
- package/src/internal/cdn/cloudflare.ts +2 -1
- package/src/internal/certified-components.ts +1 -1
- package/src/internal/codegen.ts +95 -9
- package/src/internal/dev-websocket.ts +230 -0
- package/src/internal/hole-runtime.ts +286 -0
- package/src/internal/identity-epoch.ts +239 -0
- package/src/internal/invalidation-contract.ts +35 -0
- package/src/internal/invalidation.ts +213 -0
- package/src/internal/island-bindings.ts +191 -0
- package/src/internal/island-list.ts +92 -0
- package/src/internal/island-mount.ts +68 -0
- package/src/internal/island-owner.ts +20 -0
- package/src/internal/island-resource.ts +76 -0
- package/src/internal/island-signals.ts +249 -0
- package/src/internal/keep-alive-table.ts +72 -0
- package/src/internal/live-contract.ts +111 -0
- package/src/internal/live-queries.ts +60 -0
- package/src/internal/page-events.ts +27 -0
- package/src/internal/page-execution.ts +605 -45
- package/src/internal/parity.ts +35 -6
- package/src/internal/ppr/admission.ts +300 -0
- package/src/internal/ppr/canary.ts +122 -0
- package/src/internal/ppr/compile-shell.ts +379 -0
- package/src/internal/ppr/contract.ts +265 -0
- package/src/internal/ppr/frames.ts +170 -0
- package/src/internal/ppr/integration.ts +137 -0
- package/src/internal/ppr/options.ts +221 -0
- package/src/internal/ppr/route-rules.ts +77 -0
- package/src/internal/ppr/runtime.ts +39 -0
- package/src/internal/ppr/server.ts +1766 -0
- package/src/internal/ppr/shell-store.ts +152 -0
- package/src/internal/prerendered-pages.ts +7 -4
- package/src/internal/present.ts +20 -0
- package/src/internal/query-digest.ts +34 -0
- package/src/internal/query-runtime.ts +14 -1
- package/src/internal/query-seam.ts +48 -0
- package/src/internal/render-document.ts +158 -113
- package/src/internal/request-abort.ts +26 -0
- package/src/internal/server-boundary.ts +10 -1
- package/src/internal/share-data.ts +136 -0
- package/src/internal/status-page.ts +95 -0
- package/src/internal/store-signature.ts +149 -0
- package/src/internal/vite-plugin-compat.ts +2 -2
- package/src/internal/web-app.ts +123 -3
- package/src/islands-swap.ts +283 -0
- package/src/islands.ts +169 -72
- package/src/isr.ts +348 -106
- package/src/keep-alive.ts +1354 -0
- package/src/live/bus.ts +104 -0
- package/src/live/core.ts +52 -0
- package/src/live/history.ts +109 -0
- package/src/live/hub-protocol.ts +133 -0
- package/src/live/hub.ts +620 -0
- package/src/live/keyring.ts +384 -0
- package/src/live/read-capped.ts +36 -0
- package/src/live/revocations.ts +155 -0
- package/src/live/settings.ts +46 -0
- package/src/live-client.ts +1011 -0
- package/src/live-hub.ts +658 -0
- package/src/live-redis.ts +284 -0
- package/src/live.ts +1257 -0
- package/src/manifest.ts +274 -6
- package/src/navigation.ts +14 -2
- package/src/plugins/vite-zone-guard.ts +15 -2
- package/src/ppr.ts +153 -0
- package/src/query.ts +147 -20
- package/src/render-seam.ts +37 -0
- package/src/route-types.ts +3 -1
- package/src/router.ts +534 -132
- package/src/server-components/build.ts +9 -3
- package/src/server-components/client.ts +89 -5
- package/src/server-components/definition.ts +2 -1
- package/src/server-components/hole.ts +23 -0
- package/src/server-components/html-policy.ts +63 -4
- package/src/server-components/html-scan.ts +159 -0
- package/src/server-components/html.ts +18 -4
- package/src/server-components/router-integration.ts +21 -2
- package/src/server-components/runtime.ts +114 -31
- package/src/server-components/state.ts +66 -44
- package/src/server-components/types.ts +16 -0
- package/src/server-components/vite.ts +33 -14
- package/src/vite.ts +75 -6
- package/dist/nano.d.ts +0 -125
- package/dist/nano.d.ts.map +0 -1
- package/dist/nano.js +0 -190
- package/dist/nano.js.map +0 -1
- package/src/nano.ts +0 -246
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One invalidation pipeline for everything a write makes stale, in a fixed order: data caches (tags),
|
|
3
|
+
* then the page store (paths and tags), then the live publish (keys and tags), then the CDN. A failure
|
|
4
|
+
* in either cache stage stops before the publish: clients told to reload would read the stale data and
|
|
5
|
+
* move their watermarks past the change. Every stage is idempotent, so a retry repeats from the stage
|
|
6
|
+
* that failed.
|
|
7
|
+
*
|
|
8
|
+
* `withISR` owns the pipeline when it wraps the app; without it, `createWebApp` runs the publish
|
|
9
|
+
* stage alone. Never both.
|
|
10
|
+
*/
|
|
11
|
+
import type { CacheStore, CdnPurgeTarget, ISRPlatform } from "../isr.ts"
|
|
12
|
+
import type { InvalidationOp } from "./invalidation-contract.ts"
|
|
13
|
+
|
|
14
|
+
export type { InvalidationOp } from "./invalidation-contract.ts"
|
|
15
|
+
|
|
16
|
+
export type InvalidationStage = "data" | "pages" | "publish" | "cdn"
|
|
17
|
+
|
|
18
|
+
export interface StageReport {
|
|
19
|
+
readonly stage: InvalidationStage
|
|
20
|
+
readonly ok: boolean
|
|
21
|
+
readonly retryable: boolean
|
|
22
|
+
readonly error?: string
|
|
23
|
+
/** The CDN took the purge but has not applied it yet (rate limit or retry). */
|
|
24
|
+
readonly queued?: true
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface InvalidationReport {
|
|
28
|
+
readonly id: string
|
|
29
|
+
readonly ok: boolean
|
|
30
|
+
readonly stages: readonly StageReport[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** What the pipeline calls on a page store: any {@link CacheStore} fits. */
|
|
34
|
+
export interface PagePurge {
|
|
35
|
+
delete(key: string): Promise<void>
|
|
36
|
+
readonly invalidateTag?: ((tag: string) => Promise<void>) | undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface InvalidationTargets {
|
|
40
|
+
readonly dataCaches?: readonly { invalidateTag(tag: string): unknown }[] | undefined
|
|
41
|
+
readonly store?: PagePurge | undefined
|
|
42
|
+
/** The page store key of a path, as the cache wrapper computes it; `null` when never stored. */
|
|
43
|
+
readonly keyOf?: ((path: string, req: Request) => string | null) | undefined
|
|
44
|
+
readonly publish?: ((keys: readonly string[]) => Promise<void>) | undefined
|
|
45
|
+
readonly cdn?: CdnPurgeTarget | undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const ORDER: readonly InvalidationStage[] = ["data", "pages", "publish", "cdn"]
|
|
49
|
+
/** Retry delays after a retryable failure; inline retries stop at {@link INLINE_DEADLINE_MS}. */
|
|
50
|
+
const RETRY_MS = [200, 600, 1800]
|
|
51
|
+
export const INLINE_DEADLINE_MS = 2000
|
|
52
|
+
|
|
53
|
+
/** Run the stages from `from` once. */
|
|
54
|
+
export async function runInvalidation(
|
|
55
|
+
op: InvalidationOp,
|
|
56
|
+
targets: InvalidationTargets,
|
|
57
|
+
req: Request,
|
|
58
|
+
platform: ISRPlatform | undefined,
|
|
59
|
+
from: InvalidationStage = "data",
|
|
60
|
+
): Promise<InvalidationReport> {
|
|
61
|
+
const stages: StageReport[] = []
|
|
62
|
+
for (const stage of ORDER.slice(ORDER.indexOf(from))) {
|
|
63
|
+
// Decided before any await: a stage with nothing to do costs no turn of the event loop.
|
|
64
|
+
const running = startStage(stage, op, targets, req, platform)
|
|
65
|
+
if (running === undefined) continue
|
|
66
|
+
const report = await running
|
|
67
|
+
stages.push(report)
|
|
68
|
+
// A cache stage that failed leaves data a reload would read: nothing after it may run.
|
|
69
|
+
if (!report.ok && (stage === "data" || stage === "pages")) break
|
|
70
|
+
}
|
|
71
|
+
return { id: op.id, ok: stages.every((report) => report.ok), stages }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Start a stage's work for this operation; `undefined` when it has none. Never rejects. */
|
|
75
|
+
function startStage(
|
|
76
|
+
stage: InvalidationStage,
|
|
77
|
+
op: InvalidationOp,
|
|
78
|
+
targets: InvalidationTargets,
|
|
79
|
+
req: Request,
|
|
80
|
+
platform: ISRPlatform | undefined,
|
|
81
|
+
): Promise<StageReport> | undefined {
|
|
82
|
+
const done: StageReport = { stage, ok: true, retryable: false }
|
|
83
|
+
let work: Promise<StageReport>
|
|
84
|
+
switch (stage) {
|
|
85
|
+
case "data": {
|
|
86
|
+
const caches = targets.dataCaches
|
|
87
|
+
if (op.tags.length === 0 || caches === undefined) return undefined
|
|
88
|
+
work = (async () => {
|
|
89
|
+
for (const cache of caches) {
|
|
90
|
+
for (const tag of op.tags) await cache.invalidateTag(tag)
|
|
91
|
+
}
|
|
92
|
+
return done
|
|
93
|
+
})()
|
|
94
|
+
break
|
|
95
|
+
}
|
|
96
|
+
case "pages": {
|
|
97
|
+
const store = targets.store
|
|
98
|
+
if (store === undefined || op.paths.length + op.tags.length === 0) return undefined
|
|
99
|
+
work = (async (): Promise<StageReport> => {
|
|
100
|
+
if (op.tags.length > 0 && store.invalidateTag === undefined) {
|
|
101
|
+
return { stage, ok: false, retryable: false, error: "tag_invalidation_not_supported" }
|
|
102
|
+
}
|
|
103
|
+
for (const path of op.paths) {
|
|
104
|
+
const key = targets.keyOf?.(path, req) ?? null
|
|
105
|
+
if (key !== null) await store.delete(key)
|
|
106
|
+
}
|
|
107
|
+
for (const tag of op.tags) await store.invalidateTag?.(tag)
|
|
108
|
+
return done
|
|
109
|
+
})()
|
|
110
|
+
break
|
|
111
|
+
}
|
|
112
|
+
case "publish": {
|
|
113
|
+
const publish = targets.publish
|
|
114
|
+
if (publish === undefined || op.keys.length + op.tags.length === 0) return undefined
|
|
115
|
+
work = (async () => {
|
|
116
|
+
await publish([...new Set([...op.keys, ...op.tags])])
|
|
117
|
+
return done
|
|
118
|
+
})()
|
|
119
|
+
break
|
|
120
|
+
}
|
|
121
|
+
case "cdn": {
|
|
122
|
+
const cdn = targets.cdn
|
|
123
|
+
if (cdn === undefined || op.paths.length + op.tags.length === 0) return undefined
|
|
124
|
+
work = (async (): Promise<StageReport> => {
|
|
125
|
+
const outcome = await cdn.purge({ tags: op.tags, paths: op.paths }, platform)
|
|
126
|
+
return outcome.cdn === "failed"
|
|
127
|
+
? {
|
|
128
|
+
stage,
|
|
129
|
+
ok: false,
|
|
130
|
+
retryable: outcome.retryable,
|
|
131
|
+
error: outcome.error ?? "cdn_purge_failed",
|
|
132
|
+
}
|
|
133
|
+
: outcome.cdn === "queued"
|
|
134
|
+
? { stage, ok: true, retryable: false, queued: true }
|
|
135
|
+
: done
|
|
136
|
+
})()
|
|
137
|
+
break
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return work.catch(() => ({ stage, ok: false, retryable: true, error: `${stage}_failed` }))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface SettleOptions {
|
|
144
|
+
readonly onError?: ((report: InvalidationReport) => void) | undefined
|
|
145
|
+
/** Tests drive time; production waits on timers. */
|
|
146
|
+
readonly clock?: { now(): number; sleep(ms: number): Promise<void> }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const realClock = {
|
|
150
|
+
now: () => Date.now(),
|
|
151
|
+
sleep: (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)),
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Run the pipeline for a write; never throws and never changes the response. A retryable failure
|
|
156
|
+
* retries from the failed stage, under `waitUntil` when the platform has it, else inline until
|
|
157
|
+
* {@link INLINE_DEADLINE_MS}. A failure that remains is reported once.
|
|
158
|
+
*/
|
|
159
|
+
export async function settleInvalidation(
|
|
160
|
+
op: InvalidationOp,
|
|
161
|
+
targets: InvalidationTargets,
|
|
162
|
+
req: Request,
|
|
163
|
+
platform: ISRPlatform | undefined,
|
|
164
|
+
options: SettleOptions = {},
|
|
165
|
+
): Promise<InvalidationReport> {
|
|
166
|
+
const onError = options.onError ?? logInvalidation
|
|
167
|
+
const clock = options.clock ?? realClock
|
|
168
|
+
const started = clock.now()
|
|
169
|
+
const first = await runInvalidation(op, targets, req, platform)
|
|
170
|
+
if (first.ok) return first
|
|
171
|
+
const failed = first.stages.find((report) => !report.ok)
|
|
172
|
+
if (failed === undefined || !failed.retryable) {
|
|
173
|
+
report(onError, first)
|
|
174
|
+
return first
|
|
175
|
+
}
|
|
176
|
+
const background = typeof platform?.waitUntil === "function"
|
|
177
|
+
const retry = async (): Promise<InvalidationReport> => {
|
|
178
|
+
let last = first
|
|
179
|
+
let stage = failed.stage
|
|
180
|
+
for (const delay of RETRY_MS) {
|
|
181
|
+
if (!background && clock.now() - started + delay >= INLINE_DEADLINE_MS) break
|
|
182
|
+
await clock.sleep(delay)
|
|
183
|
+
last = await runInvalidation(op, targets, req, platform, stage)
|
|
184
|
+
const next = last.stages.find((report) => !report.ok)
|
|
185
|
+
if (next === undefined) return last
|
|
186
|
+
if (!next.retryable) break
|
|
187
|
+
stage = next.stage
|
|
188
|
+
}
|
|
189
|
+
report(onError, last)
|
|
190
|
+
return last
|
|
191
|
+
}
|
|
192
|
+
if (background) {
|
|
193
|
+
platform?.waitUntil?.(retry())
|
|
194
|
+
return first
|
|
195
|
+
}
|
|
196
|
+
return retry()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function report(onError: (report: InvalidationReport) => void, value: InvalidationReport): void {
|
|
200
|
+
try {
|
|
201
|
+
onError(value)
|
|
202
|
+
} catch {
|
|
203
|
+
// A failing reporter must not reach the response.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The default report: the operation id and the stage, never a path, tag or key. */
|
|
208
|
+
export function logInvalidation(report: InvalidationReport): void {
|
|
209
|
+
const failed = report.stages.find((stage) => !stage.ok)
|
|
210
|
+
console.error(
|
|
211
|
+
`[nifra/web] invalidation ${report.id} gave up at the ${failed?.stage ?? "unknown"} stage (${failed?.error ?? "failed"})`,
|
|
212
|
+
)
|
|
213
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The declarative binding walker - attaches signal-driven behavior to SERVER-RENDERED markup.
|
|
3
|
+
* No VDOM, no hydration re-render: the HTML the server sent (e.g. via `@nifrajs/web-vanilla`) is
|
|
4
|
+
* already the initial state; bindings update it surgically when signals change. `bindIsland` runs it.
|
|
5
|
+
*
|
|
6
|
+
* The attribute set is CLOSED and documented - six bindings, no expression language. Values are
|
|
7
|
+
* signal/handler NAMES looked up in the island's scope, never evaluated code, so markup can't
|
|
8
|
+
* inject behavior:
|
|
9
|
+
*
|
|
10
|
+
* data-bind-text="count" textContent ← String(signal())
|
|
11
|
+
* data-bind-show="isOpen" hidden ← !signal()
|
|
12
|
+
* data-bind-class="active:isOpen,b:sigB" classList.toggle per pair
|
|
13
|
+
* data-bind-attr="aria-expanded:isOpen" setAttribute / removeAttribute (false/null remove);
|
|
14
|
+
* never an `on*` handler or `srcdoc`, and a URL attribute
|
|
15
|
+
* takes only an http(s)/mailto/tel or relative URL
|
|
16
|
+
* data-bind-value="query" two-way <input>/<select>/<textarea> (input and change)
|
|
17
|
+
* data-bind-on="click:inc,submit:save" addEventListener per pair
|
|
18
|
+
*
|
|
19
|
+
* Nothing inside a `data-island-ignore` element binds or mounts: render user HTML there, so its
|
|
20
|
+
* markup cannot reach the island's handlers or signals.
|
|
21
|
+
*
|
|
22
|
+
* A binding belongs to its nearest island: an island never binds inside a nested island's host. The
|
|
23
|
+
* nested host element itself is still the outer island's markup, so the outer island may bind it.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { type Signal, watch } from "./island-signals.ts"
|
|
27
|
+
|
|
28
|
+
export type IslandScope = {
|
|
29
|
+
readonly signals: Readonly<Record<string, Signal<unknown>>>
|
|
30
|
+
readonly handlers: Readonly<Record<string, (event: Event) => void>>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const warned = new Set<string>()
|
|
34
|
+
/** Unknown names warn once (loud in dev, resilient in prod - progressive enhancement must not
|
|
35
|
+
* throw over a typo'd attribute) and the binding is skipped. */
|
|
36
|
+
function warnOnce(kind: string, name: string): void {
|
|
37
|
+
const key = `${kind}:${name}`
|
|
38
|
+
if (warned.has(key)) return
|
|
39
|
+
warned.add(key)
|
|
40
|
+
console.warn(`[nifra/islands] unknown ${kind} ${JSON.stringify(name)} - binding skipped`)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Own keys only: a name such as `constructor` must not resolve to what a plain object inherits.
|
|
44
|
+
function named<V>(kind: string, record: Readonly<Record<string, V>>, name: string): V | undefined {
|
|
45
|
+
if (Object.hasOwn(record, name)) return record[name]
|
|
46
|
+
warnOnce(kind, name)
|
|
47
|
+
return undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Parse a `"key:name,key2:name2"` pair list (whitespace-tolerant). Malformed entries skip. */
|
|
51
|
+
function pairs(spec: string): Array<[string, string]> {
|
|
52
|
+
const out: Array<[string, string]> = []
|
|
53
|
+
for (const part of spec.split(",")) {
|
|
54
|
+
const sep = part.indexOf(":")
|
|
55
|
+
if (sep <= 0) continue
|
|
56
|
+
const key = part.slice(0, sep).trim()
|
|
57
|
+
const name = part.slice(sep + 1).trim()
|
|
58
|
+
if (key && name) out.push([key, name])
|
|
59
|
+
}
|
|
60
|
+
return out
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A bound value is data (often from island state the markup carries), so an attribute that would run
|
|
64
|
+
// it as code, or navigate to it as a script URL, is refused.
|
|
65
|
+
const CODE_ATTRIBUTE = /^(?:on|srcdoc$)/i
|
|
66
|
+
const URL_ATTRIBUTE =
|
|
67
|
+
/^(?:href|src|action|formaction|xlink:href|poster|data|background|cite|ping)$/i
|
|
68
|
+
const SAFE_URL = /^(?:(?:https?|mailto|tel):|[^:/?#]*(?:[/?#]|$))/i
|
|
69
|
+
|
|
70
|
+
/** The element surface the walker needs - structural, so tests can drive it without a real DOM. */
|
|
71
|
+
export interface BindableElement {
|
|
72
|
+
getAttribute(name: string): string | null
|
|
73
|
+
setAttribute(name: string, value: string): void
|
|
74
|
+
removeAttribute(name: string): void
|
|
75
|
+
textContent: string | null
|
|
76
|
+
hidden: boolean
|
|
77
|
+
value?: string
|
|
78
|
+
readonly classList: { toggle(name: string, force?: boolean): unknown }
|
|
79
|
+
addEventListener(type: string, listener: (event: Event) => void): void
|
|
80
|
+
removeEventListener(type: string, listener: (event: Event) => void): void
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface BindableRoot {
|
|
84
|
+
querySelectorAll(selector: string): Iterable<BindableElement>
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Walk `root` and attach every `data-bind-*` binding against `scope`. Each binding's disposer
|
|
88
|
+
* (reactive effect or event listener) goes onto `stops` as it attaches, so a binding that throws
|
|
89
|
+
* midway leaves the earlier ones disposable; `stops` is returned. */
|
|
90
|
+
export function bindScope(
|
|
91
|
+
root: BindableRoot,
|
|
92
|
+
scope: IslandScope,
|
|
93
|
+
stops: Array<() => void> = [],
|
|
94
|
+
): Array<() => void> {
|
|
95
|
+
// Each element carrying `data-bind-<kind>` outside any ignored subtree and any nested island, with
|
|
96
|
+
// that attribute's value.
|
|
97
|
+
const each = (kind: string): Array<[BindableElement, string]> => {
|
|
98
|
+
const attribute = `data-bind-${kind}`
|
|
99
|
+
return [
|
|
100
|
+
...root.querySelectorAll(
|
|
101
|
+
`[${attribute}]:not([data-island-ignore],[data-island-ignore] *,:scope [data-island] *)`,
|
|
102
|
+
),
|
|
103
|
+
].map((el) => [el, el.getAttribute(attribute) ?? ""])
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const [el, name] of each("text")) {
|
|
107
|
+
const s = named("signal", scope.signals, name)
|
|
108
|
+
if (s) {
|
|
109
|
+
stops.push(
|
|
110
|
+
watch(s, (v) => {
|
|
111
|
+
el.textContent = String(v)
|
|
112
|
+
}),
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const [el, name] of each("show")) {
|
|
118
|
+
const s = named("signal", scope.signals, name)
|
|
119
|
+
if (s) {
|
|
120
|
+
stops.push(
|
|
121
|
+
watch(s, (v) => {
|
|
122
|
+
el.hidden = !v
|
|
123
|
+
}),
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const [el, spec] of each("class")) {
|
|
129
|
+
for (const [className, name] of pairs(spec)) {
|
|
130
|
+
const s = named("signal", scope.signals, name)
|
|
131
|
+
if (s) {
|
|
132
|
+
stops.push(
|
|
133
|
+
watch(s, (v) => {
|
|
134
|
+
el.classList.toggle(className, !!v)
|
|
135
|
+
}),
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const [el, spec] of each("attr")) {
|
|
142
|
+
for (const [attr, name] of pairs(spec)) {
|
|
143
|
+
if (CODE_ATTRIBUTE.test(attr)) {
|
|
144
|
+
warnOnce("attribute", attr)
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
const s = named("signal", scope.signals, name)
|
|
148
|
+
if (s) {
|
|
149
|
+
const isUrl = URL_ATTRIBUTE.test(attr)
|
|
150
|
+
stops.push(
|
|
151
|
+
watch(s, (v) => {
|
|
152
|
+
if (v === false || v == null || (isUrl && !SAFE_URL.test(String(v).trim())))
|
|
153
|
+
el.removeAttribute(attr)
|
|
154
|
+
else el.setAttribute(attr, String(v))
|
|
155
|
+
}),
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (const [el, name] of each("value")) {
|
|
162
|
+
const s = named("signal", scope.signals, name)
|
|
163
|
+
if (s) {
|
|
164
|
+
stops.push(
|
|
165
|
+
watch(s, (v) => {
|
|
166
|
+
const next = String(v)
|
|
167
|
+
// Skip same-value writes: resetting an input's value mid-typing moves the caret.
|
|
168
|
+
if (el.value !== next) el.value = next
|
|
169
|
+
}),
|
|
170
|
+
)
|
|
171
|
+
const input = (): void => s.set(el.value ?? "")
|
|
172
|
+
// A <select> commits on `change`, and browsers disagree on whether `input` comes first. The
|
|
173
|
+
// second event of a pair writes the same value, which the signal skips.
|
|
174
|
+
for (const type of ["input", "change"]) {
|
|
175
|
+
el.addEventListener(type, input)
|
|
176
|
+
stops.push(() => el.removeEventListener(type, input))
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const [el, spec] of each("on")) {
|
|
182
|
+
for (const [event, name] of pairs(spec)) {
|
|
183
|
+
const handler = named("handler", scope.handlers, name)
|
|
184
|
+
if (handler === undefined) continue
|
|
185
|
+
el.addEventListener(event, handler)
|
|
186
|
+
stops.push(() => el.removeEventListener(event, handler))
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return stops
|
|
191
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { effect, untrack } from "./island-signals.ts"
|
|
2
|
+
|
|
3
|
+
/** The node surface `bindList` uses; an `Element` has it. */
|
|
4
|
+
export interface ListNode {
|
|
5
|
+
remove(): void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** The container surface `bindList` uses; an `Element` has it. */
|
|
9
|
+
export interface ListContainer<E extends ListNode> {
|
|
10
|
+
replaceChildren(...nodes: E[]): void
|
|
11
|
+
insertBefore(node: E, before: E | null): unknown
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** How `bindList` turns items into keyed DOM. */
|
|
15
|
+
export interface BindListOptions<T, E extends ListNode = HTMLElement> {
|
|
16
|
+
/** Stable and unique per item - never the array index, or a removal reuses the wrong node. */
|
|
17
|
+
key(item: T): string | number
|
|
18
|
+
create(item: T): E
|
|
19
|
+
/** Brings a node in line with its item: runs after `create` and on every later change. */
|
|
20
|
+
update?(el: E, item: T): void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Render a list signal (or any getter) into `container` with keyed reconciliation. A surviving item
|
|
25
|
+
* keeps its node, so focus, selection and input state survive a reorder. A render with no earlier
|
|
26
|
+
* nodes replaces the container's children; later renders move only the nodes outside the longest
|
|
27
|
+
* run already in order. `key`, `create` and `update` run untracked: a signal they read does not
|
|
28
|
+
* re-render the list. Returns the disposer; inside an island the binding also stops on unmount.
|
|
29
|
+
*/
|
|
30
|
+
export function bindList<T, E extends ListNode = HTMLElement>(
|
|
31
|
+
source: () => readonly T[],
|
|
32
|
+
// A DOM container's generic methods would drive E to `ListNode`; the row type comes from `create`.
|
|
33
|
+
container: ListContainer<NoInfer<E>>,
|
|
34
|
+
options: BindListOptions<T, E>,
|
|
35
|
+
): () => void {
|
|
36
|
+
let nodes = new Map<string | number, E>()
|
|
37
|
+
// Each node's position in the last render; empty until a render leaves nodes to keep.
|
|
38
|
+
let at = new Map<E, number>()
|
|
39
|
+
return effect(() => {
|
|
40
|
+
const items = source()
|
|
41
|
+
untrack(() => {
|
|
42
|
+
const next = new Map<string | number, E>()
|
|
43
|
+
for (const item of items) {
|
|
44
|
+
const key = options.key(item)
|
|
45
|
+
if (next.has(key)) console.warn("[nifra/islands] bindList: duplicate key skipped", key)
|
|
46
|
+
else {
|
|
47
|
+
const el = nodes.get(key) ?? options.create(item)
|
|
48
|
+
options.update?.(el, item)
|
|
49
|
+
next.set(key, el)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const els = [...next.values()]
|
|
53
|
+
if (at.size === 0) container.replaceChildren(...els)
|
|
54
|
+
else {
|
|
55
|
+
for (const [key, el] of nodes) if (!next.has(key)) el.remove()
|
|
56
|
+
const stays = inOrder(els.map((el) => at.get(el) ?? -1))
|
|
57
|
+
let anchor: E | null = null
|
|
58
|
+
for (let i = els.length - 1; i >= 0; i--) {
|
|
59
|
+
const el = els[i]
|
|
60
|
+
if (el === undefined) continue
|
|
61
|
+
if (!stays.has(i)) container.insertBefore(el, anchor)
|
|
62
|
+
anchor = el
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
at = new Map(els.map((el, i) => [el, i]))
|
|
66
|
+
nodes = next
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The indexes of `old` (each node's previous position, -1 for a new node) on a longest increasing
|
|
72
|
+
// run: those nodes already sit in order, and every other node is moved.
|
|
73
|
+
function inOrder(old: readonly number[]): Set<number> {
|
|
74
|
+
// tails[n] is the index ending the best run of length n + 1; back links each run.
|
|
75
|
+
const tails: number[] = []
|
|
76
|
+
const back: number[] = []
|
|
77
|
+
old.forEach((o, i) => {
|
|
78
|
+
if (o < 0) return
|
|
79
|
+
let lo = 0
|
|
80
|
+
let hi = tails.length
|
|
81
|
+
while (lo < hi) {
|
|
82
|
+
const mid = (lo + hi) >> 1
|
|
83
|
+
if ((old[tails[mid] ?? 0] ?? 0) < o) lo = mid + 1
|
|
84
|
+
else hi = mid
|
|
85
|
+
}
|
|
86
|
+
back[i] = tails[lo - 1] ?? -1
|
|
87
|
+
tails[lo] = i
|
|
88
|
+
})
|
|
89
|
+
const stays = new Set<number>()
|
|
90
|
+
for (let i = tails.at(-1) ?? -1; i >= 0; i = back[i] ?? -1) stays.add(i)
|
|
91
|
+
return stays
|
|
92
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type IslandStrategy,
|
|
3
|
+
MAX_MEDIA_QUERY_LENGTH,
|
|
4
|
+
scheduleTrigger,
|
|
5
|
+
} from "@nifrajs/island-trigger"
|
|
6
|
+
import type { IslandCleanup, IslandEnhancer } from "../islands.ts"
|
|
7
|
+
import { owned } from "./island-owner.ts"
|
|
8
|
+
|
|
9
|
+
function readProps(el: HTMLElement): unknown {
|
|
10
|
+
const raw = el.dataset.islandProps
|
|
11
|
+
if (raw === undefined || raw === "") return undefined
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(raw)
|
|
14
|
+
} catch {
|
|
15
|
+
return undefined
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Own one host so removing a subtree can stop its islands without disturbing retained siblings. */
|
|
20
|
+
export function mountIsland(
|
|
21
|
+
el: HTMLElement,
|
|
22
|
+
enhancers: Readonly<Record<string, IslandEnhancer<never>>>,
|
|
23
|
+
): IslandCleanup | undefined {
|
|
24
|
+
const id = el.dataset.island
|
|
25
|
+
if (id === undefined || el.dataset.islandMounted !== undefined) return undefined
|
|
26
|
+
const enhancer = Object.hasOwn(enhancers, id) ? enhancers[id] : undefined
|
|
27
|
+
if (enhancer === undefined) return undefined
|
|
28
|
+
const strategyAttr = el.dataset.islandStrategy
|
|
29
|
+
const media = el.dataset.islandMedia
|
|
30
|
+
const strategy: IslandStrategy | undefined =
|
|
31
|
+
strategyAttr === "idle" || strategyAttr === "visible" || strategyAttr === "load"
|
|
32
|
+
? strategyAttr
|
|
33
|
+
: strategyAttr === "media" &&
|
|
34
|
+
media !== undefined &&
|
|
35
|
+
media.length > 0 &&
|
|
36
|
+
media.length <= MAX_MEDIA_QUERY_LENGTH
|
|
37
|
+
? { media }
|
|
38
|
+
: strategyAttr === undefined
|
|
39
|
+
? "load"
|
|
40
|
+
: undefined
|
|
41
|
+
// Invalid markers must stay inert, including untrusted oversized media queries.
|
|
42
|
+
if (strategy === undefined) return undefined
|
|
43
|
+
const disposers: Array<() => void> = []
|
|
44
|
+
el.dataset.islandMounted = ""
|
|
45
|
+
disposers.push(() => {
|
|
46
|
+
delete el.dataset.islandMounted
|
|
47
|
+
})
|
|
48
|
+
const props = readProps(el)
|
|
49
|
+
const run = (): void => {
|
|
50
|
+
try {
|
|
51
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: enhancer props are the authored SSR contract, as in defineIsland
|
|
52
|
+
const cleanup = owned(disposers, () => enhancer(el, props as never))
|
|
53
|
+
if (typeof cleanup === "function") disposers.push(cleanup)
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error(`[nifra/islands] enhancer "${id}" failed:`, err)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
disposers.push(scheduleTrigger(strategy, run, { target: el }))
|
|
59
|
+
return () => {
|
|
60
|
+
for (const dispose of disposers.splice(0)) {
|
|
61
|
+
try {
|
|
62
|
+
dispose()
|
|
63
|
+
} catch {
|
|
64
|
+
// Best-effort teardown: one failing cleanup must not block the rest.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// The disposer list of the island whose enhancer is running, so every effect, computed, list binding
|
|
2
|
+
// and resource it creates stops when the island unmounts. A separate module so an imperative-only
|
|
3
|
+
// island bundle carries no signal code.
|
|
4
|
+
let owner: Array<() => void> | undefined
|
|
5
|
+
|
|
6
|
+
/** Run `fn` with every disposer it registers pushed onto `kids`. */
|
|
7
|
+
export function owned<T>(kids: Array<() => void>, fn: () => T): T {
|
|
8
|
+
const prev = owner
|
|
9
|
+
owner = kids
|
|
10
|
+
try {
|
|
11
|
+
return fn()
|
|
12
|
+
} finally {
|
|
13
|
+
owner = prev
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Register `stop` with the running island, if any. Outside an enhancer it stays the caller's. */
|
|
18
|
+
export function adopt(stop: () => void): void {
|
|
19
|
+
owner?.push(stop)
|
|
20
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { adopt } from "./island-owner.ts"
|
|
2
|
+
import { effect, signal, untrack } from "./island-signals.ts"
|
|
3
|
+
|
|
4
|
+
/** An async value's state. `status` narrows `value` and `error`, so no branch can be skipped. */
|
|
5
|
+
export type ResourceState<T> =
|
|
6
|
+
| { readonly status: "pending"; readonly value: undefined; readonly error: undefined }
|
|
7
|
+
| { readonly status: "ready"; readonly value: T; readonly error: undefined }
|
|
8
|
+
| { readonly status: "error"; readonly value: undefined; readonly error: unknown }
|
|
9
|
+
|
|
10
|
+
/** Call it to read the current state (tracking, like a signal). */
|
|
11
|
+
export type Resource<T> = {
|
|
12
|
+
(): ResourceState<T>
|
|
13
|
+
/** Fetch the current key again; once disposed, nothing. */
|
|
14
|
+
refetch(): void
|
|
15
|
+
/** Stop refetching and abort the request in flight; its result is dropped. */
|
|
16
|
+
dispose(): void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const PENDING = { status: "pending", value: undefined, error: undefined } as const
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* An async value keyed by `source`. `source` runs tracked, so a write to any signal it reads starts a
|
|
23
|
+
* new fetch; `fetcher` runs untracked with that key, so nothing it reads (before or after an `await`)
|
|
24
|
+
* is a dependency. A newer fetch aborts the older one and only the newest result lands. Inside an
|
|
25
|
+
* island, unmounting disposes it: the request in flight is aborted and its late result dropped.
|
|
26
|
+
*/
|
|
27
|
+
export function resource<K, T>(
|
|
28
|
+
source: () => K,
|
|
29
|
+
fetcher: (key: K, abort: AbortSignal) => Promise<T>,
|
|
30
|
+
): Resource<T> {
|
|
31
|
+
const state = signal<ResourceState<T>>(PENDING)
|
|
32
|
+
let generation = 0
|
|
33
|
+
let live = true
|
|
34
|
+
let controller: AbortController | undefined
|
|
35
|
+
let key: K
|
|
36
|
+
const run = (): void => {
|
|
37
|
+
const mine = ++generation
|
|
38
|
+
controller?.abort()
|
|
39
|
+
const active = new AbortController()
|
|
40
|
+
controller = active
|
|
41
|
+
state.set(PENDING)
|
|
42
|
+
let pending: Promise<T>
|
|
43
|
+
try {
|
|
44
|
+
pending = fetcher(key, active.signal)
|
|
45
|
+
} catch (error) {
|
|
46
|
+
pending = Promise.reject(error)
|
|
47
|
+
}
|
|
48
|
+
pending.then(
|
|
49
|
+
(value) => {
|
|
50
|
+
if (mine === generation) state.set({ status: "ready", value, error: undefined })
|
|
51
|
+
},
|
|
52
|
+
(error) => {
|
|
53
|
+
// A superseded or aborted fetch is not an error the reader should see.
|
|
54
|
+
if (mine === generation && !active.signal.aborted)
|
|
55
|
+
state.set({ status: "error", value: undefined, error })
|
|
56
|
+
},
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
const stop = effect(() => {
|
|
60
|
+
key = source()
|
|
61
|
+
untrack(run)
|
|
62
|
+
})
|
|
63
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: `refetch` and `dispose` are assigned below
|
|
64
|
+
const read = (() => state()) as Resource<T>
|
|
65
|
+
read.refetch = () => {
|
|
66
|
+
if (live) untrack(run)
|
|
67
|
+
}
|
|
68
|
+
read.dispose = () => {
|
|
69
|
+
live = false
|
|
70
|
+
stop()
|
|
71
|
+
generation++
|
|
72
|
+
controller?.abort()
|
|
73
|
+
}
|
|
74
|
+
adopt(read.dispose)
|
|
75
|
+
return read
|
|
76
|
+
}
|