@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
package/src/live-hub.ts
ADDED
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live hub for Cloudflare Workers: one Durable Object per app owns the live log and holds every
|
|
3
|
+
* live socket, hibernating between events. Request isolates run live loads themselves: each asks the
|
|
4
|
+
* hub for the seq and the revocations its grants need, publishes through it, and forwards the socket
|
|
5
|
+
* handshake to it.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { liveHub } from "@nifrajs/web/live-hub"
|
|
9
|
+
*
|
|
10
|
+
* // A SQLite-backed Durable Object class, bound to the app's Worker as `LIVE_HUB`.
|
|
11
|
+
* export const LiveHub = liveHub((env: Env) => ({ secret: env.LIVE_SECRET }))
|
|
12
|
+
* // The app: live({ secret: env.LIVE_SECRET, subject, hub: env.LIVE_HUB })
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* What outlives the hub's memory: the seq, the revocation floor and every stored revocation, in
|
|
16
|
+
* storage; each socket's membership (subject, expiry, tags) in storage under its sid; its sid, watch
|
|
17
|
+
* gen and rate counters in its attachment. History does not: after a wake a watermark older than the
|
|
18
|
+
* last seq before it is unknown and reads as stale. The runtime answers heartbeats without waking the
|
|
19
|
+
* hub, and one alarm runs at the earliest deadline: a grant expiry, a first watch, a revocation's
|
|
20
|
+
* retention.
|
|
21
|
+
*/
|
|
22
|
+
import type { NifraWebSocket, WebSocketData } from "@nifrajs/core/server"
|
|
23
|
+
import { type LiveBusInput, nextSeq } from "./live/bus.ts"
|
|
24
|
+
import { applyEvent, type LiveCore } from "./live/core.ts"
|
|
25
|
+
import { History, keyHasher } from "./live/history.ts"
|
|
26
|
+
import {
|
|
27
|
+
CLOSE_CAPACITY,
|
|
28
|
+
FIRST_WATCH_MS,
|
|
29
|
+
type HubStore,
|
|
30
|
+
LiveHub,
|
|
31
|
+
type LiveSocket,
|
|
32
|
+
type StoredSocket,
|
|
33
|
+
topicOf,
|
|
34
|
+
} from "./live/hub.ts"
|
|
35
|
+
import {
|
|
36
|
+
HUB_APPEND_PATH,
|
|
37
|
+
HUB_AUTHORITY_PATH,
|
|
38
|
+
HUB_CONFIG_HEADER,
|
|
39
|
+
HUB_ORIGIN,
|
|
40
|
+
hubConfigMatches,
|
|
41
|
+
MAX_SUBJECT_CHARS,
|
|
42
|
+
parseAppend,
|
|
43
|
+
parseAuthorityRequest,
|
|
44
|
+
} from "./live/hub-protocol.ts"
|
|
45
|
+
import {
|
|
46
|
+
assertLiveSecrets,
|
|
47
|
+
createKeyring,
|
|
48
|
+
KEY_PATTERN,
|
|
49
|
+
type LiveSecrets,
|
|
50
|
+
SID_PATTERN,
|
|
51
|
+
} from "./live/keyring.ts"
|
|
52
|
+
import { readCapped } from "./live/read-capped.ts"
|
|
53
|
+
import { REVOCATION_SKEW_MS, Revocations } from "./live/revocations.ts"
|
|
54
|
+
import {
|
|
55
|
+
appIdOf,
|
|
56
|
+
DEFAULT_RELOAD_RATE,
|
|
57
|
+
grantTtlOf,
|
|
58
|
+
positive,
|
|
59
|
+
spreadFloorOf,
|
|
60
|
+
} from "./live/settings.ts"
|
|
61
|
+
|
|
62
|
+
export interface LiveHubOptions {
|
|
63
|
+
/** The app's `live()` secret, or its rotation list. */
|
|
64
|
+
readonly secret: string | readonly string[]
|
|
65
|
+
/** The app's `live()` `appId`. Default `"nifra"`. */
|
|
66
|
+
readonly appId?: string | undefined
|
|
67
|
+
/** The app's `live()` `grantTtl`, at most 3600. Default 600. */
|
|
68
|
+
readonly grantTtl?: number | undefined
|
|
69
|
+
/** Origins besides the app's own that may open the socket. */
|
|
70
|
+
readonly trustedOrigins?: readonly string[] | undefined
|
|
71
|
+
/** Sockets this hub holds; past it a socket is closed with 1013. Default {@link DEFAULT_HUB_SOCKETS}. */
|
|
72
|
+
readonly maxSockets?: number | undefined
|
|
73
|
+
/**
|
|
74
|
+
* Socket-tag subscriptions this hub holds in all; past it a watch is refused with 1013. The default,
|
|
75
|
+
* {@link DEFAULT_HUB_MEMBERSHIPS}, keeps the hub's heap well inside a Durable Object's memory.
|
|
76
|
+
*/
|
|
77
|
+
readonly maxMemberships?: number | undefined
|
|
78
|
+
/** Live loads per second the deployment absorbs after one change, for spreading reloads. Default 500. */
|
|
79
|
+
readonly reloadRate?: number | undefined
|
|
80
|
+
/** The least time, in ms, clients spread their reloads over. Default 0. */
|
|
81
|
+
readonly spreadFloor?: number | undefined
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The part of a Durable Object's state the hub uses (no Cloudflare types dependency). */
|
|
85
|
+
export interface LiveHubState {
|
|
86
|
+
readonly storage: LiveHubStorage
|
|
87
|
+
blockConcurrencyWhile<T>(callback: () => Promise<T>): Promise<T>
|
|
88
|
+
acceptWebSocket(ws: LiveHubWebSocket): void
|
|
89
|
+
getWebSockets(): LiveHubWebSocket[]
|
|
90
|
+
setWebSocketAutoResponse(pair?: unknown): void
|
|
91
|
+
getWebSocketAutoResponseTimestamp(ws: LiveHubWebSocket): Date | null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface LiveHubStorage {
|
|
95
|
+
get(keys: string[]): Promise<Map<string, unknown>>
|
|
96
|
+
put(entries: Record<string, unknown>): Promise<void>
|
|
97
|
+
delete(keys: string[]): Promise<number>
|
|
98
|
+
list(options: { prefix: string; end?: string; limit?: number }): Promise<Map<string, unknown>>
|
|
99
|
+
getAlarm(): Promise<number | null>
|
|
100
|
+
setAlarm(scheduledTime: number): Promise<void>
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A hibernatable server socket, as far as the hub uses one. */
|
|
104
|
+
export interface LiveHubWebSocket {
|
|
105
|
+
send(message: string | ArrayBuffer | ArrayBufferView): void
|
|
106
|
+
close(code?: number, reason?: string): void
|
|
107
|
+
readonly readyState: number
|
|
108
|
+
serializeAttachment(value: unknown): void
|
|
109
|
+
deserializeAttachment(): unknown
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Per-socket limits the platform sets; the hub stays well under both (see the tests). */
|
|
113
|
+
export const MAX_ATTACHMENT_BYTES = 512
|
|
114
|
+
export const MAX_SOCKET_RECORD_BYTES = 30_720
|
|
115
|
+
/** The most sockets one hub was verified to hold, with every publish reaching them all well under 200 ms. */
|
|
116
|
+
export const DEFAULT_HUB_SOCKETS = 5_000
|
|
117
|
+
/**
|
|
118
|
+
* About 440 bytes of heap each when no two sockets share a tag, the worst case; far less when they do.
|
|
119
|
+
* 150,000 of them stay near 66 MB, inside the 128 MB a Durable Object's isolate may use.
|
|
120
|
+
*/
|
|
121
|
+
export const DEFAULT_HUB_MEMBERSHIPS = 150_000
|
|
122
|
+
/** Alarms come no closer together than this: expiry is checked again before every frame anyway. */
|
|
123
|
+
export const ALARM_GAP_MS = 30_000
|
|
124
|
+
|
|
125
|
+
const SEQ_KEY = "seq"
|
|
126
|
+
const APPLIED_KEY = "applied"
|
|
127
|
+
const FLOOR_KEY = "floor"
|
|
128
|
+
const REVOKED = "r:"
|
|
129
|
+
const SOCKET = "s:"
|
|
130
|
+
const DELETE_BATCH = 128
|
|
131
|
+
const MAX_CALL_BYTES = 65_536
|
|
132
|
+
const HEARTBEAT = "p"
|
|
133
|
+
|
|
134
|
+
interface Settings {
|
|
135
|
+
readonly secrets: LiveSecrets
|
|
136
|
+
readonly appId: string
|
|
137
|
+
readonly ttl: number
|
|
138
|
+
readonly trusted: ReadonlySet<string>
|
|
139
|
+
readonly maxSockets: number
|
|
140
|
+
readonly maxMemberships: number
|
|
141
|
+
readonly reloadRate: number
|
|
142
|
+
readonly spreadFloor: number
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The live hub's Durable Object class. Export it from the Worker's main module under the class name
|
|
147
|
+
* the binding names.
|
|
148
|
+
*/
|
|
149
|
+
export function liveHub<Env = Record<string, string | undefined>>(
|
|
150
|
+
options: LiveHubOptions | ((env: Env) => LiveHubOptions),
|
|
151
|
+
): new (
|
|
152
|
+
state: LiveHubState,
|
|
153
|
+
env: Env,
|
|
154
|
+
) => LiveHubObject {
|
|
155
|
+
return class extends LiveHubObject {
|
|
156
|
+
constructor(state: LiveHubState, env: Env) {
|
|
157
|
+
super(state, typeof options === "function" ? options(env) : options)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export class LiveHubObject implements HubStore {
|
|
163
|
+
private readonly ready: Promise<LiveCore>
|
|
164
|
+
private readonly settings: Settings
|
|
165
|
+
private readonly sockets = new WeakMap<LiveHubWebSocket, HubSocket>()
|
|
166
|
+
private readonly topics = new Map<string, Set<HubSocket>>()
|
|
167
|
+
private readonly hash = keyHasher()
|
|
168
|
+
private core: LiveCore | undefined
|
|
169
|
+
/** The last seq handed out, stored before its event is applied. */
|
|
170
|
+
private seq = 0
|
|
171
|
+
/** The last seq applied: a load's watermark. */
|
|
172
|
+
private applied = 0
|
|
173
|
+
/** The stored revocation floor. */
|
|
174
|
+
private floor = 0
|
|
175
|
+
/** Appends run one at a time, so history is recorded in seq order. */
|
|
176
|
+
private chain: Promise<unknown> = Promise.resolve()
|
|
177
|
+
private alarmAt: number | undefined
|
|
178
|
+
private mismatchLogged = false
|
|
179
|
+
|
|
180
|
+
constructor(
|
|
181
|
+
private readonly state: LiveHubState,
|
|
182
|
+
options: LiveHubOptions,
|
|
183
|
+
) {
|
|
184
|
+
this.settings = settingsOf(options)
|
|
185
|
+
this.ready = state.blockConcurrencyWhile(() => this.wake())
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async fetch(req: Request): Promise<Response> {
|
|
189
|
+
const core = await this.ready
|
|
190
|
+
if (req.method === "GET" && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
191
|
+
return this.accept(core, req)
|
|
192
|
+
}
|
|
193
|
+
const url = new URL(req.url)
|
|
194
|
+
const call = url.pathname
|
|
195
|
+
if (
|
|
196
|
+
req.method !== "POST" ||
|
|
197
|
+
url.origin !== HUB_ORIGIN ||
|
|
198
|
+
(call !== HUB_APPEND_PATH && call !== HUB_AUTHORITY_PATH)
|
|
199
|
+
) {
|
|
200
|
+
return Response.json({ error: "not_found" }, { status: 404 })
|
|
201
|
+
}
|
|
202
|
+
if (!hubConfigMatches(core.ring, req.headers.get(HUB_CONFIG_HEADER))) {
|
|
203
|
+
if (!this.mismatchLogged) {
|
|
204
|
+
this.mismatchLogged = true
|
|
205
|
+
console.error(
|
|
206
|
+
"[nifra/web] live hub: an app called with a different `secret`, `appId` or `grantTtl` than this hub's; every call it makes is refused",
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
return Response.json({ error: "config_mismatch" }, { status: 409 })
|
|
210
|
+
}
|
|
211
|
+
const body = await bodyOf(req)
|
|
212
|
+
if (call === HUB_APPEND_PATH) {
|
|
213
|
+
const input = parseAppend(body)
|
|
214
|
+
if (input === undefined) return Response.json({ error: "bad_request" }, { status: 400 })
|
|
215
|
+
return Response.json({ seq: await this.append(core, input) })
|
|
216
|
+
}
|
|
217
|
+
const ask = parseAuthorityRequest(body)
|
|
218
|
+
if (ask === undefined) return Response.json({ error: "bad_request" }, { status: 400 })
|
|
219
|
+
return Response.json({
|
|
220
|
+
seq: this.applied,
|
|
221
|
+
...core.revocations.snapshot(ask.subjects, ask.keys),
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async webSocketMessage(raw: LiveHubWebSocket, message: string | ArrayBuffer): Promise<void> {
|
|
226
|
+
const core = await this.ready
|
|
227
|
+
const socket = this.sockets.get(raw)
|
|
228
|
+
if (socket === undefined) return
|
|
229
|
+
const data: WebSocketData = typeof message === "string" ? message : new Uint8Array(message)
|
|
230
|
+
const installing = core.hub.message(socket, data)
|
|
231
|
+
saveAttachment(socket)
|
|
232
|
+
if (installing === undefined) return
|
|
233
|
+
await installing
|
|
234
|
+
// The watch moved its expiry and whether it holds a tag.
|
|
235
|
+
saveAttachment(socket)
|
|
236
|
+
await this.schedule(
|
|
237
|
+
Math.min(
|
|
238
|
+
socket.data.exp,
|
|
239
|
+
(socket.data.unwatchedSince ?? Number.POSITIVE_INFINITY) + FIRST_WATCH_MS,
|
|
240
|
+
),
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async webSocketClose(raw: LiveHubWebSocket): Promise<void> {
|
|
245
|
+
await this.left(raw)
|
|
246
|
+
// Complete the closing handshake; a runtime that already replied throws here.
|
|
247
|
+
closeQuietly(raw, 1000, "")
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async webSocketError(raw: LiveHubWebSocket): Promise<void> {
|
|
251
|
+
await this.left(raw)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async alarm(): Promise<void> {
|
|
255
|
+
const core = await this.ready
|
|
256
|
+
this.alarmAt = undefined
|
|
257
|
+
const sockets = core.hub.sweep()
|
|
258
|
+
const retention = await this.pruneRevocations(Date.now())
|
|
259
|
+
await this.schedule(Math.min(sockets, retention))
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// --- HubStore: what the shared hub tells this host to keep --------------------------------------
|
|
263
|
+
|
|
264
|
+
saved(ws: NifraWebSocket<LiveSocket>, socket: StoredSocket): void {
|
|
265
|
+
void this.state.storage.put({ [SOCKET + ws.data.sid]: socket }).catch(storageFailed)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
removed(ws: NifraWebSocket<LiveSocket>): void {
|
|
269
|
+
void this.state.storage.delete([SOCKET + ws.data.sid]).catch(storageFailed)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
lastBeat(ws: NifraWebSocket<LiveSocket>): number {
|
|
273
|
+
if (!(ws instanceof HubSocket)) return 0
|
|
274
|
+
return this.state.getWebSocketAutoResponseTimestamp(ws.raw)?.getTime() ?? 0
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// --- internals ----------------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
/** Everything the hub knows comes back from storage and the runtime's sockets; history does not. */
|
|
280
|
+
private async wake(): Promise<LiveCore> {
|
|
281
|
+
const { state, settings } = this
|
|
282
|
+
const storage = state.storage
|
|
283
|
+
const ring = await createKeyring({
|
|
284
|
+
secrets: settings.secrets,
|
|
285
|
+
appId: settings.appId,
|
|
286
|
+
ttl: settings.ttl,
|
|
287
|
+
})
|
|
288
|
+
const head = await storage.get([SEQ_KEY, APPLIED_KEY, FLOOR_KEY])
|
|
289
|
+
const seq = head.get(SEQ_KEY)
|
|
290
|
+
if (seqLike(seq)) {
|
|
291
|
+
this.seq = seq
|
|
292
|
+
const floor = head.get(FLOOR_KEY)
|
|
293
|
+
this.floor = seqLike(floor) ? floor : seq
|
|
294
|
+
} else {
|
|
295
|
+
// A new hub, or one whose storage is gone: no grant issued before now is honored.
|
|
296
|
+
this.seq = nextSeq(0, Date.now())
|
|
297
|
+
this.floor = this.seq
|
|
298
|
+
await storage.put({ [SEQ_KEY]: this.seq, [APPLIED_KEY]: this.seq, [FLOOR_KEY]: this.floor })
|
|
299
|
+
}
|
|
300
|
+
// An event stored but never applied may never have reached the sockets that watch it.
|
|
301
|
+
const lost = head.get(APPLIED_KEY) !== this.seq && seqLike(seq)
|
|
302
|
+
this.applied = this.seq
|
|
303
|
+
|
|
304
|
+
const revocations = new Revocations(this.floor, settings.ttl * 1000)
|
|
305
|
+
for (const [name, value] of await storage.list({ prefix: REVOKED })) {
|
|
306
|
+
const entry = revocationOf(name, value)
|
|
307
|
+
if (entry !== undefined && entry.seq >= this.floor) {
|
|
308
|
+
revocations.apply(
|
|
309
|
+
{ type: "revoke", sub: entry.sub, key: entry.key, seq: entry.seq },
|
|
310
|
+
entry.at,
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const history = new History(this.seq)
|
|
315
|
+
const hub = new LiveHub(
|
|
316
|
+
ring,
|
|
317
|
+
history,
|
|
318
|
+
revocations,
|
|
319
|
+
{
|
|
320
|
+
appId: settings.appId,
|
|
321
|
+
trusted: settings.trusted,
|
|
322
|
+
maxSockets: settings.maxSockets,
|
|
323
|
+
maxMemberships: settings.maxMemberships,
|
|
324
|
+
maxRssBytes: undefined,
|
|
325
|
+
reloadRate: settings.reloadRate,
|
|
326
|
+
instances: 1,
|
|
327
|
+
spreadFloor: settings.spreadFloor,
|
|
328
|
+
hash: this.hash,
|
|
329
|
+
store: this,
|
|
330
|
+
},
|
|
331
|
+
new Set([(topic: string, frame: string) => this.deliver(topic, frame)]),
|
|
332
|
+
)
|
|
333
|
+
const core: LiveCore = { ring, history, revocations, hub }
|
|
334
|
+
this.core = core
|
|
335
|
+
|
|
336
|
+
const records = await storage.list({ prefix: SOCKET })
|
|
337
|
+
const kept = new Set<string>()
|
|
338
|
+
let resynced = 0
|
|
339
|
+
// Each record is its own copy of every tag string: one copy per tag serves every socket.
|
|
340
|
+
const interned = new Map<string, string>()
|
|
341
|
+
for (const raw of state.getWebSockets()) {
|
|
342
|
+
const data = socketStateOf(raw.deserializeAttachment())
|
|
343
|
+
const stored =
|
|
344
|
+
data === undefined ? undefined : storedOf(records.get(SOCKET + data.sid), interned)
|
|
345
|
+
// A socket that watched but lost its membership would miss every change: it reconnects.
|
|
346
|
+
if (lost || data === undefined || (data.received >= 0 && stored === undefined)) {
|
|
347
|
+
closeQuietly(raw, CLOSE_CAPACITY, "resync")
|
|
348
|
+
resynced++
|
|
349
|
+
continue
|
|
350
|
+
}
|
|
351
|
+
const socket = new HubSocket(raw, data, this.topics)
|
|
352
|
+
this.sockets.set(raw, socket)
|
|
353
|
+
hub.restore(socket, stored)
|
|
354
|
+
kept.add(SOCKET + data.sid)
|
|
355
|
+
}
|
|
356
|
+
const orphans = [...records.keys()].filter((name) => !kept.has(name))
|
|
357
|
+
for (let at = 0; at < orphans.length; at += DELETE_BATCH) {
|
|
358
|
+
await storage.delete(orphans.slice(at, at + DELETE_BATCH))
|
|
359
|
+
}
|
|
360
|
+
if (lost) await storage.put({ [APPLIED_KEY]: this.seq })
|
|
361
|
+
hub.burst(resynced)
|
|
362
|
+
|
|
363
|
+
const Pair: unknown = Reflect.get(globalThis, "WebSocketRequestResponsePair")
|
|
364
|
+
if (typeof Pair === "function") {
|
|
365
|
+
state.setWebSocketAutoResponse(Reflect.construct(Pair, [HEARTBEAT, HEARTBEAT]))
|
|
366
|
+
}
|
|
367
|
+
this.alarmAt = (await storage.getAlarm()) ?? undefined
|
|
368
|
+
return core
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private async accept(core: LiveCore, req: Request): Promise<Response> {
|
|
372
|
+
const data = core.hub.upgrade(req)
|
|
373
|
+
if (data instanceof Response) return data
|
|
374
|
+
const pair = socketPair()
|
|
375
|
+
if (pair === undefined) return Response.json({ error: "unavailable" }, { status: 503 })
|
|
376
|
+
const [client, server] = pair
|
|
377
|
+
this.state.acceptWebSocket(server)
|
|
378
|
+
const socket = new HubSocket(server, data, this.topics)
|
|
379
|
+
this.sockets.set(server, socket)
|
|
380
|
+
saveAttachment(socket)
|
|
381
|
+
core.hub.open(socket)
|
|
382
|
+
await this.schedule(Date.now() + FIRST_WATCH_MS)
|
|
383
|
+
const init = { status: 101, webSocket: client }
|
|
384
|
+
return new Response(null, init)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private append(core: LiveCore, input: LiveBusInput): Promise<number> {
|
|
388
|
+
const run = this.chain.then(() => this.applyNext(core, input))
|
|
389
|
+
this.chain = run.catch(() => {})
|
|
390
|
+
return run
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private async applyNext(core: LiveCore, input: LiveBusInput): Promise<number> {
|
|
394
|
+
const storage = this.state.storage
|
|
395
|
+
const now = Date.now()
|
|
396
|
+
const seq = nextSeq(this.seq, now)
|
|
397
|
+
this.seq = seq
|
|
398
|
+
const kept = input.type === "revoke" && (input.sub !== null || input.key !== null)
|
|
399
|
+
const writes: Record<string, unknown> = { [SEQ_KEY]: seq }
|
|
400
|
+
if (kept) writes[revokedKey(seq)] = { sub: input.sub, key: input.key, at: now }
|
|
401
|
+
// Stored before any socket hears of it: a revocation is never applied and then forgotten.
|
|
402
|
+
await storage.put(writes)
|
|
403
|
+
try {
|
|
404
|
+
await applyEvent(core, { ...input, seq }, this.hash)
|
|
405
|
+
} catch (err) {
|
|
406
|
+
console.error(
|
|
407
|
+
"[nifra/web] live hub: an event could not be applied; closing live sockets",
|
|
408
|
+
err,
|
|
409
|
+
)
|
|
410
|
+
core.history.reset(seq)
|
|
411
|
+
core.revocations.raise(seq)
|
|
412
|
+
core.hub.closeAll(CLOSE_CAPACITY, "resync")
|
|
413
|
+
}
|
|
414
|
+
this.applied = seq
|
|
415
|
+
const after: Record<string, unknown> = { [APPLIED_KEY]: seq }
|
|
416
|
+
const raised = core.revocations.floor > this.floor
|
|
417
|
+
if (raised) {
|
|
418
|
+
this.floor = core.revocations.floor
|
|
419
|
+
after[FLOOR_KEY] = this.floor
|
|
420
|
+
}
|
|
421
|
+
await storage.put(after)
|
|
422
|
+
if (kept) await this.schedule(now + this.settings.ttl * 1000 + REVOCATION_SKEW_MS)
|
|
423
|
+
else if (raised) await this.schedule(now)
|
|
424
|
+
return seq
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** One frame to every socket on the topic whose grants are still valid. */
|
|
428
|
+
private deliver(topic: string, frame: string): void {
|
|
429
|
+
const members = this.topics.get(topic)
|
|
430
|
+
const hub = this.core?.hub
|
|
431
|
+
if (members === undefined || hub === undefined) return
|
|
432
|
+
const now = Date.now()
|
|
433
|
+
for (const socket of members) if (!hub.expired(socket, now)) socket.send(frame)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private async left(raw: LiveHubWebSocket): Promise<void> {
|
|
437
|
+
const core = await this.ready
|
|
438
|
+
const socket = this.sockets.get(raw)
|
|
439
|
+
if (socket === undefined) return
|
|
440
|
+
this.sockets.delete(raw)
|
|
441
|
+
// The hub leaves a closed socket's topics without unsubscribing it; this registry is the host's.
|
|
442
|
+
for (const tag of socket.data.tags) socket.unsubscribe(topicOf(this.settings.appId, tag))
|
|
443
|
+
core.hub.close(socket)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Drop revocations every grant they could refuse has outlived. Returns the next one's deadline. */
|
|
447
|
+
private async pruneRevocations(now: number): Promise<number> {
|
|
448
|
+
const storage = this.state.storage
|
|
449
|
+
const ttlMs = this.settings.ttl * 1000
|
|
450
|
+
const horizon = (now - ttlMs - REVOCATION_SKEW_MS) * 1000
|
|
451
|
+
const end = revokedKey(Math.max(this.floor, Math.floor(horizon)))
|
|
452
|
+
for (;;) {
|
|
453
|
+
const stale = [...(await storage.list({ prefix: REVOKED, end, limit: DELETE_BATCH })).keys()]
|
|
454
|
+
if (stale.length === 0) break
|
|
455
|
+
await storage.delete(stale)
|
|
456
|
+
}
|
|
457
|
+
for (const [name, value] of await storage.list({ prefix: REVOKED, limit: 1 })) {
|
|
458
|
+
const entry = revocationOf(name, value)
|
|
459
|
+
if (entry !== undefined) return entry.at + ttlMs + REVOCATION_SKEW_MS
|
|
460
|
+
}
|
|
461
|
+
return Number.POSITIVE_INFINITY
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
private async schedule(deadline: number): Promise<void> {
|
|
465
|
+
if (!Number.isFinite(deadline)) return
|
|
466
|
+
const at = Math.max(deadline, Date.now() + ALARM_GAP_MS)
|
|
467
|
+
if (this.alarmAt !== undefined && this.alarmAt <= at) return
|
|
468
|
+
this.alarmAt = at
|
|
469
|
+
await this.state.storage.setAlarm(at)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/** A hibernatable socket under the hub's own topic registry. */
|
|
474
|
+
class HubSocket implements NifraWebSocket<LiveSocket> {
|
|
475
|
+
constructor(
|
|
476
|
+
readonly raw: LiveHubWebSocket,
|
|
477
|
+
public data: LiveSocket,
|
|
478
|
+
private readonly topics: Map<string, Set<HubSocket>>,
|
|
479
|
+
) {}
|
|
480
|
+
|
|
481
|
+
get readyState(): number {
|
|
482
|
+
return this.raw.readyState
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
send(data: string | ArrayBufferView | ArrayBuffer): void {
|
|
486
|
+
try {
|
|
487
|
+
this.raw.send(data)
|
|
488
|
+
} catch {
|
|
489
|
+
// Closed meanwhile: its close event settles the rest.
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
close(code?: number, reason?: string): void {
|
|
494
|
+
closeQuietly(this.raw, code, reason)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
subscribe(topic: string): void {
|
|
498
|
+
let members = this.topics.get(topic)
|
|
499
|
+
if (members === undefined) {
|
|
500
|
+
members = new Set()
|
|
501
|
+
this.topics.set(topic, members)
|
|
502
|
+
}
|
|
503
|
+
members.add(this)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
unsubscribe(topic: string): void {
|
|
507
|
+
const members = this.topics.get(topic)
|
|
508
|
+
if (members === undefined || !members.delete(this)) return
|
|
509
|
+
if (members.size === 0) this.topics.delete(topic)
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function settingsOf(options: LiveHubOptions): Settings {
|
|
514
|
+
const secret = options.secret
|
|
515
|
+
const secrets: readonly unknown[] = typeof secret === "string" ? [secret] : (secret ?? [])
|
|
516
|
+
assertLiveSecrets(secrets)
|
|
517
|
+
return {
|
|
518
|
+
secrets,
|
|
519
|
+
appId: appIdOf(options.appId),
|
|
520
|
+
ttl: grantTtlOf(options.grantTtl),
|
|
521
|
+
trusted: new Set(options.trustedOrigins ?? []),
|
|
522
|
+
maxSockets: positive(options.maxSockets, DEFAULT_HUB_SOCKETS, "maxSockets"),
|
|
523
|
+
maxMemberships: positive(options.maxMemberships, DEFAULT_HUB_MEMBERSHIPS, "maxMemberships"),
|
|
524
|
+
reloadRate: positive(options.reloadRate, DEFAULT_RELOAD_RATE, "reloadRate"),
|
|
525
|
+
spreadFloor: spreadFloorOf(options.spreadFloor),
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Sid, gen, rate counters and since when it held no tag (-1 while it holds one): what a socket needs
|
|
531
|
+
* back after a wake that its record does not hold.
|
|
532
|
+
*/
|
|
533
|
+
function saveAttachment(socket: HubSocket): void {
|
|
534
|
+
const s = socket.data
|
|
535
|
+
try {
|
|
536
|
+
socket.raw.serializeAttachment([
|
|
537
|
+
s.sid,
|
|
538
|
+
s.received,
|
|
539
|
+
s.window,
|
|
540
|
+
s.frames,
|
|
541
|
+
s.watches,
|
|
542
|
+
s.lastSeen,
|
|
543
|
+
s.unwatchedSince ?? -1,
|
|
544
|
+
])
|
|
545
|
+
} catch {
|
|
546
|
+
// Closed meanwhile: nothing comes back for it.
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function socketStateOf(value: unknown): LiveSocket | undefined {
|
|
551
|
+
if (!Array.isArray(value) || value.length !== 7) return undefined
|
|
552
|
+
const [sid, received, window, frames, watches, lastSeen, unwatchedSince] = value
|
|
553
|
+
if (typeof sid !== "string" || !SID_PATTERN.test(sid)) return undefined
|
|
554
|
+
if (!integer(received) || received < -1) return undefined
|
|
555
|
+
if (!integer(unwatchedSince) || unwatchedSince < -1) return undefined
|
|
556
|
+
if (!seqLike(window) || !seqLike(frames) || !seqLike(watches) || !seqLike(lastSeen)) {
|
|
557
|
+
return undefined
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
sid,
|
|
561
|
+
sub: undefined,
|
|
562
|
+
received,
|
|
563
|
+
tags: new Set(),
|
|
564
|
+
exp: Number.POSITIVE_INFINITY,
|
|
565
|
+
unwatchedSince: unwatchedSince === -1 ? undefined : unwatchedSince,
|
|
566
|
+
window,
|
|
567
|
+
frames,
|
|
568
|
+
watches,
|
|
569
|
+
lastSeen,
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function storedOf(value: unknown, interned: Map<string, string>): StoredSocket | undefined {
|
|
574
|
+
if (!isRecord(value)) return undefined
|
|
575
|
+
const { sub, exp, tags } = value
|
|
576
|
+
if (sub !== null && !subjectLike(sub)) return undefined
|
|
577
|
+
if (typeof exp !== "number" || Number.isNaN(exp) || !Array.isArray(tags)) return undefined
|
|
578
|
+
const list: string[] = []
|
|
579
|
+
for (const tag of tags) {
|
|
580
|
+
if (typeof tag !== "string") return undefined
|
|
581
|
+
const known = interned.get(tag)
|
|
582
|
+
if (known === undefined) interned.set(tag, tag)
|
|
583
|
+
list.push(known ?? tag)
|
|
584
|
+
}
|
|
585
|
+
return { sub, exp, tags: list }
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
interface StoredRevocation {
|
|
589
|
+
readonly seq: number
|
|
590
|
+
readonly sub: string | null
|
|
591
|
+
readonly key: string | null
|
|
592
|
+
readonly at: number
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function revocationOf(name: string, value: unknown): StoredRevocation | undefined {
|
|
596
|
+
const seq = Number(name.slice(REVOKED.length))
|
|
597
|
+
if (!seqLike(seq) || !isRecord(value)) return undefined
|
|
598
|
+
const { sub, key, at } = value
|
|
599
|
+
if (sub !== null && !subjectLike(sub)) return undefined
|
|
600
|
+
if (key !== null && (typeof key !== "string" || !KEY_PATTERN.test(key))) return undefined
|
|
601
|
+
if (!seqLike(at)) return undefined
|
|
602
|
+
return { seq, sub, key, at }
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/** Fixed width, so storage order is seq order. */
|
|
606
|
+
const revokedKey = (seq: number): string => REVOKED + String(seq).padStart(16, "0")
|
|
607
|
+
|
|
608
|
+
function socketPair(): [unknown, LiveHubWebSocket] | undefined {
|
|
609
|
+
const Pair: unknown = Reflect.get(globalThis, "WebSocketPair")
|
|
610
|
+
if (typeof Pair !== "function") return undefined
|
|
611
|
+
const pair: unknown = Reflect.construct(Pair, [])
|
|
612
|
+
if (!isRecord(pair)) return undefined
|
|
613
|
+
const server = pair[1]
|
|
614
|
+
return isHubWebSocket(server) ? [pair[0], server] : undefined
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function isHubWebSocket(value: unknown): value is LiveHubWebSocket {
|
|
618
|
+
return (
|
|
619
|
+
isRecord(value) &&
|
|
620
|
+
typeof value.send === "function" &&
|
|
621
|
+
typeof value.close === "function" &&
|
|
622
|
+
typeof value.serializeAttachment === "function" &&
|
|
623
|
+
typeof value.deserializeAttachment === "function"
|
|
624
|
+
)
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function closeQuietly(raw: LiveHubWebSocket, code?: number, reason?: string): void {
|
|
628
|
+
try {
|
|
629
|
+
raw.close(code, reason)
|
|
630
|
+
} catch {
|
|
631
|
+
// Already closed.
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
async function bodyOf(req: Request): Promise<unknown> {
|
|
636
|
+
const text = await readCapped(req, MAX_CALL_BYTES)
|
|
637
|
+
if (typeof text === "number") return undefined
|
|
638
|
+
try {
|
|
639
|
+
return JSON.parse(text)
|
|
640
|
+
} catch {
|
|
641
|
+
return undefined
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function storageFailed(err: unknown): void {
|
|
646
|
+
console.error("[nifra/web] live hub: a storage write failed", err)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const integer = (value: unknown): value is number =>
|
|
650
|
+
typeof value === "number" && Number.isSafeInteger(value)
|
|
651
|
+
|
|
652
|
+
const seqLike = (value: unknown): value is number => integer(value) && value >= 0
|
|
653
|
+
|
|
654
|
+
const subjectLike = (value: unknown): value is string =>
|
|
655
|
+
typeof value === "string" && value.length <= MAX_SUBJECT_CHARS
|
|
656
|
+
|
|
657
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
658
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|