@mostra/protocol 0.0.0-stage → 0.1.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/LICENSE +202 -0
- package/README.md +38 -2
- package/dist/actions.d.ts +273 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +154 -0
- package/dist/actions.js.map +1 -0
- package/dist/artifact.d.ts +113 -0
- package/dist/artifact.d.ts.map +1 -0
- package/dist/artifact.js +79 -0
- package/dist/artifact.js.map +1 -0
- package/dist/bridge.d.ts +295 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +343 -0
- package/dist/bridge.js.map +1 -0
- package/dist/browser.d.ts +195 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +156 -0
- package/dist/browser.js.map +1 -0
- package/dist/bundle.d.ts +155 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/bundle.js +130 -0
- package/dist/bundle.js.map +1 -0
- package/dist/canonical.d.ts +23 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +45 -0
- package/dist/canonical.js.map +1 -0
- package/dist/capability.d.ts +38 -0
- package/dist/capability.d.ts.map +1 -0
- package/dist/capability.js +61 -0
- package/dist/capability.js.map +1 -0
- package/dist/diagnostics/codes/a11y.d.ts +8 -0
- package/dist/diagnostics/codes/a11y.d.ts.map +1 -0
- package/dist/diagnostics/codes/a11y.js +30 -0
- package/dist/diagnostics/codes/a11y.js.map +1 -0
- package/dist/diagnostics/codes/action.d.ts +8 -0
- package/dist/diagnostics/codes/action.d.ts.map +1 -0
- package/dist/diagnostics/codes/action.js +110 -0
- package/dist/diagnostics/codes/action.js.map +1 -0
- package/dist/diagnostics/codes/artifact.d.ts +8 -0
- package/dist/diagnostics/codes/artifact.d.ts.map +1 -0
- package/dist/diagnostics/codes/artifact.js +121 -0
- package/dist/diagnostics/codes/artifact.js.map +1 -0
- package/dist/diagnostics/codes/build.d.ts +8 -0
- package/dist/diagnostics/codes/build.d.ts.map +1 -0
- package/dist/diagnostics/codes/build.js +55 -0
- package/dist/diagnostics/codes/build.js.map +1 -0
- package/dist/diagnostics/codes/cli.d.ts +8 -0
- package/dist/diagnostics/codes/cli.d.ts.map +1 -0
- package/dist/diagnostics/codes/cli.js +44 -0
- package/dist/diagnostics/codes/cli.js.map +1 -0
- package/dist/diagnostics/codes/console.d.ts +8 -0
- package/dist/diagnostics/codes/console.d.ts.map +1 -0
- package/dist/diagnostics/codes/console.js +30 -0
- package/dist/diagnostics/codes/console.js.map +1 -0
- package/dist/diagnostics/codes/dep.d.ts +8 -0
- package/dist/diagnostics/codes/dep.d.ts.map +1 -0
- package/dist/diagnostics/codes/dep.js +88 -0
- package/dist/diagnostics/codes/dep.js.map +1 -0
- package/dist/diagnostics/codes/env.d.ts +8 -0
- package/dist/diagnostics/codes/env.d.ts.map +1 -0
- package/dist/diagnostics/codes/env.js +132 -0
- package/dist/diagnostics/codes/env.js.map +1 -0
- package/dist/diagnostics/codes/field.d.ts +8 -0
- package/dist/diagnostics/codes/field.d.ts.map +1 -0
- package/dist/diagnostics/codes/field.js +99 -0
- package/dist/diagnostics/codes/field.js.map +1 -0
- package/dist/diagnostics/codes/home.d.ts +8 -0
- package/dist/diagnostics/codes/home.d.ts.map +1 -0
- package/dist/diagnostics/codes/home.js +132 -0
- package/dist/diagnostics/codes/home.js.map +1 -0
- package/dist/diagnostics/codes/host.d.ts +8 -0
- package/dist/diagnostics/codes/host.d.ts.map +1 -0
- package/dist/diagnostics/codes/host.js +176 -0
- package/dist/diagnostics/codes/host.js.map +1 -0
- package/dist/diagnostics/codes/hygiene.d.ts +8 -0
- package/dist/diagnostics/codes/hygiene.d.ts.map +1 -0
- package/dist/diagnostics/codes/hygiene.js +154 -0
- package/dist/diagnostics/codes/hygiene.js.map +1 -0
- package/dist/diagnostics/codes/index.d.ts +51 -0
- package/dist/diagnostics/codes/index.d.ts.map +1 -0
- package/dist/diagnostics/codes/index.js +72 -0
- package/dist/diagnostics/codes/index.js.map +1 -0
- package/dist/diagnostics/codes/live.d.ts +8 -0
- package/dist/diagnostics/codes/live.d.ts.map +1 -0
- package/dist/diagnostics/codes/live.js +30 -0
- package/dist/diagnostics/codes/live.js.map +1 -0
- package/dist/diagnostics/codes/manifest.d.ts +8 -0
- package/dist/diagnostics/codes/manifest.d.ts.map +1 -0
- package/dist/diagnostics/codes/manifest.js +44 -0
- package/dist/diagnostics/codes/manifest.js.map +1 -0
- package/dist/diagnostics/codes/network.d.ts +8 -0
- package/dist/diagnostics/codes/network.d.ts.map +1 -0
- package/dist/diagnostics/codes/network.js +30 -0
- package/dist/diagnostics/codes/network.js.map +1 -0
- package/dist/diagnostics/codes/platform.d.ts +8 -0
- package/dist/diagnostics/codes/platform.d.ts.map +1 -0
- package/dist/diagnostics/codes/platform.js +18 -0
- package/dist/diagnostics/codes/platform.js.map +1 -0
- package/dist/diagnostics/codes/publish.d.ts +8 -0
- package/dist/diagnostics/codes/publish.d.ts.map +1 -0
- package/dist/diagnostics/codes/publish.js +44 -0
- package/dist/diagnostics/codes/publish.js.map +1 -0
- package/dist/diagnostics/codes/regression.d.ts +8 -0
- package/dist/diagnostics/codes/regression.d.ts.map +1 -0
- package/dist/diagnostics/codes/regression.js +77 -0
- package/dist/diagnostics/codes/regression.js.map +1 -0
- package/dist/diagnostics/codes/runtime.d.ts +8 -0
- package/dist/diagnostics/codes/runtime.d.ts.map +1 -0
- package/dist/diagnostics/codes/runtime.js +66 -0
- package/dist/diagnostics/codes/runtime.js.map +1 -0
- package/dist/diagnostics/codes/secret.d.ts +8 -0
- package/dist/diagnostics/codes/secret.d.ts.map +1 -0
- package/dist/diagnostics/codes/secret.js +18 -0
- package/dist/diagnostics/codes/secret.js.map +1 -0
- package/dist/diagnostics/codes/smoke.d.ts +8 -0
- package/dist/diagnostics/codes/smoke.d.ts.map +1 -0
- package/dist/diagnostics/codes/smoke.js +30 -0
- package/dist/diagnostics/codes/smoke.js.map +1 -0
- package/dist/diagnostics/codes/submit.d.ts +8 -0
- package/dist/diagnostics/codes/submit.d.ts.map +1 -0
- package/dist/diagnostics/codes/submit.js +99 -0
- package/dist/diagnostics/codes/submit.js.map +1 -0
- package/dist/diagnostics/codes/target.d.ts +8 -0
- package/dist/diagnostics/codes/target.d.ts.map +1 -0
- package/dist/diagnostics/codes/target.js +18 -0
- package/dist/diagnostics/codes/target.js.map +1 -0
- package/dist/diagnostics/codes/type.d.ts +8 -0
- package/dist/diagnostics/codes/type.d.ts.map +1 -0
- package/dist/diagnostics/codes/type.js +30 -0
- package/dist/diagnostics/codes/type.js.map +1 -0
- package/dist/diagnostics/codes/verify.d.ts +8 -0
- package/dist/diagnostics/codes/verify.d.ts.map +1 -0
- package/dist/diagnostics/codes/verify.js +99 -0
- package/dist/diagnostics/codes/verify.js.map +1 -0
- package/dist/diagnostics/diagnostic.d.ts +79 -0
- package/dist/diagnostics/diagnostic.d.ts.map +1 -0
- package/dist/diagnostics/diagnostic.js +131 -0
- package/dist/diagnostics/diagnostic.js.map +1 -0
- package/dist/diagnostics/spec.d.ts +49 -0
- package/dist/diagnostics/spec.d.ts.map +1 -0
- package/dist/diagnostics/spec.js +31 -0
- package/dist/diagnostics/spec.js.map +1 -0
- package/dist/hygiene.d.ts +66 -0
- package/dist/hygiene.d.ts.map +1 -0
- package/dist/hygiene.js +134 -0
- package/dist/hygiene.js.map +1 -0
- package/dist/ids.d.ts +119 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/ids.js +135 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +86 -0
- package/dist/interaction.d.ts +391 -0
- package/dist/interaction.d.ts.map +1 -0
- package/dist/interaction.js +306 -0
- package/dist/interaction.js.map +1 -0
- package/dist/manifest.d.ts +97 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +101 -0
- package/dist/manifest.js.map +1 -0
- package/dist/method.d.ts +26 -0
- package/dist/method.d.ts.map +1 -0
- package/dist/method.js +10 -0
- package/dist/method.js.map +1 -0
- package/dist/methods/artifact/create.d.ts +29 -0
- package/dist/methods/artifact/create.d.ts.map +1 -0
- package/dist/methods/artifact/create.js +31 -0
- package/dist/methods/artifact/create.js.map +1 -0
- package/dist/methods/artifact/get.d.ts +17 -0
- package/dist/methods/artifact/get.d.ts.map +1 -0
- package/dist/methods/artifact/get.js +21 -0
- package/dist/methods/artifact/get.js.map +1 -0
- package/dist/methods/artifact/list.d.ts +22 -0
- package/dist/methods/artifact/list.d.ts.map +1 -0
- package/dist/methods/artifact/list.js +19 -0
- package/dist/methods/artifact/list.js.map +1 -0
- package/dist/methods/artifact/publish.d.ts +28 -0
- package/dist/methods/artifact/publish.d.ts.map +1 -0
- package/dist/methods/artifact/publish.js +28 -0
- package/dist/methods/artifact/publish.js.map +1 -0
- package/dist/methods/artifact/verify.d.ts +29 -0
- package/dist/methods/artifact/verify.d.ts.map +1 -0
- package/dist/methods/artifact/verify.js +27 -0
- package/dist/methods/artifact/verify.js.map +1 -0
- package/dist/methods/deps/add.d.ts +43 -0
- package/dist/methods/deps/add.d.ts.map +1 -0
- package/dist/methods/deps/add.js +40 -0
- package/dist/methods/deps/add.js.map +1 -0
- package/dist/methods/dev/start.d.ts +52 -0
- package/dist/methods/dev/start.d.ts.map +1 -0
- package/dist/methods/dev/start.js +56 -0
- package/dist/methods/dev/start.js.map +1 -0
- package/dist/methods/dev/stop.d.ts +16 -0
- package/dist/methods/dev/stop.d.ts.map +1 -0
- package/dist/methods/dev/stop.js +21 -0
- package/dist/methods/dev/stop.js.map +1 -0
- package/dist/methods/events/ack.d.ts +20 -0
- package/dist/methods/events/ack.d.ts.map +1 -0
- package/dist/methods/events/ack.js +26 -0
- package/dist/methods/events/ack.js.map +1 -0
- package/dist/methods/events/read.d.ts +40 -0
- package/dist/methods/events/read.d.ts.map +1 -0
- package/dist/methods/events/read.js +35 -0
- package/dist/methods/events/read.js.map +1 -0
- package/dist/methods/events/subscribe.d.ts +54 -0
- package/dist/methods/events/subscribe.d.ts.map +1 -0
- package/dist/methods/events/subscribe.js +44 -0
- package/dist/methods/events/subscribe.js.map +1 -0
- package/dist/methods/events/unsubscribe.d.ts +16 -0
- package/dist/methods/events/unsubscribe.d.ts.map +1 -0
- package/dist/methods/events/unsubscribe.js +21 -0
- package/dist/methods/events/unsubscribe.js.map +1 -0
- package/dist/methods/gc/run.d.ts +76 -0
- package/dist/methods/gc/run.d.ts.map +1 -0
- package/dist/methods/gc/run.js +56 -0
- package/dist/methods/gc/run.js.map +1 -0
- package/dist/methods/index.d.ts +6 -0
- package/dist/methods/index.d.ts.map +1 -0
- package/dist/methods/index.js +78 -0
- package/dist/methods/index.js.map +1 -0
- package/dist/methods/instance/check.d.ts +30 -0
- package/dist/methods/instance/check.d.ts.map +1 -0
- package/dist/methods/instance/check.js +34 -0
- package/dist/methods/instance/check.js.map +1 -0
- package/dist/methods/instance/status.d.ts +102 -0
- package/dist/methods/instance/status.d.ts.map +1 -0
- package/dist/methods/instance/status.js +83 -0
- package/dist/methods/instance/status.js.map +1 -0
- package/dist/methods/live/push.d.ts +28 -0
- package/dist/methods/live/push.d.ts.map +1 -0
- package/dist/methods/live/push.js +35 -0
- package/dist/methods/live/push.js.map +1 -0
- package/dist/methods/mostra/initialize.d.ts +143 -0
- package/dist/methods/mostra/initialize.d.ts.map +1 -0
- package/dist/methods/mostra/initialize.js +97 -0
- package/dist/methods/mostra/initialize.js.map +1 -0
- package/dist/methods/mostra/shutdown.d.ts +14 -0
- package/dist/methods/mostra/shutdown.d.ts.map +1 -0
- package/dist/methods/mostra/shutdown.js +22 -0
- package/dist/methods/mostra/shutdown.js.map +1 -0
- package/dist/methods/port/drafts-mirror.d.ts +30 -0
- package/dist/methods/port/drafts-mirror.d.ts.map +1 -0
- package/dist/methods/port/drafts-mirror.js +33 -0
- package/dist/methods/port/drafts-mirror.js.map +1 -0
- package/dist/methods/port/secrets-get.d.ts +19 -0
- package/dist/methods/port/secrets-get.d.ts.map +1 -0
- package/dist/methods/port/secrets-get.js +31 -0
- package/dist/methods/port/secrets-get.js.map +1 -0
- package/dist/methods/port/submissions-deliver.d.ts +38 -0
- package/dist/methods/port/submissions-deliver.d.ts.map +1 -0
- package/dist/methods/port/submissions-deliver.js +38 -0
- package/dist/methods/port/submissions-deliver.js.map +1 -0
- package/dist/methods/port/trusted-controls-activated.d.ts +27 -0
- package/dist/methods/port/trusted-controls-activated.d.ts.map +1 -0
- package/dist/methods/port/trusted-controls-activated.js +33 -0
- package/dist/methods/port/trusted-controls-activated.js.map +1 -0
- package/dist/methods/port/trusted-controls-policy.d.ts +21 -0
- package/dist/methods/port/trusted-controls-policy.d.ts.map +1 -0
- package/dist/methods/port/trusted-controls-policy.js +27 -0
- package/dist/methods/port/trusted-controls-policy.js.map +1 -0
- package/dist/methods/results/get.d.ts +32 -0
- package/dist/methods/results/get.d.ts.map +1 -0
- package/dist/methods/results/get.js +36 -0
- package/dist/methods/results/get.js.map +1 -0
- package/dist/methods/secret/delete.d.ts +22 -0
- package/dist/methods/secret/delete.d.ts.map +1 -0
- package/dist/methods/secret/delete.js +32 -0
- package/dist/methods/secret/delete.js.map +1 -0
- package/dist/methods/secret/list.d.ts +21 -0
- package/dist/methods/secret/list.d.ts.map +1 -0
- package/dist/methods/secret/list.js +29 -0
- package/dist/methods/secret/list.js.map +1 -0
- package/dist/methods/secret/set.d.ts +37 -0
- package/dist/methods/secret/set.d.ts.map +1 -0
- package/dist/methods/secret/set.js +41 -0
- package/dist/methods/secret/set.js.map +1 -0
- package/dist/methods/state/get.d.ts +25 -0
- package/dist/methods/state/get.d.ts.map +1 -0
- package/dist/methods/state/get.js +29 -0
- package/dist/methods/state/get.js.map +1 -0
- package/dist/methods/state/patch.d.ts +32 -0
- package/dist/methods/state/patch.d.ts.map +1 -0
- package/dist/methods/state/patch.js +29 -0
- package/dist/methods/state/patch.js.map +1 -0
- package/dist/methods/view/default.d.ts +19 -0
- package/dist/methods/view/default.d.ts.map +1 -0
- package/dist/methods/view/default.js +23 -0
- package/dist/methods/view/default.js.map +1 -0
- package/dist/methods/view/launch.d.ts +27 -0
- package/dist/methods/view/launch.d.ts.map +1 -0
- package/dist/methods/view/launch.js +30 -0
- package/dist/methods/view/launch.js.map +1 -0
- package/dist/methods/view/revoke.d.ts +19 -0
- package/dist/methods/view/revoke.d.ts.map +1 -0
- package/dist/methods/view/revoke.js +26 -0
- package/dist/methods/view/revoke.js.map +1 -0
- package/dist/methods/view/rotate.d.ts +21 -0
- package/dist/methods/view/rotate.d.ts.map +1 -0
- package/dist/methods/view/rotate.js +27 -0
- package/dist/methods/view/rotate.js.map +1 -0
- package/dist/methods/view/url.d.ts +52 -0
- package/dist/methods/view/url.d.ts.map +1 -0
- package/dist/methods/view/url.js +43 -0
- package/dist/methods/view/url.js.map +1 -0
- package/dist/methods/wait/wait.d.ts +37 -0
- package/dist/methods/wait/wait.d.ts.map +1 -0
- package/dist/methods/wait/wait.js +39 -0
- package/dist/methods/wait/wait.js.map +1 -0
- package/dist/revision.d.ts +43 -0
- package/dist/revision.d.ts.map +1 -0
- package/dist/revision.js +69 -0
- package/dist/revision.js.map +1 -0
- package/dist/rpc/error.d.ts +47 -0
- package/dist/rpc/error.d.ts.map +1 -0
- package/dist/rpc/error.js +77 -0
- package/dist/rpc/error.js.map +1 -0
- package/dist/rpc/lines.d.ts +30 -0
- package/dist/rpc/lines.d.ts.map +1 -0
- package/dist/rpc/lines.js +63 -0
- package/dist/rpc/lines.js.map +1 -0
- package/dist/rpc/messages.d.ts +92 -0
- package/dist/rpc/messages.d.ts.map +1 -0
- package/dist/rpc/messages.js +133 -0
- package/dist/rpc/messages.js.map +1 -0
- package/dist/rpc/peer.d.ts +81 -0
- package/dist/rpc/peer.d.ts.map +1 -0
- package/dist/rpc/peer.js +208 -0
- package/dist/rpc/peer.js.map +1 -0
- package/dist/serializer.d.ts +178 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/serializer.js +929 -0
- package/dist/serializer.js.map +1 -0
- package/dist/verify.d.ts +254 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +243 -0
- package/dist/verify.js.map +1 -0
- package/dist/view.d.ts +103 -0
- package/dist/view.d.ts.map +1 -0
- package/dist/view.js +110 -0
- package/dist/view.js.map +1 -0
- package/dist/wire-registry.js +11 -0
- package/dist/wire-registry.js.map +1 -0
- package/dist/worker.d.ts +249 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +158 -0
- package/dist/worker.js.map +1 -0
- package/package.json +114 -3
- package/schemas/protocol.draft-07.json +5120 -0
- package/schemas/protocol.schema.json +4964 -0
- package/src/actions.ts +418 -0
- package/src/artifact.ts +187 -0
- package/src/bridge.ts +618 -0
- package/src/browser.ts +374 -0
- package/src/bundle.ts +264 -0
- package/src/canonical.ts +49 -0
- package/src/capability.ts +92 -0
- package/src/diagnostics/codes/a11y.ts +41 -0
- package/src/diagnostics/codes/action.ts +125 -0
- package/src/diagnostics/codes/artifact.ts +137 -0
- package/src/diagnostics/codes/build.ts +65 -0
- package/src/diagnostics/codes/cli.ts +53 -0
- package/src/diagnostics/codes/console.ts +41 -0
- package/src/diagnostics/codes/dep.ts +101 -0
- package/src/diagnostics/codes/env.ts +149 -0
- package/src/diagnostics/codes/field.ts +113 -0
- package/src/diagnostics/codes/home.ts +150 -0
- package/src/diagnostics/codes/host.ts +197 -0
- package/src/diagnostics/codes/hygiene.ts +175 -0
- package/src/diagnostics/codes/index.ts +142 -0
- package/src/diagnostics/codes/live.ts +41 -0
- package/src/diagnostics/codes/manifest.ts +53 -0
- package/src/diagnostics/codes/network.ts +41 -0
- package/src/diagnostics/codes/platform.ts +30 -0
- package/src/diagnostics/codes/publish.ts +53 -0
- package/src/diagnostics/codes/regression.ts +89 -0
- package/src/diagnostics/codes/runtime.ts +77 -0
- package/src/diagnostics/codes/secret.ts +29 -0
- package/src/diagnostics/codes/smoke.ts +41 -0
- package/src/diagnostics/codes/submit.ts +113 -0
- package/src/diagnostics/codes/target.ts +29 -0
- package/src/diagnostics/codes/type.ts +41 -0
- package/src/diagnostics/codes/verify.ts +113 -0
- package/src/diagnostics/diagnostic.ts +240 -0
- package/src/diagnostics/spec.ts +90 -0
- package/src/hygiene.ts +224 -0
- package/src/ids.ts +279 -0
- package/src/index.ts +576 -0
- package/src/interaction.ts +728 -0
- package/src/manifest.ts +206 -0
- package/src/method.ts +42 -0
- package/src/methods/artifact/create.ts +56 -0
- package/src/methods/artifact/get.ts +30 -0
- package/src/methods/artifact/list.ts +42 -0
- package/src/methods/artifact/publish.ts +46 -0
- package/src/methods/artifact/verify.ts +46 -0
- package/src/methods/deps/add.ts +72 -0
- package/src/methods/dev/start.ts +84 -0
- package/src/methods/dev/stop.ts +32 -0
- package/src/methods/events/ack.ts +41 -0
- package/src/methods/events/read.ts +67 -0
- package/src/methods/events/subscribe.ts +91 -0
- package/src/methods/events/unsubscribe.ts +35 -0
- package/src/methods/gc/run.ts +125 -0
- package/src/methods/index.ts +81 -0
- package/src/methods/instance/check.ts +61 -0
- package/src/methods/instance/status.ts +172 -0
- package/src/methods/live/push.ts +54 -0
- package/src/methods/mostra/initialize.ts +249 -0
- package/src/methods/mostra/shutdown.ts +31 -0
- package/src/methods/port/drafts-mirror.ts +52 -0
- package/src/methods/port/secrets-get.ts +40 -0
- package/src/methods/port/submissions-deliver.ts +70 -0
- package/src/methods/port/trusted-controls-activated.ts +51 -0
- package/src/methods/port/trusted-controls-policy.ts +38 -0
- package/src/methods/results/get.ts +59 -0
- package/src/methods/secret/delete.ts +43 -0
- package/src/methods/secret/list.ts +40 -0
- package/src/methods/secret/set.ts +69 -0
- package/src/methods/state/get.ts +44 -0
- package/src/methods/state/patch.ts +51 -0
- package/src/methods/view/default.ts +32 -0
- package/src/methods/view/launch.ts +47 -0
- package/src/methods/view/revoke.ts +39 -0
- package/src/methods/view/rotate.ts +37 -0
- package/src/methods/view/url.ts +84 -0
- package/src/methods/wait/wait.ts +58 -0
- package/src/revision.ts +101 -0
- package/src/rpc/error.ts +107 -0
- package/src/rpc/lines.ts +70 -0
- package/src/rpc/messages.ts +188 -0
- package/src/rpc/peer.ts +341 -0
- package/src/serializer.ts +1217 -0
- package/src/timers.d.ts +8 -0
- package/src/verify.ts +458 -0
- package/src/view.ts +200 -0
- package/src/web-crypto.d.ts +9 -0
- package/src/web-platform.d.ts +35 -0
- package/src/wire-registry.ts +14 -0
- package/src/worker.ts +374 -0
|
@@ -0,0 +1,1217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The serializer of server-function arguments and results (D19 seam 2, D54): exactly the type set
|
|
3
|
+
* React documents for Server Functions, plus `AsyncIterable` results, in one JSON wire form the
|
|
4
|
+
* browser, the gateway and the worker share. It has no I/O and no Node built-ins.
|
|
5
|
+
*
|
|
6
|
+
* A value is written as a series of **batches**, each a JSON array of {@link Frame}s. The first
|
|
7
|
+
* batch holds the value's root (chunk 0); a promise or an async iterable in it is a reference whose
|
|
8
|
+
* settlement or items arrive in later batches, so a result streams as it settles. Shared objects
|
|
9
|
+
* and cycles are kept: an object met twice is written once, in a chunk of its own.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* const batches: Frame[][] = [];
|
|
13
|
+
* const writer = new ValueWriter({ mode: "result", onBatch: (batch) => batches.push(batch) });
|
|
14
|
+
* await writer.write({ when: new Date(), items: new Map([["a", 1]]) });
|
|
15
|
+
* await writer.done;
|
|
16
|
+
* const reader = new ValueReader();
|
|
17
|
+
* for (const batch of batches) reader.push(batch);
|
|
18
|
+
* const value = await reader.root;
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* What it accepts (react.dev, `'use server'`, "Serializable arguments and return values"):
|
|
22
|
+
* strings, numbers (`NaN`, infinities and `-0` too), bigints, booleans, `undefined`, `null`,
|
|
23
|
+
* symbols registered with `Symbol.for`, arrays, `Map`, `Set`, typed arrays, `DataView`,
|
|
24
|
+
* `ArrayBuffer`, `Date`, `FormData` (with string and file values), plain objects, Server Functions
|
|
25
|
+
* and promises; results may also be async iterables. Anything else (class instances, objects with
|
|
26
|
+
* a null prototype, other functions, unregistered symbols, React elements) is refused with a
|
|
27
|
+
* {@link SerializationError} naming where it is.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** One step of a written value. */
|
|
31
|
+
export type Frame =
|
|
32
|
+
/** An outlined chunk: chunk 0 is the root; others hold shared objects, maps, sets and bytes. */
|
|
33
|
+
| { readonly c: number; readonly v: unknown }
|
|
34
|
+
/** A promise settled: with its value, or rejected with an error. */
|
|
35
|
+
| { readonly p: number; readonly v: unknown }
|
|
36
|
+
| { readonly p: number; readonly e: WireError }
|
|
37
|
+
/** An async iterable's item, its end, or its failure. */
|
|
38
|
+
| { readonly i: number; readonly v: unknown }
|
|
39
|
+
| { readonly i: number; readonly end: true }
|
|
40
|
+
| { readonly i: number; readonly e: WireError };
|
|
41
|
+
|
|
42
|
+
/** A frame about a promise or an async iterable. */
|
|
43
|
+
type AsyncFrame = Exclude<Frame, { readonly c: number }>;
|
|
44
|
+
|
|
45
|
+
/** An error as the wire carries it: a rejected promise, a failed iteration. */
|
|
46
|
+
export interface WireError {
|
|
47
|
+
/** The error's message. */
|
|
48
|
+
readonly message: string;
|
|
49
|
+
/** Its diagnostic code, when it carried one (`MOSTRA_SECRET_MISSING`, say). */
|
|
50
|
+
readonly code?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Any function: a Server Function as the serializer sees it. */
|
|
54
|
+
export type AnyFunction = (...args: never) => unknown;
|
|
55
|
+
|
|
56
|
+
/** Whether a value is written as a call's arguments or as its result. */
|
|
57
|
+
export type SerializeMode = "argument" | "result";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The most digits a bigint argument may have. A page's arguments are decoded before the action
|
|
61
|
+
* runs, and parsing a bigint takes time that grows faster than its length: 16 M digits held a
|
|
62
|
+
* worker's event loop for seconds. Results aren't limited; server code writes them.
|
|
63
|
+
*/
|
|
64
|
+
export const ARGUMENT_BIGINT_DIGITS_MAX = 10_000;
|
|
65
|
+
|
|
66
|
+
/** The diagnostic code of a value outside the type set (INTERFACES §2.4). */
|
|
67
|
+
export const UNSERIALIZABLE_CODE = "MOSTRA_ACTION_UNSERIALIZABLE";
|
|
68
|
+
|
|
69
|
+
/** A value outside the type set, or a malformed written value. */
|
|
70
|
+
export class SerializationError extends Error {
|
|
71
|
+
/** Always `MOSTRA_ACTION_UNSERIALIZABLE`. */
|
|
72
|
+
readonly code: string = UNSERIALIZABLE_CODE;
|
|
73
|
+
/** Where in the value: `.items[2].when`, or empty for the value itself. */
|
|
74
|
+
readonly path: string;
|
|
75
|
+
constructor(message: string, path = "") {
|
|
76
|
+
super(path === "" ? message : `${where(path)} ${message}`);
|
|
77
|
+
this.name = "SerializationError";
|
|
78
|
+
this.path = path;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A path as people read it: `[1].when` is "argument 2's .when". */
|
|
83
|
+
function where(path: string): string {
|
|
84
|
+
const argument = /^\[(\d+)\](.*)$/.exec(path);
|
|
85
|
+
if (argument) {
|
|
86
|
+
const rest = argument[2] ?? "";
|
|
87
|
+
return `Argument ${Number(argument[1]) + 1}${rest === "" ? "" : ` at ${rest}`}`;
|
|
88
|
+
}
|
|
89
|
+
return path.startsWith("result") ? `The ${path}` : `The value at ${path}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The error a rejected promise or a failed iteration carries on the wire. */
|
|
93
|
+
export function wireError(error: unknown): WireError {
|
|
94
|
+
if (error instanceof Error) {
|
|
95
|
+
const code = (error as Error & { code?: unknown }).code;
|
|
96
|
+
return { message: error.message, ...(typeof code === "string" ? { code } : {}) };
|
|
97
|
+
}
|
|
98
|
+
return { message: String(error) };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** An error read back from the wire, as the reader's promises and iterables reject with it. */
|
|
102
|
+
export class RemoteError extends Error {
|
|
103
|
+
/** The diagnostic code the error carried, if any. */
|
|
104
|
+
readonly code: string | undefined;
|
|
105
|
+
constructor(error: WireError) {
|
|
106
|
+
super(error.message);
|
|
107
|
+
this.name = "RemoteError";
|
|
108
|
+
this.code = error.code;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Base64, without Node's Buffer ──────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
115
|
+
const LOOKUP = new Int16Array(128).fill(-1);
|
|
116
|
+
for (let index = 0; index < ALPHABET.length; index += 1) LOOKUP[ALPHABET.charCodeAt(index)] = index;
|
|
117
|
+
|
|
118
|
+
/** Standard base64 of `bytes`, with padding. */
|
|
119
|
+
export function toBase64(bytes: Uint8Array): string {
|
|
120
|
+
let out = "";
|
|
121
|
+
let index = 0;
|
|
122
|
+
for (; index + 2 < bytes.length; index += 3) {
|
|
123
|
+
const n =
|
|
124
|
+
((bytes[index] ?? 0) << 16) | ((bytes[index + 1] ?? 0) << 8) | (bytes[index + 2] ?? 0);
|
|
125
|
+
out +=
|
|
126
|
+
ALPHABET.charAt((n >> 18) & 63) +
|
|
127
|
+
ALPHABET.charAt((n >> 12) & 63) +
|
|
128
|
+
ALPHABET.charAt((n >> 6) & 63) +
|
|
129
|
+
ALPHABET.charAt(n & 63);
|
|
130
|
+
}
|
|
131
|
+
const rest = bytes.length - index;
|
|
132
|
+
if (rest === 1) {
|
|
133
|
+
const n = (bytes[index] ?? 0) << 16;
|
|
134
|
+
out += `${ALPHABET[(n >> 18) & 63]}${ALPHABET[(n >> 12) & 63]}==`;
|
|
135
|
+
} else if (rest === 2) {
|
|
136
|
+
const n = ((bytes[index] ?? 0) << 16) | ((bytes[index + 1] ?? 0) << 8);
|
|
137
|
+
out += `${ALPHABET[(n >> 18) & 63]}${ALPHABET[(n >> 12) & 63]}${ALPHABET[(n >> 6) & 63]}=`;
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The bytes of standard base64 text; throws a {@link SerializationError} on anything else. */
|
|
143
|
+
export function fromBase64(text: string): Uint8Array {
|
|
144
|
+
if (text.length % 4 !== 0) throw new SerializationError("Malformed base64 bytes.");
|
|
145
|
+
const padding = text.endsWith("==") ? 2 : text.endsWith("=") ? 1 : 0;
|
|
146
|
+
const out = new Uint8Array((text.length / 4) * 3 - padding);
|
|
147
|
+
let at = 0;
|
|
148
|
+
for (let index = 0; index < text.length; index += 4) {
|
|
149
|
+
const quad = [0, 1, 2, 3].map((offset) => {
|
|
150
|
+
const char = text.charCodeAt(index + offset);
|
|
151
|
+
if (char === 61 && index + offset >= text.length - padding) return 0;
|
|
152
|
+
const value = char < 128 ? (LOOKUP[char] ?? -1) : -1;
|
|
153
|
+
if (value < 0) throw new SerializationError("Malformed base64 bytes.");
|
|
154
|
+
return value;
|
|
155
|
+
});
|
|
156
|
+
const n =
|
|
157
|
+
((quad[0] ?? 0) << 18) | ((quad[1] ?? 0) << 12) | ((quad[2] ?? 0) << 6) | (quad[3] ?? 0);
|
|
158
|
+
if (at < out.length) out[at++] = (n >> 16) & 255;
|
|
159
|
+
if (at < out.length) out[at++] = (n >> 8) & 255;
|
|
160
|
+
if (at < out.length) out[at++] = n & 255;
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Kinds ──────────────────────────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
/** The typed-array kinds, by the tag the wire uses. */
|
|
168
|
+
const TYPED_ARRAYS = {
|
|
169
|
+
Int8Array: "i8",
|
|
170
|
+
Uint8Array: "u8",
|
|
171
|
+
Uint8ClampedArray: "u8c",
|
|
172
|
+
Int16Array: "i16",
|
|
173
|
+
Uint16Array: "u16",
|
|
174
|
+
Int32Array: "i32",
|
|
175
|
+
Uint32Array: "u32",
|
|
176
|
+
Float32Array: "f32",
|
|
177
|
+
Float64Array: "f64",
|
|
178
|
+
BigInt64Array: "i64",
|
|
179
|
+
BigUint64Array: "u64",
|
|
180
|
+
DataView: "dv",
|
|
181
|
+
} as const;
|
|
182
|
+
|
|
183
|
+
type TypedKind = (typeof TYPED_ARRAYS)[keyof typeof TYPED_ARRAYS];
|
|
184
|
+
|
|
185
|
+
const TYPED_CONSTRUCTORS: Readonly<Record<TypedKind, (buffer: ArrayBuffer) => ArrayBufferView>> = {
|
|
186
|
+
i8: (b) => new Int8Array(b),
|
|
187
|
+
u8: (b) => new Uint8Array(b),
|
|
188
|
+
u8c: (b) => new Uint8ClampedArray(b),
|
|
189
|
+
i16: (b) => new Int16Array(b),
|
|
190
|
+
u16: (b) => new Uint16Array(b),
|
|
191
|
+
i32: (b) => new Int32Array(b),
|
|
192
|
+
u32: (b) => new Uint32Array(b),
|
|
193
|
+
f32: (b) => new Float32Array(b),
|
|
194
|
+
f64: (b) => new Float64Array(b),
|
|
195
|
+
i64: (b) => new BigInt64Array(b),
|
|
196
|
+
u64: (b) => new BigUint64Array(b),
|
|
197
|
+
dv: (b) => new DataView(b),
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The wire tag of a typed array or `DataView`, or `undefined` for anything else. A subclass is
|
|
202
|
+
* its built-in base (Node's `Buffer` is a `Uint8Array`), found along the prototype chain, so
|
|
203
|
+
* what Node's file and crypto APIs return crosses as the bytes it holds.
|
|
204
|
+
*/
|
|
205
|
+
function typedKind(value: object): TypedKind | undefined {
|
|
206
|
+
if (!ArrayBuffer.isView(value)) return undefined;
|
|
207
|
+
for (
|
|
208
|
+
let proto = Object.getPrototypeOf(value) as { constructor?: { name?: string } } | null;
|
|
209
|
+
proto !== null;
|
|
210
|
+
proto = Object.getPrototypeOf(proto) as { constructor?: { name?: string } } | null
|
|
211
|
+
) {
|
|
212
|
+
const name = Object.hasOwn(proto, "constructor") ? proto.constructor?.name : undefined;
|
|
213
|
+
if (name !== undefined && name in TYPED_ARRAYS) {
|
|
214
|
+
return TYPED_ARRAYS[name as keyof typeof TYPED_ARRAYS];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const isPlainObject = (value: object): boolean => Object.getPrototypeOf(value) === Object.prototype;
|
|
221
|
+
|
|
222
|
+
const isBlob = (value: unknown): value is Blob =>
|
|
223
|
+
typeof Blob !== "undefined" && value instanceof Blob;
|
|
224
|
+
|
|
225
|
+
const isFormData = (value: unknown): value is FormData =>
|
|
226
|
+
typeof FormData !== "undefined" && value instanceof FormData;
|
|
227
|
+
|
|
228
|
+
const isPromiseLike = (value: object): value is PromiseLike<unknown> =>
|
|
229
|
+
value instanceof Promise ||
|
|
230
|
+
(typeof (value as { then?: unknown }).then === "function" && !isPlainObject(value));
|
|
231
|
+
|
|
232
|
+
const isAsyncIterable = (value: object): value is AsyncIterable<unknown> =>
|
|
233
|
+
typeof (value as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator] === "function";
|
|
234
|
+
|
|
235
|
+
/** React's element marker: elements are refused (they belong to the `rsc` mode, D19). */
|
|
236
|
+
const REACT_ELEMENT = Symbol.for("react.transitional.element");
|
|
237
|
+
const REACT_LEGACY_ELEMENT = Symbol.for("react.element");
|
|
238
|
+
|
|
239
|
+
function describe(value: object): string {
|
|
240
|
+
const proto = Object.getPrototypeOf(value) as { constructor?: { name?: string } } | null;
|
|
241
|
+
if (proto === null) return "an object with a null prototype";
|
|
242
|
+
const name = proto.constructor?.name;
|
|
243
|
+
return name ? `an instance of ${name}` : "an object that isn't plain";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const pathKey = (key: string): string =>
|
|
247
|
+
/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`;
|
|
248
|
+
|
|
249
|
+
// ── Writing ────────────────────────────────────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
/** Options for {@link ValueWriter}. */
|
|
252
|
+
export interface ValueWriterOptions {
|
|
253
|
+
/** `argument`: no async iterables. `result`: everything. */
|
|
254
|
+
readonly mode: SerializeMode;
|
|
255
|
+
/** Receives each batch, in order. */
|
|
256
|
+
readonly onBatch: (batch: Frame[]) => void;
|
|
257
|
+
/**
|
|
258
|
+
* The ID of a function that is a Server Function, or `undefined` for any other function. The
|
|
259
|
+
* client's stubs carry their ID; the worker knows its registry.
|
|
260
|
+
*/
|
|
261
|
+
readonly serverFunctionId?: (fn: AnyFunction) => string | undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Writes one value as batches. `write` throws a {@link SerializationError} when the value, or
|
|
266
|
+
* anything it holds synchronously, is outside the type set; a promise that later settles with such
|
|
267
|
+
* a value is written as rejected with that error. `done` settles once every promise and iterable
|
|
268
|
+
* in the value has settled or ended; `abort` stops iterables early (the reader went away).
|
|
269
|
+
*/
|
|
270
|
+
export class ValueWriter {
|
|
271
|
+
readonly #options: ValueWriterOptions;
|
|
272
|
+
#nextId = 1;
|
|
273
|
+
#pending = 0;
|
|
274
|
+
#aborted = false;
|
|
275
|
+
#cutShort = false;
|
|
276
|
+
#wroteRoot = false;
|
|
277
|
+
readonly #rootPath: string;
|
|
278
|
+
#queue: Promise<void> = Promise.resolve();
|
|
279
|
+
readonly #iterators = new Set<AsyncIterator<unknown>>();
|
|
280
|
+
#resolveDone!: () => void;
|
|
281
|
+
#rejectDone!: (error: unknown) => void;
|
|
282
|
+
/** Settles once every async part of the value has been written. */
|
|
283
|
+
readonly done: Promise<void>;
|
|
284
|
+
|
|
285
|
+
constructor(options: ValueWriterOptions) {
|
|
286
|
+
this.#options = options;
|
|
287
|
+
this.#rootPath = options.mode === "result" ? "result" : "";
|
|
288
|
+
this.done = new Promise((resolve, reject) => {
|
|
289
|
+
this.#resolveDone = resolve;
|
|
290
|
+
this.#rejectDone = reject;
|
|
291
|
+
});
|
|
292
|
+
// `done` may be awaited by nobody when `write` fails.
|
|
293
|
+
this.done.catch(() => {});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Writes the value's first batch; async parts follow in later batches. */
|
|
297
|
+
async write(value: unknown): Promise<void> {
|
|
298
|
+
if (this.#wroteRoot) throw new Error("A ValueWriter writes one value.");
|
|
299
|
+
this.#wroteRoot = true;
|
|
300
|
+
// The root goes first: batches of promises that settle while it is still being encoded (it
|
|
301
|
+
// may read files) queue behind it.
|
|
302
|
+
this.#pending += 1;
|
|
303
|
+
const root = this.#encodeBatch(value, this.#rootPath, (json, frames) => {
|
|
304
|
+
frames.unshift({ c: 0, v: json });
|
|
305
|
+
});
|
|
306
|
+
this.#queue = root.then(
|
|
307
|
+
(batch) => this.#emit(batch),
|
|
308
|
+
() => {},
|
|
309
|
+
);
|
|
310
|
+
try {
|
|
311
|
+
await root;
|
|
312
|
+
} catch (error) {
|
|
313
|
+
this.#aborted = true;
|
|
314
|
+
this.#rejectDone(error);
|
|
315
|
+
throw error;
|
|
316
|
+
} finally {
|
|
317
|
+
this.#pending -= 1;
|
|
318
|
+
}
|
|
319
|
+
await this.#queue;
|
|
320
|
+
this.#settleIfIdle();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Whether {@link ValueWriter.abort} stopped the value before all of it was written: a promise
|
|
325
|
+
* still unsettled, an iterable not yet ended.
|
|
326
|
+
*/
|
|
327
|
+
get cutShort(): boolean {
|
|
328
|
+
return this.#cutShort;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Stops every iterable still being read, and settles `done`. */
|
|
332
|
+
abort(): void {
|
|
333
|
+
if (this.#aborted) return;
|
|
334
|
+
this.#cutShort = this.#pending > 0;
|
|
335
|
+
this.#aborted = true;
|
|
336
|
+
for (const iterator of this.#iterators) void iterator.return?.()?.catch?.(() => {});
|
|
337
|
+
this.#iterators.clear();
|
|
338
|
+
this.#resolveDone();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
#emit(batch: Frame[]): void {
|
|
342
|
+
if (this.#aborted) return;
|
|
343
|
+
this.#options.onBatch(batch);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
#settleIfIdle(): void {
|
|
347
|
+
if (this.#pending === 0) this.#resolveDone();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Runs batch production one at a time, so batches leave in the order they were started. */
|
|
351
|
+
#serial(task: () => Promise<Frame[] | undefined>): void {
|
|
352
|
+
this.#pending += 1;
|
|
353
|
+
this.#queue = this.#queue
|
|
354
|
+
.then(async () => {
|
|
355
|
+
const batch = await task();
|
|
356
|
+
if (batch) this.#emit(batch);
|
|
357
|
+
})
|
|
358
|
+
.catch((error: unknown) => {
|
|
359
|
+
this.#aborted = true;
|
|
360
|
+
this.#rejectDone(error);
|
|
361
|
+
})
|
|
362
|
+
.finally(() => {
|
|
363
|
+
this.#pending -= 1;
|
|
364
|
+
this.#settleIfIdle();
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Encodes `value` into a batch: `place` puts the encoded root into the batch, whose other
|
|
370
|
+
* frames are the chunks it needs.
|
|
371
|
+
*/
|
|
372
|
+
async #encodeBatch(
|
|
373
|
+
value: unknown,
|
|
374
|
+
path: string,
|
|
375
|
+
place: (json: unknown, frames: Frame[]) => void,
|
|
376
|
+
): Promise<Frame[]> {
|
|
377
|
+
const frames: Frame[] = [];
|
|
378
|
+
const shared = countShared(value);
|
|
379
|
+
const outlined = new Map<object, number>();
|
|
380
|
+
const blobs: { id: number; blob: Blob }[] = [];
|
|
381
|
+
const encode = (item: unknown, at: string, depth: number): unknown => {
|
|
382
|
+
if (depth > MAX_DEPTH) throw new SerializationError("The value nests too deeply.", at);
|
|
383
|
+
switch (typeof item) {
|
|
384
|
+
case "string":
|
|
385
|
+
return item.startsWith("$") ? `$${item}` : item;
|
|
386
|
+
case "number":
|
|
387
|
+
if (Number.isNaN(item)) return "$NaN";
|
|
388
|
+
if (item === Number.POSITIVE_INFINITY) return "$Infinity";
|
|
389
|
+
if (item === Number.NEGATIVE_INFINITY) return "$-Infinity";
|
|
390
|
+
if (Object.is(item, -0)) return "$-0";
|
|
391
|
+
return item;
|
|
392
|
+
case "boolean":
|
|
393
|
+
return item;
|
|
394
|
+
case "undefined":
|
|
395
|
+
return "$undefined";
|
|
396
|
+
case "bigint": {
|
|
397
|
+
const digits = item.toString();
|
|
398
|
+
if (
|
|
399
|
+
this.#options.mode === "argument" &&
|
|
400
|
+
digits.replace("-", "").length > ARGUMENT_BIGINT_DIGITS_MAX
|
|
401
|
+
) {
|
|
402
|
+
throw new SerializationError(
|
|
403
|
+
`is a bigint of more than ${ARGUMENT_BIGINT_DIGITS_MAX} digits; send a larger number as a string.`,
|
|
404
|
+
at,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
return `$n${digits}`;
|
|
408
|
+
}
|
|
409
|
+
case "symbol": {
|
|
410
|
+
if (this.#options.mode === "argument") {
|
|
411
|
+
// React 19.3's reply codec refuses symbols in arguments too (VERIFIED_FACTS, WP-08):
|
|
412
|
+
// a registered symbol lives as long as the process, so a page could fill the
|
|
413
|
+
// worker's memory with them.
|
|
414
|
+
throw new SerializationError(
|
|
415
|
+
"is a symbol; an argument can't be one, only a result can (a registered symbol).",
|
|
416
|
+
at,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
const key = Symbol.keyFor(item);
|
|
420
|
+
if (key === undefined) {
|
|
421
|
+
throw new SerializationError(
|
|
422
|
+
"is a symbol not registered with Symbol.for; only registered symbols can be sent.",
|
|
423
|
+
at,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
return `$S${key}`;
|
|
427
|
+
}
|
|
428
|
+
case "function": {
|
|
429
|
+
const id = this.#options.serverFunctionId?.(item as AnyFunction);
|
|
430
|
+
if (id === undefined) {
|
|
431
|
+
throw new SerializationError(
|
|
432
|
+
"is a function that isn't a Server Function; only Server Functions can be sent.",
|
|
433
|
+
at,
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
return `$h${id}`;
|
|
437
|
+
}
|
|
438
|
+
case "object":
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
if (item === null) return null;
|
|
442
|
+
const object = item as object;
|
|
443
|
+
const known = outlined.get(object);
|
|
444
|
+
if (known !== undefined) return `$${known}`;
|
|
445
|
+
if (
|
|
446
|
+
(object as { $$typeof?: unknown }).$$typeof === REACT_ELEMENT ||
|
|
447
|
+
(object as { $$typeof?: unknown }).$$typeof === REACT_LEGACY_ELEMENT
|
|
448
|
+
) {
|
|
449
|
+
throw new SerializationError(
|
|
450
|
+
"is a React element; JSX can't cross a server call in the SPA render mode.",
|
|
451
|
+
at,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
if (object instanceof Date) {
|
|
455
|
+
return Number.isNaN(object.getTime()) ? "$DInvalid" : `$D${object.toISOString()}`;
|
|
456
|
+
}
|
|
457
|
+
if (isPromiseLike(object)) {
|
|
458
|
+
const id = this.#nextId++;
|
|
459
|
+
this.#watchPromise(id, object, `${at}.then`);
|
|
460
|
+
return `$@${id}`;
|
|
461
|
+
}
|
|
462
|
+
if (isFormData(object)) {
|
|
463
|
+
const id = this.#nextId++;
|
|
464
|
+
const entries: unknown[] = [];
|
|
465
|
+
for (const [name, entry] of object.entries()) {
|
|
466
|
+
if (typeof entry === "string") entries.push([name, encode(entry, at, depth + 1)]);
|
|
467
|
+
else {
|
|
468
|
+
const blobId = this.#nextId++;
|
|
469
|
+
blobs.push({ id: blobId, blob: entry });
|
|
470
|
+
entries.push([name, `$B${blobId}`]);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
frames.push({ c: id, v: entries });
|
|
474
|
+
return `$K${id}`;
|
|
475
|
+
}
|
|
476
|
+
if (object instanceof ArrayBuffer) {
|
|
477
|
+
const id = this.#nextId++;
|
|
478
|
+
frames.push({ c: id, v: toBase64(new Uint8Array(object)) });
|
|
479
|
+
return `$A${id}`;
|
|
480
|
+
}
|
|
481
|
+
const typed = typedKind(object);
|
|
482
|
+
if (typed !== undefined) {
|
|
483
|
+
const view = object as ArrayBufferView;
|
|
484
|
+
const id = this.#nextId++;
|
|
485
|
+
const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
|
486
|
+
frames.push({ c: id, v: [typed, toBase64(bytes)] });
|
|
487
|
+
return `$T${id}`;
|
|
488
|
+
}
|
|
489
|
+
const outline = (encoded: () => unknown, tag: string): string => {
|
|
490
|
+
const id = this.#nextId++;
|
|
491
|
+
outlined.set(object, id);
|
|
492
|
+
const frame: { c: number; v: unknown } = { c: id, v: null };
|
|
493
|
+
frames.push(frame);
|
|
494
|
+
frame.v = encoded();
|
|
495
|
+
return `${tag}${id}`;
|
|
496
|
+
};
|
|
497
|
+
if (object instanceof Map) {
|
|
498
|
+
return outline(
|
|
499
|
+
() =>
|
|
500
|
+
[...object.entries()].map(([key, entry], index) => [
|
|
501
|
+
encode(key, `${at}.<map key ${index}>`, depth + 1),
|
|
502
|
+
encode(entry, `${at}.<map value ${index}>`, depth + 1),
|
|
503
|
+
]),
|
|
504
|
+
"$Q",
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
if (object instanceof Set) {
|
|
508
|
+
return outline(
|
|
509
|
+
() =>
|
|
510
|
+
[...object].map((entry, index) =>
|
|
511
|
+
encode(entry, `${at}.<set item ${index}>`, depth + 1),
|
|
512
|
+
),
|
|
513
|
+
"$W",
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
if (Array.isArray(object)) {
|
|
517
|
+
const build = (): unknown[] => {
|
|
518
|
+
const out: unknown[] = [];
|
|
519
|
+
for (let index = 0; index < object.length; index += 1) {
|
|
520
|
+
out.push(encode(object[index], `${at}[${index}]`, depth + 1));
|
|
521
|
+
}
|
|
522
|
+
return out;
|
|
523
|
+
};
|
|
524
|
+
return shared.has(object) ? outline(build, "$") : build();
|
|
525
|
+
}
|
|
526
|
+
if (this.#options.mode === "result" && isAsyncIterable(object)) {
|
|
527
|
+
const id = this.#nextId++;
|
|
528
|
+
this.#watchIterable(id, object, at);
|
|
529
|
+
return `$x${id}`;
|
|
530
|
+
}
|
|
531
|
+
if (isBlob(object)) {
|
|
532
|
+
throw new SerializationError(
|
|
533
|
+
"is a Blob or File; send files inside a FormData, as React's Server Functions take them.",
|
|
534
|
+
at,
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
if (!isPlainObject(object)) {
|
|
538
|
+
if (isAsyncIterable(object)) {
|
|
539
|
+
throw new SerializationError(
|
|
540
|
+
"is an async iterable; only a result may be one, never an argument.",
|
|
541
|
+
at,
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
throw new SerializationError(
|
|
545
|
+
`is ${describe(object)}; only plain objects and the built-ins React's Server Functions accept can be sent.`,
|
|
546
|
+
at,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
const build = (): Record<string, unknown> => {
|
|
550
|
+
const out: Record<string, unknown> = {};
|
|
551
|
+
for (const key of Object.keys(object)) {
|
|
552
|
+
const encoded = encode(
|
|
553
|
+
(object as Record<string, unknown>)[key],
|
|
554
|
+
`${at}${pathKey(key)}`,
|
|
555
|
+
depth + 1,
|
|
556
|
+
);
|
|
557
|
+
Object.defineProperty(out, key, {
|
|
558
|
+
value: encoded,
|
|
559
|
+
enumerable: true,
|
|
560
|
+
writable: true,
|
|
561
|
+
configurable: true,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return out;
|
|
565
|
+
};
|
|
566
|
+
return shared.has(object) ? outline(build, "$") : build();
|
|
567
|
+
};
|
|
568
|
+
const root = encode(value, path, 0);
|
|
569
|
+
for (const { id, blob } of blobs) {
|
|
570
|
+
const bytes = new Uint8Array(await blob.arrayBuffer());
|
|
571
|
+
const file = blob as Blob & { name?: unknown; lastModified?: unknown };
|
|
572
|
+
frames.push({
|
|
573
|
+
c: id,
|
|
574
|
+
v: {
|
|
575
|
+
type: blob.type,
|
|
576
|
+
data: toBase64(bytes),
|
|
577
|
+
...(typeof file.name === "string" ? { name: file.name } : {}),
|
|
578
|
+
...(typeof file.lastModified === "number" ? { lastModified: file.lastModified } : {}),
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
place(root, frames);
|
|
583
|
+
return frames;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
#watchPromise(id: number, promise: PromiseLike<unknown>, path: string): void {
|
|
587
|
+
this.#pending += 1;
|
|
588
|
+
Promise.resolve(promise).then(
|
|
589
|
+
(settled) => {
|
|
590
|
+
this.#serial(() =>
|
|
591
|
+
this.#encodeBatch(settled, path, (json, frames) => frames.push({ p: id, v: json })).catch(
|
|
592
|
+
(error: unknown) => [{ p: id, e: wireError(error) }],
|
|
593
|
+
),
|
|
594
|
+
);
|
|
595
|
+
this.#pending -= 1;
|
|
596
|
+
},
|
|
597
|
+
(error: unknown) => {
|
|
598
|
+
// React's `encodeReply` fails the call when an argument's promise rejects; only a result
|
|
599
|
+
// carries a rejection to the other side.
|
|
600
|
+
if (this.#options.mode === "argument") {
|
|
601
|
+
this.#serial(async () => {
|
|
602
|
+
throw error;
|
|
603
|
+
});
|
|
604
|
+
} else this.#serial(async () => [{ p: id, e: wireError(error) }]);
|
|
605
|
+
this.#pending -= 1;
|
|
606
|
+
},
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
#watchIterable(id: number, iterable: AsyncIterable<unknown>, path: string): void {
|
|
611
|
+
let iterator: AsyncIterator<unknown>;
|
|
612
|
+
try {
|
|
613
|
+
iterator = iterable[Symbol.asyncIterator]();
|
|
614
|
+
} catch (error) {
|
|
615
|
+
this.#serial(async () => [{ i: id, e: wireError(error) }]);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
this.#iterators.add(iterator);
|
|
619
|
+
this.#pending += 1;
|
|
620
|
+
const step = async (): Promise<void> => {
|
|
621
|
+
for (let index = 0; ; index += 1) {
|
|
622
|
+
if (this.#aborted) return;
|
|
623
|
+
let next: IteratorResult<unknown>;
|
|
624
|
+
try {
|
|
625
|
+
next = await iterator.next();
|
|
626
|
+
} catch (error) {
|
|
627
|
+
this.#serial(async () => [{ i: id, e: wireError(error) }]);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (next.done) {
|
|
631
|
+
this.#serial(async () => [{ i: id, end: true }]);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const item = next.value;
|
|
635
|
+
const at = `${path}<item ${index}>`;
|
|
636
|
+
let failed = false;
|
|
637
|
+
this.#serial(() =>
|
|
638
|
+
this.#encodeBatch(item, at, (json, frames) => frames.push({ i: id, v: json })).catch(
|
|
639
|
+
(error: unknown) => {
|
|
640
|
+
failed = true;
|
|
641
|
+
return [{ i: id, e: wireError(error) }];
|
|
642
|
+
},
|
|
643
|
+
),
|
|
644
|
+
);
|
|
645
|
+
// Wait for the item to be written before pulling the next, so a slow reader paces it.
|
|
646
|
+
await this.#queue;
|
|
647
|
+
if (failed) {
|
|
648
|
+
void iterator.return?.()?.catch?.(() => {});
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
void step().finally(() => {
|
|
654
|
+
this.#iterators.delete(iterator);
|
|
655
|
+
this.#pending -= 1;
|
|
656
|
+
this.#settleIfIdle();
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/** The deepest nesting a value may have, so a hostile value can't exhaust the stack. */
|
|
662
|
+
const MAX_DEPTH = 512;
|
|
663
|
+
|
|
664
|
+
/** The objects and arrays `value` reaches more than once (shared, or part of a cycle). */
|
|
665
|
+
function countShared(value: unknown): Set<object> {
|
|
666
|
+
const seen = new Set<object>();
|
|
667
|
+
const shared = new Set<object>();
|
|
668
|
+
const stack: unknown[] = [value];
|
|
669
|
+
while (stack.length > 0) {
|
|
670
|
+
const item = stack.pop();
|
|
671
|
+
if (typeof item !== "object" || item === null) continue;
|
|
672
|
+
if (seen.has(item)) {
|
|
673
|
+
shared.add(item);
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
seen.add(item);
|
|
677
|
+
if (Array.isArray(item)) {
|
|
678
|
+
for (const entry of item) stack.push(entry);
|
|
679
|
+
} else if (item instanceof Map) {
|
|
680
|
+
for (const [key, entry] of item) stack.push(key, entry);
|
|
681
|
+
} else if (item instanceof Set) {
|
|
682
|
+
for (const entry of item) stack.push(entry);
|
|
683
|
+
} else if (Object.getPrototypeOf(item) === Object.prototype) {
|
|
684
|
+
for (const key of Object.keys(item)) stack.push((item as Record<string, unknown>)[key]);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return shared;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Writes `value` completely: every promise settled, so the batches can be sent at once. For
|
|
692
|
+
* arguments, which a browser sends in one request.
|
|
693
|
+
*/
|
|
694
|
+
export async function serializeAll(
|
|
695
|
+
value: unknown,
|
|
696
|
+
options: Omit<ValueWriterOptions, "onBatch">,
|
|
697
|
+
): Promise<Frame[][]> {
|
|
698
|
+
const batches: Frame[][] = [];
|
|
699
|
+
const writer = new ValueWriter({ ...options, onBatch: (batch) => batches.push(batch) });
|
|
700
|
+
await writer.write(value);
|
|
701
|
+
await writer.done;
|
|
702
|
+
return batches;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// ── Reading ────────────────────────────────────────────────────────────────────────────────
|
|
706
|
+
|
|
707
|
+
/** Options for {@link ValueReader}. */
|
|
708
|
+
export interface ValueReaderOptions {
|
|
709
|
+
/** The function a Server Function ID stands for; absent: a reference is refused. */
|
|
710
|
+
readonly serverFunction?: (id: string) => AnyFunction;
|
|
711
|
+
/**
|
|
712
|
+
* `argument`: what a page sent, read where server code runs: no symbols, no async iterables,
|
|
713
|
+
* bigints of at most {@link ARGUMENT_BIGINT_DIGITS_MAX} digits, the writer's own limits. Default
|
|
714
|
+
* `result`: everything the writer writes.
|
|
715
|
+
*/
|
|
716
|
+
readonly mode?: SerializeMode;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
interface Deferred {
|
|
720
|
+
promise: Promise<unknown>;
|
|
721
|
+
resolve(value: unknown): void;
|
|
722
|
+
reject(error: unknown): void;
|
|
723
|
+
settled: boolean;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function deferred(): Deferred {
|
|
727
|
+
let resolve!: (value: unknown) => void;
|
|
728
|
+
let reject!: (error: unknown) => void;
|
|
729
|
+
const promise = new Promise<unknown>((res, rej) => {
|
|
730
|
+
resolve = res;
|
|
731
|
+
reject = rej;
|
|
732
|
+
});
|
|
733
|
+
promise.catch(() => {});
|
|
734
|
+
const entry: Deferred = {
|
|
735
|
+
promise,
|
|
736
|
+
resolve: (value) => {
|
|
737
|
+
entry.settled = true;
|
|
738
|
+
resolve(value);
|
|
739
|
+
},
|
|
740
|
+
reject: (error) => {
|
|
741
|
+
entry.settled = true;
|
|
742
|
+
reject(error);
|
|
743
|
+
},
|
|
744
|
+
settled: false,
|
|
745
|
+
};
|
|
746
|
+
return entry;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/** An async iterable fed by frames: one reader, items buffered until taken. */
|
|
750
|
+
class FedIterable implements AsyncIterable<unknown> {
|
|
751
|
+
readonly #items: unknown[] = [];
|
|
752
|
+
#ended = false;
|
|
753
|
+
#error: unknown;
|
|
754
|
+
#wake: (() => void) | undefined;
|
|
755
|
+
#taken = false;
|
|
756
|
+
|
|
757
|
+
push(value: unknown): void {
|
|
758
|
+
this.#items.push(value);
|
|
759
|
+
this.#wake?.();
|
|
760
|
+
}
|
|
761
|
+
end(): void {
|
|
762
|
+
this.#ended = true;
|
|
763
|
+
this.#wake?.();
|
|
764
|
+
}
|
|
765
|
+
fail(error: unknown): void {
|
|
766
|
+
if (this.#ended) return;
|
|
767
|
+
this.#error = error;
|
|
768
|
+
this.#ended = true;
|
|
769
|
+
this.#wake?.();
|
|
770
|
+
}
|
|
771
|
+
get ended(): boolean {
|
|
772
|
+
return this.#ended;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
[Symbol.asyncIterator](): AsyncIterator<unknown> {
|
|
776
|
+
if (this.#taken) throw new Error("This async iterable can be read once.");
|
|
777
|
+
this.#taken = true;
|
|
778
|
+
return {
|
|
779
|
+
next: async (): Promise<IteratorResult<unknown>> => {
|
|
780
|
+
for (;;) {
|
|
781
|
+
if (this.#items.length > 0) return { done: false, value: this.#items.shift() };
|
|
782
|
+
if (this.#ended) {
|
|
783
|
+
if (this.#error !== undefined) throw this.#error;
|
|
784
|
+
return { done: true, value: undefined };
|
|
785
|
+
}
|
|
786
|
+
await new Promise<void>((resolve) => {
|
|
787
|
+
this.#wake = resolve;
|
|
788
|
+
});
|
|
789
|
+
this.#wake = undefined;
|
|
790
|
+
}
|
|
791
|
+
},
|
|
792
|
+
return: async (): Promise<IteratorResult<unknown>> => {
|
|
793
|
+
this.#ended = true;
|
|
794
|
+
this.#items.length = 0;
|
|
795
|
+
return { done: true, value: undefined };
|
|
796
|
+
},
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const malformed = (what: string): SerializationError =>
|
|
802
|
+
new SerializationError(`The written value is malformed: ${what}.`);
|
|
803
|
+
|
|
804
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
805
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Reads batches back into a value. `root` resolves once the first batch arrived; promises and
|
|
809
|
+
* async iterables in it settle and yield as later batches arrive. A malformed batch throws a
|
|
810
|
+
* {@link SerializationError} from `push`; `fail` rejects everything still open, for a stream that
|
|
811
|
+
* broke before its end.
|
|
812
|
+
*/
|
|
813
|
+
export class ValueReader {
|
|
814
|
+
readonly #options: ValueReaderOptions;
|
|
815
|
+
/** Raw chunks by ID, and what each became. */
|
|
816
|
+
readonly #raw = new Map<number, unknown>();
|
|
817
|
+
readonly #decoded = new Map<number, unknown>();
|
|
818
|
+
readonly #promises = new Map<number, Deferred>();
|
|
819
|
+
readonly #iterables = new Map<number, FedIterable>();
|
|
820
|
+
/** Frames for promises or iterables not referenced yet. */
|
|
821
|
+
readonly #early: AsyncFrame[] = [];
|
|
822
|
+
readonly #root = deferred();
|
|
823
|
+
#hasRoot = false;
|
|
824
|
+
/** The value, once the first batch arrived. */
|
|
825
|
+
readonly root: Promise<unknown> = this.#root.promise;
|
|
826
|
+
|
|
827
|
+
constructor(options: ValueReaderOptions = {}) {
|
|
828
|
+
this.#options = options;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/** Whether every promise and iterable read so far has settled or ended. */
|
|
832
|
+
get complete(): boolean {
|
|
833
|
+
return (
|
|
834
|
+
this.#hasRoot &&
|
|
835
|
+
[...this.#promises.values()].every((entry) => entry.settled) &&
|
|
836
|
+
[...this.#iterables.values()].every((entry) => entry.ended)
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/** Takes one batch. */
|
|
841
|
+
push(batch: unknown): void {
|
|
842
|
+
if (!Array.isArray(batch)) throw malformed("a batch isn't an array");
|
|
843
|
+
const ordered: AsyncFrame[] = [];
|
|
844
|
+
for (const frame of batch) {
|
|
845
|
+
if (!isRecord(frame)) throw malformed("a frame isn't an object");
|
|
846
|
+
if (typeof frame["c"] === "number") {
|
|
847
|
+
if (!Number.isSafeInteger(frame["c"]) || frame["c"] < 0) throw malformed("a chunk ID");
|
|
848
|
+
if (this.#raw.has(frame["c"])) throw malformed(`chunk ${frame["c"]} was written twice`);
|
|
849
|
+
this.#raw.set(frame["c"], frame["v"]);
|
|
850
|
+
} else if (typeof frame["p"] === "number" || typeof frame["i"] === "number") {
|
|
851
|
+
ordered.push(frame as unknown as AsyncFrame);
|
|
852
|
+
} else {
|
|
853
|
+
throw malformed("a frame has no chunk, promise or iterable ID");
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
if (!this.#hasRoot && this.#raw.has(0)) {
|
|
857
|
+
this.#hasRoot = true;
|
|
858
|
+
try {
|
|
859
|
+
this.#root.resolve(this.#decode(this.#raw.get(0), 0));
|
|
860
|
+
} catch (error) {
|
|
861
|
+
this.#root.reject(error);
|
|
862
|
+
throw error;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
for (const frame of ordered) this.#apply(frame);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Ends the value: every frame taken so far is applied, and the value must be complete. Throws a
|
|
870
|
+
* {@link SerializationError} when a promise or iterable it holds is still open or a frame names
|
|
871
|
+
* one the value never refers to, so a reader that has all the batches (a page's arguments)
|
|
872
|
+
* never hands on a promise that can't settle.
|
|
873
|
+
*/
|
|
874
|
+
finish(): void {
|
|
875
|
+
if (!this.#hasRoot) throw malformed("the value has no root");
|
|
876
|
+
for (;;) {
|
|
877
|
+
const before = this.#early.length;
|
|
878
|
+
if (before === 0) break;
|
|
879
|
+
this.#flush();
|
|
880
|
+
if (this.#early.length === before) break;
|
|
881
|
+
}
|
|
882
|
+
if (this.#early.length > 0) throw malformed("a frame names a promise the value doesn't hold");
|
|
883
|
+
if (!this.complete) throw malformed("the value ended before its promises settled");
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/** Rejects every promise and iterable still open: the stream broke. */
|
|
887
|
+
fail(error: unknown): void {
|
|
888
|
+
if (!this.#hasRoot) this.#root.reject(error);
|
|
889
|
+
for (const entry of this.#promises.values()) if (!entry.settled) entry.reject(error);
|
|
890
|
+
for (const entry of this.#iterables.values()) entry.fail(error);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
#apply(frame: AsyncFrame): void {
|
|
894
|
+
if ("p" in frame) {
|
|
895
|
+
const entry = this.#promises.get(frame.p);
|
|
896
|
+
if (!entry) {
|
|
897
|
+
this.#early.push(frame);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
if (entry.settled) throw malformed(`promise ${frame.p} settled twice`);
|
|
901
|
+
if ("e" in frame) entry.reject(new RemoteError(readError(frame.e)));
|
|
902
|
+
else entry.resolve(this.#decode(frame.v, 0));
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
const entry = this.#iterables.get(frame.i);
|
|
906
|
+
if (!entry) {
|
|
907
|
+
this.#early.push(frame);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
if ("e" in frame) entry.fail(new RemoteError(readError(frame.e)));
|
|
911
|
+
else if ("end" in frame) entry.end();
|
|
912
|
+
else entry.push(this.#decode(frame.v, 0));
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
#chunk(id: number): unknown {
|
|
916
|
+
if (!this.#raw.has(id)) throw malformed(`chunk ${id} is missing`);
|
|
917
|
+
return this.#raw.get(id);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
#take(id: number, build: (raw: unknown) => unknown): unknown {
|
|
921
|
+
if (this.#decoded.has(id)) return this.#decoded.get(id);
|
|
922
|
+
const value = build(this.#chunk(id));
|
|
923
|
+
if (!this.#decoded.has(id)) this.#decoded.set(id, value);
|
|
924
|
+
return value;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
#flush(): void {
|
|
928
|
+
const early = this.#early.splice(0);
|
|
929
|
+
for (const frame of early) this.#apply(frame);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
#decode(json: unknown, depth: number): unknown {
|
|
933
|
+
if (depth > MAX_DEPTH) throw malformed("the value nests too deeply");
|
|
934
|
+
if (json === null || typeof json === "boolean") return json;
|
|
935
|
+
if (typeof json === "number") {
|
|
936
|
+
if (!Number.isFinite(json)) throw malformed("a number isn't finite");
|
|
937
|
+
return json;
|
|
938
|
+
}
|
|
939
|
+
if (typeof json === "string") return this.#string(json, depth);
|
|
940
|
+
if (Array.isArray(json)) return json.map((item) => this.#decode(item, depth + 1));
|
|
941
|
+
if (isRecord(json)) {
|
|
942
|
+
const out: Record<string, unknown> = {};
|
|
943
|
+
for (const key of Object.keys(json)) {
|
|
944
|
+
Object.defineProperty(out, key, {
|
|
945
|
+
value: this.#decode(json[key], depth + 1),
|
|
946
|
+
enumerable: true,
|
|
947
|
+
writable: true,
|
|
948
|
+
configurable: true,
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
return out;
|
|
952
|
+
}
|
|
953
|
+
throw malformed("a value isn't JSON");
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
#id(text: string, at: number): number {
|
|
957
|
+
const id = Number(text.slice(at));
|
|
958
|
+
if (!/^[0-9]+$/.test(text.slice(at)) || !Number.isSafeInteger(id)) {
|
|
959
|
+
throw malformed(`the reference ${JSON.stringify(text.slice(0, 40))}`);
|
|
960
|
+
}
|
|
961
|
+
return id;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
#string(text: string, depth: number): unknown {
|
|
965
|
+
if (!text.startsWith("$")) return text;
|
|
966
|
+
if (text.startsWith("$$")) return text.slice(1);
|
|
967
|
+
switch (text) {
|
|
968
|
+
case "$undefined":
|
|
969
|
+
return undefined;
|
|
970
|
+
case "$NaN":
|
|
971
|
+
return Number.NaN;
|
|
972
|
+
case "$Infinity":
|
|
973
|
+
return Number.POSITIVE_INFINITY;
|
|
974
|
+
case "$-Infinity":
|
|
975
|
+
return Number.NEGATIVE_INFINITY;
|
|
976
|
+
case "$-0":
|
|
977
|
+
return -0;
|
|
978
|
+
case "$DInvalid":
|
|
979
|
+
return new Date(Number.NaN);
|
|
980
|
+
}
|
|
981
|
+
const tag = text[1];
|
|
982
|
+
switch (tag) {
|
|
983
|
+
case "n":
|
|
984
|
+
if (!/^-?[0-9]+$/.test(text.slice(2))) throw malformed("a bigint");
|
|
985
|
+
if (
|
|
986
|
+
this.#options.mode === "argument" &&
|
|
987
|
+
text.replace("-", "").length - 2 > ARGUMENT_BIGINT_DIGITS_MAX
|
|
988
|
+
) {
|
|
989
|
+
throw malformed(`a bigint argument has more than ${ARGUMENT_BIGINT_DIGITS_MAX} digits`);
|
|
990
|
+
}
|
|
991
|
+
return BigInt(text.slice(2));
|
|
992
|
+
case "S":
|
|
993
|
+
// Symbol.for keeps every name it is given for the life of the process.
|
|
994
|
+
if (this.#options.mode === "argument") throw malformed("an argument is a symbol");
|
|
995
|
+
return Symbol.for(text.slice(2));
|
|
996
|
+
case "D": {
|
|
997
|
+
const date = new Date(text.slice(2));
|
|
998
|
+
if (Number.isNaN(date.getTime())) throw malformed("a date");
|
|
999
|
+
return date;
|
|
1000
|
+
}
|
|
1001
|
+
case "h": {
|
|
1002
|
+
const id = text.slice(2);
|
|
1003
|
+
if (!this.#options.serverFunction) throw malformed(`a Server Function reference ${id}`);
|
|
1004
|
+
return this.#options.serverFunction(id);
|
|
1005
|
+
}
|
|
1006
|
+
case "@": {
|
|
1007
|
+
const id = this.#id(text, 2);
|
|
1008
|
+
let entry = this.#promises.get(id);
|
|
1009
|
+
if (!entry) {
|
|
1010
|
+
entry = deferred();
|
|
1011
|
+
this.#promises.set(id, entry);
|
|
1012
|
+
queueMicrotask(() => this.#flushSafely());
|
|
1013
|
+
}
|
|
1014
|
+
return entry.promise;
|
|
1015
|
+
}
|
|
1016
|
+
case "x": {
|
|
1017
|
+
if (this.#options.mode === "argument") throw malformed("an argument is an async iterable");
|
|
1018
|
+
const id = this.#id(text, 2);
|
|
1019
|
+
let entry = this.#iterables.get(id);
|
|
1020
|
+
if (!entry) {
|
|
1021
|
+
entry = new FedIterable();
|
|
1022
|
+
this.#iterables.set(id, entry);
|
|
1023
|
+
queueMicrotask(() => this.#flushSafely());
|
|
1024
|
+
}
|
|
1025
|
+
return entry;
|
|
1026
|
+
}
|
|
1027
|
+
case "Q": {
|
|
1028
|
+
const id = this.#id(text, 2);
|
|
1029
|
+
if (this.#decoded.has(id)) return this.#decoded.get(id);
|
|
1030
|
+
const map = new Map<unknown, unknown>();
|
|
1031
|
+
this.#decoded.set(id, map);
|
|
1032
|
+
const raw = this.#chunk(id);
|
|
1033
|
+
if (!Array.isArray(raw)) throw malformed("a map");
|
|
1034
|
+
for (const pair of raw) {
|
|
1035
|
+
if (!Array.isArray(pair) || pair.length !== 2) throw malformed("a map entry");
|
|
1036
|
+
map.set(this.#decode(pair[0], depth + 1), this.#decode(pair[1], depth + 1));
|
|
1037
|
+
}
|
|
1038
|
+
return map;
|
|
1039
|
+
}
|
|
1040
|
+
case "W": {
|
|
1041
|
+
const id = this.#id(text, 2);
|
|
1042
|
+
if (this.#decoded.has(id)) return this.#decoded.get(id);
|
|
1043
|
+
const set = new Set<unknown>();
|
|
1044
|
+
this.#decoded.set(id, set);
|
|
1045
|
+
const raw = this.#chunk(id);
|
|
1046
|
+
if (!Array.isArray(raw)) throw malformed("a set");
|
|
1047
|
+
for (const item of raw) set.add(this.#decode(item, depth + 1));
|
|
1048
|
+
return set;
|
|
1049
|
+
}
|
|
1050
|
+
case "A":
|
|
1051
|
+
return this.#take(this.#id(text, 2), (raw) => {
|
|
1052
|
+
if (typeof raw !== "string") throw malformed("an ArrayBuffer");
|
|
1053
|
+
const bytes = fromBase64(raw);
|
|
1054
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
1055
|
+
});
|
|
1056
|
+
case "T":
|
|
1057
|
+
return this.#take(this.#id(text, 2), (raw) => {
|
|
1058
|
+
if (!Array.isArray(raw) || typeof raw[1] !== "string") throw malformed("a typed array");
|
|
1059
|
+
const make = TYPED_CONSTRUCTORS[raw[0] as TypedKind];
|
|
1060
|
+
if (!make) throw malformed("a typed array's kind");
|
|
1061
|
+
const bytes = fromBase64(raw[1]);
|
|
1062
|
+
const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
1063
|
+
try {
|
|
1064
|
+
return make(buffer as ArrayBuffer);
|
|
1065
|
+
} catch {
|
|
1066
|
+
throw malformed("a typed array's length");
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
case "B":
|
|
1070
|
+
return this.#take(this.#id(text, 2), (raw) => {
|
|
1071
|
+
if (
|
|
1072
|
+
!isRecord(raw) ||
|
|
1073
|
+
typeof raw["data"] !== "string" ||
|
|
1074
|
+
typeof raw["type"] !== "string"
|
|
1075
|
+
) {
|
|
1076
|
+
throw malformed("a file");
|
|
1077
|
+
}
|
|
1078
|
+
const bytes = fromBase64(raw["data"]);
|
|
1079
|
+
const parts = [bytes as Uint8Array<ArrayBuffer>];
|
|
1080
|
+
if (typeof raw["name"] === "string" && typeof File !== "undefined") {
|
|
1081
|
+
return new File(parts, raw["name"], {
|
|
1082
|
+
type: raw["type"],
|
|
1083
|
+
...(typeof raw["lastModified"] === "number"
|
|
1084
|
+
? { lastModified: raw["lastModified"] }
|
|
1085
|
+
: {}),
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
return new Blob(parts, { type: raw["type"] });
|
|
1089
|
+
});
|
|
1090
|
+
case "K":
|
|
1091
|
+
return this.#take(this.#id(text, 2), (raw) => {
|
|
1092
|
+
if (!Array.isArray(raw)) throw malformed("a FormData");
|
|
1093
|
+
const form = new FormData();
|
|
1094
|
+
for (const entry of raw) {
|
|
1095
|
+
if (!Array.isArray(entry) || typeof entry[0] !== "string")
|
|
1096
|
+
throw malformed("a FormData entry");
|
|
1097
|
+
const value = this.#decode(entry[1], depth + 1);
|
|
1098
|
+
if (typeof value === "string") form.append(entry[0], value);
|
|
1099
|
+
else if (isBlob(value)) form.append(entry[0], value);
|
|
1100
|
+
else throw malformed("a FormData value");
|
|
1101
|
+
}
|
|
1102
|
+
return form;
|
|
1103
|
+
});
|
|
1104
|
+
default: {
|
|
1105
|
+
// `$<id>`: a shared object or array, outlined.
|
|
1106
|
+
const id = this.#id(text, 1);
|
|
1107
|
+
if (this.#decoded.has(id)) return this.#decoded.get(id);
|
|
1108
|
+
const raw = this.#chunk(id);
|
|
1109
|
+
if (Array.isArray(raw)) {
|
|
1110
|
+
const array: unknown[] = [];
|
|
1111
|
+
this.#decoded.set(id, array);
|
|
1112
|
+
for (const item of raw) array.push(this.#decode(item, depth + 1));
|
|
1113
|
+
return array;
|
|
1114
|
+
}
|
|
1115
|
+
if (!isRecord(raw)) throw malformed("a shared object");
|
|
1116
|
+
const object: Record<string, unknown> = {};
|
|
1117
|
+
this.#decoded.set(id, object);
|
|
1118
|
+
for (const key of Object.keys(raw)) {
|
|
1119
|
+
Object.defineProperty(object, key, {
|
|
1120
|
+
value: this.#decode(raw[key], depth + 1),
|
|
1121
|
+
enumerable: true,
|
|
1122
|
+
writable: true,
|
|
1123
|
+
configurable: true,
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
return object;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
#flushSafely(): void {
|
|
1132
|
+
try {
|
|
1133
|
+
this.#flush();
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
this.fail(error);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function readError(value: unknown): WireError {
|
|
1141
|
+
if (!isRecord(value) || typeof value["message"] !== "string") throw malformed("an error");
|
|
1142
|
+
return {
|
|
1143
|
+
message: value["message"],
|
|
1144
|
+
...(typeof value["code"] === "string" ? { code: value["code"] } : {}),
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/**
|
|
1149
|
+
* Reads complete batches back into a value: every promise in it settled from the batches, every
|
|
1150
|
+
* async iterable replaying its items. For arguments and stored results.
|
|
1151
|
+
*/
|
|
1152
|
+
export async function deserializeAll(
|
|
1153
|
+
batches: readonly unknown[],
|
|
1154
|
+
options: ValueReaderOptions = {},
|
|
1155
|
+
): Promise<unknown> {
|
|
1156
|
+
const reader = new ValueReader(options);
|
|
1157
|
+
for (const batch of batches) reader.push(batch);
|
|
1158
|
+
// Arguments arrive whole: one that can't be finished is refused before anything runs.
|
|
1159
|
+
if (options.mode === "argument") reader.finish();
|
|
1160
|
+
if (!reader.complete) {
|
|
1161
|
+
// Settle whatever the batches never settled: nothing more will come.
|
|
1162
|
+
queueMicrotask(() => reader.fail(new SerializationError("The written value ended early.")));
|
|
1163
|
+
}
|
|
1164
|
+
return reader.root;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* Checks a page's argument batches without decoding them, before they reach server code: no
|
|
1169
|
+
* symbol (`$S`), no async iterable (`$x`), no bigint of more than
|
|
1170
|
+
* {@link ARGUMENT_BIGINT_DIGITS_MAX} digits. The supervisor runs it so such arguments are refused
|
|
1171
|
+
* before any worker sees them (Opus review 1); the worker's reader refuses them again.
|
|
1172
|
+
*
|
|
1173
|
+
* @throws SerializationError naming what was found.
|
|
1174
|
+
*/
|
|
1175
|
+
export function checkArgumentFrames(batches: readonly unknown[]): void {
|
|
1176
|
+
const check = (text: string): void => {
|
|
1177
|
+
if (text.length < 2 || text[0] !== "$" || text[1] === "$") return;
|
|
1178
|
+
if (text[1] === "S") throw malformed("an argument is a symbol");
|
|
1179
|
+
if (text[1] === "x") throw malformed("an argument is an async iterable");
|
|
1180
|
+
if (text[1] === "n" && text.replace("-", "").length - 2 > ARGUMENT_BIGINT_DIGITS_MAX) {
|
|
1181
|
+
throw malformed(`a bigint argument has more than ${ARGUMENT_BIGINT_DIGITS_MAX} digits`);
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
const scan = (value: unknown, visit: (text: string) => void): void => {
|
|
1185
|
+
const stack: unknown[] = [value];
|
|
1186
|
+
while (stack.length > 0) {
|
|
1187
|
+
const item = stack.pop();
|
|
1188
|
+
if (typeof item === "string") visit(item);
|
|
1189
|
+
else if (Array.isArray(item)) for (const entry of item) stack.push(entry);
|
|
1190
|
+
else if (item !== null && typeof item === "object") {
|
|
1191
|
+
for (const entry of Object.values(item)) stack.push(entry);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
// A FormData chunk's entry names are written as they are, never escaped: only its values are
|
|
1196
|
+
// encoded strings, so those chunks are checked value by value.
|
|
1197
|
+
const forms = new Set<number>();
|
|
1198
|
+
scan(batches, (text) => {
|
|
1199
|
+
const match = /^\$K([0-9]+)$/.exec(text);
|
|
1200
|
+
if (match) forms.add(Number(match[1]));
|
|
1201
|
+
});
|
|
1202
|
+
for (const batch of batches) {
|
|
1203
|
+
if (!Array.isArray(batch)) continue;
|
|
1204
|
+
for (const frame of batch) {
|
|
1205
|
+
const record = frame as { c?: unknown; v?: unknown } | null;
|
|
1206
|
+
if (
|
|
1207
|
+
record !== null &&
|
|
1208
|
+
typeof record === "object" &&
|
|
1209
|
+
typeof record.c === "number" &&
|
|
1210
|
+
forms.has(record.c) &&
|
|
1211
|
+
Array.isArray(record.v)
|
|
1212
|
+
) {
|
|
1213
|
+
for (const entry of record.v) scan(Array.isArray(entry) ? entry[1] : entry, check);
|
|
1214
|
+
} else scan(frame, check);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|