From 1ed7241ebc253dc510a1f3996fc707b7ae5bb9c3 Mon Sep 17 00:00:00 2001 From: Lann Martin Date: Mon, 7 Sep 2026 19:07:38 -0400 Subject: [PATCH] Receiver: ts-proto generated codec replaces the hand-rolled reader/writer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The JS receiver decoded frames with a hand-written protobuf Reader and a per-op tag switch (receiver/src/proto.ts + frames.ts), and encoded event payloads with a hand-written Writer. docs/design.md said the codec was pbf; it never was — the hand-rolled codec was chosen silently in a dispatch and the design record was not updated. Measured over the bench producers' real captured streams (19 of them, every op; harness kept untracked under bench/decoders): ts-proto generated readers over @bufbuild/protobuf/wire decode in ~0.7x the time of the hand-rolled decoder, and a hand-driven hybrid on the same runtime is no faster. protobuf.js generated is 4x slower. pbf is fastest but silently wrong on sint64 above 2^53, has no bounds checks, and cannot represent proto3 `optional` presence (ns/parent/anchor). So: generated readers and writers everywhere, no hand-written wire code. Codegen is `buf` from npm under Deno spawning the ts-proto plugin via `deno run` — no Node, no protoc (`just proto-ts`); generated code is committed and drift-gated in `just check`. FrameSink and its types are unchanged. Framing keeps partial-frame buffering, MAX_FRAME_BYTES, and the wait-vs-malformed distinction for a truncated length prefix; each frame is bounded by a post-decode position check, so a nested length that overruns the frame (or a decode that stops early on a zero tag) is a malformed frame in both modes. Strict mode walks the generated messages' `_unknownFields` with the same message-name errors. sint64 outside Number.isSafeInteger now throws instead of losing bits. The unanchored `gen/` .gitignore rule (for host/desktop/gen) is anchored so receiver/src/gen can be tracked. --- .gitignore | 3 +- buf.gen.yaml | 16 + buf.yaml | 3 + deno.json | 7 + deno.lock | 93 +- docs/design.md | 43 +- justfile | 22 +- receiver/src/events.ts | 208 +- receiver/src/frames.ts | 854 +++----- receiver/src/gen/stream-dom-events.ts | 2631 +++++++++++++++++++++++++ receiver/src/gen/stream-dom.ts | 2307 ++++++++++++++++++++++ receiver/src/mod.ts | 1 - receiver/src/proto.ts | 313 --- receiver/tests/driver_test.ts | 213 +- receiver/tests/events_test.ts | 32 +- receiver/tests/frames_test.ts | 53 +- receiver/tests/hostile_test.ts | 333 ++-- receiver/tests/policy_desktop_test.ts | 120 +- receiver/tests/policy_test.ts | 109 +- receiver/tests/proto_test.ts | 125 -- receiver/tests/wire.ts | 25 + 21 files changed, 5946 insertions(+), 1565 deletions(-) create mode 100644 buf.gen.yaml create mode 100644 buf.yaml create mode 100644 receiver/src/gen/stream-dom-events.ts create mode 100644 receiver/src/gen/stream-dom.ts delete mode 100644 receiver/src/proto.ts delete mode 100644 receiver/tests/proto_test.ts create mode 100644 receiver/tests/wire.ts diff --git a/.gitignore b/.gitignore index 0ee1181..a3652b8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,8 @@ playwright-report/ bench/results/ bench/tachometer.json # Tauri's generated capability schemas (host/desktop/gen/schemas/...). -gen/ +# Anchored: receiver/src/gen/ is committed generated protobuf code. +host/desktop/gen/ # Regenerated by tauri-build (AppManifest::commands) on every build. host/desktop/permissions/autogenerated/ diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..5b1160b --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,16 @@ +version: v2 +plugins: + - local: ["deno", "run", "-A", "npm:ts-proto@2.12.2/protoc-gen-ts_proto"] + out: receiver/src/gen + opt: + - forceLong=number + - oneof=unions-value + - outputJsonMethods=false + - outputPartialMethods=false + - outputClientImpl=false + - outputServices=false + - esModuleInterop=true + - importSuffix=.ts + - unknownFields=true + - env=browser + - annotateFilesWithVersion=false diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..ba5ddf5 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,3 @@ +version: v2 +modules: + - path: proto diff --git a/deno.json b/deno.json index da64242..eaba011 100644 --- a/deno.json +++ b/deno.json @@ -7,6 +7,7 @@ "@polyengine/translator": "jsr:@polyengine/translator@^0.6.2", "@polyengine/protocol": "jsr:@polyengine/protocol@^0.3.1", "@polyengine/wasi": "jsr:@polyengine/wasi@^0.6.2", + "@bufbuild/protobuf/wire": "npm:@bufbuild/protobuf@^2.14.1/wire", "@remote-dom/core": "npm:@remote-dom/core@^1.11.1", "@remote-dom/core/receivers": "npm:@remote-dom/core@^1.11.1/receivers", "@remote-dom/core/elements": "npm:@remote-dom/core@^1.11.1/elements", @@ -22,6 +23,12 @@ "lib": ["deno.ns", "dom", "dom.iterable", "esnext"], "strict": true }, + "fmt": { + "exclude": ["receiver/src/gen/"] + }, + "lint": { + "exclude": ["receiver/src/gen/"] + }, "tasks": { "check": "deno check receiver/src receiver/tests web/build.ts web/translate.ts web/entry.ts web/bench.ts web/e2e bench/tachometer.ts bench/run.ts bench/wire.ts host/desktop/ui host/desktop/build.ts host/desktop/e2e", "test": "deno test --allow-read=. receiver/tests/" diff --git a/deno.lock b/deno.lock index 1da1689..14333e7 100644 --- a/deno.lock +++ b/deno.lock @@ -24,11 +24,14 @@ "jsr:@std/path@^1.1.5": "1.1.6", "jsr:@std/path@^1.1.6": "1.1.6", "jsr:@std/streams@^1.1.2": "1.1.2", + "npm:@bufbuild/buf@1.72.0": "1.72.0", + "npm:@bufbuild/protobuf@^2.14.1": "2.14.1", "npm:@remote-dom/core@^1.11.1": "1.11.1", "npm:@tauri-apps/api@2": "2.11.1", "npm:linkedom@0.18": "0.18.13", "npm:playwright@1.58": "1.58.2", - "npm:playwright@^1.58.0": "1.58.2" + "npm:playwright@^1.58.0": "1.58.2", + "npm:ts-proto@2.12.2": "2.12.2" }, "jsr": { "@polyengine/protocol@0.3.1": { @@ -111,6 +114,58 @@ } }, "npm": { + "@bufbuild/buf-darwin-arm64@1.72.0": { + "integrity": "sha512-rKHRvjwAThapxIoOn92vIoTjYSz5FmRemDRLU4BYT4T6QWMEC13PM3/pPnqVgsNKZ5aW7iYDm9ztnisEqSi5yA==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@bufbuild/buf-darwin-x64@1.72.0": { + "integrity": "sha512-4TQ1AGft8sGspNg9NMsEjsKKis7nGaVV8tZLnNa3cKUBmx22gwOnB6VRhgKWwjf+BDqr85lUEzQ6wHCboNUutg==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@bufbuild/buf-linux-aarch64@1.72.0": { + "integrity": "sha512-cbIsUcgM5bHhbZWcDaAXqaYOAi8N0c0u+NiDydwVmZ04Et3s1EZ3TDqfQDRzwvoBPDP+lsO6YuTRXX6nI28x4w==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@bufbuild/buf-linux-armv7@1.72.0": { + "integrity": "sha512-v/bXVsFL8YNm2HgosGb9r3+nAt4jQiUc3r3JipYuiVY3DAJZAjoEvcak6/BkxQMTEQz9Zb8gRRlule9IFkbc5g==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@bufbuild/buf-linux-x64@1.72.0": { + "integrity": "sha512-4xHGXEjqFxo1wX1zMGq4CzhYt5++nrj4C7k30j+YmGtvqCnipfdSe+V6kknBYRfYswVZEUwUbQOh6pnMTcGcrA==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@bufbuild/buf-win32-arm64@1.72.0": { + "integrity": "sha512-WH7ClsoB9A0e/5fFhx0DLqLzillYPRdHBhlwzihgvjGci0bBdyJVHSQGf0B9uspCMU6sn6W/N1S9/2vvQBNMug==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@bufbuild/buf-win32-x64@1.72.0": { + "integrity": "sha512-X3eWqFzhDmu8CYQZz+Fu7i+PgH+yUl8UwJ5+x+bhZRYAIdcijikthodk60c5u/qq42m1Z2XAnAGyp/mTf7IffA==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@bufbuild/buf@1.72.0": { + "integrity": "sha512-BwBKTX/WXkhAhqWJGrEKnqU03/4tK1O0OozSlwUMBCOEo8pLL3xu3M24RT3+umExEeM0wjlANO6axqGWMqtt4Q==", + "optionalDependencies": [ + "@bufbuild/buf-darwin-arm64", + "@bufbuild/buf-darwin-x64", + "@bufbuild/buf-linux-aarch64", + "@bufbuild/buf-linux-armv7", + "@bufbuild/buf-linux-x64", + "@bufbuild/buf-win32-arm64", + "@bufbuild/buf-win32-x64" + ], + "scripts": true, + "bin": true + }, + "@bufbuild/protobuf@2.14.1": { + "integrity": "sha512-agRJn3+EJDUe8AvxTx/LnHA/GErvLE62pSaSk7+MwFOtOv8eWBu/qCq2qoZjBjVZ3C2aiFJCveuSs17KMkYGOw==" + }, "@remote-dom/core@1.11.1": { "integrity": "sha512-0v29Q7F/iunb2cJm3E7r780X6jcPEFEgetSGisgRssJuoP9217yC1MKLVFNdXRmJYKX+oaugQKqaD2m8XA9dTQ==", "dependencies": [ @@ -127,6 +182,9 @@ "boolbase@2.0.0": { "integrity": "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==" }, + "case-anything@2.1.13": { + "integrity": "sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==" + }, "css-select@7.0.0": { "integrity": "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==", "dependencies": [ @@ -143,6 +201,10 @@ "cssom@0.5.0": { "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==" }, + "detect-libc@1.0.3": { + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "bin": true + }, "dom-serializer@2.0.0": { "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dependencies": [ @@ -193,6 +255,12 @@ "domhandler@6.0.1" ] }, + "dprint-node@1.0.8": { + "integrity": "sha512-iVKnUtYfGrYcW1ZAlfR/F59cUVL8QIhWoBJoSjkkdua/dkWIgjZfiLMeTjiB06X0ZLkQ0M2C1VbUj/CxkIf1zg==", + "dependencies": [ + "detect-libc" + ] + }, "entities@4.5.0": { "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" }, @@ -252,6 +320,28 @@ ], "bin": true }, + "ts-poet@6.12.0": { + "integrity": "sha512-xo+iRNMWqyvXpFTaOAvLPA5QAWO6TZrSUs5s4Odaya3epqofBu/fMLHEWl8jPmjhA0s9sgj9sNvF1BmaQlmQkA==", + "dependencies": [ + "dprint-node" + ] + }, + "ts-proto-descriptors@2.1.0": { + "integrity": "sha512-S5EZYEQ6L9KLFfjSRpZWDIXDV/W7tAj8uW7pLsihIxyr62EAVSiKuVPwE8iWnr849Bqa53enex1jhDUcpgquzA==", + "dependencies": [ + "@bufbuild/protobuf" + ] + }, + "ts-proto@2.12.2": { + "integrity": "sha512-osbffME+UulBWYF+dNhOzQqCTxb43BsAIfGwPaCyYp47HJS/F83mR4OUGuknc5VGR6jYBcXxPdIUe98/H7KhRg==", + "dependencies": [ + "@bufbuild/protobuf", + "case-anything", + "ts-poet", + "ts-proto-descriptors" + ], + "bin": true + }, "uhyphen@0.2.0": { "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==" } @@ -266,6 +356,7 @@ "jsr:@std/fs@1", "jsr:@std/http@1", "jsr:@std/path@1", + "npm:@bufbuild/protobuf@^2.14.1", "npm:@remote-dom/core@^1.11.1", "npm:@tauri-apps/api@2", "npm:linkedom@0.18", diff --git a/docs/design.md b/docs/design.md index e9e7215..1fafdc5 100644 --- a/docs/design.md +++ b/docs/design.md @@ -190,27 +190,32 @@ draft, for four things it could not offer cheaply: bump. Open questions 9 and 10 will touch existing ops. - **Tooling.** `protoc --decode` and `buf` read the corpus; the frame-to-text decoder the corpus needs is stock. -- **Generated codecs** in every producer language (prost, pbf, +- **Generated codecs** in every producer language (prost, ts-proto, protobuf-go, Kotlin) instead of ~20 hand-written layouts per language, forever. - **Events in the same schema.** The remote tier needed a second hand-rolled encoding for event payloads; now it is the same file. The cost is decode speed with *generated* readers, which build an object -per message — roughly the cost profile of the typed lift declined above. -pbf (mapbox) removes it: alongside `.proto`-generated readers it exposes -`readFields((tag, obj, pbf) => …)` with `readVarint` / `readString` / -`skip`, so the hot structural ops are decoded with a switch on field tag -straight into DOM calls, no intermediate object, while cold ops -(`register-template`, nested messages) use the generated readers. Same -`.proto`, same library, ~3 KB; built for Mapbox vector tiles, which is the -same shape of problem. Its writer side (`writeVarintField`, -`writeStringField`, `writeMessage`) gives the JS producer adapters and the -remote-dom transcoder an allocation-light encoder. protobuf.js has the -equivalent `Reader` / `pbjs` split and is the fallback if pbf's codegen -proves too thin; protobuf-es or ts-proto only if generated TypeScript -types are wanted, since pbf emits JS with JSDoc. Rust layers the same way: -prost generated code by default, `prost::encoding`'s public +per message. Measured, not assumed: `bench/decoders` (an untracked +harness) replays the bench producers' real captured streams — 19 of them, +every op the producers emit — through each candidate JS codec against the +same `FrameSink`. Generated readers won. ts-proto over +`@bufbuild/protobuf/wire` decodes at ~0.7x the time of the hand-rolled +tag-switch decoder it replaced, and a hand-driven hybrid over the same +runtime was no faster. protobuf.js generated is 4x slower. pbf is the +fastest arm and unusable: it silently loses precision on `sint64` above +2^53, has no bounds checks, and cannot represent proto3 `optional` +presence, which this schema uses for `ns`, `parent` and `anchor`. + +So: the JS receiver uses ts-proto generated readers *and* writers (event +payloads included) over `@bufbuild/protobuf/wire`, and contains no +hand-written wire code. A hand path returns only if profiling the real +receiver singles out an op. Codegen runs `buf` from npm under Deno, which +spawns the ts-proto plugin through `deno run` — no Node, no protoc +(`just proto-ts`). The generated code is committed and drift-gated: +`just check` regenerates into a temp dir and diffs. Rust layers the same +way: prost generated code by default, `prost::encoding`'s public `encode_varint` / `encode_key` for a hand-written hot encoder if a profile asks for one. @@ -1221,10 +1226,10 @@ event families beyond mouse/keyboard/form, files and `DataTransfer`. 5. **Resync.** `reset` semantics and whether a full snapshot is a special batch or the normal initial-mount batch replayed. Also the id-space exhaustion path, since ids are never reused. -6. **Codec tiers.** Whether the pbf hybrid decode is needed at all, or - generated readers are fast enough: measure both on the corpus before - writing the hand-switch. Whether generated TypeScript types (ts-proto, - protobuf-es) are worth a second toolchain beside pbf. +6. **Codec tiers.** *Settled by measurement* (`bench/decoders`, see + "Encoding"): generated readers are the codec. ts-proto over + `@bufbuild/protobuf/wire` beat the hand-rolled decoder and the hybrids; + a hand path only if profiling the real receiver demands one. 7. **Conformance corpus.** Recorded length-delimited `Frame` streams (`.pb`) plus the event payloads that answer them, as the shared test vector across adapters × transports; readable with stock protobuf diff --git a/justfile b/justfile index 0500be6..5fde710 100644 --- a/justfile +++ b/justfile @@ -7,12 +7,32 @@ default: check test # for the wasm target, the native host workspace; TS: receiver + web + # desktop UI). Depends on desktop-ui because tauri's generate_context! # refuses to compile without the frontendDist directory present. -check: desktop-ui +check: desktop-ui proto-drift cargo clippy --workspace --target wasm32-wasip2 -- -D warnings cargo clippy --manifest-path guests/web-sys/Cargo.toml --workspace --target wasm32-wasip2 -- -D warnings cargo clippy --manifest-path host/Cargo.toml --workspace -- -D warnings deno task check +# Regenerate the receiver's protobuf readers/writers from proto/ into +# receiver/src/gen/. `buf` runs from npm under Deno and spawns the ts-proto +# plugin through `deno run`, so this needs no Node and no protoc. +proto-ts: + deno run -A npm:@bufbuild/buf@1.72.0 generate + +# Drift gate for the committed generated code: regenerate into a temp dir +# and compare. `buf generate -o ` reproduces the plugin's `out` path +# under it, so the comparison is /receiver/src/gen vs receiver/src/gen. +proto-drift: + #!/usr/bin/env bash + set -euo pipefail + tmp=$(mktemp -d) + trap 'rm -rf "$tmp"' EXIT + deno run -A npm:@bufbuild/buf@1.72.0 generate --template buf.gen.yaml -o "$tmp" + if ! diff -r "$tmp/receiver/src/gen" receiver/src/gen; then + echo "receiver/src/gen is out of date with proto/ — run \`just proto-ts\`" >&2 + exit 1 + fi + # Native unit tests (encoder, transcoder fixtures) + receiver tests + the # wasmtime host, whose integration test runs the TodoMVC component. test: host-component diff --git a/receiver/src/events.ts b/receiver/src/events.ts index 4ad8a8c..b3d1308 100644 --- a/receiver/src/events.ts +++ b/receiver/src/events.ts @@ -1,62 +1,32 @@ // `handle-event` payload family selection and encoding. -// proto/stream-dom-events.proto is normative for every field number below; -// only the families this bring-up receiver's mount layer wires up are -// implemented (docs/design.md "Events": the family the receiver chooses by -// event name) — mouse, keyboard, form, navigation. Every other name gets the empty -// payload (`EventPayload` with no `family` case set), which is legal per -// the proto's header comment ("No case set is the empty payload — focus, -// selection, ... the event name ... already says everything those -// carry"). - -import { Writer } from "./proto.ts"; - -const EVENT_PAYLOAD_MOUSE = 1; -const EVENT_PAYLOAD_KEYBOARD = 2; -const EVENT_PAYLOAD_FORM = 3; -const EVENT_PAYLOAD_NAVIGATION = 14; - -const MOUSE_CLIENT_X = 1; -const MOUSE_CLIENT_Y = 2; -const MOUSE_PAGE_X = 3; -const MOUSE_PAGE_Y = 4; -const MOUSE_SCREEN_X = 5; -const MOUSE_SCREEN_Y = 6; -const MOUSE_OFFSET_X = 7; -const MOUSE_OFFSET_Y = 8; -const MOUSE_BUTTON = 9; -const MOUSE_PRIMARY = 10; -const MOUSE_SECONDARY = 11; -const MOUSE_AUXILIARY = 12; -const MOUSE_BACK = 13; -const MOUSE_FORWARD = 14; -const MOUSE_MODIFIERS = 15; -// MOUSE_RELATED_TARGET (16) is not populated: mapping a native +// proto/stream-dom-events.proto is normative; the generated writers in +// `src/gen/stream-dom-events.ts` carry every field number, so this module +// only decides which family an event name belongs to and which DOM +// properties fill it. Only the families this bring-up receiver's mount +// layer wires up are implemented (docs/design.md "Events": the family the +// receiver chooses by event name) — mouse, keyboard, form, navigation. +// Every other name gets the empty payload (`EventPayload` with no `family` +// case set), which is legal per the proto's header comment ("No case set +// is the empty payload — focus, selection, ... the event name ... already +// says everything those carry"). + +import { + EventPayload, + type FormData as FormDataMessage, + type FormField, + type KeyboardData, + type Modifiers, + type MouseData, + type NavigationData, +} from "./gen/stream-dom-events.ts"; + +// `MouseData.related_target` (16) is not populated: mapping a native // `relatedTarget` Node to a producer node id needs the transcoder's // reverse lookup (`RemoteDomTranscoder.producerIdForRid`/`remoteId`), // which this module's `(name, ev)` signature has no access to. Left // absent — a real gap, flagged in the track report rather than plumbed // through here speculatively. -const MODIFIERS_ALT = 1; -const MODIFIERS_CTRL = 2; -const MODIFIERS_META = 3; -const MODIFIERS_SHIFT = 4; - -const KEYBOARD_KEY = 1; -const KEYBOARD_CODE = 2; -const KEYBOARD_LOCATION = 3; -const KEYBOARD_REPEAT = 4; -const KEYBOARD_IS_COMPOSING = 5; -const KEYBOARD_MODIFIERS = 6; - -const FORM_VALUE = 1; -const FORM_CHECKED = 2; -const FORM_FIELDS = 3; -const FORM_FIELD_NAME = 1; -const FORM_FIELD_VALUE = 2; - -const NAVIGATION_HREF = 1; - const MOUSE_EVENTS = new Set([ "click", "dblclick", @@ -110,57 +80,49 @@ export function familyFor(name: string): Family { return "none"; } -function writeModifiers( - w: Writer, - fieldNumber: number, - ev: MouseEvent | KeyboardEvent, -): void { - w.writeMessage(fieldNumber, (m) => { - if (ev.altKey) m.writeBool(MODIFIERS_ALT, true); - if (ev.ctrlKey) m.writeBool(MODIFIERS_CTRL, true); - if (ev.metaKey) m.writeBool(MODIFIERS_META, true); - if (ev.shiftKey) m.writeBool(MODIFIERS_SHIFT, true); - }); +function modifiers(ev: MouseEvent | KeyboardEvent): Modifiers { + return { + alt: ev.altKey, + ctrl: ev.ctrlKey, + meta: ev.metaKey, + shift: ev.shiftKey, + }; } -function writeMouseData(w: Writer, name: string, ev: MouseEvent): void { - w.writeDouble(MOUSE_CLIENT_X, ev.clientX); - w.writeDouble(MOUSE_CLIENT_Y, ev.clientY); - w.writeDouble(MOUSE_PAGE_X, ev.pageX); - w.writeDouble(MOUSE_PAGE_Y, ev.pageY); - w.writeDouble(MOUSE_SCREEN_X, ev.screenX); - w.writeDouble(MOUSE_SCREEN_Y, ev.screenY); - w.writeDouble( - MOUSE_OFFSET_X, - (ev as MouseEvent & { offsetX?: number }).offsetX ?? 0, - ); - w.writeDouble( - MOUSE_OFFSET_Y, - (ev as MouseEvent & { offsetY?: number }).offsetY ?? 0, - ); - if (BUTTON_EVENTS.has(name)) { +function mouseData(name: string, ev: MouseEvent): MouseData { + const buttons = ev.buttons; + return { + clientX: ev.clientX, + clientY: ev.clientY, + pageX: ev.pageX, + pageY: ev.pageY, + screenX: ev.screenX, + screenY: ev.screenY, + offsetX: (ev as MouseEvent & { offsetX?: number }).offsetX ?? 0, + offsetY: (ev as MouseEvent & { offsetY?: number }).offsetY ?? 0, // MouseButton's case values (PRIMARY=0, AUXILIARY=1, SECONDARY=2, // BACK=3, FORWARD=4) coincide numerically with MouseEvent.button's // own encoding, so the raw DOM value is the wire value with no // remapping. - w.writeUint32(MOUSE_BUTTON, ev.button); - } - const buttons = ev.buttons; - if (buttons & 1) w.writeBool(MOUSE_PRIMARY, true); - if (buttons & 2) w.writeBool(MOUSE_SECONDARY, true); - if (buttons & 4) w.writeBool(MOUSE_AUXILIARY, true); - if (buttons & 8) w.writeBool(MOUSE_BACK, true); - if (buttons & 16) w.writeBool(MOUSE_FORWARD, true); - writeModifiers(w, MOUSE_MODIFIERS, ev); + button: BUTTON_EVENTS.has(name) ? ev.button : undefined, + primary: (buttons & 1) !== 0, + secondary: (buttons & 2) !== 0, + auxiliary: (buttons & 4) !== 0, + back: (buttons & 8) !== 0, + forward: (buttons & 16) !== 0, + modifiers: modifiers(ev), + }; } -function writeKeyboardData(w: Writer, ev: KeyboardEvent): void { - w.writeString(KEYBOARD_KEY, ev.key ?? ""); - w.writeString(KEYBOARD_CODE, ev.code ?? ""); - w.writeUint32(KEYBOARD_LOCATION, ev.location ?? 0); - if (ev.repeat) w.writeBool(KEYBOARD_REPEAT, true); - if (ev.isComposing) w.writeBool(KEYBOARD_IS_COMPOSING, true); - writeModifiers(w, KEYBOARD_MODIFIERS, ev); +function keyboardData(ev: KeyboardEvent): KeyboardData { + return { + key: ev.key ?? "", + code: ev.code ?? "", + location: ev.location ?? 0, + repeat: ev.repeat === true, + isComposing: ev.isComposing === true, + modifiers: modifiers(ev), + }; } interface FormControlLike { @@ -169,24 +131,25 @@ interface FormControlLike { type?: string; } -function writeFormData(w: Writer, name: string, ev: Event): void { +function formData(name: string, ev: Event): FormDataMessage { const target = ev.target as (EventTarget & FormControlLike) | null; - w.writeString(FORM_VALUE, target?.value ?? ""); - if (target?.type === "checkbox" || target?.type === "radio") { - w.writeBool(FORM_CHECKED, target.checked === true); - } + const fields: FormField[] = []; if (name === "submit" && target instanceof HTMLFormElement) { // FormData(form).entries() on submit only — proto comment: "dioxus-web // populates them on every event inside a form, which serializes the // whole form per keystroke"; this receiver follows the proto's // narrower contract instead. for (const [fieldName, value] of new FormData(target).entries()) { - w.writeMessage(FORM_FIELDS, (f) => { - f.writeString(FORM_FIELD_NAME, fieldName); - f.writeString(FORM_FIELD_VALUE, String(value)); - }); + fields.push({ name: fieldName, value: String(value) }); } } + return { + value: target?.value ?? "", + checked: target?.type === "checkbox" || target?.type === "radio" + ? target.checked === true + : undefined, + fields, + }; } /** `popstate`/`hashchange`: a producer has no `location` to read @@ -198,7 +161,7 @@ function writeFormData(w: Writer, name: string, ev: Event): void { * and behaves sanely under `deno test`), but a non-browser embedding of * this module is still conceivable, and a `TypeError` here would be a * strange way to lose an otherwise-fine event. */ -function writeNavigationData(w: Writer): void { +function navigationData(): NavigationData { let href = ""; try { href = globalThis.location?.href ?? ""; @@ -206,30 +169,33 @@ function writeNavigationData(w: Writer): void { // Accessing `location` can throw under exotic embeddings (sandboxed // iframes, some SSR shims); "" degrades the same as "we don't know". } - w.writeString(NAVIGATION_HREF, href); + return { href }; } /** Encode `ev` as an `EventPayload` for `name`'s family. Names outside - * mouse/keyboard/form (including focus/blur and every family this receiver - * does not yet implement) get the empty payload — zero bytes, which is a - * valid `EventPayload` with no `family` case (proto3 default). */ + * mouse/keyboard/form/navigation (including focus/blur and every family + * this receiver does not yet implement) get the empty payload — zero + * bytes, which is a valid `EventPayload` with no `family` case (proto3 + * default). */ export function encodePayload(name: string, ev: Event): Uint8Array { - const w = new Writer(); const family = familyFor(name); + let payload: EventPayload; if (family === "mouse") { - w.writeMessage( - EVENT_PAYLOAD_MOUSE, - (m) => writeMouseData(m, name, ev as MouseEvent), - ); + payload = { + family: { $case: "mouse", value: mouseData(name, ev as MouseEvent) }, + }; } else if (family === "keyboard") { - w.writeMessage( - EVENT_PAYLOAD_KEYBOARD, - (m) => writeKeyboardData(m, ev as KeyboardEvent), - ); + payload = { + family: { $case: "keyboard", value: keyboardData(ev as KeyboardEvent) }, + }; } else if (family === "form") { - w.writeMessage(EVENT_PAYLOAD_FORM, (m) => writeFormData(m, name, ev)); + payload = { family: { $case: "form", value: formData(name, ev) } }; } else if (family === "navigation") { - w.writeMessage(EVENT_PAYLOAD_NAVIGATION, (m) => writeNavigationData(m)); + payload = { + family: { $case: "navigation", value: navigationData() }, + }; + } else { + payload = { family: undefined }; } - return w.finish(); + return EventPayload.encode(payload).finish(); } diff --git a/receiver/src/frames.ts b/receiver/src/frames.ts index e2d4d5f..fcee5f2 100644 --- a/receiver/src/frames.ts +++ b/receiver/src/frames.ts @@ -1,11 +1,24 @@ -// Frame decoder: `proto/stream-dom.proto`'s `Frame` message, decoded -// field-by-field with a switch on tag straight into `FrameSink` calls — the -// "pbf hybrid decode" docs/design.md's Encoding section describes, with no -// intermediate object for the hot structural ops. Field numbers below are -// copied from the .proto (which is normative and not owned by this -// track); each constant is named after its proto message and field. - -import { Reader, TruncatedError, WireType } from "./proto.ts"; +// Frame decoder: `proto/stream-dom.proto`'s `Frame` message, decoded by the +// ts-proto generated reader in `src/gen/stream-dom.ts` over +// `@bufbuild/protobuf/wire`'s `BinaryReader`, then adapted to `FrameSink`'s +// calls at the dispatch site below. No hand-written wire code — docs/design.md +// "Encoding: protobuf, and why not a hand-rolled layout" records the +// measurement that chose generated readers over the hand-rolled decoder this +// replaces. + +import { BinaryReader } from "@bufbuild/protobuf/wire"; +import { + type AddListener, + Frame, + Global, + type Listener as WireListener, + type RegisterTemplate, + type RemoveListener, + type SetAttribute, + type TemplateAttr as WireTemplateAttr, + type TemplateElement as WireTemplateElement, + type TemplateNode as WireTemplateNode, +} from "./gen/stream-dom.ts"; /** The wire protocol version this receiver implements — the number on * proto/stream-dom.proto's `// PROTOCOL VERSION: 1` header line, which is @@ -16,120 +29,18 @@ export const PROTOCOL_VERSION = 1; /** Strict-mode rejection of wire content this receiver does not know: an * open receiver skips it, a receiver enforcing a policy must not * (proto/stream-dom.proto header, docs/design.md "Policy"). Off the hot - * path — the skip branches test one boolean and call this only to throw. */ + * path — the strict walk runs only when strict mode is on. */ function unknownField(field: number, message: string): never { throw new Error( `stream-dom: unknown field ${field} in ${message} (receiver PROTOCOL_VERSION ${PROTOCOL_VERSION})`, ); } -// -- Frame -------------------------------------------------------------- - -const FRAME_COMMIT = 1; -const FRAME_INSERT_BEFORE = 2; -const FRAME_SET_TEXT = 3; -const FRAME_SET_ATTRIBUTE = 4; -const FRAME_SET_PROPERTY = 5; -const FRAME_CREATE_ELEMENT = 6; -const FRAME_CREATE_TEXT = 7; -const FRAME_REMOVE = 8; -const FRAME_CLONE_TEMPLATE = 9; -const FRAME_BIND_PATH = 10; -const FRAME_CREATE_PLACEHOLDER = 11; -const FRAME_ADD_LISTENER = 12; -const FRAME_REMOVE_LISTENER = 13; -const FRAME_INTERN = 14; -const FRAME_REGISTER_TEMPLATE = 15; -const FRAME_INSERT_AFTER = 16; -const FRAME_BIND_MARKER = 17; -/** Lowest oneof `op` field number — used to recognize "some op field was - * present, even one this decoder does not know" for the no-op/no-commit - * check below. */ +/** Lowest oneof `op` field number in `Frame` — an unknown field at or above + * it is an op case from a future schema version, so it counts as "some op + * field was present" for the no-op/no-commit check below. */ const FRAME_OP_FIELD_MIN = 2; -const INTERN_ID = 1; -const INTERN_S = 2; - -const CREATE_ELEMENT_ID = 1; -const CREATE_ELEMENT_TAG = 2; -const CREATE_ELEMENT_NS = 3; - -const CREATE_TEXT_ID = 1; -const CREATE_TEXT_TEXT = 2; - -const CREATE_PLACEHOLDER_ID = 1; - -const INSERT_BEFORE_PARENT = 1; -const INSERT_BEFORE_ID = 2; -const INSERT_BEFORE_ANCHOR = 3; - -const INSERT_AFTER_PARENT = 1; -const INSERT_AFTER_ID = 2; -const INSERT_AFTER_ANCHOR = 3; - -const REMOVE_ID = 1; - -const SET_TEXT_ID = 1; -const SET_TEXT_TEXT = 2; - -const SET_ATTRIBUTE_ID = 1; -const SET_ATTRIBUTE_NAME = 2; -const SET_ATTRIBUTE_NS = 3; -const SET_ATTRIBUTE_TEXT = 4; -const SET_ATTRIBUTE_ASSET = 5; - -const SET_PROPERTY_ID = 1; -const SET_PROPERTY_NAME = 2; -const SET_PROPERTY_TEXT = 3; -const SET_PROPERTY_INT = 4; -const SET_PROPERTY_FLOAT = 5; -const SET_PROPERTY_BOOLEAN = 6; - -const LISTENER_ID = 1; -const LISTENER_NAME = 2; -const LISTENER_BUBBLES = 3; -const LISTENER_CAPTURE = 4; -const LISTENER_PASSIVE = 5; -const LISTENER_PREVENT_DEFAULT = 6; -const LISTENER_STOP_PROPAGATION = 7; -const LISTENER_GLOBAL = 8; - -/** `Global` enum values (proto/stream-dom.proto). */ -const GLOBAL_WINDOW = 0; -const GLOBAL_DOCUMENT = 1; - -const ADD_LISTENER_LISTENER = 1; -const REMOVE_LISTENER_LISTENER = 1; - -const TEMPLATE_ATTR_NAME = 1; -const TEMPLATE_ATTR_NS = 2; -const TEMPLATE_ATTR_TEXT = 3; -const TEMPLATE_ATTR_ASSET = 4; - -const TEMPLATE_ELEMENT_TAG = 1; -const TEMPLATE_ELEMENT_NS = 2; -const TEMPLATE_ELEMENT_ATTRS = 3; -const TEMPLATE_ELEMENT_CHILDREN = 4; - -const TEMPLATE_NODE_ELEMENT = 1; -const TEMPLATE_NODE_TEXT = 2; -const TEMPLATE_NODE_DYNAMIC = 3; - -const REGISTER_TEMPLATE_ID = 1; -const REGISTER_TEMPLATE_NODES = 2; -const REGISTER_TEMPLATE_ROOTS = 3; - -const CLONE_TEMPLATE_TMPL = 1; -const CLONE_TEMPLATE_ROOT = 2; -const CLONE_TEMPLATE_ID = 3; - -const BIND_PATH_ROOT = 1; -const BIND_PATH_PATH = 2; -const BIND_PATH_ID = 3; - -const BIND_MARKER_KEY = 1; -const BIND_MARKER_ID = 2; - // -- decoded shapes ------------------------------------------------------- /** `Listener.target`'s oneof (proto/stream-dom.proto): a node id, or one @@ -226,180 +137,205 @@ export interface FrameSink { commit(): void; } -function decodeListener(r: Reader, strict: boolean): Listener { - let target: ListenerTarget | undefined; - let name = 0; - let bubbles = false; - let capture = false; - let passive = false; - let preventDefault = false; - let stopPropagation = false; - while (!r.finished()) { - const [field, wireType] = r.readTag(); - switch (field) { - case LISTENER_ID: - target = { kind: "node", id: r.readVarint32() }; - break; - case LISTENER_GLOBAL: { - const g = r.readVarint32(); - if (g === GLOBAL_WINDOW) target = { kind: "window" }; - else if (g === GLOBAL_DOCUMENT) target = { kind: "document" }; - else throw new Error(`stream-dom: Listener.global unknown value ${g}`); - break; - } - case LISTENER_NAME: - name = r.readVarint32(); - break; - case LISTENER_BUBBLES: - bubbles = r.readBool(); - break; - case LISTENER_CAPTURE: - capture = r.readBool(); - break; - case LISTENER_PASSIVE: - passive = r.readBool(); - break; - case LISTENER_PREVENT_DEFAULT: - preventDefault = r.readBool(); - break; - case LISTENER_STOP_PROPAGATION: - stopPropagation = r.readBool(); - break; - default: - if (strict) unknownField(field, "Listener"); - r.skip(wireType); - } - } - // Proto3 `oneof` has no default case: neither field set is a genuine - // absence, not "id 0" (which IS the mount root and a legal target). - if (!target) { +// -- generated message -> FrameSink shapes -------------------------------- + +/** `SetAttribute.value` / `TemplateAttr.value`. `BinaryReader.bytes()` + * returns a VIEW over the decode buffer, while this decoder's buffer is + * reused across `push` calls (and, under the direct transport, aliases + * guest memory that is invalid once the read callback returns), so an + * asset handle retained past this call must own its bytes: `.slice()`. */ +function attrValue(m: SetAttribute | WireTemplateAttr): AttrValue | undefined { + const v = m.value; + if (v === undefined) return undefined; + if (v.$case === "text") return { kind: "text", value: v.value }; + return { kind: "asset", handle: v.value.slice() }; +} + +function listener(m: WireListener): Listener { + const t = m.target; + let target: ListenerTarget; + if (t === undefined) { + // Proto3 `oneof` has no default case: neither field set is a genuine + // absence, not "id 0" (which IS the mount root and a legal target). throw new Error( "stream-dom: Listener has no target (neither id nor global set)", ); + } else if (t.$case === "id") { + target = { kind: "node", id: t.value }; + } else if (t.value === Global.WINDOW) { + target = { kind: "window" }; + } else if (t.value === Global.DOCUMENT) { + target = { kind: "document" }; + } else { + throw new Error(`stream-dom: Listener.global unknown value ${t.value}`); } return { target, - name, - bubbles, - capture, - passive, - preventDefault, - stopPropagation, + name: m.name, + bubbles: m.bubbles, + capture: m.capture, + passive: m.passive, + preventDefault: m.preventDefault, + stopPropagation: m.stopPropagation, }; } -/** A `repeated uint32` field: proto3 packs scalar-numeric repeated fields - * by default (length-delimited, consecutive varints with no per-element - * tag), but an unpacked encoder emitting one tag/varint pair per element - * is equally valid wire; both are accepted here. */ -function readPackedOrRepeatedUint32( - r: Reader, - wireType: WireType, - into: number[], -): void { - if (wireType === WireType.LengthDelimited) { - const sub = r.readMessage(); - while (!sub.finished()) into.push(sub.readVarint32()); - } else { - into.push(r.readVarint32()); +function templateAttrs(ms: WireTemplateAttr[]): TemplateAttr[] { + const out: TemplateAttr[] = []; + for (const a of ms) { + // No `value` case set keeps the proto3 default, an empty text value. + out.push({ + name: a.name, + ns: a.ns, + value: attrValue(a) ?? { kind: "text", value: "" }, + }); } + return out; } -function decodeTemplateAttr(r: Reader, strict: boolean): TemplateAttr { - const a: TemplateAttr = { - name: 0, - ns: undefined, - value: { kind: "text", value: "" }, +function templateElement(e: WireTemplateElement): TemplateElement { + return { + tag: e.tag, + ns: e.ns, + attrs: templateAttrs(e.attrs), + children: e.children, }; - while (!r.finished()) { - const [field, wireType] = r.readTag(); - switch (field) { - case TEMPLATE_ATTR_NAME: - a.name = r.readVarint32(); - break; - case TEMPLATE_ATTR_NS: - a.ns = r.readVarint32(); - break; - case TEMPLATE_ATTR_TEXT: - a.value = { kind: "text", value: r.readString() }; - break; - case TEMPLATE_ATTR_ASSET: - // `readBytes` COPIES (proto.ts) rather than returning a view over - // the decode buffer: under the direct transport those bytes alias - // guest memory and are invalid once the read callback returns, so - // a handle retained past this call must own them. - a.value = { kind: "asset", handle: r.readBytes() }; - break; - default: - if (strict) unknownField(field, "TemplateAttr"); - r.skip(wireType); +} + +function templateNodes( + ms: WireTemplateNode[], + strict: boolean, +): TemplateNode[] { + const out: TemplateNode[] = []; + for (const n of ms) { + const kind = n.kind; + if (kind === undefined) { + // A `TemplateNode` naming no kind is wire content this receiver + // cannot act on, so strict mode rejects it — with its own message, + // since an absent oneof has no field number to report. Non-strict + // keeps the proto3 default (an empty text node). + if (strict) { + throw new Error( + `stream-dom: TemplateNode has no kind set (receiver PROTOCOL_VERSION ${PROTOCOL_VERSION})`, + ); + } + out.push({ kind: "text", text: "" }); + } else if (kind.$case === "text") { + out.push({ kind: "text", text: kind.value }); + } else if (kind.$case === "dynamic") { + out.push({ kind: "dynamic" }); + } else { + out.push({ kind: "element", element: templateElement(kind.value) }); } } - return a; + return out; } -function decodeTemplateElement(r: Reader, strict: boolean): TemplateElement { - const e: TemplateElement = { tag: 0, ns: undefined, attrs: [], children: [] }; - while (!r.finished()) { - const [field, wireType] = r.readTag(); - switch (field) { - case TEMPLATE_ELEMENT_TAG: - e.tag = r.readVarint32(); - break; - case TEMPLATE_ELEMENT_NS: - e.ns = r.readVarint32(); - break; - case TEMPLATE_ELEMENT_ATTRS: - e.attrs.push(decodeTemplateAttr(r.readMessage(), strict)); - break; - case TEMPLATE_ELEMENT_CHILDREN: - readPackedOrRepeatedUint32(r, wireType, e.children); - break; - default: - if (strict) unknownField(field, "TemplateElement"); - r.skip(wireType); - } +// -- strict-mode unknown-field walk --------------------------------------- + +/** Every generated message carries `_unknownFields` (`unknownFields=true`), + * keyed by the full tag; the field number is `tag >>> 3`. Empty (`{}`) when + * the message had none. */ +interface MaybeUnknown { + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +function checkUnknown(m: MaybeUnknown, name: string): void { + const u = m._unknownFields; + if (u === undefined) return; + for (const tag in u) { + unknownField(Number(tag) >>> 3, name); } - return e; } -function decodeTemplateNode(r: Reader, strict: boolean): TemplateNode { - let node: TemplateNode = { kind: "text", text: "" }; - let sawKind = false; - while (!r.finished()) { - const [field, wireType] = r.readTag(); - switch (field) { - case TEMPLATE_NODE_ELEMENT: - node = { - kind: "element", - element: decodeTemplateElement(r.readMessage(), strict), - }; - sawKind = true; - break; - case TEMPLATE_NODE_TEXT: - node = { kind: "text", text: r.readString() }; - sawKind = true; - break; - case TEMPLATE_NODE_DYNAMIC: - r.readMessage(); // Dynamic {} — no fields to read. - node = { kind: "dynamic" }; - sawKind = true; - break; - default: - if (strict) unknownField(field, "TemplateNode"); - r.skip(wireType); +/** Does `frame` carry an unknown field whose number is an `op` field + * number — an op case from a future schema version? Reproduces the old + * decoder's `sawAnyOpField` for cases it cannot decode. */ +function sawUnknownOpField(frame: Frame): boolean { + const u = frame._unknownFields; + if (u === undefined) return false; + for (const tag in u) { + if ((Number(tag) >>> 3) >= FRAME_OP_FIELD_MIN) return true; + } + return false; +} + +function checkListenerUnknown(m: AddListener | RemoveListener): void { + const l = m.listener; + if (l !== undefined) checkUnknown(l, "Listener"); +} + +function checkTemplateUnknown(m: RegisterTemplate): void { + for (const n of m.nodes) { + checkUnknown(n, "TemplateNode"); + const kind = n.kind; + if (kind !== undefined && kind.$case === "element") { + checkUnknown(kind.value, "TemplateElement"); + for (const a of kind.value.attrs) checkUnknown(a, "TemplateAttr"); } } - // A `TemplateNode` naming no kind is wire content this receiver cannot - // act on, so strict mode rejects it — with its own message, since an - // absent oneof has no field number to report. Non-strict keeps the - // proto3 default (an empty text node). - if (strict && !sawKind) { - throw new Error( - `stream-dom: TemplateNode has no kind set (receiver PROTOCOL_VERSION ${PROTOCOL_VERSION})`, - ); +} + +/** Reject anything on the wire this receiver does not know, in the same + * message-name vocabulary the hand-rolled decoder used. Runs only in + * strict mode; the non-strict path never touches `_unknownFields`. */ +function checkFrameStrict(frame: Frame): void { + checkUnknown(frame, "Frame"); + const op = frame.op; + if (op === undefined) return; + switch (op.$case) { + case "intern": + checkUnknown(op.value, "Intern"); + break; + case "createElement": + checkUnknown(op.value, "CreateElement"); + break; + case "createText": + checkUnknown(op.value, "CreateText"); + break; + case "createPlaceholder": + checkUnknown(op.value, "CreatePlaceholder"); + break; + case "insertBefore": + checkUnknown(op.value, "InsertBefore"); + break; + case "insertAfter": + checkUnknown(op.value, "InsertAfter"); + break; + case "remove": + checkUnknown(op.value, "Remove"); + break; + case "setText": + checkUnknown(op.value, "SetText"); + break; + case "setAttribute": + checkUnknown(op.value, "SetAttribute"); + break; + case "setProperty": + checkUnknown(op.value, "SetProperty"); + break; + case "addListener": + checkUnknown(op.value, "AddListener"); + checkListenerUnknown(op.value); + break; + case "removeListener": + checkUnknown(op.value, "RemoveListener"); + checkListenerUnknown(op.value); + break; + case "registerTemplate": + checkUnknown(op.value, "RegisterTemplate"); + checkTemplateUnknown(op.value); + break; + case "cloneTemplate": + checkUnknown(op.value, "CloneTemplate"); + break; + case "bindPath": + checkUnknown(op.value, "BindPath"); + break; + case "bindMarker": + checkUnknown(op.value, "BindMarker"); + break; } - return node; } /** Hard ceiling on one frame's length prefix. A `uint32` varint can @@ -412,6 +348,12 @@ function decodeTemplateNode(r: Reader, strict: boolean): TemplateNode { * (docs/design.md "Policy", "The seam"). */ export const MAX_FRAME_BYTES = 16 * 1024 * 1024; +/** A `uint32` varint is at most 5 bytes. `BinaryReader.uint32()` does not + * enforce that — it keeps consuming continuation bytes and wraps — and it + * reports "ran out of bytes" and "never terminated" the same way, so the + * length probe below bounds the width itself. */ +const MAX_VARINT32_BYTES = 5; + /** * Feeds arbitrary byte chunks (a frame may straddle chunks per * docs/design.md: "Rendezvous copies split at byte granularity") and @@ -454,313 +396,167 @@ export class FrameDecoder { #drain(): void { let offset = 0; const buf = this.#pending; + // One reader for the whole pending buffer: the length probe and the + // frame bodies share it (`Frame.decode(reader, len)` decodes in place), + // so a frame costs no allocation beyond the message objects. The + // reader's own bound is therefore the whole buffer, not the frame — + // the per-frame bound is the position check after each decode below. + const reader = new BinaryReader(buf); for (;;) { const lenStart = offset; let len: number; + let malformedPrefix = false; try { - const probe = new Reader(buf, lenStart, buf.length); - len = probe.readVarint32(); - offset = probe.pos; + reader.pos = lenStart; + len = reader.uint32(); + // Terminated, but only past the legal width for a u32: the value + // read is meaningless (`varint32read` keeps consuming continuation + // bytes and wraps), so it is as malformed as one that never + // terminated at all. + malformedPrefix = reader.pos - lenStart > MAX_VARINT32_BYTES; } catch (err) { // A genuine truncation (fewer bytes buffered than the varint - // needs) means "wait for more"; anything else (a malformed - // varint that is fully present but never terminates within the - // legal width) is a real protocol error and must not be treated - // as a buffering situation forever. - if (err instanceof TruncatedError) break; - throw err; + // needs) means "wait for more". The same `RangeError("premature + // EOF")` raised with all five legal bytes present means the varint + // is malformed, not incomplete — a real protocol error, which must + // not be treated as a buffering situation forever. + if (!(err instanceof RangeError)) throw err; + if (buf.length - lenStart < MAX_VARINT32_BYTES) break; + len = 0; + malformedPrefix = true; + } + if (malformedPrefix) { + throw new RangeError( + "stream-dom: frame length prefix is not a valid u32 varint", + ); } + const bodyStart = reader.pos; if (len > MAX_FRAME_BYTES) { throw new Error( `stream-dom: frame length ${len} exceeds MAX_FRAME_BYTES (${MAX_FRAME_BYTES})`, ); } - if (offset + len > buf.length) { + if (bodyStart + len > buf.length) { offset = lenStart; // Frame body straddles the buffer — wait for more. break; } - // Only the length probe above is allowed to treat a `TruncatedError` - // as "wait for more". Inside a frame whose bytes are all here, a - // truncated sub-message is a malformed frame and propagates out of - // `push`, aborting the stream. - const frame = new Reader(buf, offset, offset + len); - offset += len; + // Only the length probe above may treat a premature EOF as "wait for + // more" — it is deliberately outside this decode. + const frameEnd = bodyStart + len; + const frame = Frame.decode(reader, len); + if (reader.pos !== frameEnd) { + // The frame's own length is the only bound the shared reader has: + // a sub-message whose length runs past the frame end reads into + // the NEXT frame's bytes (over-consumption), and the generated + // decoder stops early — without throwing — on a zero tag or an + // end-group tag (under-consumption). Both are malformed frames in + // BOTH modes: a zero tag is invalid protobuf (field number 0 does + // not exist), and this receiver does not guess where a frame that + // does not fill its own length was meant to end. + throw new Error( + `stream-dom: malformed frame: decoder consumed ${ + reader.pos - bodyStart + } of ${len} bytes`, + ); + } + offset = frameEnd; this.#decodeFrame(frame); } this.#pending = buf.subarray(offset); } - #decodeFrame(r: Reader): void { + #decodeFrame(frame: Frame): void { this.#frameCount++; - let commit = false; - let sawAnyOpField = false; - let dispatch: (() => void) | undefined; - - while (!r.finished()) { - const [field, wireType] = r.readTag(); - if (field === FRAME_COMMIT) { - commit = r.readBool(); - continue; + if (this.#strict) checkFrameStrict(frame); + + const op = frame.op; + if (op === undefined) { + // An unknown field at an op field number is an op case this receiver + // does not know (a future schema version): the frame is skipped, but + // it did carry an op. + if (!frame.commit && !sawUnknownOpField(frame)) { + throw new Error("stream-dom: frame has no op and commit=false"); } - if (field < FRAME_OP_FIELD_MIN) { - if (this.#strict) unknownField(field, "Frame"); - r.skip(wireType); - continue; - } - sawAnyOpField = true; - switch (field) { - case FRAME_INTERN: { - const sub = r.readMessage(); - let id = 0, s = ""; - while (!sub.finished()) { - const [f, wt] = sub.readTag(); - if (f === INTERN_ID) id = sub.readVarint32(); - else if (f === INTERN_S) s = sub.readString(); - else if (this.#strict) unknownField(f, "Intern"); - else sub.skip(wt); - } - dispatch = () => this.#sink.internString(id, s); + } else { + const sink = this.#sink; + switch (op.$case) { + case "intern": + sink.internString(op.value.id, op.value.s); break; - } - case FRAME_CREATE_ELEMENT: { - const sub = r.readMessage(); - let id = 0, tag = 0, ns: number | undefined; - while (!sub.finished()) { - const [f, wt] = sub.readTag(); - if (f === CREATE_ELEMENT_ID) id = sub.readVarint32(); - else if (f === CREATE_ELEMENT_TAG) tag = sub.readVarint32(); - else if (f === CREATE_ELEMENT_NS) ns = sub.readVarint32(); - else if (this.#strict) unknownField(f, "CreateElement"); - else sub.skip(wt); - } - dispatch = () => this.#sink.createElement(id, tag, ns); + case "createElement": + sink.createElement(op.value.id, op.value.tag, op.value.ns); break; - } - case FRAME_CREATE_TEXT: { - const sub = r.readMessage(); - let id = 0, text = ""; - while (!sub.finished()) { - const [f, wt] = sub.readTag(); - if (f === CREATE_TEXT_ID) id = sub.readVarint32(); - else if (f === CREATE_TEXT_TEXT) text = sub.readString(); - else if (this.#strict) unknownField(f, "CreateText"); - else sub.skip(wt); - } - dispatch = () => this.#sink.createText(id, text); + case "createText": + sink.createText(op.value.id, op.value.text); break; - } - case FRAME_CREATE_PLACEHOLDER: { - const sub = r.readMessage(); - let id = 0; - while (!sub.finished()) { - const [f, wt] = sub.readTag(); - if (f === CREATE_PLACEHOLDER_ID) id = sub.readVarint32(); - else if (this.#strict) unknownField(f, "CreatePlaceholder"); - else sub.skip(wt); - } - dispatch = () => this.#sink.createPlaceholder(id); + case "createPlaceholder": + sink.createPlaceholder(op.value.id); break; - } - case FRAME_INSERT_BEFORE: { - const sub = r.readMessage(); - let parent: number | undefined, id = 0, anchor: number | undefined; - while (!sub.finished()) { - const [f, wt] = sub.readTag(); - if (f === INSERT_BEFORE_PARENT) parent = sub.readVarint32(); - else if (f === INSERT_BEFORE_ID) id = sub.readVarint32(); - else if (f === INSERT_BEFORE_ANCHOR) anchor = sub.readVarint32(); - else if (this.#strict) unknownField(f, "InsertBefore"); - else sub.skip(wt); - } - dispatch = () => this.#sink.insertBefore(parent, id, anchor); + case "insertBefore": + sink.insertBefore(op.value.parent, op.value.id, op.value.anchor); break; - } - case FRAME_INSERT_AFTER: { - const sub = r.readMessage(); - let parent: number | undefined, id = 0, anchor = 0; - while (!sub.finished()) { - const [f, wt] = sub.readTag(); - if (f === INSERT_AFTER_PARENT) parent = sub.readVarint32(); - else if (f === INSERT_AFTER_ID) id = sub.readVarint32(); - else if (f === INSERT_AFTER_ANCHOR) anchor = sub.readVarint32(); - else if (this.#strict) unknownField(f, "InsertAfter"); - else sub.skip(wt); - } - dispatch = () => this.#sink.insertAfter(parent, id, anchor); + case "insertAfter": + sink.insertAfter(op.value.parent, op.value.id, op.value.anchor); break; - } - case FRAME_REMOVE: { - const sub = r.readMessage(); - let id = 0; - while (!sub.finished()) { - const [f, wt] = sub.readTag(); - if (f === REMOVE_ID) id = sub.readVarint32(); - else if (this.#strict) unknownField(f, "Remove"); - else sub.skip(wt); - } - dispatch = () => this.#sink.remove(id); + case "remove": + sink.remove(op.value.id); break; - } - case FRAME_SET_TEXT: { - const sub = r.readMessage(); - let id = 0, text = ""; - while (!sub.finished()) { - const [f, wt] = sub.readTag(); - if (f === SET_TEXT_ID) id = sub.readVarint32(); - else if (f === SET_TEXT_TEXT) text = sub.readString(); - else if (this.#strict) unknownField(f, "SetText"); - else sub.skip(wt); - } - dispatch = () => this.#sink.setText(id, text); + case "setText": + sink.setText(op.value.id, op.value.text); break; - } - case FRAME_SET_ATTRIBUTE: { - const sub = r.readMessage(); - let id = 0, - name = 0, - ns: number | undefined, - value: AttrValue | undefined; - while (!sub.finished()) { - const [f, wt] = sub.readTag(); - if (f === SET_ATTRIBUTE_ID) id = sub.readVarint32(); - else if (f === SET_ATTRIBUTE_NAME) name = sub.readVarint32(); - else if (f === SET_ATTRIBUTE_NS) ns = sub.readVarint32(); - else if (f === SET_ATTRIBUTE_TEXT) { - value = { kind: "text", value: sub.readString() }; - } else if (f === SET_ATTRIBUTE_ASSET) { - // Copied, not aliased — see decodeTemplateAttr's note. - value = { kind: "asset", handle: sub.readBytes() }; - } else if (this.#strict) unknownField(f, "SetAttribute"); - else sub.skip(wt); - } - dispatch = () => this.#sink.setAttribute(id, name, ns, value); + case "setAttribute": + sink.setAttribute( + op.value.id, + op.value.name, + op.value.ns, + attrValue(op.value), + ); break; - } - case FRAME_SET_PROPERTY: { - const sub = r.readMessage(); - let id = 0, name = 0; - let value: PropertyValue = { kind: "none" }; - while (!sub.finished()) { - const [f, wt] = sub.readTag(); - if (f === SET_PROPERTY_ID) id = sub.readVarint32(); - else if (f === SET_PROPERTY_NAME) name = sub.readVarint32(); - else if (f === SET_PROPERTY_TEXT) { - value = { kind: "text", value: sub.readString() }; - } else if (f === SET_PROPERTY_INT) { - value = { kind: "int", value: sub.readSInt64() }; - } else if (f === SET_PROPERTY_FLOAT) { - value = { kind: "float", value: sub.readDouble() }; - } else if (f === SET_PROPERTY_BOOLEAN) { - value = { kind: "boolean", value: sub.readBool() }; - } else if (this.#strict) unknownField(f, "SetProperty"); - else sub.skip(wt); - } - dispatch = () => this.#sink.setProperty(id, name, value); + case "setProperty": { + const v = op.value.value; + let value: PropertyValue; + if (v === undefined) value = { kind: "none" }; + else if (v.$case === "text") value = { kind: "text", value: v.value }; + else if (v.$case === "int") value = { kind: "int", value: v.value }; + else if (v.$case === "float") { + value = { kind: "float", value: v.value }; + } else value = { kind: "boolean", value: v.value }; + sink.setProperty(op.value.id, op.value.name, value); break; } - case FRAME_ADD_LISTENER: { - const sub = r.readMessage(); - let listener: Listener | undefined; - while (!sub.finished()) { - const [f, wt] = sub.readTag(); - if (f === ADD_LISTENER_LISTENER) { - listener = decodeListener(sub.readMessage(), this.#strict); - } else if (this.#strict) unknownField(f, "AddListener"); - else sub.skip(wt); - } - if (listener) { - const l = listener; - dispatch = () => this.#sink.addListener(l); - } + case "addListener": { + // A present `AddListener` with no `Listener` set names no + // registration to make: the op field was there, so the frame is + // not "no op", but there is nothing to dispatch. + const l = op.value.listener; + if (l !== undefined) sink.addListener(listener(l)); break; } - case FRAME_REMOVE_LISTENER: { - const sub = r.readMessage(); - let listener: Listener | undefined; - while (!sub.finished()) { - const [f, wt] = sub.readTag(); - if (f === REMOVE_LISTENER_LISTENER) { - listener = decodeListener(sub.readMessage(), this.#strict); - } else if (this.#strict) unknownField(f, "RemoveListener"); - else sub.skip(wt); - } - if (listener) { - const l = listener; - dispatch = () => this.#sink.removeListener(l); - } + case "removeListener": { + const l = op.value.listener; + if (l !== undefined) sink.removeListener(listener(l)); break; } - case FRAME_REGISTER_TEMPLATE: { - const sub = r.readMessage(); - let id = 0; - const nodes: TemplateNode[] = []; - const roots: number[] = []; - while (!sub.finished()) { - const [f, wt] = sub.readTag(); - if (f === REGISTER_TEMPLATE_ID) id = sub.readVarint32(); - else if (f === REGISTER_TEMPLATE_NODES) { - nodes.push(decodeTemplateNode(sub.readMessage(), this.#strict)); - } else if (f === REGISTER_TEMPLATE_ROOTS) { - readPackedOrRepeatedUint32(sub, wt, roots); - } else if (this.#strict) unknownField(f, "RegisterTemplate"); - else sub.skip(wt); - } - dispatch = () => this.#sink.registerTemplate(id, nodes, roots); + case "registerTemplate": + sink.registerTemplate( + op.value.id, + templateNodes(op.value.nodes, this.#strict), + op.value.roots, + ); break; - } - case FRAME_CLONE_TEMPLATE: { - const sub = r.readMessage(); - let tmpl = 0, root = 0, id = 0; - while (!sub.finished()) { - const [f, wt] = sub.readTag(); - if (f === CLONE_TEMPLATE_TMPL) tmpl = sub.readVarint32(); - else if (f === CLONE_TEMPLATE_ROOT) root = sub.readVarint32(); - else if (f === CLONE_TEMPLATE_ID) id = sub.readVarint32(); - else if (this.#strict) unknownField(f, "CloneTemplate"); - else sub.skip(wt); - } - dispatch = () => this.#sink.cloneTemplate(tmpl, root, id); + case "cloneTemplate": + sink.cloneTemplate(op.value.tmpl, op.value.root, op.value.id); break; - } - case FRAME_BIND_PATH: { - const sub = r.readMessage(); - let root = 0, id = 0; - let path: Uint8Array = new Uint8Array(0); - while (!sub.finished()) { - const [f, wt] = sub.readTag(); - if (f === BIND_PATH_ROOT) root = sub.readVarint32(); - else if (f === BIND_PATH_PATH) path = sub.readBytes(); - else if (f === BIND_PATH_ID) id = sub.readVarint32(); - else if (this.#strict) unknownField(f, "BindPath"); - else sub.skip(wt); - } - dispatch = () => this.#sink.bindPath(root, path, id); + case "bindPath": + // Copied, not aliased — see `attrValue`. + sink.bindPath(op.value.root, op.value.path.slice(), op.value.id); break; - } - case FRAME_BIND_MARKER: { - const sub = r.readMessage(); - let key = 0, id = 0; - while (!sub.finished()) { - const [f, wt] = sub.readTag(); - if (f === BIND_MARKER_KEY) key = sub.readVarint32(); - else if (f === BIND_MARKER_ID) id = sub.readVarint32(); - else if (this.#strict) unknownField(f, "BindMarker"); - else sub.skip(wt); - } - dispatch = () => this.#sink.bindMarker(key, id); + case "bindMarker": + sink.bindMarker(op.value.key, op.value.id); break; - } - default: - // Unknown op field: skip the frame's bytes, still honor commit. - if (this.#strict) unknownField(field, "Frame"); - r.skip(wireType); } } - if (!commit && !sawAnyOpField) { - throw new Error("stream-dom: frame has no op and commit=false"); - } - if (dispatch) dispatch(); - if (commit) this.#sink.commit(); + if (frame.commit) this.#sink.commit(); } } diff --git a/receiver/src/gen/stream-dom-events.ts b/receiver/src/gen/stream-dom-events.ts new file mode 100644 index 0000000..87f4a8c --- /dev/null +++ b/receiver/src/gen/stream-dom-events.ts @@ -0,0 +1,2631 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// source: stream-dom-events.proto + +/* eslint-disable */ +import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; + +export const protobufPackage = "polymorph.stream_dom"; + +/** + * MouseEvent.button. Note the order differs from the `buttons` bits: + * auxiliary (middle) is 1 here and the 4 bit there. Proto3 enums are open, + * so a device's higher-numbered buttons arrive as their raw code. + */ +export enum MouseButton { + PRIMARY = 0, + AUXILIARY = 1, + SECONDARY = 2, + BACK = 3, + FORWARD = 4, + UNRECOGNIZED = -1, +} + +/** WheelEvent.deltaMode: the unit of the deltas. */ +export enum DeltaMode { + PIXEL = 0, + LINE = 1, + PAGE = 2, + UNRECOGNIZED = -1, +} + +/** KeyboardEvent.location. */ +export enum KeyLocation { + STANDARD = 0, + LEFT = 1, + RIGHT = 2, + NUMPAD = 3, + UNRECOGNIZED = -1, +} + +/** + * Keyboard modifiers held during the event: the DOM's altKey / ctrlKey / + * metaKey / shiftKey. Absent means none held. + */ +export interface Modifiers { + alt: boolean; + ctrl: boolean; + meta: boolean; + shift: boolean; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** MouseEvent. Also the base of pointer, wheel and drag events. */ +export interface MouseData { + /** Viewport-relative (clientX/Y). */ + clientX: number; + clientY: number; + /** Document-relative (pageX/Y). */ + pageX: number; + pageY: number; + /** Physical screen (screenX/Y). */ + screenX: number; + screenY: number; + /** Relative to the target's padding edge (offsetX/Y). */ + offsetX: number; + offsetY: number; + /** + * The button that changed state. Set only on events a press or release + * caused (mousedown, mouseup, click, dblclick, auxclick, contextmenu and + * their pointer / drag counterparts); the DOM reports a meaningless 0 on + * mousemove and the enter / leave / over / out family, so it is absent + * there rather than misreported as PRIMARY. + */ + button?: + | MouseButton + | undefined; + /** Buttons currently held (the bits of MouseEvent.buttons). */ + primary: boolean; + secondary: boolean; + auxiliary: boolean; + back: boolean; + forward: boolean; + modifiers: + | Modifiers + | undefined; + /** + * relatedTarget as a node id (mouseover / out, mouseenter / leave); + * absent when there is none or it is outside the mount. + */ + relatedTarget?: number | undefined; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** PointerEvent: a MouseEvent plus the device geometry. */ +export interface PointerData { + mouse: MouseData | undefined; + pointerId: number; + /** Contact geometry in CSS pixels (width / height); 1x1 for a mouse. */ + width: number; + height: number; + /** 0..1; 0.5 for hardware without pressure while a button is held. */ + pressure: number; + /** -1..1. */ + tangentialPressure: number; + /** Degrees, -90..90. */ + tiltX: number; + tiltY: number; + /** Degrees, 0..359. */ + twist: number; + /** + * "mouse", "pen", "touch", "" when unknown; the DOM allows further + * vendor-prefixed values, so this is not an enum. + */ + pointerType: string; + isPrimary: boolean; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** WheelEvent: a MouseEvent plus the scroll deltas. */ +export interface WheelData { + mouse: MouseData | undefined; + deltaX: number; + deltaY: number; + deltaZ: number; + deltaMode: DeltaMode; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** KeyboardEvent. */ +export interface KeyboardData { + /** The logical key ("a", "Enter", "ArrowLeft"); layout-dependent. */ + key: string; + /** The physical key ("KeyA", "Enter"); layout-independent. */ + code: string; + location: KeyLocation; + repeat: boolean; + /** The key was part of an IME composition. */ + isComposing: boolean; + modifiers: Modifiers | undefined; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** + * One form entry. The whole form's entries are a list, not a map: order + * matters and names repeat (checkbox groups, multi-select). + */ +export interface FormField { + name: string; + value: string; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** + * input / change / submit and kin. Files chosen in an `` + * are not here; they are a WIT resource. + */ +export interface FormData { + /** + * The target control's current value (input, textarea, select); "" when + * the target has none. + */ + value: string; + /** Set for checkable controls (checkbox, radio). */ + checked?: + | boolean + | undefined; + /** + * The enclosing form's entries (FormData(form).entries()), populated on + * submit; empty otherwise. dioxus-web populates them on every event + * inside a form, which serializes the whole form per keystroke — a + * Dioxus adapter wanting that reads the controls it needs instead. + */ + fields: FormField[]; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** + * The target's scroll state at the time of a `scroll` event. The offsets + * are fractional; the sizes are integer in the DOM. + */ +export interface ScrollData { + scrollTop: number; + scrollLeft: number; + scrollWidth: number; + scrollHeight: number; + clientWidth: number; + clientHeight: number; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** CompositionEvent (compositionstart / update / end). */ +export interface CompositionData { + /** + * The text the IME contributed (CompositionEvent.data). "" is legitimate: + * a composition can be cancelled to nothing. + */ + data: string; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** AnimationEvent. */ +export interface AnimationData { + /** Which @keyframes animation fired (animationName). */ + animationName: string; + /** The pseudo-element it ran on ("::before"), "" for the element itself. */ + pseudoElement: string; + /** + * Seconds the animation had run when the event fired, excluding paused + * time. + */ + elapsedTime: number; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** TransitionEvent. One `transitionend` fires per transitioned property. */ +export interface TransitionData { + propertyName: string; + pseudoElement: string; + elapsedTime: number; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** One finger: the DOM Touch object. */ +export interface TouchPoint { + /** + * Stable for the life of one contact, so a handler can follow a finger + * across touchmove events. + */ + identifier: number; + clientX: number; + clientY: number; + pageX: number; + pageY: number; + screenX: number; + screenY: number; + /** + * Half-axes of the ellipse approximating the contact area; 0 on hardware + * that does not report contact geometry. + */ + radiusX: number; + radiusY: number; + /** Degrees, 0..90, that the contact ellipse is rotated by. */ + rotationAngle: number; + /** Contact pressure, 0..1; 0 when unsupported. */ + force: number; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** + * TouchEvent. The three lists are the DOM's; a finger commonly appears in + * more than one. + */ +export interface TouchData { + /** Every finger currently on the surface (touches). */ + touches: TouchPoint[]; + /** + * Only the fingers whose state this event reports (changedTouches) — on + * touchend, the ones that lifted. + */ + changedTouches: TouchPoint[]; + /** The subset of `touches` that started on the target (targetTouches). */ + targetTouches: TouchPoint[]; + modifiers: Modifiers | undefined; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** + * Geometry for the observer families. Structurally the same as the WIT + * `queries` records: those are typed RPC results and these are wire + * snapshots, and neither side can use the other's definition. + */ +export interface Size { + width: number; + height: number; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +export interface Rect { + x: number; + y: number; + width: number; + height: number; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** + * Synthetic `resize`: one ResizeObserverEntry for the listening element. + * The DOM reports both boxes writing-mode-relative (blockSize / + * inlineSize); the receiver resolves that to width / height so the + * producer need not know the element's writing mode. + */ +export interface ResizeData { + /** Content plus padding plus border (borderBoxSize). */ + borderBox: + | Size + | undefined; + /** The content box alone (contentBoxSize). */ + contentBox: Size | undefined; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** + * Synthetic `visible`: one IntersectionObserverEntry for the listening + * element. + */ +export interface VisibleData { + /** The target's own box at the time of observation (boundingClientRect). */ + boundingClientRect: + | Rect + | undefined; + /** How much of the target is visible, 0..1 (intersectionRatio). */ + intersectionRatio: number; + /** The visible part of the target (intersectionRect); all-zero when none. */ + intersectionRect: + | Rect + | undefined; + /** + * Whether the target intersects the root at all (isIntersecting). + * Distinct from a nonzero ratio: an entry fires on the transition out of + * view too. + */ + isIntersecting: boolean; + /** + * The root's clipping box (rootBounds). Absent when the DOM reports null: + * the root is the implicit viewport and the document is in a cross-origin + * frame, so the browser withholds the parent's geometry. + */ + rootBounds?: + | Rect + | undefined; + /** + * See the file header for the clock. The DOM's `time` is + * document-relative; the receiver adds `performance.timeOrigin`. + */ + timeMs: number; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** + * Synthetic `frame`: the receiver's requestAnimationFrame fired. Subscribed + * on the mount root; tells an animating producer that the last commit + * reached the screen and the next one is due. + */ +export interface FrameData { + /** The frame's start time, on the same clock as VisibleData.time_ms. */ + timeMs: number; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** + * Families ordered roughly by frequency; all tags are one byte. + * `popstate` and `hashchange` on `window`. The only reason to listen to + * either is to learn where the document now is, and a producer has no + * `location` to read, so the receiver snapshots it. (HashChangeEvent + * carries newURL itself; PopStateEvent carries only `state`, so the + * snapshot is uniform rather than per-event.) + */ +export interface NavigationData { + /** location.href after the navigation. */ + href: string; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +export interface EventPayload { + family: + | { $case: "mouse"; value: MouseData } + | { $case: "keyboard"; value: KeyboardData } + | { $case: "form"; value: FormData } + | { $case: "pointer"; value: PointerData } + | { $case: "wheel"; value: WheelData } + | { $case: "touch"; value: TouchData } + | { $case: "scroll"; value: ScrollData } + | { $case: "frame"; value: FrameData } + | { $case: "composition"; value: CompositionData } + | { $case: "animation"; value: AnimationData } + | { $case: "transition"; value: TransitionData } + | { $case: "resize"; value: ResizeData } + | { $case: "visible"; value: VisibleData } + | { $case: "navigation"; value: NavigationData } + | undefined; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +function createBaseModifiers(): Modifiers { + return { alt: false, ctrl: false, meta: false, shift: false, _unknownFields: {} }; +} + +export const Modifiers: MessageFns = { + encode(message: Modifiers, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.alt !== false) { + writer.uint32(8).bool(message.alt); + } + if (message.ctrl !== false) { + writer.uint32(16).bool(message.ctrl); + } + if (message.meta !== false) { + writer.uint32(24).bool(message.meta); + } + if (message.shift !== false) { + writer.uint32(32).bool(message.shift); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Modifiers { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseModifiers(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.alt = reader.bool(); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.ctrl = reader.bool(); + continue; + } + case 3: { + if (tag !== 24) { + break; + } + + message.meta = reader.bool(); + continue; + } + case 4: { + if (tag !== 32) { + break; + } + + message.shift = reader.bool(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseMouseData(): MouseData { + return { + clientX: 0, + clientY: 0, + pageX: 0, + pageY: 0, + screenX: 0, + screenY: 0, + offsetX: 0, + offsetY: 0, + button: undefined, + primary: false, + secondary: false, + auxiliary: false, + back: false, + forward: false, + modifiers: undefined, + relatedTarget: undefined, + _unknownFields: {}, + }; +} + +export const MouseData: MessageFns = { + encode(message: MouseData, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.clientX !== 0) { + writer.uint32(9).double(message.clientX); + } + if (message.clientY !== 0) { + writer.uint32(17).double(message.clientY); + } + if (message.pageX !== 0) { + writer.uint32(25).double(message.pageX); + } + if (message.pageY !== 0) { + writer.uint32(33).double(message.pageY); + } + if (message.screenX !== 0) { + writer.uint32(41).double(message.screenX); + } + if (message.screenY !== 0) { + writer.uint32(49).double(message.screenY); + } + if (message.offsetX !== 0) { + writer.uint32(57).double(message.offsetX); + } + if (message.offsetY !== 0) { + writer.uint32(65).double(message.offsetY); + } + if (message.button !== undefined) { + writer.uint32(72).int32(message.button); + } + if (message.primary !== false) { + writer.uint32(80).bool(message.primary); + } + if (message.secondary !== false) { + writer.uint32(88).bool(message.secondary); + } + if (message.auxiliary !== false) { + writer.uint32(96).bool(message.auxiliary); + } + if (message.back !== false) { + writer.uint32(104).bool(message.back); + } + if (message.forward !== false) { + writer.uint32(112).bool(message.forward); + } + if (message.modifiers !== undefined) { + Modifiers.encode(message.modifiers, writer.uint32(122).fork()).join(); + } + if (message.relatedTarget !== undefined) { + writer.uint32(128).uint32(message.relatedTarget); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): MouseData { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseMouseData(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 9) { + break; + } + + message.clientX = reader.double(); + continue; + } + case 2: { + if (tag !== 17) { + break; + } + + message.clientY = reader.double(); + continue; + } + case 3: { + if (tag !== 25) { + break; + } + + message.pageX = reader.double(); + continue; + } + case 4: { + if (tag !== 33) { + break; + } + + message.pageY = reader.double(); + continue; + } + case 5: { + if (tag !== 41) { + break; + } + + message.screenX = reader.double(); + continue; + } + case 6: { + if (tag !== 49) { + break; + } + + message.screenY = reader.double(); + continue; + } + case 7: { + if (tag !== 57) { + break; + } + + message.offsetX = reader.double(); + continue; + } + case 8: { + if (tag !== 65) { + break; + } + + message.offsetY = reader.double(); + continue; + } + case 9: { + if (tag !== 72) { + break; + } + + message.button = reader.int32() as any; + continue; + } + case 10: { + if (tag !== 80) { + break; + } + + message.primary = reader.bool(); + continue; + } + case 11: { + if (tag !== 88) { + break; + } + + message.secondary = reader.bool(); + continue; + } + case 12: { + if (tag !== 96) { + break; + } + + message.auxiliary = reader.bool(); + continue; + } + case 13: { + if (tag !== 104) { + break; + } + + message.back = reader.bool(); + continue; + } + case 14: { + if (tag !== 112) { + break; + } + + message.forward = reader.bool(); + continue; + } + case 15: { + if (tag !== 122) { + break; + } + + message.modifiers = Modifiers.decode(reader, reader.uint32()); + continue; + } + case 16: { + if (tag !== 128) { + break; + } + + message.relatedTarget = reader.uint32(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBasePointerData(): PointerData { + return { + mouse: undefined, + pointerId: 0, + width: 0, + height: 0, + pressure: 0, + tangentialPressure: 0, + tiltX: 0, + tiltY: 0, + twist: 0, + pointerType: "", + isPrimary: false, + _unknownFields: {}, + }; +} + +export const PointerData: MessageFns = { + encode(message: PointerData, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.mouse !== undefined) { + MouseData.encode(message.mouse, writer.uint32(10).fork()).join(); + } + if (message.pointerId !== 0) { + writer.uint32(16).sint32(message.pointerId); + } + if (message.width !== 0) { + writer.uint32(25).double(message.width); + } + if (message.height !== 0) { + writer.uint32(33).double(message.height); + } + if (message.pressure !== 0) { + writer.uint32(45).float(message.pressure); + } + if (message.tangentialPressure !== 0) { + writer.uint32(53).float(message.tangentialPressure); + } + if (message.tiltX !== 0) { + writer.uint32(56).sint32(message.tiltX); + } + if (message.tiltY !== 0) { + writer.uint32(64).sint32(message.tiltY); + } + if (message.twist !== 0) { + writer.uint32(72).sint32(message.twist); + } + if (message.pointerType !== "") { + writer.uint32(82).string(message.pointerType); + } + if (message.isPrimary !== false) { + writer.uint32(88).bool(message.isPrimary); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): PointerData { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBasePointerData(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.mouse = MouseData.decode(reader, reader.uint32()); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.pointerId = reader.sint32(); + continue; + } + case 3: { + if (tag !== 25) { + break; + } + + message.width = reader.double(); + continue; + } + case 4: { + if (tag !== 33) { + break; + } + + message.height = reader.double(); + continue; + } + case 5: { + if (tag !== 45) { + break; + } + + message.pressure = reader.float(); + continue; + } + case 6: { + if (tag !== 53) { + break; + } + + message.tangentialPressure = reader.float(); + continue; + } + case 7: { + if (tag !== 56) { + break; + } + + message.tiltX = reader.sint32(); + continue; + } + case 8: { + if (tag !== 64) { + break; + } + + message.tiltY = reader.sint32(); + continue; + } + case 9: { + if (tag !== 72) { + break; + } + + message.twist = reader.sint32(); + continue; + } + case 10: { + if (tag !== 82) { + break; + } + + message.pointerType = reader.string(); + continue; + } + case 11: { + if (tag !== 88) { + break; + } + + message.isPrimary = reader.bool(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseWheelData(): WheelData { + return { mouse: undefined, deltaX: 0, deltaY: 0, deltaZ: 0, deltaMode: 0, _unknownFields: {} }; +} + +export const WheelData: MessageFns = { + encode(message: WheelData, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.mouse !== undefined) { + MouseData.encode(message.mouse, writer.uint32(10).fork()).join(); + } + if (message.deltaX !== 0) { + writer.uint32(17).double(message.deltaX); + } + if (message.deltaY !== 0) { + writer.uint32(25).double(message.deltaY); + } + if (message.deltaZ !== 0) { + writer.uint32(33).double(message.deltaZ); + } + if (message.deltaMode !== 0) { + writer.uint32(40).int32(message.deltaMode); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): WheelData { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseWheelData(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.mouse = MouseData.decode(reader, reader.uint32()); + continue; + } + case 2: { + if (tag !== 17) { + break; + } + + message.deltaX = reader.double(); + continue; + } + case 3: { + if (tag !== 25) { + break; + } + + message.deltaY = reader.double(); + continue; + } + case 4: { + if (tag !== 33) { + break; + } + + message.deltaZ = reader.double(); + continue; + } + case 5: { + if (tag !== 40) { + break; + } + + message.deltaMode = reader.int32() as any; + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseKeyboardData(): KeyboardData { + return { + key: "", + code: "", + location: 0, + repeat: false, + isComposing: false, + modifiers: undefined, + _unknownFields: {}, + }; +} + +export const KeyboardData: MessageFns = { + encode(message: KeyboardData, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.key !== "") { + writer.uint32(10).string(message.key); + } + if (message.code !== "") { + writer.uint32(18).string(message.code); + } + if (message.location !== 0) { + writer.uint32(24).int32(message.location); + } + if (message.repeat !== false) { + writer.uint32(32).bool(message.repeat); + } + if (message.isComposing !== false) { + writer.uint32(40).bool(message.isComposing); + } + if (message.modifiers !== undefined) { + Modifiers.encode(message.modifiers, writer.uint32(50).fork()).join(); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): KeyboardData { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseKeyboardData(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.key = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.code = reader.string(); + continue; + } + case 3: { + if (tag !== 24) { + break; + } + + message.location = reader.int32() as any; + continue; + } + case 4: { + if (tag !== 32) { + break; + } + + message.repeat = reader.bool(); + continue; + } + case 5: { + if (tag !== 40) { + break; + } + + message.isComposing = reader.bool(); + continue; + } + case 6: { + if (tag !== 50) { + break; + } + + message.modifiers = Modifiers.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseFormField(): FormField { + return { name: "", value: "", _unknownFields: {} }; +} + +export const FormField: MessageFns = { + encode(message: FormField, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.name !== "") { + writer.uint32(10).string(message.name); + } + if (message.value !== "") { + writer.uint32(18).string(message.value); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): FormField { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseFormField(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.name = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.value = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseFormData(): FormData { + return { value: "", checked: undefined, fields: [], _unknownFields: {} }; +} + +export const FormData: MessageFns = { + encode(message: FormData, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.value !== "") { + writer.uint32(10).string(message.value); + } + if (message.checked !== undefined) { + writer.uint32(16).bool(message.checked); + } + for (const v of message.fields) { + FormField.encode(v!, writer.uint32(26).fork()).join(); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): FormData { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseFormData(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.value = reader.string(); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.checked = reader.bool(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.fields.push(FormField.decode(reader, reader.uint32())); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseScrollData(): ScrollData { + return { + scrollTop: 0, + scrollLeft: 0, + scrollWidth: 0, + scrollHeight: 0, + clientWidth: 0, + clientHeight: 0, + _unknownFields: {}, + }; +} + +export const ScrollData: MessageFns = { + encode(message: ScrollData, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.scrollTop !== 0) { + writer.uint32(9).double(message.scrollTop); + } + if (message.scrollLeft !== 0) { + writer.uint32(17).double(message.scrollLeft); + } + if (message.scrollWidth !== 0) { + writer.uint32(24).sint32(message.scrollWidth); + } + if (message.scrollHeight !== 0) { + writer.uint32(32).sint32(message.scrollHeight); + } + if (message.clientWidth !== 0) { + writer.uint32(40).sint32(message.clientWidth); + } + if (message.clientHeight !== 0) { + writer.uint32(48).sint32(message.clientHeight); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): ScrollData { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseScrollData(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 9) { + break; + } + + message.scrollTop = reader.double(); + continue; + } + case 2: { + if (tag !== 17) { + break; + } + + message.scrollLeft = reader.double(); + continue; + } + case 3: { + if (tag !== 24) { + break; + } + + message.scrollWidth = reader.sint32(); + continue; + } + case 4: { + if (tag !== 32) { + break; + } + + message.scrollHeight = reader.sint32(); + continue; + } + case 5: { + if (tag !== 40) { + break; + } + + message.clientWidth = reader.sint32(); + continue; + } + case 6: { + if (tag !== 48) { + break; + } + + message.clientHeight = reader.sint32(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseCompositionData(): CompositionData { + return { data: "", _unknownFields: {} }; +} + +export const CompositionData: MessageFns = { + encode(message: CompositionData, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.data !== "") { + writer.uint32(10).string(message.data); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): CompositionData { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseCompositionData(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.data = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseAnimationData(): AnimationData { + return { animationName: "", pseudoElement: "", elapsedTime: 0, _unknownFields: {} }; +} + +export const AnimationData: MessageFns = { + encode(message: AnimationData, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.animationName !== "") { + writer.uint32(10).string(message.animationName); + } + if (message.pseudoElement !== "") { + writer.uint32(18).string(message.pseudoElement); + } + if (message.elapsedTime !== 0) { + writer.uint32(25).double(message.elapsedTime); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): AnimationData { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseAnimationData(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.animationName = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.pseudoElement = reader.string(); + continue; + } + case 3: { + if (tag !== 25) { + break; + } + + message.elapsedTime = reader.double(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseTransitionData(): TransitionData { + return { propertyName: "", pseudoElement: "", elapsedTime: 0, _unknownFields: {} }; +} + +export const TransitionData: MessageFns = { + encode(message: TransitionData, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.propertyName !== "") { + writer.uint32(10).string(message.propertyName); + } + if (message.pseudoElement !== "") { + writer.uint32(18).string(message.pseudoElement); + } + if (message.elapsedTime !== 0) { + writer.uint32(25).double(message.elapsedTime); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): TransitionData { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseTransitionData(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.propertyName = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.pseudoElement = reader.string(); + continue; + } + case 3: { + if (tag !== 25) { + break; + } + + message.elapsedTime = reader.double(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseTouchPoint(): TouchPoint { + return { + identifier: 0, + clientX: 0, + clientY: 0, + pageX: 0, + pageY: 0, + screenX: 0, + screenY: 0, + radiusX: 0, + radiusY: 0, + rotationAngle: 0, + force: 0, + _unknownFields: {}, + }; +} + +export const TouchPoint: MessageFns = { + encode(message: TouchPoint, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.identifier !== 0) { + writer.uint32(8).sint32(message.identifier); + } + if (message.clientX !== 0) { + writer.uint32(17).double(message.clientX); + } + if (message.clientY !== 0) { + writer.uint32(25).double(message.clientY); + } + if (message.pageX !== 0) { + writer.uint32(33).double(message.pageX); + } + if (message.pageY !== 0) { + writer.uint32(41).double(message.pageY); + } + if (message.screenX !== 0) { + writer.uint32(49).double(message.screenX); + } + if (message.screenY !== 0) { + writer.uint32(57).double(message.screenY); + } + if (message.radiusX !== 0) { + writer.uint32(65).double(message.radiusX); + } + if (message.radiusY !== 0) { + writer.uint32(73).double(message.radiusY); + } + if (message.rotationAngle !== 0) { + writer.uint32(81).double(message.rotationAngle); + } + if (message.force !== 0) { + writer.uint32(89).double(message.force); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): TouchPoint { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseTouchPoint(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.identifier = reader.sint32(); + continue; + } + case 2: { + if (tag !== 17) { + break; + } + + message.clientX = reader.double(); + continue; + } + case 3: { + if (tag !== 25) { + break; + } + + message.clientY = reader.double(); + continue; + } + case 4: { + if (tag !== 33) { + break; + } + + message.pageX = reader.double(); + continue; + } + case 5: { + if (tag !== 41) { + break; + } + + message.pageY = reader.double(); + continue; + } + case 6: { + if (tag !== 49) { + break; + } + + message.screenX = reader.double(); + continue; + } + case 7: { + if (tag !== 57) { + break; + } + + message.screenY = reader.double(); + continue; + } + case 8: { + if (tag !== 65) { + break; + } + + message.radiusX = reader.double(); + continue; + } + case 9: { + if (tag !== 73) { + break; + } + + message.radiusY = reader.double(); + continue; + } + case 10: { + if (tag !== 81) { + break; + } + + message.rotationAngle = reader.double(); + continue; + } + case 11: { + if (tag !== 89) { + break; + } + + message.force = reader.double(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseTouchData(): TouchData { + return { touches: [], changedTouches: [], targetTouches: [], modifiers: undefined, _unknownFields: {} }; +} + +export const TouchData: MessageFns = { + encode(message: TouchData, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + for (const v of message.touches) { + TouchPoint.encode(v!, writer.uint32(10).fork()).join(); + } + for (const v of message.changedTouches) { + TouchPoint.encode(v!, writer.uint32(18).fork()).join(); + } + for (const v of message.targetTouches) { + TouchPoint.encode(v!, writer.uint32(26).fork()).join(); + } + if (message.modifiers !== undefined) { + Modifiers.encode(message.modifiers, writer.uint32(34).fork()).join(); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): TouchData { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseTouchData(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.touches.push(TouchPoint.decode(reader, reader.uint32())); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.changedTouches.push(TouchPoint.decode(reader, reader.uint32())); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.targetTouches.push(TouchPoint.decode(reader, reader.uint32())); + continue; + } + case 4: { + if (tag !== 34) { + break; + } + + message.modifiers = Modifiers.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseSize(): Size { + return { width: 0, height: 0, _unknownFields: {} }; +} + +export const Size: MessageFns = { + encode(message: Size, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.width !== 0) { + writer.uint32(9).double(message.width); + } + if (message.height !== 0) { + writer.uint32(17).double(message.height); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Size { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseSize(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 9) { + break; + } + + message.width = reader.double(); + continue; + } + case 2: { + if (tag !== 17) { + break; + } + + message.height = reader.double(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseRect(): Rect { + return { x: 0, y: 0, width: 0, height: 0, _unknownFields: {} }; +} + +export const Rect: MessageFns = { + encode(message: Rect, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.x !== 0) { + writer.uint32(9).double(message.x); + } + if (message.y !== 0) { + writer.uint32(17).double(message.y); + } + if (message.width !== 0) { + writer.uint32(25).double(message.width); + } + if (message.height !== 0) { + writer.uint32(33).double(message.height); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Rect { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseRect(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 9) { + break; + } + + message.x = reader.double(); + continue; + } + case 2: { + if (tag !== 17) { + break; + } + + message.y = reader.double(); + continue; + } + case 3: { + if (tag !== 25) { + break; + } + + message.width = reader.double(); + continue; + } + case 4: { + if (tag !== 33) { + break; + } + + message.height = reader.double(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseResizeData(): ResizeData { + return { borderBox: undefined, contentBox: undefined, _unknownFields: {} }; +} + +export const ResizeData: MessageFns = { + encode(message: ResizeData, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.borderBox !== undefined) { + Size.encode(message.borderBox, writer.uint32(10).fork()).join(); + } + if (message.contentBox !== undefined) { + Size.encode(message.contentBox, writer.uint32(18).fork()).join(); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): ResizeData { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseResizeData(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.borderBox = Size.decode(reader, reader.uint32()); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.contentBox = Size.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseVisibleData(): VisibleData { + return { + boundingClientRect: undefined, + intersectionRatio: 0, + intersectionRect: undefined, + isIntersecting: false, + rootBounds: undefined, + timeMs: 0, + _unknownFields: {}, + }; +} + +export const VisibleData: MessageFns = { + encode(message: VisibleData, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.boundingClientRect !== undefined) { + Rect.encode(message.boundingClientRect, writer.uint32(10).fork()).join(); + } + if (message.intersectionRatio !== 0) { + writer.uint32(17).double(message.intersectionRatio); + } + if (message.intersectionRect !== undefined) { + Rect.encode(message.intersectionRect, writer.uint32(26).fork()).join(); + } + if (message.isIntersecting !== false) { + writer.uint32(32).bool(message.isIntersecting); + } + if (message.rootBounds !== undefined) { + Rect.encode(message.rootBounds, writer.uint32(42).fork()).join(); + } + if (message.timeMs !== 0) { + writer.uint32(49).double(message.timeMs); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): VisibleData { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseVisibleData(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.boundingClientRect = Rect.decode(reader, reader.uint32()); + continue; + } + case 2: { + if (tag !== 17) { + break; + } + + message.intersectionRatio = reader.double(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.intersectionRect = Rect.decode(reader, reader.uint32()); + continue; + } + case 4: { + if (tag !== 32) { + break; + } + + message.isIntersecting = reader.bool(); + continue; + } + case 5: { + if (tag !== 42) { + break; + } + + message.rootBounds = Rect.decode(reader, reader.uint32()); + continue; + } + case 6: { + if (tag !== 49) { + break; + } + + message.timeMs = reader.double(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseFrameData(): FrameData { + return { timeMs: 0, _unknownFields: {} }; +} + +export const FrameData: MessageFns = { + encode(message: FrameData, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.timeMs !== 0) { + writer.uint32(9).double(message.timeMs); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): FrameData { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseFrameData(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 9) { + break; + } + + message.timeMs = reader.double(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseNavigationData(): NavigationData { + return { href: "", _unknownFields: {} }; +} + +export const NavigationData: MessageFns = { + encode(message: NavigationData, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.href !== "") { + writer.uint32(10).string(message.href); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): NavigationData { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseNavigationData(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.href = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseEventPayload(): EventPayload { + return { family: undefined, _unknownFields: {} }; +} + +export const EventPayload: MessageFns = { + encode(message: EventPayload, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + switch (message.family?.$case) { + case "mouse": + MouseData.encode(message.family.value, writer.uint32(10).fork()).join(); + break; + case "keyboard": + KeyboardData.encode(message.family.value, writer.uint32(18).fork()).join(); + break; + case "form": + FormData.encode(message.family.value, writer.uint32(26).fork()).join(); + break; + case "pointer": + PointerData.encode(message.family.value, writer.uint32(34).fork()).join(); + break; + case "wheel": + WheelData.encode(message.family.value, writer.uint32(42).fork()).join(); + break; + case "touch": + TouchData.encode(message.family.value, writer.uint32(50).fork()).join(); + break; + case "scroll": + ScrollData.encode(message.family.value, writer.uint32(58).fork()).join(); + break; + case "frame": + FrameData.encode(message.family.value, writer.uint32(66).fork()).join(); + break; + case "composition": + CompositionData.encode(message.family.value, writer.uint32(74).fork()).join(); + break; + case "animation": + AnimationData.encode(message.family.value, writer.uint32(82).fork()).join(); + break; + case "transition": + TransitionData.encode(message.family.value, writer.uint32(90).fork()).join(); + break; + case "resize": + ResizeData.encode(message.family.value, writer.uint32(98).fork()).join(); + break; + case "visible": + VisibleData.encode(message.family.value, writer.uint32(106).fork()).join(); + break; + case "navigation": + NavigationData.encode(message.family.value, writer.uint32(114).fork()).join(); + break; + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): EventPayload { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseEventPayload(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.family = { $case: "mouse", value: MouseData.decode(reader, reader.uint32()) }; + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.family = { $case: "keyboard", value: KeyboardData.decode(reader, reader.uint32()) }; + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.family = { $case: "form", value: FormData.decode(reader, reader.uint32()) }; + continue; + } + case 4: { + if (tag !== 34) { + break; + } + + message.family = { $case: "pointer", value: PointerData.decode(reader, reader.uint32()) }; + continue; + } + case 5: { + if (tag !== 42) { + break; + } + + message.family = { $case: "wheel", value: WheelData.decode(reader, reader.uint32()) }; + continue; + } + case 6: { + if (tag !== 50) { + break; + } + + message.family = { $case: "touch", value: TouchData.decode(reader, reader.uint32()) }; + continue; + } + case 7: { + if (tag !== 58) { + break; + } + + message.family = { $case: "scroll", value: ScrollData.decode(reader, reader.uint32()) }; + continue; + } + case 8: { + if (tag !== 66) { + break; + } + + message.family = { $case: "frame", value: FrameData.decode(reader, reader.uint32()) }; + continue; + } + case 9: { + if (tag !== 74) { + break; + } + + message.family = { $case: "composition", value: CompositionData.decode(reader, reader.uint32()) }; + continue; + } + case 10: { + if (tag !== 82) { + break; + } + + message.family = { $case: "animation", value: AnimationData.decode(reader, reader.uint32()) }; + continue; + } + case 11: { + if (tag !== 90) { + break; + } + + message.family = { $case: "transition", value: TransitionData.decode(reader, reader.uint32()) }; + continue; + } + case 12: { + if (tag !== 98) { + break; + } + + message.family = { $case: "resize", value: ResizeData.decode(reader, reader.uint32()) }; + continue; + } + case 13: { + if (tag !== 106) { + break; + } + + message.family = { $case: "visible", value: VisibleData.decode(reader, reader.uint32()) }; + continue; + } + case 14: { + if (tag !== 114) { + break; + } + + message.family = { $case: "navigation", value: NavigationData.decode(reader, reader.uint32()) }; + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +export interface MessageFns { + encode(message: T, writer?: BinaryWriter): BinaryWriter; + decode(input: BinaryReader | Uint8Array, length?: number): T; +} diff --git a/receiver/src/gen/stream-dom.ts b/receiver/src/gen/stream-dom.ts new file mode 100644 index 0000000..7771efb --- /dev/null +++ b/receiver/src/gen/stream-dom.ts @@ -0,0 +1,2307 @@ +// Code generated by protoc-gen-ts_proto. DO NOT EDIT. +// source: stream-dom.proto + +/* eslint-disable */ +import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; + +export const protobufPackage = "polymorph.stream_dom"; + +/** + * The two receiver-side singletons a producer can listen on without + * creating them. Frameworks register `popstate`, `hashchange`, `resize`, + * `visibilitychange`, media-query and global keyboard listeners here, and + * nothing in the tree under the mount root stands in for them. + */ +export enum Global { + WINDOW = 0, + DOCUMENT = 1, + UNRECOGNIZED = -1, +} + +/** + * One frame: at most one op, plus the batch-boundary flag. + * + * `commit` marks the last frame of a batch. It is a signal, not a + * buffering instruction: after-commit work (synthetic `mounted`, islands) + * runs once the frame's op, if any, is applied. A frame with no op set is + * legal only when it commits (an empty batch). A transformer that drops + * the frame carrying the flag re-emits it on the batch's last surviving + * frame, or on an op-less frame if none survives; one that merges batches + * sets it once, on the final frame. Receivers honor the flag after the op + * whatever the field order in the frame. + * + * Field numbers 1-15 encode as one tag byte, 16+ as two. `commit` rides on + * the hottest frames and takes 1; ops are ordered roughly by frequency so + * the two-byte tag lands on a rare one. A receiver that does not know an + * op case skips the frame (still honoring `commit`). + */ +export interface Frame { + commit: boolean; + op: + | { $case: "insertBefore"; value: InsertBefore } + | { $case: "setText"; value: SetText } + | { $case: "setAttribute"; value: SetAttribute } + | { $case: "setProperty"; value: SetProperty } + | { $case: "createElement"; value: CreateElement } + | { $case: "createText"; value: CreateText } + | // + /** Detach and free the subtree rooted at this id. */ + { $case: "remove"; value: Remove } + | { $case: "cloneTemplate"; value: CloneTemplate } + | { $case: "bindPath"; value: BindPath } + | // + /** A comment node used as an insertion anchor for fragments. */ + { $case: "createPlaceholder"; value: CreatePlaceholder } + | { $case: "addListener"; value: AddListener } + | { $case: "removeListener"; value: RemoveListener } + | { $case: "intern"; value: Intern } + | { $case: "registerTemplate"; value: RegisterTemplate } + | { $case: "insertAfter"; value: InsertAfter } + | { $case: "bindMarker"; value: BindMarker } + | undefined; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** Define (or overwrite) interned slot `id`. */ +export interface Intern { + id: number; + s: string; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** Create a detached element. `ns` is an interned namespace URI. */ +export interface CreateElement { + id: number; + tag: number; + ns?: number | undefined; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** Create a detached text node. */ +export interface CreateText { + id: number; + text: string; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +export interface CreatePlaceholder { + id: number; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** + * Insert `id` before `anchor`, or append it to `parent` when there is no + * `anchor`. `parent` is required without an `anchor` and optional with + * one (the anchor's parent is then implied, as in the DOM's insertBefore); + * a frame with neither is an error. Producers whose renderer never names + * a parent (Dioxus) omit it; a coalescer keys the op on the anchor's + * subtree. Moving an already-inserted node is allowed and is exactly what + * the DOM's insertBefore does; receivers prefer moveBefore where + * available so the move preserves node state. + */ +export interface InsertBefore { + parent?: number | undefined; + id: number; + anchor?: number | undefined; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** + * Insert `id` immediately after `anchor` (under `parent`, which is + * optional and implied by the anchor). The DOM has no insertAfter, but + * diffing renderers emit "insert after this sibling" for list appends, + * and without this op a producer either tracks every node's next sibling + * or moves the anchor around the new nodes. Same move semantics as + * InsertBefore. + */ +export interface InsertAfter { + parent?: number | undefined; + id: number; + anchor: number; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +export interface Remove { + id: number; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** Set a text node's content. */ +export interface SetText { + id: number; + text: string; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** + * Set or remove (no `value`) an attribute. The producer adapter decides + * attribute vs property using its framework's own table. `asset` is an + * opaque handle the receiver materializes into a URL (a content hash by + * convention, but the receiver's `resolveAsset` decides); the producer + * never names a URL through it — docs/design.md "Assets". + */ +export interface SetAttribute { + id: number; + name: number; + ns?: number | undefined; + value: { $case: "text"; value: string } | { $case: "asset"; value: Uint8Array } | undefined; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** + * Set a DOM property (`value`, `checked`, `innerHTML`, ...) to a typed + * value. No `value` case set deletes / sets undefined. + */ +export interface SetProperty { + id: number; + name: number; + value: { $case: "text"; value: string } | { $case: "int"; value: number } | { $case: "float"; value: number } | { + $case: "boolean"; + value: boolean; + } | undefined; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** + * Listener registration. + * + * The target is a node id or a global. `bubbles` is the producer's + * verdict for this event name; the receiver delegates bubbling events at + * the mount root and attaches non-bubbling ones per element (globals are + * always attached directly), and needs the same bit on removal. + * `prevent_default` / `stop_propagation` are the declarative path a remote + * receiver can honor without a round trip; see docs/design.md "Events". + */ +export interface Listener { + target: { $case: "id"; value: number } | { $case: "global"; value: Global } | undefined; + name: number; + bubbles: boolean; + capture: boolean; + passive: boolean; + preventDefault: boolean; + stopPropagation: boolean; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +export interface AddListener { + listener: Listener | undefined; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +export interface RemoveListener { + listener: Listener | undefined; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +export interface TemplateAttr { + name: number; + ns?: number | undefined; + value: { $case: "text"; value: string } | { $case: "asset"; value: Uint8Array } | undefined; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** Children are indices into the enclosing RegisterTemplate.nodes. */ +export interface TemplateElement { + tag: number; + ns?: number | undefined; + attrs: TemplateAttr[]; + children: number[]; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** + * An element hole (nested component, control-flow site). Clones as a + * placeholder to InsertBefore against. Text and attribute holes need no + * marker: BindPath to the node, then SetText / SetAttribute. + */ +export interface Dynamic { + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +export interface TemplateNode { + kind: { $case: "element"; value: TemplateElement } | { $case: "text"; value: string } | { + $case: "dynamic"; + value: Dynamic; + } | undefined; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** + * A template is a flat pre-order arena. A receiver validates indices + * (range, acyclicity) rather than trusting them. An arena rather than an + * HTML string because the HTML parser reshapes trees, which would make + * BindPath paths unreliable; a compiler that has the HTML parses it into + * the arena at build time. + */ +export interface RegisterTemplate { + id: number; + nodes: TemplateNode[]; + roots: number[]; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** Deep-clone template `tmpl`'s root number `root` as a detached node `id`. */ +export interface CloneTemplate { + tmpl: number; + root: number; + id: number; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** + * Bind `id` to the interior node of a cloned template reached from `root` + * by walking child indices in `path` (one byte per step). Positions are + * reliable here because the producer built the tree. Self-contained: the + * root is named, not implied. + */ +export interface BindPath { + root: number; + path: Uint8Array; + id: number; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +/** + * Bind `id` to the prerendered node the server-side render stamped with + * `key` (an attribute on elements, a comment beside text nodes and + * placeholders; the syntax is an SSR/receiver agreement, not part of this + * stream). By marker rather than by position because the HTML parser + * reshapes the served tree — insertion,

auto-close, adjacent + * text merged — so child indices from the mount root are not trustworthy. + * A missing key is a receiver error. + */ +export interface BindMarker { + key: number; + id: number; + _unknownFields?: { [key: number]: Uint8Array[] } | undefined; +} + +function createBaseFrame(): Frame { + return { commit: false, op: undefined, _unknownFields: {} }; +} + +export const Frame: MessageFns = { + encode(message: Frame, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.commit !== false) { + writer.uint32(8).bool(message.commit); + } + switch (message.op?.$case) { + case "insertBefore": + InsertBefore.encode(message.op.value, writer.uint32(18).fork()).join(); + break; + case "setText": + SetText.encode(message.op.value, writer.uint32(26).fork()).join(); + break; + case "setAttribute": + SetAttribute.encode(message.op.value, writer.uint32(34).fork()).join(); + break; + case "setProperty": + SetProperty.encode(message.op.value, writer.uint32(42).fork()).join(); + break; + case "createElement": + CreateElement.encode(message.op.value, writer.uint32(50).fork()).join(); + break; + case "createText": + CreateText.encode(message.op.value, writer.uint32(58).fork()).join(); + break; + case "remove": + Remove.encode(message.op.value, writer.uint32(66).fork()).join(); + break; + case "cloneTemplate": + CloneTemplate.encode(message.op.value, writer.uint32(74).fork()).join(); + break; + case "bindPath": + BindPath.encode(message.op.value, writer.uint32(82).fork()).join(); + break; + case "createPlaceholder": + CreatePlaceholder.encode(message.op.value, writer.uint32(90).fork()).join(); + break; + case "addListener": + AddListener.encode(message.op.value, writer.uint32(98).fork()).join(); + break; + case "removeListener": + RemoveListener.encode(message.op.value, writer.uint32(106).fork()).join(); + break; + case "intern": + Intern.encode(message.op.value, writer.uint32(114).fork()).join(); + break; + case "registerTemplate": + RegisterTemplate.encode(message.op.value, writer.uint32(122).fork()).join(); + break; + case "insertAfter": + InsertAfter.encode(message.op.value, writer.uint32(130).fork()).join(); + break; + case "bindMarker": + BindMarker.encode(message.op.value, writer.uint32(138).fork()).join(); + break; + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Frame { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseFrame(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.commit = reader.bool(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.op = { $case: "insertBefore", value: InsertBefore.decode(reader, reader.uint32()) }; + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.op = { $case: "setText", value: SetText.decode(reader, reader.uint32()) }; + continue; + } + case 4: { + if (tag !== 34) { + break; + } + + message.op = { $case: "setAttribute", value: SetAttribute.decode(reader, reader.uint32()) }; + continue; + } + case 5: { + if (tag !== 42) { + break; + } + + message.op = { $case: "setProperty", value: SetProperty.decode(reader, reader.uint32()) }; + continue; + } + case 6: { + if (tag !== 50) { + break; + } + + message.op = { $case: "createElement", value: CreateElement.decode(reader, reader.uint32()) }; + continue; + } + case 7: { + if (tag !== 58) { + break; + } + + message.op = { $case: "createText", value: CreateText.decode(reader, reader.uint32()) }; + continue; + } + case 8: { + if (tag !== 66) { + break; + } + + message.op = { $case: "remove", value: Remove.decode(reader, reader.uint32()) }; + continue; + } + case 9: { + if (tag !== 74) { + break; + } + + message.op = { $case: "cloneTemplate", value: CloneTemplate.decode(reader, reader.uint32()) }; + continue; + } + case 10: { + if (tag !== 82) { + break; + } + + message.op = { $case: "bindPath", value: BindPath.decode(reader, reader.uint32()) }; + continue; + } + case 11: { + if (tag !== 90) { + break; + } + + message.op = { $case: "createPlaceholder", value: CreatePlaceholder.decode(reader, reader.uint32()) }; + continue; + } + case 12: { + if (tag !== 98) { + break; + } + + message.op = { $case: "addListener", value: AddListener.decode(reader, reader.uint32()) }; + continue; + } + case 13: { + if (tag !== 106) { + break; + } + + message.op = { $case: "removeListener", value: RemoveListener.decode(reader, reader.uint32()) }; + continue; + } + case 14: { + if (tag !== 114) { + break; + } + + message.op = { $case: "intern", value: Intern.decode(reader, reader.uint32()) }; + continue; + } + case 15: { + if (tag !== 122) { + break; + } + + message.op = { $case: "registerTemplate", value: RegisterTemplate.decode(reader, reader.uint32()) }; + continue; + } + case 16: { + if (tag !== 130) { + break; + } + + message.op = { $case: "insertAfter", value: InsertAfter.decode(reader, reader.uint32()) }; + continue; + } + case 17: { + if (tag !== 138) { + break; + } + + message.op = { $case: "bindMarker", value: BindMarker.decode(reader, reader.uint32()) }; + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseIntern(): Intern { + return { id: 0, s: "", _unknownFields: {} }; +} + +export const Intern: MessageFns = { + encode(message: Intern, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== 0) { + writer.uint32(8).uint32(message.id); + } + if (message.s !== "") { + writer.uint32(18).string(message.s); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Intern { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseIntern(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.id = reader.uint32(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.s = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseCreateElement(): CreateElement { + return { id: 0, tag: 0, ns: undefined, _unknownFields: {} }; +} + +export const CreateElement: MessageFns = { + encode(message: CreateElement, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== 0) { + writer.uint32(8).uint32(message.id); + } + if (message.tag !== 0) { + writer.uint32(16).uint32(message.tag); + } + if (message.ns !== undefined) { + writer.uint32(24).uint32(message.ns); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): CreateElement { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseCreateElement(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.id = reader.uint32(); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.tag = reader.uint32(); + continue; + } + case 3: { + if (tag !== 24) { + break; + } + + message.ns = reader.uint32(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseCreateText(): CreateText { + return { id: 0, text: "", _unknownFields: {} }; +} + +export const CreateText: MessageFns = { + encode(message: CreateText, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== 0) { + writer.uint32(8).uint32(message.id); + } + if (message.text !== "") { + writer.uint32(18).string(message.text); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): CreateText { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseCreateText(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.id = reader.uint32(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.text = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseCreatePlaceholder(): CreatePlaceholder { + return { id: 0, _unknownFields: {} }; +} + +export const CreatePlaceholder: MessageFns = { + encode(message: CreatePlaceholder, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== 0) { + writer.uint32(8).uint32(message.id); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): CreatePlaceholder { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseCreatePlaceholder(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.id = reader.uint32(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseInsertBefore(): InsertBefore { + return { parent: undefined, id: 0, anchor: undefined, _unknownFields: {} }; +} + +export const InsertBefore: MessageFns = { + encode(message: InsertBefore, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.parent !== undefined) { + writer.uint32(8).uint32(message.parent); + } + if (message.id !== 0) { + writer.uint32(16).uint32(message.id); + } + if (message.anchor !== undefined) { + writer.uint32(24).uint32(message.anchor); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): InsertBefore { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseInsertBefore(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.parent = reader.uint32(); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.id = reader.uint32(); + continue; + } + case 3: { + if (tag !== 24) { + break; + } + + message.anchor = reader.uint32(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseInsertAfter(): InsertAfter { + return { parent: undefined, id: 0, anchor: 0, _unknownFields: {} }; +} + +export const InsertAfter: MessageFns = { + encode(message: InsertAfter, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.parent !== undefined) { + writer.uint32(8).uint32(message.parent); + } + if (message.id !== 0) { + writer.uint32(16).uint32(message.id); + } + if (message.anchor !== 0) { + writer.uint32(24).uint32(message.anchor); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): InsertAfter { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseInsertAfter(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.parent = reader.uint32(); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.id = reader.uint32(); + continue; + } + case 3: { + if (tag !== 24) { + break; + } + + message.anchor = reader.uint32(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseRemove(): Remove { + return { id: 0, _unknownFields: {} }; +} + +export const Remove: MessageFns = { + encode(message: Remove, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== 0) { + writer.uint32(8).uint32(message.id); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Remove { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseRemove(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.id = reader.uint32(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseSetText(): SetText { + return { id: 0, text: "", _unknownFields: {} }; +} + +export const SetText: MessageFns = { + encode(message: SetText, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== 0) { + writer.uint32(8).uint32(message.id); + } + if (message.text !== "") { + writer.uint32(18).string(message.text); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): SetText { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseSetText(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.id = reader.uint32(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.text = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseSetAttribute(): SetAttribute { + return { id: 0, name: 0, ns: undefined, value: undefined, _unknownFields: {} }; +} + +export const SetAttribute: MessageFns = { + encode(message: SetAttribute, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== 0) { + writer.uint32(8).uint32(message.id); + } + if (message.name !== 0) { + writer.uint32(16).uint32(message.name); + } + if (message.ns !== undefined) { + writer.uint32(24).uint32(message.ns); + } + switch (message.value?.$case) { + case "text": + writer.uint32(34).string(message.value.value); + break; + case "asset": + writer.uint32(42).bytes(message.value.value); + break; + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): SetAttribute { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseSetAttribute(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.id = reader.uint32(); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.name = reader.uint32(); + continue; + } + case 3: { + if (tag !== 24) { + break; + } + + message.ns = reader.uint32(); + continue; + } + case 4: { + if (tag !== 34) { + break; + } + + message.value = { $case: "text", value: reader.string() }; + continue; + } + case 5: { + if (tag !== 42) { + break; + } + + message.value = { $case: "asset", value: reader.bytes() }; + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseSetProperty(): SetProperty { + return { id: 0, name: 0, value: undefined, _unknownFields: {} }; +} + +export const SetProperty: MessageFns = { + encode(message: SetProperty, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== 0) { + writer.uint32(8).uint32(message.id); + } + if (message.name !== 0) { + writer.uint32(16).uint32(message.name); + } + switch (message.value?.$case) { + case "text": + writer.uint32(26).string(message.value.value); + break; + case "int": + writer.uint32(32).sint64(message.value.value); + break; + case "float": + writer.uint32(41).double(message.value.value); + break; + case "boolean": + writer.uint32(48).bool(message.value.value); + break; + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): SetProperty { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseSetProperty(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.id = reader.uint32(); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.name = reader.uint32(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.value = { $case: "text", value: reader.string() }; + continue; + } + case 4: { + if (tag !== 32) { + break; + } + + message.value = { $case: "int", value: longToNumber(reader.sint64()) }; + continue; + } + case 5: { + if (tag !== 41) { + break; + } + + message.value = { $case: "float", value: reader.double() }; + continue; + } + case 6: { + if (tag !== 48) { + break; + } + + message.value = { $case: "boolean", value: reader.bool() }; + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseListener(): Listener { + return { + target: undefined, + name: 0, + bubbles: false, + capture: false, + passive: false, + preventDefault: false, + stopPropagation: false, + _unknownFields: {}, + }; +} + +export const Listener: MessageFns = { + encode(message: Listener, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + switch (message.target?.$case) { + case "id": + writer.uint32(8).uint32(message.target.value); + break; + case "global": + writer.uint32(64).int32(message.target.value); + break; + } + if (message.name !== 0) { + writer.uint32(16).uint32(message.name); + } + if (message.bubbles !== false) { + writer.uint32(24).bool(message.bubbles); + } + if (message.capture !== false) { + writer.uint32(32).bool(message.capture); + } + if (message.passive !== false) { + writer.uint32(40).bool(message.passive); + } + if (message.preventDefault !== false) { + writer.uint32(48).bool(message.preventDefault); + } + if (message.stopPropagation !== false) { + writer.uint32(56).bool(message.stopPropagation); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Listener { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseListener(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.target = { $case: "id", value: reader.uint32() }; + continue; + } + case 8: { + if (tag !== 64) { + break; + } + + message.target = { $case: "global", value: reader.int32() as any }; + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.name = reader.uint32(); + continue; + } + case 3: { + if (tag !== 24) { + break; + } + + message.bubbles = reader.bool(); + continue; + } + case 4: { + if (tag !== 32) { + break; + } + + message.capture = reader.bool(); + continue; + } + case 5: { + if (tag !== 40) { + break; + } + + message.passive = reader.bool(); + continue; + } + case 6: { + if (tag !== 48) { + break; + } + + message.preventDefault = reader.bool(); + continue; + } + case 7: { + if (tag !== 56) { + break; + } + + message.stopPropagation = reader.bool(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseAddListener(): AddListener { + return { listener: undefined, _unknownFields: {} }; +} + +export const AddListener: MessageFns = { + encode(message: AddListener, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.listener !== undefined) { + Listener.encode(message.listener, writer.uint32(10).fork()).join(); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): AddListener { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseAddListener(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.listener = Listener.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseRemoveListener(): RemoveListener { + return { listener: undefined, _unknownFields: {} }; +} + +export const RemoveListener: MessageFns = { + encode(message: RemoveListener, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.listener !== undefined) { + Listener.encode(message.listener, writer.uint32(10).fork()).join(); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): RemoveListener { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseRemoveListener(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.listener = Listener.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseTemplateAttr(): TemplateAttr { + return { name: 0, ns: undefined, value: undefined, _unknownFields: {} }; +} + +export const TemplateAttr: MessageFns = { + encode(message: TemplateAttr, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.name !== 0) { + writer.uint32(8).uint32(message.name); + } + if (message.ns !== undefined) { + writer.uint32(16).uint32(message.ns); + } + switch (message.value?.$case) { + case "text": + writer.uint32(26).string(message.value.value); + break; + case "asset": + writer.uint32(34).bytes(message.value.value); + break; + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): TemplateAttr { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseTemplateAttr(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.name = reader.uint32(); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.ns = reader.uint32(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.value = { $case: "text", value: reader.string() }; + continue; + } + case 4: { + if (tag !== 34) { + break; + } + + message.value = { $case: "asset", value: reader.bytes() }; + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseTemplateElement(): TemplateElement { + return { tag: 0, ns: undefined, attrs: [], children: [], _unknownFields: {} }; +} + +export const TemplateElement: MessageFns = { + encode(message: TemplateElement, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.tag !== 0) { + writer.uint32(8).uint32(message.tag); + } + if (message.ns !== undefined) { + writer.uint32(16).uint32(message.ns); + } + for (const v of message.attrs) { + TemplateAttr.encode(v!, writer.uint32(26).fork()).join(); + } + writer.uint32(34).fork(); + for (const v of message.children) { + writer.uint32(v); + } + writer.join(); + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): TemplateElement { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseTemplateElement(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.tag = reader.uint32(); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.ns = reader.uint32(); + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.attrs.push(TemplateAttr.decode(reader, reader.uint32())); + continue; + } + case 4: { + if (tag === 32) { + message.children.push(reader.uint32()); + + continue; + } + + if (tag === 34) { + const end2 = reader.uint32() + reader.pos; + while (reader.pos < end2) { + message.children.push(reader.uint32()); + } + + continue; + } + + break; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseDynamic(): Dynamic { + return { _unknownFields: {} }; +} + +export const Dynamic: MessageFns = { + encode(message: Dynamic, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Dynamic { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseDynamic(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseTemplateNode(): TemplateNode { + return { kind: undefined, _unknownFields: {} }; +} + +export const TemplateNode: MessageFns = { + encode(message: TemplateNode, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + switch (message.kind?.$case) { + case "element": + TemplateElement.encode(message.kind.value, writer.uint32(10).fork()).join(); + break; + case "text": + writer.uint32(18).string(message.kind.value); + break; + case "dynamic": + Dynamic.encode(message.kind.value, writer.uint32(26).fork()).join(); + break; + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): TemplateNode { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseTemplateNode(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.kind = { $case: "element", value: TemplateElement.decode(reader, reader.uint32()) }; + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.kind = { $case: "text", value: reader.string() }; + continue; + } + case 3: { + if (tag !== 26) { + break; + } + + message.kind = { $case: "dynamic", value: Dynamic.decode(reader, reader.uint32()) }; + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseRegisterTemplate(): RegisterTemplate { + return { id: 0, nodes: [], roots: [], _unknownFields: {} }; +} + +export const RegisterTemplate: MessageFns = { + encode(message: RegisterTemplate, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== 0) { + writer.uint32(8).uint32(message.id); + } + for (const v of message.nodes) { + TemplateNode.encode(v!, writer.uint32(18).fork()).join(); + } + writer.uint32(26).fork(); + for (const v of message.roots) { + writer.uint32(v); + } + writer.join(); + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): RegisterTemplate { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseRegisterTemplate(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.id = reader.uint32(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.nodes.push(TemplateNode.decode(reader, reader.uint32())); + continue; + } + case 3: { + if (tag === 24) { + message.roots.push(reader.uint32()); + + continue; + } + + if (tag === 26) { + const end2 = reader.uint32() + reader.pos; + while (reader.pos < end2) { + message.roots.push(reader.uint32()); + } + + continue; + } + + break; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseCloneTemplate(): CloneTemplate { + return { tmpl: 0, root: 0, id: 0, _unknownFields: {} }; +} + +export const CloneTemplate: MessageFns = { + encode(message: CloneTemplate, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.tmpl !== 0) { + writer.uint32(8).uint32(message.tmpl); + } + if (message.root !== 0) { + writer.uint32(16).uint32(message.root); + } + if (message.id !== 0) { + writer.uint32(24).uint32(message.id); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): CloneTemplate { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseCloneTemplate(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.tmpl = reader.uint32(); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.root = reader.uint32(); + continue; + } + case 3: { + if (tag !== 24) { + break; + } + + message.id = reader.uint32(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseBindPath(): BindPath { + return { root: 0, path: new Uint8Array(0), id: 0, _unknownFields: {} }; +} + +export const BindPath: MessageFns = { + encode(message: BindPath, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.root !== 0) { + writer.uint32(8).uint32(message.root); + } + if (message.path.length !== 0) { + writer.uint32(18).bytes(message.path); + } + if (message.id !== 0) { + writer.uint32(24).uint32(message.id); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): BindPath { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseBindPath(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.root = reader.uint32(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.path = reader.bytes(); + continue; + } + case 3: { + if (tag !== 24) { + break; + } + + message.id = reader.uint32(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function createBaseBindMarker(): BindMarker { + return { key: 0, id: 0, _unknownFields: {} }; +} + +export const BindMarker: MessageFns = { + encode(message: BindMarker, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.key !== 0) { + writer.uint32(8).uint32(message.key); + } + if (message.id !== 0) { + writer.uint32(16).uint32(message.id); + } + if (message._unknownFields !== undefined) { + for (const [key, values] of globalThis.Object.entries(message._unknownFields)) { + const tag = parseInt(key, 10); + for (const value of values) { + writer.uint32(tag).raw(value); + } + } + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): BindMarker { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const previousRecursionDepth = (reader as any).__tsProtoDecodeDepth ?? 0; + if (previousRecursionDepth >= 100) { + throw new globalThis.Error("protobuf decode recursion limit exceeded"); + } + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth + 1; + try { + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseBindMarker(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 8) { + break; + } + + message.key = reader.uint32(); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.id = reader.uint32(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + const buf = reader.skip(tag & 7); + + const list = message._unknownFields![tag]; + + if (list === undefined) { + message._unknownFields![tag] = [buf]; + } else { + list.push(buf); + } + } + return message; + } finally { + (reader as any).__tsProtoDecodeDepth = previousRecursionDepth; + } + }, +}; + +function longToNumber(int64: { toString(): string }): number { + const num = globalThis.Number(int64.toString()); + if (num > globalThis.Number.MAX_SAFE_INTEGER) { + throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER"); + } + if (num < globalThis.Number.MIN_SAFE_INTEGER) { + throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER"); + } + return num; +} + +export interface MessageFns { + encode(message: T, writer?: BinaryWriter): BinaryWriter; + decode(input: BinaryReader | Uint8Array, length?: number): T; +} diff --git a/receiver/src/mod.ts b/receiver/src/mod.ts index 041e400..fb29182 100644 --- a/receiver/src/mod.ts +++ b/receiver/src/mod.ts @@ -1,6 +1,5 @@ // Re-exports for `@polymorph/stream-dom-receiver`. -export { Reader, WireType, Writer } from "./proto.ts"; export { FrameDecoder, PROTOCOL_VERSION } from "./frames.ts"; export type { AttrValue, diff --git a/receiver/src/proto.ts b/receiver/src/proto.ts deleted file mode 100644 index 42e7ca6..0000000 --- a/receiver/src/proto.ts +++ /dev/null @@ -1,313 +0,0 @@ -// Minimal protobuf (proto3) wire codec — no dependencies, no generated -// code. proto/stream-dom.proto and proto/stream-dom-events.proto are -// normative for every field number and type; this module only knows the -// wire format (docs/design.md "Encoding: protobuf, and why not a -// hand-rolled layout"). -// -// Deliberately thin: `frames.ts` and `events.ts` decode field-by-field with -// a switch on tag (the "pbf hybrid decode" the design record names), so -// there is no generated-message layer here at all — just varint/fixed/ -// length-delimited primitives plus wire-type skip. - -/** Protobuf wire types (varint tag = (fieldNumber << 3) | wireType). */ -export const enum WireType { - Varint = 0, - Fixed64 = 1, - LengthDelimited = 2, - Fixed32 = 5, -} - -/** Thrown only when a `Reader` ran out of bytes before it could finish - * decoding a value — as opposed to a value that IS fully present but - * malformed (e.g. a varint whose continuation bit stays set past the - * widest legal encoding). `FrameDecoder`'s straddling-frame buffering - * depends on telling these apart: a truncation means "wait for more - * bytes"; a malformed value is a real protocol error and must not be - * swallowed as if more bytes would fix it. */ -export class TruncatedError extends RangeError { - constructor(message: string) { - super(message); - this.name = "TruncatedError"; - } -} - -const textDecoder = new TextDecoder(); -const textEncoder = new TextEncoder(); - -/** Reads one message (or a length-delimited slice of one) from a - * `Uint8Array`. Proto3 semantics: a field absent from the wire has its - * type's default value; the caller (frames.ts) is responsible for - * presence-tracking `optional` scalars by noting which field numbers it - * saw. */ -export class Reader { - #buf: Uint8Array; - #pos: number; - #end: number; - - constructor(buf: Uint8Array, pos = 0, end = buf.length) { - this.#buf = buf; - this.#pos = pos; - this.#end = end; - } - - get pos(): number { - return this.#pos; - } - - /** Bytes remaining before `end`. */ - get remaining(): number { - return this.#end - this.#pos; - } - - finished(): boolean { - return this.#pos >= this.#end; - } - - #need(n: number): void { - if (this.#pos + n > this.#end) { - throw new TruncatedError( - `proto.Reader: truncated (need ${n} bytes, have ${this.remaining})`, - ); - } - } - - /** Unsigned varint, up to 32 significant bits (field tags, node ids, - * str-refs, `uint32` fields — everything in stream-dom.proto except the - * `sint64` case). Throws a plain `RangeError` (NOT `TruncatedError`) if - * the encoded value does not fit a u32 — that is a malformed varint, - * fully present on the wire, not a buffering situation. */ - readVarint32(): number { - let result = 0; - let shift = 0; - for (;;) { - this.#need(1); - const b = this.#buf[this.#pos++]; - result |= (b & 0x7f) << shift; - if ((b & 0x80) === 0) break; - shift += 7; - if (shift > 35) { - throw new RangeError("proto.Reader: varint too long for u32"); - } - } - return result >>> 0; - } - - /** Unsigned varint as a bigint, for fields wider than 32 bits (`sint64`). - */ - readVarint64(): bigint { - let result = 0n; - let shift = 0n; - for (;;) { - this.#need(1); - const b = this.#buf[this.#pos++]; - result |= BigInt(b & 0x7f) << shift; - if ((b & 0x80) === 0) break; - shift += 7n; - } - return result; - } - - /** Zigzag-decoded `sint32`. */ - readSInt32(): number { - const u = this.readVarint32(); - return (u >>> 1) ^ -(u & 1); - } - - /** Zigzag-decoded `sint64`, narrowed to `number` (`Number(bigint)`): - * every `sint64` on this wire (`SetProperty.int`) carries ordinary DOM - * property integers, never a value outside `Number.isSafeInteger` - * range, so bigint precision is not worth the API friction of a mixed - * number/bigint value type in `frames.ts`'s `FrameSink`. */ - readSInt64(): number { - const u = this.readVarint64(); - const doubled = u & 1n ? -(u + 1n) / 2n : u / 2n; - return Number(doubled); - } - - readBool(): boolean { - return this.readVarint32() !== 0; - } - - readFixed32(): number { - this.#need(4); - const v = new DataView( - this.#buf.buffer, - this.#buf.byteOffset + this.#pos, - 4, - ) - .getUint32(0, true); - this.#pos += 4; - return v; - } - - readDouble(): number { - this.#need(8); - const v = new DataView( - this.#buf.buffer, - this.#buf.byteOffset + this.#pos, - 8, - ) - .getFloat64(0, true); - this.#pos += 8; - return v; - } - - readFloat(): number { - this.#need(4); - const v = new DataView( - this.#buf.buffer, - this.#buf.byteOffset + this.#pos, - 4, - ) - .getFloat32(0, true); - this.#pos += 4; - return v; - } - - /** Length-delimited bytes, copied out (a view would alias a chunk buffer - * a caller may not retain — see FrameDecoder's straddling-frame - * handling). */ - readBytes(): Uint8Array { - const len = this.readVarint32(); - this.#need(len); - const out = this.#buf.slice(this.#pos, this.#pos + len); - this.#pos += len; - return out; - } - - readString(): string { - const len = this.readVarint32(); - this.#need(len); - const s = textDecoder.decode( - this.#buf.subarray(this.#pos, this.#pos + len), - ); - this.#pos += len; - return s; - } - - /** A length-delimited sub-message, as a `Reader` scoped to its bytes. */ - readMessage(): Reader { - const len = this.readVarint32(); - this.#need(len); - const r = new Reader(this.#buf, this.#pos, this.#pos + len); - this.#pos += len; - return r; - } - - /** Read a field tag, returning `[fieldNumber, wireType]`. */ - readTag(): [number, WireType] { - const tag = this.readVarint32(); - return [tag >>> 3, (tag & 0x7) as WireType]; - } - - /** Skip a field's value given its wire type (unknown-field / unknown-op - * tolerance — docs/design.md: "receivers skip what they do not know"). */ - skip(wireType: WireType): void { - switch (wireType) { - case WireType.Varint: - this.readVarint64(); - break; - case WireType.Fixed64: - this.#need(8); - this.#pos += 8; - break; - case WireType.LengthDelimited: { - const len = this.readVarint32(); - this.#need(len); - this.#pos += len; - break; - } - case WireType.Fixed32: - this.#need(4); - this.#pos += 4; - break; - default: - throw new RangeError(`proto.Reader: unknown wire type ${wireType}`); - } - } -} - -/** Writes one message into a growable byte buffer. Used by remote.ts's - * template validation error messages? No — by the receiver-side event - * re-encoder is not needed; this is here for `events.ts`'s `encodePayload` - * (an `EventPayload` the receiver sends back is decoded on the guest side, - * so this Writer needs only the cases stream-dom-events.proto's messages - * use: varint, bool, double, string, embedded messages). */ -export class Writer { - #chunks: number[] = []; - - #byte(b: number): void { - this.#chunks.push(b & 0xff); - } - - writeVarint32(value: number): void { - let v = value >>> 0; - for (;;) { - if ((v & ~0x7f) === 0) { - this.#byte(v); - return; - } - this.#byte((v & 0x7f) | 0x80); - v >>>= 7; - } - } - - writeSInt32(value: number): void { - this.writeVarint32(((value << 1) ^ (value >> 31)) >>> 0); - } - - writeTag(fieldNumber: number, wireType: WireType): void { - this.writeVarint32((fieldNumber << 3) | wireType); - } - - writeBool(fieldNumber: number, value: boolean): void { - this.writeTag(fieldNumber, WireType.Varint); - this.writeVarint32(value ? 1 : 0); - } - - writeUint32(fieldNumber: number, value: number): void { - this.writeTag(fieldNumber, WireType.Varint); - this.writeVarint32(value); - } - - writeSInt32Field(fieldNumber: number, value: number): void { - this.writeTag(fieldNumber, WireType.Varint); - this.writeSInt32(value); - } - - writeDouble(fieldNumber: number, value: number): void { - this.writeTag(fieldNumber, WireType.Fixed64); - const buf = new Uint8Array(8); - new DataView(buf.buffer).setFloat64(0, value, true); - for (const b of buf) this.#byte(b); - } - - writeFloat(fieldNumber: number, value: number): void { - this.writeTag(fieldNumber, WireType.Fixed32); - const buf = new Uint8Array(4); - new DataView(buf.buffer).setFloat32(0, value, true); - for (const b of buf) this.#byte(b); - } - - writeString(fieldNumber: number, value: string): void { - const bytes = textEncoder.encode(value); - this.writeTag(fieldNumber, WireType.LengthDelimited); - this.writeVarint32(bytes.length); - for (const b of bytes) this.#byte(b); - } - - /** Embedded message with a length prefix: builds `build` into a fresh - * `Writer`, then writes its bytes behind a length-delimited tag. */ - writeMessage(fieldNumber: number, build: (w: Writer) => void): void { - const sub = new Writer(); - build(sub); - const bytes = sub.finish(); - this.writeTag(fieldNumber, WireType.LengthDelimited); - this.writeVarint32(bytes.length); - for (const b of bytes) this.#byte(b); - } - - finish(): Uint8Array { - return Uint8Array.from(this.#chunks); - } -} diff --git a/receiver/tests/driver_test.ts b/receiver/tests/driver_test.ts index f56b8ee..8ccd066 100644 --- a/receiver/tests/driver_test.ts +++ b/receiver/tests/driver_test.ts @@ -11,7 +11,8 @@ import type { ProducerEventTarget } from "../src/driver.ts"; import { PROTOCOL_VERSION } from "../src/frames.ts"; import { PolicyError } from "../src/policy.ts"; import type { Policy } from "../src/policy.ts"; -import { Writer } from "../src/proto.ts"; +import { Frame } from "../src/gen/stream-dom.ts"; +import { frame as framed, stream } from "./wire.ts"; /** A policy that allows every op — the baseline these tests vary from. */ function allowAll(extra?: Partial): Policy { @@ -71,58 +72,10 @@ async function pushAndAwaitCommit( await p; } -// Frame field numbers, transcribed from proto/stream-dom.proto (see -// policy_test.ts's header comment for why these are independent literals -// rather than imports from src/). -const FRAME_COMMIT = 1; -const FRAME_INSERT_BEFORE = 2; -const FRAME_SET_TEXT = 3; -const FRAME_CREATE_ELEMENT = 6; -const FRAME_CREATE_TEXT = 7; -const FRAME_ADD_LISTENER = 12; -const FRAME_INTERN = 14; - -const INTERN_ID = 1; -const INTERN_S = 2; -const CREATE_ELEMENT_ID = 1; -const CREATE_ELEMENT_TAG = 2; -const CREATE_TEXT_ID = 1; -const CREATE_TEXT_TEXT = 2; -const INSERT_BEFORE_PARENT = 1; -const INSERT_BEFORE_ID = 2; -const SET_TEXT_ID = 1; -const SET_TEXT_TEXT = 2; -const LISTENER_ID = 1; -const LISTENER_NAME = 2; -const LISTENER_BUBBLES = 3; -const LISTENER_PREVENT_DEFAULT = 6; -const ADD_LISTENER_LISTENER = 1; - -/** One length-delimited `Frame` message. `build` writes the op field(s); - * `commit` sets `Frame.commit`. */ -function frame(commit: boolean, build?: (w: Writer) => void): Uint8Array { - const inner = new Writer(); - if (commit) inner.writeBool(FRAME_COMMIT, true); - build?.(inner); - const body = inner.finish(); - const framed = new Writer(); - framed.writeVarint32(body.length); - const lenBytes = framed.finish(); - const out = new Uint8Array(lenBytes.length + body.length); - out.set(lenBytes, 0); - out.set(body, lenBytes.length); - return out; -} - -function concat(chunks: Uint8Array[]): Uint8Array { - const total = chunks.reduce((n, c) => n + c.length, 0); - const out = new Uint8Array(total); - let off = 0; - for (const c of chunks) { - out.set(c, off); - off += c.length; - } - return out; +/** One length-delimited `Frame` message, built with the generated + * writer; `commit` sets `Frame.commit`. */ +function frame(commit: boolean, op?: Frame["op"]): Uint8Array { + return framed(Frame.encode({ commit, op }).finish()); } const DIV = 1, CLICK = 2; @@ -131,48 +84,22 @@ const DIV = 1, CLICK = 2; * "hi"), insert(10, 11), set-text(11, "hi there"), commit. */ function basicFrames(): Uint8Array[] { return [ - frame(false, (w) => { - w.writeMessage(FRAME_INTERN, (m) => { - m.writeUint32(INTERN_ID, DIV); - m.writeString(INTERN_S, "div"); - }); - }), - frame(false, (w) => { - w.writeMessage(FRAME_INTERN, (m) => { - m.writeUint32(INTERN_ID, CLICK); - m.writeString(INTERN_S, "click"); - }); - }), - frame(false, (w) => { - w.writeMessage(FRAME_CREATE_ELEMENT, (m) => { - m.writeUint32(CREATE_ELEMENT_ID, 10); - m.writeUint32(CREATE_ELEMENT_TAG, DIV); - }); - }), - frame(false, (w) => { - w.writeMessage(FRAME_INSERT_BEFORE, (m) => { - m.writeUint32(INSERT_BEFORE_PARENT, 0); - m.writeUint32(INSERT_BEFORE_ID, 10); - }); - }), - frame(false, (w) => { - w.writeMessage(FRAME_CREATE_TEXT, (m) => { - m.writeUint32(CREATE_TEXT_ID, 11); - m.writeString(CREATE_TEXT_TEXT, "hi"); - }); + frame(false, { $case: "intern", value: { id: DIV, s: "div" } }), + frame(false, { $case: "intern", value: { id: CLICK, s: "click" } }), + frame(false, { + $case: "createElement", + value: { id: 10, tag: DIV, ns: undefined }, }), - frame(false, (w) => { - w.writeMessage(FRAME_INSERT_BEFORE, (m) => { - m.writeUint32(INSERT_BEFORE_PARENT, 10); - m.writeUint32(INSERT_BEFORE_ID, 11); - }); + frame(false, { + $case: "insertBefore", + value: { parent: 0, id: 10, anchor: undefined }, }), - frame(true, (w) => { - w.writeMessage(FRAME_SET_TEXT, (m) => { - m.writeUint32(SET_TEXT_ID, 11); - m.writeString(SET_TEXT_TEXT, "hi there"); - }); + frame(false, { $case: "createText", value: { id: 11, text: "hi" } }), + frame(false, { + $case: "insertBefore", + value: { parent: 10, id: 11, anchor: undefined }, }), + frame(true, { $case: "setText", value: { id: 11, text: "hi there" } }), ]; } @@ -180,15 +107,19 @@ function basicFrames(): Uint8Array[] { function addClickListenerFrame( opts?: { preventDefault?: boolean }, ): Uint8Array { - return frame(true, (w) => { - w.writeMessage(FRAME_ADD_LISTENER, (m) => { - m.writeMessage(ADD_LISTENER_LISTENER, (l) => { - l.writeUint32(LISTENER_ID, 10); - l.writeUint32(LISTENER_NAME, CLICK); - l.writeBool(LISTENER_BUBBLES, true); - if (opts?.preventDefault) l.writeBool(LISTENER_PREVENT_DEFAULT, true); - }); - }); + return frame(true, { + $case: "addListener", + value: { + listener: { + target: { $case: "id", value: 10 }, + name: CLICK, + bubbles: true, + capture: false, + passive: false, + preventDefault: opts?.preventDefault === true, + stopPropagation: false, + }, + }, }); } @@ -204,7 +135,7 @@ Deno.test("Driver: bytes in, DOM out, split mid-frame, no component", async () = }, }); - const all = concat(basicFrames()); + const all = stream(...basicFrames()); // Split mid-frame: partway through the fixed-point of the sequence, // well inside a frame's body rather than on a frame boundary. const mid = Math.floor(all.length / 2); @@ -235,7 +166,7 @@ Deno.test("Driver: a bubbling click listener fires handleEvent with the right ta calls.push([target, nameRef, payload]); }, }); - await pushAndAwaitCommit(driver, concat(basicFrames())); + await pushAndAwaitCommit(driver, stream(...basicFrames())); await pushAndAwaitCommit(driver, addClickListenerFrame()); const div = root.querySelector("div")!; @@ -263,7 +194,7 @@ Deno.test("Driver: prevent_default is honored even though handleEvent does nothi root, handleEvent: () => {}, // does nothing — flag must still land }); - await pushAndAwaitCommit(driver, concat(basicFrames())); + await pushAndAwaitCommit(driver, stream(...basicFrames())); await pushAndAwaitCommit( driver, addClickListenerFrame({ preventDefault: true }), @@ -292,7 +223,7 @@ Deno.test("Driver: policy.query gates individual queries; absent means allow", a policy: allowAll({ query: (name) => name !== "set-focus" }), handleEvent: () => {}, }); - await pushAndAwaitCommit(gated, concat(basicFrames())); + await pushAndAwaitCommit(gated, stream(...basicFrames())); assertEquals(gated.queries.setFocus(10, true), false); // ...while a query the same policy does not refuse still answers. assertEquals(gated.queries.getClientRect(10), { @@ -307,7 +238,7 @@ Deno.test("Driver: policy.query gates individual queries; absent means allow", a policy: allowAll(), handleEvent: () => {}, }); - await pushAndAwaitCommit(open, concat(basicFrames())); + await pushAndAwaitCommit(open, stream(...basicFrames())); assertEquals(open.queries.setFocus(10, true), true); assertEquals(open.queries.getClientRect(10), { origin: { x: 0, y: 0 }, @@ -328,7 +259,7 @@ Deno.test("Driver: a policy rejection throws PolicyError out of push", () => { }); const driver = createDriver({ root, policy, handleEvent: () => {} }); assertThrows( - () => driver.push(concat(basicFrames())), + () => driver.push(stream(...basicFrames())), PolicyError, "div is not in the host vocabulary", ); @@ -352,11 +283,9 @@ Deno.test("Driver: a policy pinning another protocol version is refused at const Deno.test("Driver: an unknown node id throws", () => { const { win, root } = fixture(); const driver = createDriver({ root, handleEvent: () => {} }); - const bad = frame(true, (w) => { - w.writeMessage(FRAME_SET_TEXT, (m) => { - m.writeUint32(SET_TEXT_ID, 999); - m.writeString(SET_TEXT_TEXT, "x"); - }); + const bad = frame(true, { + $case: "setText", + value: { id: 999, text: "x" }, }); assertThrows(() => driver.push(bad), Error, "unknown node id 999"); withGlobalWindow(win, () => driver.dispose()); @@ -376,7 +305,7 @@ Deno.test("Driver: defaultPreventDefault prevents a submit with NO producer list defaultPreventDefault: on, handleEvent: () => {}, }); - await pushAndAwaitCommit(driver, concat(basicFrames())); + await pushAndAwaitCommit(driver, stream(...basicFrames())); const div = root.querySelector("div")!; const ev = new (win as unknown as { Event: typeof Event }).Event( @@ -420,26 +349,17 @@ async function fixtureWithLeaf( }); await pushAndAwaitCommit( driver, - concat([ - frame(false, (w) => { - w.writeMessage(FRAME_INTERN, (m) => { - m.writeUint32(INTERN_ID, DIV); - m.writeString(INTERN_S, tag); - }); - }), - frame(false, (w) => { - w.writeMessage(FRAME_CREATE_ELEMENT, (m) => { - m.writeUint32(CREATE_ELEMENT_ID, 10); - m.writeUint32(CREATE_ELEMENT_TAG, DIV); - }); + stream( + frame(false, { $case: "intern", value: { id: DIV, s: tag } }), + frame(false, { + $case: "createElement", + value: { id: 10, tag: DIV, ns: undefined }, }), - frame(true, (w) => { - w.writeMessage(FRAME_INSERT_BEFORE, (m) => { - m.writeUint32(INSERT_BEFORE_PARENT, 0); - m.writeUint32(INSERT_BEFORE_ID, 10); - }); + frame(true, { + $case: "insertBefore", + value: { parent: 0, id: 10, anchor: undefined }, }), - ]), + ), ); if (href !== undefined) { root.querySelector(tag)!.setAttribute("href", href); @@ -489,26 +409,17 @@ Deno.test("Driver: defaultPreventDefault off means nothing is prevented, even a }); await pushAndAwaitCommit( driver, - concat([ - frame(false, (w) => { - w.writeMessage(FRAME_INTERN, (m) => { - m.writeUint32(INTERN_ID, DIV); - m.writeString(INTERN_S, "a"); - }); - }), - frame(false, (w) => { - w.writeMessage(FRAME_CREATE_ELEMENT, (m) => { - m.writeUint32(CREATE_ELEMENT_ID, 10); - m.writeUint32(CREATE_ELEMENT_TAG, DIV); - }); + stream( + frame(false, { $case: "intern", value: { id: DIV, s: "a" } }), + frame(false, { + $case: "createElement", + value: { id: 10, tag: DIV, ns: undefined }, }), - frame(true, (w) => { - w.writeMessage(FRAME_INSERT_BEFORE, (m) => { - m.writeUint32(INSERT_BEFORE_PARENT, 0); - m.writeUint32(INSERT_BEFORE_ID, 10); - }); + frame(true, { + $case: "insertBefore", + value: { parent: 0, id: 10, anchor: undefined }, }), - ]), + ), ); root.querySelector("a")!.setAttribute("href", "http://x"); const ev = new (win as unknown as { Event: typeof Event }).Event("click", { @@ -529,7 +440,7 @@ Deno.test("Driver: after dispose, root listeners are gone (dispatch no longer ca calls.push(a); }, }); - await pushAndAwaitCommit(driver, concat(basicFrames())); + await pushAndAwaitCommit(driver, stream(...basicFrames())); await pushAndAwaitCommit(driver, addClickListenerFrame()); withGlobalWindow(win, () => driver.dispose()); @@ -544,5 +455,5 @@ Deno.test("Driver: after dispose, root listeners are gone (dispatch no longer ca assertEquals(calls.length, 0); // push is a no-op after dispose (Driver.push's documented contract). - driver.push(concat(basicFrames())); + driver.push(stream(...basicFrames())); }); diff --git a/receiver/tests/events_test.ts b/receiver/tests/events_test.ts index 12278f0..1699d80 100644 --- a/receiver/tests/events_test.ts +++ b/receiver/tests/events_test.ts @@ -1,35 +1,27 @@ import { assertEquals } from "@std/assert"; import { encodePayload } from "../src/events.ts"; -import { Reader } from "../src/proto.ts"; - -// proto/stream-dom-events.proto: EventPayload.navigation = 14, -// NavigationData.href = 1. -const EVENT_PAYLOAD_NAVIGATION = 14; -const NAVIGATION_HREF = 1; +import { EventPayload } from "../src/gen/stream-dom-events.ts"; Deno.test("encodePayload('hashchange') yields a navigation payload with the current location.href", () => { - const bytes = encodePayload("hashchange", new Event("hashchange")); - const r = new Reader(bytes); - const [field] = r.readTag(); - assertEquals(field, EVENT_PAYLOAD_NAVIGATION); - const sub = r.readMessage(); - const [hrefField] = sub.readTag(); - assertEquals(hrefField, NAVIGATION_HREF); - const href = sub.readString(); + const payload = EventPayload.decode( + encodePayload("hashchange", new Event("hashchange")), + ); + assertEquals(payload.family?.$case, "navigation"); + const href = payload.family?.$case === "navigation" + ? payload.family.value.href + : undefined; // `deno test` runs with no `--location`, so `globalThis.location` is // typically `undefined` here — assert against whatever the runtime // actually reports rather than a hardcoded string, so this test stays // meaningful under `--location` too. assertEquals(href, globalThis.location?.href ?? ""); - assertEquals(sub.finished(), true); - assertEquals(r.finished(), true); }); Deno.test("encodePayload('popstate') also yields the navigation family", () => { - const bytes = encodePayload("popstate", new Event("popstate")); - const r = new Reader(bytes); - const [field] = r.readTag(); - assertEquals(field, EVENT_PAYLOAD_NAVIGATION); + const payload = EventPayload.decode( + encodePayload("popstate", new Event("popstate")), + ); + assertEquals(payload.family?.$case, "navigation"); }); Deno.test("encodePayload for an unrecognized name yields the empty payload (zero bytes)", () => { diff --git a/receiver/tests/frames_test.ts b/receiver/tests/frames_test.ts index 09ad3b5..951e3f9 100644 --- a/receiver/tests/frames_test.ts +++ b/receiver/tests/frames_test.ts @@ -1,6 +1,7 @@ import { assertEquals, assertThrows } from "@std/assert"; import { FrameDecoder } from "../src/frames.ts"; -import { Writer } from "../src/proto.ts"; +import { Frame, Global } from "../src/gen/stream-dom.ts"; +import { frame } from "./wire.ts"; import type { AttrValue, FrameSink, @@ -255,27 +256,41 @@ Deno.test("FrameDecoder throws (does not silently wait forever) on a malformed l assertThrows(() => decoder.push(malformed)); }); +Deno.test("FrameDecoder throws on a continuation-only length prefix past the legal width", () => { + const sink = new RecordingSink(); + const decoder = new FrameDecoder(sink); + // Six continuation bytes and no terminator: a u32 varint is at most + // five, so this can never become a valid length however many bytes + // follow. `BinaryReader.uint32()` reports it the same way it reports a + // genuine truncation, so the decoder's five-byte probe window — not the + // reader — is what tells the two apart. + assertThrows(() => + decoder.push(Uint8Array.of(0x80, 0x80, 0x80, 0x80, 0x80, 0x80)) + ); +}); + // Global.DOCUMENT (value 1) isn't exercised by basic.pb (which only uses -// WINDOW), so hand-encode a frame for it: Frame.add_listener (field 12) -> -// AddListener.listener (field 1) -> Listener.global (field 8, enum varint). +// WINDOW), so build a frame for it with the generated writer. Deno.test("FrameDecoder decodes Listener.target = Global(DOCUMENT)", () => { - const inner = new Writer(); - inner.writeBool(1, false); // Frame.commit - inner.writeMessage(12, (addListenerW) => { - addListenerW.writeMessage(1, (listenerW) => { - listenerW.writeUint32(8, 1); // Global.DOCUMENT = 1 - listenerW.writeUint32(2, 42); // Listener.name - }); - }); - const frameBytes = inner.finish(); - - const lengthPrefix = new Writer(); - lengthPrefix.writeVarint32(frameBytes.length); - const framed = new Uint8Array( - lengthPrefix.finish().length + frameBytes.length, + const framed = frame( + Frame.encode({ + commit: false, + op: { + $case: "addListener", + value: { + listener: { + target: { $case: "global", value: Global.DOCUMENT }, + name: 42, + bubbles: false, + capture: false, + passive: false, + preventDefault: false, + stopPropagation: false, + }, + }, + }, + }).finish(), ); - framed.set(lengthPrefix.finish(), 0); - framed.set(frameBytes, lengthPrefix.finish().length); const sink = new RecordingSink(); const decoder = new FrameDecoder(sink); diff --git a/receiver/tests/hostile_test.ts b/receiver/tests/hostile_test.ts index bac2ce0..49c3d08 100644 --- a/receiver/tests/hostile_test.ts +++ b/receiver/tests/hostile_test.ts @@ -13,7 +13,8 @@ import { assert } from "@std/assert"; import { parseHTML } from "linkedom"; import { FrameDecoder, MAX_FRAME_BYTES } from "../src/frames.ts"; import { NativeDomReceiver } from "../src/native.ts"; -import { Writer } from "../src/proto.ts"; +import { BinaryWriter, WireType } from "@bufbuild/protobuf/wire"; +import { frame as framed, stream } from "./wire.ts"; // -- deterministic PRNG --------------------------------------------------- @@ -118,32 +119,37 @@ function assertMountIntact(h: Harness, where: string): void { // -- frame encoding ------------------------------------------------------- -/** One length-delimited `Frame`, as the stream layout specifies (varint - * byte length, then the message). */ -function frame(build: (w: Writer) => void): Uint8Array { - const body = (() => { - const w = new Writer(); - build(w); - return w.finish(); - })(); - const lp = new Writer(); - lp.writeVarint32(body.length); - const prefix = lp.finish(); - const out = new Uint8Array(prefix.length + body.length); - out.set(prefix, 0); - out.set(body, prefix.length); - return out; +/** Hostile fixtures are written field by field with `BinaryWriter` + * directly rather than through the generated `Frame` writer: the point of + * the generator below is to emit field combinations no valid message + * object can express (absent required-ish fields, unknown enum values, + * repeated oneof arms). These four helpers are the whole vocabulary. */ +type Build = (w: BinaryWriter) => void; + +function msg(w: BinaryWriter, field: number, build: Build): void { + w.tag(field, WireType.LengthDelimited).fork(); + build(w); + w.join(); } -function concat(parts: Uint8Array[]): Uint8Array { - const total = parts.reduce((n, p) => n + p.length, 0); - const out = new Uint8Array(total); - let at = 0; - for (const p of parts) { - out.set(p, at); - at += p.length; - } - return out; +function u32(w: BinaryWriter, field: number, v: number): void { + w.tag(field, WireType.Varint).uint32(v); +} + +function str(w: BinaryWriter, field: number, v: string): void { + w.tag(field, WireType.LengthDelimited).string(v); +} + +function bool(w: BinaryWriter, field: number, v: boolean): void { + w.tag(field, WireType.Varint).bool(v); +} + +/** One length-delimited `Frame`, as the stream layout specifies (varint + * byte length, then the message). */ +function frame(build: Build): Uint8Array { + const w = new BinaryWriter(); + build(w); + return framed(w.finish()); } // Frame.op field numbers (proto/stream-dom.proto). @@ -223,27 +229,27 @@ Deno.test("hostile: random op sequences never damage the embedder's DOM", () => const ref = () => rand() < HOSTILE_RATE ? pick(badRefs) : pick(goodRefs); const frames: Uint8Array[] = [ frame((w) => - w.writeMessage(F_INTERN, (m) => { - m.writeUint32(1, 1); - m.writeString(2, "div"); + msg(w, F_INTERN, (m) => { + u32(m, 1, 1); + str(m, 2, "div"); }) ), frame((w) => - w.writeMessage(F_INTERN, (m) => { - m.writeUint32(1, 2); - m.writeString(2, "span"); + msg(w, F_INTERN, (m) => { + u32(m, 1, 2); + str(m, 2, "span"); }) ), frame((w) => - w.writeMessage(F_INTERN, (m) => { - m.writeUint32(1, 3); - m.writeString(2, "class"); + msg(w, F_INTERN, (m) => { + u32(m, 1, 3); + str(m, 2, "class"); }) ), frame((w) => - w.writeMessage(F_INTERN, (m) => { - m.writeUint32(1, 4); - m.writeString(2, "click"); + msg(w, F_INTERN, (m) => { + u32(m, 1, 4); + str(m, 2, "click"); }) ), ]; @@ -253,15 +259,15 @@ Deno.test("hostile: random op sequences never damage the embedder's DOM", () => for (let k = 0; k < 3; k++) { const id = newId(true); frames.push(frame((w) => - w.writeMessage(F_CREATE_ELEMENT, (m) => { - m.writeUint32(1, id); - m.writeUint32(2, 1); + msg(w, F_CREATE_ELEMENT, (m) => { + u32(m, 1, id); + u32(m, 2, 1); }) )); frames.push(frame((w) => - w.writeMessage(F_INSERT_BEFORE, (m) => { - m.writeUint32(1, k === 0 ? 0 : k); - m.writeUint32(2, id); + msg(w, F_INSERT_BEFORE, (m) => { + u32(m, 1, k === 0 ? 0 : k); + u32(m, 2, id); }) )); } @@ -270,46 +276,43 @@ Deno.test("hostile: random op sequences never damage the embedder's DOM", () => switch (int(15)) { case 0: frames.push(frame((w) => - w.writeMessage(F_CREATE_ELEMENT, (m) => { - m.writeUint32(1, newId(true)); - m.writeUint32(2, ref()); - if (rand() < 0.3) m.writeUint32(3, ref()); + msg(w, F_CREATE_ELEMENT, (m) => { + u32(m, 1, newId(true)); + u32(m, 2, ref()); + if (rand() < 0.3) u32(m, 3, ref()); }) )); break; case 1: frames.push(frame((w) => - w.writeMessage(F_CREATE_TEXT, (m) => { - m.writeUint32(1, newId(false)); - m.writeString(2, "t" + i); + msg(w, F_CREATE_TEXT, (m) => { + u32(m, 1, newId(false)); + str(m, 2, "t" + i); }) )); break; case 2: frames.push( frame((w) => - w.writeMessage( - F_CREATE_PLACEHOLDER, - (m) => m.writeUint32(1, newId(false)), - ) + msg(w, F_CREATE_PLACEHOLDER, (m) => u32(m, 1, newId(false))) ), ); break; case 3: frames.push(frame((w) => - w.writeMessage(F_INSERT_BEFORE, (m) => { - if (rand() < 0.75) m.writeUint32(1, parent()); - m.writeUint32(2, target()); - if (rand() < 0.25) m.writeUint32(3, target()); + msg(w, F_INSERT_BEFORE, (m) => { + if (rand() < 0.75) u32(m, 1, parent()); + u32(m, 2, target()); + if (rand() < 0.25) u32(m, 3, target()); }) )); break; case 4: frames.push(frame((w) => - w.writeMessage(F_INSERT_AFTER, (m) => { - if (rand() < 0.75) m.writeUint32(1, parent()); - m.writeUint32(2, target()); - m.writeUint32(3, target()); + msg(w, F_INSERT_AFTER, (m) => { + if (rand() < 0.75) u32(m, 1, parent()); + u32(m, 2, target()); + u32(m, 3, target()); }) )); break; @@ -318,123 +321,125 @@ Deno.test("hostile: random op sequences never damage the embedder's DOM", () => const at = live.indexOf(gone); if (at >= 0) live.splice(at, 1); frames.push( - frame((w) => - w.writeMessage(F_REMOVE, (m) => m.writeUint32(1, gone)) - ), + frame((w) => msg(w, F_REMOVE, (m) => u32(m, 1, gone))), ); break; } case 6: frames.push(frame((w) => - w.writeMessage(F_SET_TEXT, (m) => { - m.writeUint32(1, target()); - m.writeString(2, "x" + i); + msg(w, F_SET_TEXT, (m) => { + u32(m, 1, target()); + str(m, 2, "x" + i); }) )); break; case 7: frames.push(frame((w) => - w.writeMessage(F_SET_ATTRIBUTE, (m) => { - m.writeUint32(1, target()); - m.writeUint32(2, ref()); - if (rand() < 0.3) m.writeUint32(3, ref()); - if (rand() < 0.7) m.writeString(4, "v" + i); + msg(w, F_SET_ATTRIBUTE, (m) => { + u32(m, 1, target()); + u32(m, 2, ref()); + if (rand() < 0.3) u32(m, 3, ref()); + if (rand() < 0.7) str(m, 4, "v" + i); }) )); break; case 8: frames.push(frame((w) => - w.writeMessage(F_SET_PROPERTY, (m) => { - m.writeUint32(1, target()); - m.writeUint32(2, ref()); - if (rand() < 0.5) m.writeString(3, "v" + i); - else m.writeBool(6, rand() < 0.5); + msg(w, F_SET_PROPERTY, (m) => { + u32(m, 1, target()); + u32(m, 2, ref()); + if (rand() < 0.5) str(m, 3, "v" + i); + else bool(m, 6, rand() < 0.5); }) )); break; case 9: - frames.push(frame((w) => - w.writeMessage( - rand() < 0.5 ? F_ADD_LISTENER : F_REMOVE_LISTENER, - (m) => - m.writeMessage(1, (l) => { - if (rand() < 0.8) l.writeUint32(1, target()); - else l.writeUint32(8, int(3)); // Global, sometimes unknown - l.writeUint32(2, ref()); - l.writeBool(3, rand() < 0.5); - }), - ) - )); + frames.push( + frame((w) => + msg( + w, + rand() < 0.5 ? F_ADD_LISTENER : F_REMOVE_LISTENER, + (m) => + msg(m, 1, (l) => { + if (rand() < 0.8) { + u32(l, 1, target()); + } else u32(l, 8, int(3)); // Global, sometimes unknown + u32(l, 2, ref()); + bool(l, 3, rand() < 0.5); + }), + ) + ), + ); break; case 10: // A template arena with random (often out-of-range or cyclic) // child indices. frames.push(frame((w) => - w.writeMessage(F_REGISTER_TEMPLATE, (m) => { - m.writeUint32(1, int(3)); + msg(w, F_REGISTER_TEMPLATE, (m) => { + u32(m, 1, int(3)); const n = 1 + int(4); for (let k = 0; k < n; k++) { - m.writeMessage(2, (tn) => { + msg(m, 2, (tn) => { if (rand() < 0.6) { - tn.writeMessage(1, (el) => { - el.writeUint32(1, ref()); + msg(tn, 1, (el) => { + u32(el, 1, ref()); if (rand() < 0.5) { - el.writeMessage(3, (at) => { - at.writeUint32(1, ref()); - at.writeString(3, "a"); + msg(el, 3, (at) => { + u32(at, 1, ref()); + str(at, 3, "a"); }); } for (let c = 0; c < int(3); c++) { - el.writeUint32(4, int(6)); + u32(el, 4, int(6)); } }); - } else if (rand() < 0.5) tn.writeString(2, "t"); - else tn.writeMessage(3, () => {}); + } else if (rand() < 0.5) str(tn, 2, "t"); + else msg(tn, 3, () => {}); }); } - m.writeUint32(3, int(4)); + u32(m, 3, int(4)); }) )); break; case 11: frames.push(frame((w) => - w.writeMessage(F_CLONE_TEMPLATE, (m) => { - m.writeUint32(1, int(4)); - m.writeUint32(2, int(4)); - m.writeUint32(3, newId(true)); + msg(w, F_CLONE_TEMPLATE, (m) => { + u32(m, 1, int(4)); + u32(m, 2, int(4)); + u32(m, 3, newId(true)); }) )); break; case 12: frames.push(frame((w) => - w.writeMessage(F_BIND_PATH, (m) => { - m.writeUint32(1, target()); + msg(w, F_BIND_PATH, (m) => { + u32(m, 1, target()); // BindPath.path is `bytes`; each step is written as a // one-byte varint, which is byte-identical to the raw byte // for values < 128 — the only range used here. - m.writeMessage(2, (p) => { - for (let s = 0; s < int(4); s++) p.writeVarint32(int(4)); + msg(m, 2, (p) => { + for (let s = 0; s < int(4); s++) p.uint32(int(4)); }); - m.writeUint32(3, newId(true)); + u32(m, 3, newId(true)); }) )); break; case 13: frames.push(frame((w) => - w.writeMessage(F_BIND_MARKER, (m) => { - m.writeUint32(1, int(4)); - m.writeUint32(2, newId(false)); + msg(w, F_BIND_MARKER, (m) => { + u32(m, 1, int(4)); + u32(m, 2, newId(false)); }) )); break; default: - frames.push(frame((w) => w.writeBool(F_COMMIT, true))); + frames.push(frame((w) => bool(w, F_COMMIT, true))); } } // Feed the whole stream in chunks split at random byte boundaries: a // frame straddling a chunk must behave exactly as one that does not. - const bytes = concat(frames); + const bytes = stream(...frames); let at = 0; let threw = false; while (at < bytes.length && !threw) { @@ -496,7 +501,7 @@ Deno.test("hostile: byte mutations of basic.pb neither hang nor damage the mount const at = int(bytes.length); const junk = new Uint8Array(1 + int(8)); for (let k = 0; k < junk.length; k++) junk[k] = int(256); - bytes = concat([bytes.subarray(0, at), junk, bytes.subarray(at)]); + bytes = stream(bytes.subarray(0, at), junk, bytes.subarray(at)); break; } default: { // replace a byte outright (hits length prefixes hardest) @@ -537,11 +542,9 @@ Deno.test("hostile: byte mutations of basic.pb neither hang nor damage the mount Deno.test("FrameDecoder: a length prefix over MAX_FRAME_BYTES throws instead of buffering forever", () => { const h = harness(); - const lp = new Writer(); - lp.writeVarint32(MAX_FRAME_BYTES + 1); let caught: unknown; try { - h.decoder.push(lp.finish()); + h.decoder.push(new BinaryWriter().uint32(MAX_FRAME_BYTES + 1).finish()); } catch (err) { caught = err; } @@ -552,26 +555,98 @@ Deno.test("FrameDecoder: a length prefix over MAX_FRAME_BYTES throws instead of ); // A length just under the bound is still "wait for more bytes", not an // error — the bound is a ceiling, not a size limit on real frames. - const ok = new Writer(); - ok.writeVarint32(MAX_FRAME_BYTES); - harness().decoder.push(ok.finish()); + harness().decoder.push( + new BinaryWriter().uint32(MAX_FRAME_BYTES).finish(), + ); }); Deno.test("FrameDecoder: a truncated sub-message inside a COMPLETE frame is an error, not a wait", () => { const h = harness(); // Frame { create_text: }, wrapped - // in a frame length prefix that is itself correct. Only the length probe - // in `#drain` may treat a truncation as "wait for more"; inside a frame - // whose bytes are all present, it is a malformed frame. + // in a frame length prefix that is itself correct, and followed by a + // perfectly good frame. Only the length probe in `#drain` may treat a + // truncation as "wait for more"; inside a frame whose bytes are all + // present it is a malformed frame — and the frame AFTER it must not be + // applied either, since the throw aborts the stream. const body = Uint8Array.of((F_CREATE_TEXT << 3) | 2, 20, 0x08, 0x01); - const lp = new Writer(); - lp.writeVarint32(body.length); + const good = frame((w) => + msg(w, F_CREATE_TEXT, (m) => { + u32(m, 1, 9); + str(m, 2, "ok"); + }) + ); let caught: unknown; try { - h.decoder.push(concat([lp.finish(), body])); + h.decoder.push(stream(framed(body), good)); } catch (err) { caught = err; } assert(caught instanceof Error, "expected an Error, got " + String(caught)); + assert( + h.recv.resolveNode(9) === undefined, + "the frame after the malformed one was applied", + ); assertMountIntact(h, "truncated sub-message"); }); + +Deno.test("FrameDecoder: a sub-message length that overruns the frame does not read into the next frame", () => { + const h = harness(); + // `Frame { create_text: }`: the frame's own length prefix is honest about those 6 + // bytes, so the over-long inner length can only be satisfied by reading + // the FOLLOWING frames' bytes. Without a per-frame bound the decoder + // does exactly that and then resumes at the right place, applying both + // frames from bytes that were never one frame. + const overrun = frame((w) => { + w.tag(F_CREATE_TEXT, WireType.LengthDelimited).uint32(16).raw( + Uint8Array.of(0x08, 0x05, 0x12, 0x02, 0x68, 0x69), // id 5, text "hi" + ); + }); + const good = frame((w) => { + msg(w, F_CREATE_TEXT, (m) => { + u32(m, 1, 9); + str(m, 2, "abcdefghijklmnop"); + }); + bool(w, F_COMMIT, true); + }); + let caught: unknown; + try { + h.decoder.push(stream(overrun, good)); + } catch (err) { + caught = err; + } + assert(caught instanceof Error, "expected an Error, got " + String(caught)); + assert( + h.recv.resolveNode(5) === undefined, + "the malformed frame reached the sink", + ); + assert( + h.recv.resolveNode(9) === undefined, + "the frame after the malformed one was applied", + ); + assertMountIntact(h, "over-long sub-message length"); +}); + +Deno.test("FrameDecoder: a frame body that stops short of its own length is an error in both modes", () => { + // `Frame { commit: true }` followed by a zero tag and junk: the + // generated decoder stops at the zero tag WITHOUT throwing, leaving the + // reader inside the frame. Field number 0 does not exist in protobuf, + // so this is malformed wire, not a skippable unknown — rejected in + // strict and open mode alike. + const body = Uint8Array.of(0x08, 0x01, 0x00, 0xff, 0xff, 0xff); + for (const strict of [false, true]) { + const h = harness(); + const decoder = new FrameDecoder(h.recv.sink, { strict }); + let caught: unknown; + try { + decoder.push(framed(body)); + } catch (err) { + caught = err; + } + assert( + caught instanceof Error, + `strict=${strict}: expected an Error, got ${String(caught)}`, + ); + assertMountIntact(h, `short frame body (strict=${strict})`); + } +}); diff --git a/receiver/tests/policy_desktop_test.ts b/receiver/tests/policy_desktop_test.ts index 83ad7f6..8675663 100644 --- a/receiver/tests/policy_desktop_test.ts +++ b/receiver/tests/policy_desktop_test.ts @@ -10,7 +10,8 @@ import { parseHTML } from "linkedom"; import { createDriver } from "../src/driver.ts"; import { PolicyError } from "../src/policy.ts"; import { desktopPolicy } from "../src/policy-desktop.ts"; -import { Writer } from "../src/proto.ts"; +import { Frame } from "../src/gen/stream-dom.ts"; +import { frame as framed, stream } from "./wire.ts"; // -- unit harness ---------------------------------------------------------- @@ -749,80 +750,28 @@ Deno.test("desktopPolicy: maxListeners exhausts after that many addListener chec // -- whole-stream, through createDriver + linkedom -------------------------- // -// Reuses the frame-encoder pattern from policy_test.ts / driver_test.ts: -// a `Writer`-backed length-delimited `Frame` builder, rather than a new -// encoder. - -const FRAME_SET_ATTRIBUTE = 4; -const FRAME_CREATE_ELEMENT = 6; -const FRAME_INSERT_BEFORE = 2; -const FRAME_ADD_LISTENER = 12; -const FRAME_INTERN = 14; -const FRAME_COMMIT = 1; - -const INTERN_ID = 1; -const INTERN_S = 2; -const CREATE_ELEMENT_ID = 1; -const CREATE_ELEMENT_TAG = 2; -const INSERT_BEFORE_PARENT = 1; -const INSERT_BEFORE_ID = 2; -const SET_ATTRIBUTE_ID = 1; -const SET_ATTRIBUTE_NAME = 2; -const SET_ATTRIBUTE_TEXT = 4; -const LISTENER_ID = 1; -const LISTENER_NAME = 2; -const LISTENER_BUBBLES = 3; -const ADD_LISTENER_LISTENER = 1; - -function frame(commit: boolean, build?: (w: Writer) => void): Uint8Array { - const inner = new Writer(); - if (commit) inner.writeBool(FRAME_COMMIT, true); - build?.(inner); - const body = inner.finish(); - const lenW = new Writer(); - lenW.writeVarint32(body.length); - const lenBytes = lenW.finish(); - const out = new Uint8Array(lenBytes.length + body.length); - out.set(lenBytes, 0); - out.set(body, lenBytes.length); - return out; -} +// Reuses the frame-builder pattern from policy_test.ts / driver_test.ts: +// the generated `Frame` writer plus the shared length prefix in wire.ts. -function concat(chunks: Uint8Array[]): Uint8Array { - const total = chunks.reduce((n, c) => n + c.length, 0); - const out = new Uint8Array(total); - let off = 0; - for (const c of chunks) { - out.set(c, off); - off += c.length; - } - return out; +function frame(commit: boolean, op?: Frame["op"]): Uint8Array { + return framed(Frame.encode({ commit, op }).finish()); } function internFrame(id: number, s: string): Uint8Array { - return frame(false, (w) => { - w.writeMessage(FRAME_INTERN, (m) => { - m.writeUint32(INTERN_ID, id); - m.writeString(INTERN_S, s); - }); - }); + return frame(false, { $case: "intern", value: { id, s } }); } function createElementFrame(id: number, tagRef: number): Uint8Array { - return frame(false, (w) => { - w.writeMessage(FRAME_CREATE_ELEMENT, (m) => { - m.writeUint32(CREATE_ELEMENT_ID, id); - m.writeUint32(CREATE_ELEMENT_TAG, tagRef); - }); + return frame(false, { + $case: "createElement", + value: { id, tag: tagRef, ns: undefined }, }); } function insertBeforeFrame(parent: number, id: number): Uint8Array { - return frame(false, (w) => { - w.writeMessage(FRAME_INSERT_BEFORE, (m) => { - m.writeUint32(INSERT_BEFORE_PARENT, parent); - m.writeUint32(INSERT_BEFORE_ID, id); - }); + return frame(false, { + $case: "insertBefore", + value: { parent, id, anchor: undefined }, }); } @@ -831,24 +780,31 @@ function setAttributeTextFrame( nameRef: number, text: string, ): Uint8Array { - return frame(false, (w) => { - w.writeMessage(FRAME_SET_ATTRIBUTE, (m) => { - m.writeUint32(SET_ATTRIBUTE_ID, id); - m.writeUint32(SET_ATTRIBUTE_NAME, nameRef); - m.writeString(SET_ATTRIBUTE_TEXT, text); - }); + return frame(false, { + $case: "setAttribute", + value: { + id, + name: nameRef, + ns: undefined, + value: { $case: "text", value: text }, + }, }); } function addListenerFrame(id: number, nameRef: number): Uint8Array { - return frame(true, (w) => { - w.writeMessage(FRAME_ADD_LISTENER, (m) => { - m.writeMessage(ADD_LISTENER_LISTENER, (l) => { - l.writeUint32(LISTENER_ID, id); - l.writeUint32(LISTENER_NAME, nameRef); - l.writeBool(LISTENER_BUBBLES, true); - }); - }); + return frame(true, { + $case: "addListener", + value: { + listener: { + target: { $case: "id", value: id }, + name: nameRef, + bubbles: true, + capture: false, + passive: false, + preventDefault: false, + stopPropagation: false, + }, + }, }); } @@ -871,7 +827,7 @@ Deno.test("desktopPolicy: a div > form > input + button stream with a submit lis handleEvent: () => {}, }); - driver.push(concat([ + driver.push(stream( internFrame(DIV, "div"), internFrame(FORM, "form"), internFrame(INPUT, "input"), @@ -886,7 +842,7 @@ Deno.test("desktopPolicy: a div > form > input + button stream with a submit lis createElementFrame(13, BUTTON), insertBeforeFrame(11, 13), addListenerFrame(11, SUBMIT), - ])); + )); assertEquals( root.innerHTML, @@ -904,13 +860,13 @@ Deno.test("desktopPolicy: an with a javascript: text value is rejected assertThrows( () => - driver.push(concat([ + driver.push(stream( internFrame(A, "a"), internFrame(HREF, "href"), createElementFrame(20, A), insertBeforeFrame(0, 20), setAttributeTextFrame(20, HREF, "javascript:alert(1)"), - ])), + )), PolicyError, "asset handle", ); diff --git a/receiver/tests/policy_test.ts b/receiver/tests/policy_test.ts index f17d8a6..a175af9 100644 --- a/receiver/tests/policy_test.ts +++ b/receiver/tests/policy_test.ts @@ -14,7 +14,9 @@ import type { } from "../src/frames.ts"; import { assertPolicyVersion, PolicyError, PolicySink } from "../src/policy.ts"; import type { Policy, PolicyOp } from "../src/policy.ts"; -import { Writer } from "../src/proto.ts"; +import { BinaryWriter } from "@bufbuild/protobuf/wire"; +import { Frame } from "../src/gen/stream-dom.ts"; +import { frame } from "./wire.ts"; import { RemoteDomTranscoder } from "../src/remote.ts"; import type { RemoteConnection, RemoteMutationRecord } from "@remote-dom/core"; @@ -97,18 +99,10 @@ class RecordingSink implements FrameSink { } } -/** One length-delimited `Frame` on the wire. */ -function frame(build: (w: Writer) => void): Uint8Array { - const body = new Writer(); - build(body); - const bytes = body.finish(); - const out = new Writer(); - out.writeVarint32(bytes.length); - const prefix = out.finish(); - const framed = new Uint8Array(prefix.length + bytes.length); - framed.set(prefix, 0); - framed.set(bytes, prefix.length); - return framed; +/** One length-delimited `Frame` on the wire, from a generated-writer + * body. */ +function wellFormed(op: Frame["op"], commit = false): Uint8Array { + return frame(Frame.encode({ commit, op }).finish()); } /** A policy that records what it saw and rejects whatever `reject` says. */ @@ -153,10 +147,10 @@ Deno.test("PROTOCOL_VERSION matches proto/stream-dom.proto's header line", async Deno.test("strict decoder rejects an unknown Frame op field; open decoder skips it", () => { // Field 30 is not an op this receiver knows (Frame's ops stop at 17). - const bytes = frame((w) => { - w.writeBool(1, true); // Frame.commit - w.writeMessage(30, (unknown) => unknown.writeUint32(1, 5)); - }); + const body = new BinaryWriter(); + body.uint32((1 << 3) | 0).bool(true); // Frame.commit + body.uint32((30 << 3) | 2).fork().uint32((1 << 3) | 0).uint32(5).join(); + const bytes = frame(body.finish()); const open = new RecordingSink(); new FrameDecoder(open).push(bytes); @@ -172,13 +166,13 @@ Deno.test("strict decoder rejects an unknown Frame op field; open decoder skips Deno.test("strict decoder rejects an unknown sub-message field; open decoder skips it", () => { // CreateElement { id: 7, tag: 1, }. - const bytes = frame((w) => { - w.writeMessage(6, (ce) => { - ce.writeUint32(1, 7); - ce.writeUint32(2, 1); - ce.writeUint32(9, 123); - }); - }); + const body = new BinaryWriter(); + body.uint32((6 << 3) | 2).fork() + .uint32((1 << 3) | 0).uint32(7) + .uint32((2 << 3) | 0).uint32(1) + .uint32((9 << 3) | 0).uint32(123) + .join(); + const bytes = frame(body.finish()); const open = new RecordingSink(); new FrameDecoder(open).push(bytes); @@ -195,11 +189,15 @@ Deno.test("strict decoder rejects an unknown sub-message field; open decoder ski }); Deno.test("an unknown Global enum value is rejected in both modes", () => { - const bytes = frame((w) => { - w.writeMessage(12, (add) => { - add.writeMessage(1, (l) => l.writeUint32(8, 99)); // Listener.global = 99 - }); - }); + // Listener.global = 99, a value outside the `Global` enum: built by hand + // because the generated writer's `Global` type admits only the two. + const body = new BinaryWriter(); + body.uint32((12 << 3) | 2).fork() + .uint32((1 << 3) | 2).fork() + .uint32((8 << 3) | 0).int32(99) + .join() + .join(); + const bytes = frame(body.finish()); for (const strict of [false, true]) { assertThrows( () => new FrameDecoder(new RecordingSink(), { strict }).push(bytes), @@ -213,14 +211,14 @@ Deno.test("an unknown Global enum value is rejected in both modes", () => { Deno.test("SetAttribute's asset arm (field 5) decodes to an asset handle", () => { const handle = Uint8Array.of(0, 1, 2, 3); - const bytes = frame((w) => { - w.writeMessage(4, (sa) => { - sa.writeUint32(1, 10); // id - sa.writeUint32(2, 3); // name - sa.writeTag(5, 2); // SetAttribute.asset - sa.writeVarint32(handle.length); - for (const b of handle) sa.writeVarint32(b); - }); + const bytes = wellFormed({ + $case: "setAttribute", + value: { + id: 10, + name: 3, + ns: undefined, + value: { $case: "asset", value: handle }, + }, }); const sink = new RecordingSink(); new FrameDecoder(sink).push(bytes); @@ -235,22 +233,27 @@ Deno.test("SetAttribute's asset arm (field 5) decodes to an asset handle", () => Deno.test("TemplateAttr's asset arm (field 4) decodes to an asset handle", () => { const handle = Uint8Array.of(9, 8); - const bytes = frame((w) => { - w.writeMessage(15, (rt) => { - rt.writeUint32(1, 1); // RegisterTemplate.id - rt.writeMessage(2, (node) => { - node.writeMessage(1, (el) => { - el.writeUint32(1, 1); // TemplateElement.tag - el.writeMessage(3, (attr) => { - attr.writeUint32(1, 3); // TemplateAttr.name - attr.writeTag(4, 2); // TemplateAttr.asset - attr.writeVarint32(handle.length); - for (const b of handle) attr.writeVarint32(b); - }); - }); - }); - rt.writeUint32(3, 0); // roots = [0] - }); + const bytes = wellFormed({ + $case: "registerTemplate", + value: { + id: 1, + nodes: [{ + kind: { + $case: "element", + value: { + tag: 1, + ns: undefined, + attrs: [{ + name: 3, + ns: undefined, + value: { $case: "asset", value: handle }, + }], + children: [], + }, + }, + }], + roots: [0], + }, }); const sink = new RecordingSink(); new FrameDecoder(sink).push(bytes); diff --git a/receiver/tests/proto_test.ts b/receiver/tests/proto_test.ts deleted file mode 100644 index 4a92816..0000000 --- a/receiver/tests/proto_test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { assertEquals, assertThrows } from "@std/assert"; -import { Reader, TruncatedError, WireType, Writer } from "../src/proto.ts"; - -Deno.test("varint round trip", () => { - for (const v of [0, 1, 127, 128, 300, 16384, 0xffffffff >>> 0]) { - const w = new Writer(); - w.writeVarint32(v); - const r = new Reader(w.finish()); - assertEquals(r.readVarint32(), v); - } -}); - -Deno.test("sint32 zigzag round trip", () => { - for (const v of [0, 1, -1, 2, -2, 2147483647, -2147483648]) { - const w = new Writer(); - w.writeSInt32(v); - const r = new Reader(w.finish()); - assertEquals(r.readSInt32(), v); - } -}); - -Deno.test("bool, double, string round trip", () => { - const w = new Writer(); - w.writeBool(1, true); - w.writeDouble(2, 3.5); - w.writeString(3, "hello, world"); - const r = new Reader(w.finish()); - const [f1, wt1] = r.readTag(); - assertEquals(f1, 1); - assertEquals(wt1, WireType.Varint); - assertEquals(r.readBool(), true); - const [f2, wt2] = r.readTag(); - assertEquals(f2, 2); - assertEquals(wt2, WireType.Fixed64); - assertEquals(r.readDouble(), 3.5); - const [f3, wt3] = r.readTag(); - assertEquals(f3, 3); - assertEquals(wt3, WireType.LengthDelimited); - assertEquals(r.readString(), "hello, world"); - assertEquals(r.finished(), true); -}); - -Deno.test("embedded message with length prefix", () => { - const w = new Writer(); - w.writeMessage(1, (m) => { - m.writeUint32(1, 42); - m.writeString(2, "inner"); - }); - const r = new Reader(w.finish()); - const [field, wireType] = r.readTag(); - assertEquals(field, 1); - assertEquals(wireType, WireType.LengthDelimited); - const sub = r.readMessage(); - const [f1] = sub.readTag(); - assertEquals(f1, 1); - assertEquals(sub.readVarint32(), 42); - const [f2] = sub.readTag(); - assertEquals(f2, 2); - assertEquals(sub.readString(), "inner"); - assertEquals(sub.finished(), true); - assertEquals(r.finished(), true); -}); - -Deno.test("skip by wire type: varint, fixed64, length-delimited, fixed32", () => { - const w = new Writer(); - w.writeUint32(1, 999); // varint - w.writeDouble(2, 1.25); // fixed64 - w.writeString(3, "skip me"); // length-delimited - w.writeFloat(4, 2.5); // fixed32 - w.writeUint32(5, 7); // a field we actually read, to prove skip advanced correctly - const r = new Reader(w.finish()); - for (let i = 0; i < 4; i++) { - const [, wt] = r.readTag(); - r.skip(wt); - } - const [field] = r.readTag(); - assertEquals(field, 5); - assertEquals(r.readVarint32(), 7); - assertEquals(r.finished(), true); -}); - -Deno.test("readVarint32 rejects truncated input", () => { - const buf = Uint8Array.of(0x80); // continuation bit set, no follow-up byte - const r = new Reader(buf); - assertThrows(() => r.readVarint32()); -}); - -Deno.test("readVarint32 throws TruncatedError specifically when bytes run out", () => { - const buf = Uint8Array.of(0x80, 0x80); // still continuing, buffer just ends - const r = new Reader(buf); - assertThrows(() => r.readVarint32(), TruncatedError); -}); - -Deno.test("readVarint32 throws a plain (non-Truncated) error for a malformed varint that is fully present", () => { - // Every byte present, continuation bit set throughout — never - // terminates within the legal width for a u32. This is NOT a buffering - // situation (the bytes are all there); FrameDecoder must not treat it - // as "wait for more". - const buf = Uint8Array.of(0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00); - const r = new Reader(buf); - let threw: unknown; - try { - r.readVarint32(); - } catch (e) { - threw = e; - } - if (!(threw instanceof RangeError) || threw instanceof TruncatedError) { - throw new Error(`expected a plain RangeError, got ${String(threw)}`); - } -}); - -Deno.test("sint64 zigzag round trip via SetProperty-shaped field", () => { - for (const v of [0, 1, -1, 1000000, -1000000]) { - const w = new Writer(); - w.writeTag(4, WireType.Varint); - // sint64 has no dedicated Writer method (not needed by encodePayload), - // so round-trip it through the sint32 zigzag encoding for values that - // fit — readSInt64 must decode the same zigzag scheme regardless of - // width. - w.writeSInt32(v); - const r = new Reader(w.finish()); - r.readTag(); - assertEquals(r.readSInt64(), v); - } -}); diff --git a/receiver/tests/wire.ts b/receiver/tests/wire.ts new file mode 100644 index 0000000..e2fae32 --- /dev/null +++ b/receiver/tests/wire.ts @@ -0,0 +1,25 @@ +// Length-prefixed framing for test fixtures: the stream layout +// proto/stream-dom.proto's header describes (varint byte length, then one +// Frame, repeated). Frame bodies come from the generated writers +// (`Frame.encode(...).finish()`) for well-formed fixtures and from +// `BinaryWriter` directly for the malformed ones. + +import { BinaryWriter } from "@bufbuild/protobuf/wire"; + +/** Prepend `bytes`'s length as a varint32 — one frame on the wire. */ +export function frame(bytes: Uint8Array): Uint8Array { + return new BinaryWriter().uint32(bytes.length).raw(bytes).finish(); +} + +/** Concatenate already-framed byte runs. */ +export function stream(...frames: Uint8Array[]): Uint8Array { + let total = 0; + for (const f of frames) total += f.length; + const out = new Uint8Array(total); + let at = 0; + for (const f of frames) { + out.set(f, at); + at += f.length; + } + return out; +}