Skip to content

fix(runtime): hydrate a tuple captured by a runtime expression - #255

Open
AmeinEskinder wants to merge 1 commit into
tokio-rs:mainfrom
AmeinEskinder:fix/tuple-surrogate-hydration
Open

fix(runtime): hydrate a tuple captured by a runtime expression#255
AmeinEskinder wants to merge 1 commit into
tokio-rs:mainfrom
AmeinEskinder:fix/tuple-surrogate-hydration

Conversation

@AmeinEskinder

Copy link
Copy Markdown
Contributor

Closes #254.

expr.md lists tuples in the shared vocabulary, and expr_field.rs:21 compiles pair.0 into JavaScript pair[0], so a tuple is an array on the browser side. Serde writes a tuple surrogate as one. hydrateSurrogate had no array case, so the array fell past the tag switch into the default arm and threw:

Error: Unknown surrogate type: undefined
 ❯ hydrateSurrogate src/surrogate/index.ts:73:12

Every expression capturing a tuple failed to hydrate, and scan walks the document in a single loop with no try/catch, so nothing after it hydrated either.

The change

Hydrate an array by hydrating its elements. The check sits before the tag switch because an array is an object with no t, and the result stays a real array so the generated pair[0] still indexes it.

That alone would trade a throw for a quieter disagreement, so both render paths follow the server too:

server browser before browser after
$(pair) on (1.5, 2.5) 1.52.5 throws 1.52.5
:title=$(pair) on ("a", "b") title="ab" throws title="ab"
tuple holding a None element writes nothing throws element writes nothing

NodeViewParts for (T1, T2) writes its elements one after another with no separator, where String(array) would have given 1.5,2.5. AttributeValueViewParts for (T1, T2) is present when any element is, and skips the ones that are not, the way a None writes nothing — calling toAttributeValue() on a None would itself have thrown.

Verification

The strings below are what topcoat-runtime actually rendered for let pair = (1.5f64, 2.5f64), taken from a rendered view rather than written by hand:

$(pair.0)  ->  (() => { const [__external0] = [cx.hydrate([1.5,2.5])]; return __external0[0]; })()
$(pair)    ->  (() => { const [__external0] = [cx.hydrate([1.5,2.5])]; return __external0; })()

Running those exact strings through the patched runtime gives 1.5 and 1.52.5, matching the server's 1.5 and 1.52.5. Both threw before.

Eleven tests cover hydration, both render paths, and the wire format the fix depends on. I disabled each of the three new branches in turn and confirmed every test fails without it, with the failures being the divergences above (a,b for ab, 1.5,2.5 for 1.52.5, a,[object Object],b for a tuple holding a None).

The Rust-side tests pin the serialized shape ([1.5,2.5], [1.0,{"t":"Option","v":2.0},true], [[1.0,2.0],3.0]) so a change to tuple serialization can't silently break the browser half.

Checks

yarn build, yarn test (31 passed), cargo test -p topcoat-runtime, cargo clippy -p topcoat-runtime --all-targets (0 warnings), cargo fmt --check. dist/index.js is rebuilt.

`expr.md` lists tuples in the shared vocabulary and `expr_field.rs`
compiles `pair.0` to JavaScript `pair[0]`, so a tuple is an array on the
browser side, and serde writes a tuple surrogate as one. `hydrateSurrogate`
had no array case, so an array fell past the tag switch into the default
arm and threw `Unknown surrogate type: undefined`. Every expression that
captured a tuple failed to hydrate, and since `scan` walks the document in
one loop with no `try`/`catch`, nothing after it hydrated either.

Hydrate an array by hydrating its elements, checked before the tag because
an array is an object with no `t`. It stays a real array so the generated
`pair[0]` still indexes it.

That alone would trade the throw for a quieter disagreement, so the two
render paths follow the server as well. `NodeViewParts for (T1, T2)` writes
its elements one after another with no separator, where `String(array)`
would have inserted commas: `$(pair)` on `(1.5, 2.5)` renders `1.52.5` on
both sides now, not `1.5,2.5` in the browser. `AttributeValueViewParts for
(T1, T2)` is present when any element is and skips the ones that are not,
the way a `None` writes nothing.

Tests cover hydration, both render paths, and the wire format the fix
depends on. Each one fails with its branch removed.

Closes tokio-rs#254.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Capturing a tuple in a runtime expression throws at hydration

1 participant