feat(platform,server): match manifest execution kinds and serve custom chat rendering from a JS host - #459
Open
johnthecat wants to merge 1 commit into
Open
feat(platform,server): match manifest execution kinds and serve custom chat rendering from a JS host#459johnthecat wants to merge 1 commit into
johnthecat wants to merge 1 commit into
Conversation
…m chat rendering from a JS host `ProductExecutionKind` named two surfaces while product manifests declare three, so a widget had to masquerade as an app. Rename the variants to match `["app", "widget", "worker"]`: `Spa` becomes `App`, `Widget` is new, and `Worker` replaces `Chat`. `App` and `Widget` are one capability class, since execution gating is exact equality and nothing requires either specifically. This moves the worker kind's SCALE discriminant from 1 to 2, so it is a wire change rather than a rename; the wasm entrypoint now rejects an unknown kind at construction instead of degrading silently. Add the two host-initiated Chat entry points a JS host was missing. `renderCustomMessage` streams product-drawn trees for one stored custom message, and `publishChatAction` carries a tapped action back to the product. Shipping only the first would render cells whose buttons do nothing, so both land together. `ChatConnection::publish_action` was excluded from wasm by a `cfg`, despite being in-memory buffering with nothing native about it. Give host-initiated subscriptions an error terminal. A product that cannot serve a render sends `_interrupt`; one that finishes sends nothing and leaves its last tree standing. Both previously ended the stream the same way, so a failed render reported completion and the host would show a partial tree as final. `HostInitiatedSubscription` now yields `Result`, and the interrupt payload reaches the stream instead of being dropped on the floor. Host-side cancellation stays a clean end, because it is not a product failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three changes that Polkadot Desktop needed to move its worker onto the shared core. They are separable in principle, but the second depends on the first naming a worker execution, and the third is a correctness fix in the machinery the second uses.
Execution kinds match the manifest
ProductExecutionKindisApp | Widget | Worker, matching the executable kinds a product manifest declares (["app", "widget", "worker"]in dotli'spackages/shared/src/executables.ts).AppandWidgetare one capability class. Execution gating is exact equality and no service requires either kind specifically, so they reach the same APIs and differ only in how the host presents them.Workeris the only kind that may serve Chat.The SCALE discriminants are
App=0, Widget=1, Worker=2. The worker kind sits at 2, so this is a wire change and not only a rename. A host that keeps encoding 1 for its worker now claims to be a widget and loses Chat access. Paired Rust and TypeScript encoding tests pin all three indices, and the wasm entrypoint rejects an unrecognised kind at construction rather than falling back to a default.The explorer's compatibility page matches the set of kind names that serve Chat, so its frozen per-version snapshots keep resolving against the names each released version declared.
Custom chat messages reach a JS host
Two host-initiated entry points on
TrUApiProductProvider, present on runtimes that hold a live channel to the core:renderCustomMessagestreams the trees a product draws for one stored custom message.publishChatActioncarries a tapped button back to the product. A host with only the first would draw cells whose buttons do nothing, which is worse than drawing nothing, so both land together.Both sit behind the Chat access policy that already governs every other Chat call. A connection that is not a
Workerexecution with a live session is refused.ChatConnection::publish_actioncompiles for wasm. It buffers an action in memory until the product subscribes, with nothing native about it.Host-initiated subscriptions report failure
A product that cannot serve a render sends
_interrupt. A product that finishes sends nothing at all, and its last tree stays on screen until the host stops the render (js/packages/truapi/src/client.ts:405-433).HostInitiatedSubscriptionyieldsResult<Item, GenericError>so those two outcomes stay distinct. An interrupt and an item that fails to decode are errors. A dropped sender is a clean end, because that is the host closing the manager or disposing the core rather than the product failing.handle_messagedelivers the interrupt to the stream before dropping the sender.The host contract is exactly one terminal per render.
onCompletemeans the last tree delivered stands.onErrormeans it is partial and must not be shown as final. Without this, a failed render reports completion and a host draws half a message as though the product had finished.NativeCustomRendererObservergainson_error(reason). The Swift conformer is anAsyncThrowingStreamand finishes throwing aCustomRendererStreamError.Two crash-shaped bugs in the worker bridge
Both found while reviewing the above, and both invisible to a single-provider smoke test.
A tree the host's codec cannot decode, or a renderer that throws on one, ends that render through
onError. Such a throw escaping the worker message listener leaves the render with no terminal at all: the sink stays registered, the wasm subscription stays live, and a caller awaiting a terminal waits forever. Products built against a newer protocol than the host's bundled@parity/truapiare the realistic trigger.Render subscriptions are keyed by core on both sides of the worker boundary, and a disposed provider fails its outstanding renders. Desktop runs one provider per open product plus one per worker, so cross-product cancellation is reachable there.
Verification
cargo test --workspace --all-features(26 result blocks, 0 failures),cargo clippy --workspace --all-targets --all-features -- -D warnings,cargo +nightly fmt --all --check,cargo check --target wasm32-unknown-unknown -p truapi-server, codegen goldens,sync-bindings.sh --check,@parity/truapi231 tests,@parity/truapi-host64 tests, explorer typecheck, playground build and lint.The Swift package is not built locally. CI's
ios-swiftjob covers the hand-written conformer, which is whereCustomRendererStreamErrorand the new protocol method live.js/packages/truapi-host/src/host-callbacks-adapter.test.tstypechecks.tsconfig.jsonexcludes test files fromtsc -bandbun testtranspiles without typechecking, so nothing in CI reads them. Eleven type errors had accumulated there. The other test files in the package are worth the same sweep.