Skip to content

Model eval as one call returning stream + future - #24

Merged
lannbot merged 1 commit into
mainfrom
feat/eval-as-stream-future
Sep 5, 2026
Merged

Model eval as one call returning stream + future#24
lannbot merged 1 commit into
mainfrom
feat/eval-as-stream-future

Conversation

@lannbot

@lannbot lannbot commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Follow-up to #23. interface eval becomes a single sync import:

eval: func(js: string, send: stream<string>) -> tuple<stream<string>, future<result<string, error>>>;
variant error { invalid-js(string), communication(string) }

Why: the CABI now owns every lifetime question (stream close = no more values; a future is read once; fire-and-forget = dropped reader), so the finished error case and the resource are gone. And it removes the macrotask workaround for polyengine#280: measured on the same build, a subtask import settling right after handle-event's initial activation is lost, while a future-typed import settling at the same moment is delivered (comment on #280 has the matrix).

Guest (src/document.rs): sends stage into a Vec drained by a spawned writer-owning task (one in-flight write per end); the recv reader is moved out of its slot per await. Owner-lifetime scheme unchanged.

Host (host/src/eval.ts): returns [AsyncIterable, Promise] synchronously, lowered by the runtime; stream closes before the future resolves. No deferral anywhere.

Fixture and eval_component_test.ts unchanged and green; unit tests rewritten for the new API. Full just check/fixtures/example counter/test pass.

Automerge armed (merge commit).

`interface eval` drops its `evaluation` resource for a single synchronous
import: `eval(js, send: stream<string>) -> tuple<stream<string>,
future<result<string, error>>>`. The channels are component-model streams
and the completion a future, so every lifetime question now has the
CABI's answer: "no more values" is the stream closing, "already
completed" cannot be asked twice of a future, and a fire-and-forget eval
is a dropped reader the host's eventual write sees as `dropped`. The
`finished` error case goes away with them.

This also removes the macrotask deferral the host carried against
polyengine#280. Measured on the same build and click path: a subtask
import settling in the microtask checkpoint after `handle-event`'s
initial activation returns is lost, while a future-typed import settling
at the same moment (even from an already-resolved promise) is delivered.
Future and stream writes take a different path through the runtime than
subtask settlement, so the shape sidesteps the bug rather than papering
over it.

Guest: `Evaluator::send` is synchronous and at most one write may be in
flight per stream end, so sends stage into a `Vec` drained by a spawned
task that owns the writer for the drain (the renderer's own
`stream`/`pending` pattern). The `recv` reader is moved out of its slot
for each await and put back. Owner-lifetime handling is unchanged.

Host: returns `[AsyncIterable, Promise]` synchronously; the runtime lowers
both ("Lowering accepts the natural JS producers"). The stream is closed
before the future resolves so a reader never observes completion with a
value still in flight. Fixture and end-to-end tests are unchanged.
@lannbot
lannbot enabled auto-merge September 5, 2026 04:41
@lannbot
lannbot merged commit 01db0de into main Sep 5, 2026
1 check passed
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.

2 participants