Skip to content

stream.forward/splice sketch #658

@lukewagner

Description

@lukewagner

(Filing this issue to sketch a post-0.3.0 follow-up addition, for whenever someone hits the use cases and is ready to implement.)

Currently, streams can be zero-cost forwarded across component boundaries via the stream<T> type, where the producer passes the i32 index of a readable stream end in the producer's handle table, and then this readable end is transferred to the consumer. However, this still leaves some cases where we'd like to do zero-cost forwarding, but can't:

  • when the function taking the stream<T> needs to be called before the caller has received the readable end that they'd like to forward from
  • when there's a partial copy
  • when there's a need to find out after the forwarding is finished

These cases will end up requiring:

  1. a stream.new to create a fresh (readable, writable) end pair
  2. passing out the fresh readable end
  3. copying into the fresh writable end from some other readable end at some point in the future

Steps 1 and 2 are fine/cheap, but ideally we'd avoid the O(n) copy in step 3. To do this, I was thinking there could be a new built-in definition:

(canon stream.forward $streamT async? (core func $stream.forward))

where $stream.forward is given type:

(func $stream.forward (param $readable-end i32) (param $writable-end i32) (param $n i32) (result i32))

where $readable-end and $writable-end logically borrow the given readable/writable ends for the duration of the $stream.forward. The return value would then either be a sentinel "blocked" (-1) value (if async was set) or else the synchronous result of the copy. In the "blocked" case, until the forwarding completes, both the readable and writable ends of the stream would be in the "copying" state (so that they couldn't be dropped or separately read/written). To avoid ambiguous event delivery, only one end (e.g., maybe just the writing end) could be put in a waitable-set or stream.canceled. Care would need to be taken when delivering the "result" to unambiguously deliver drops that occurred on either end during the forwarding operation.

While stream.forward addresses the general case (in particular the 2nd and 3rd bulleted use cases above), in some cases (like the first bulleted use case), "everything" is forwarded and the caller wants "dropping" to propagate in both directions in the obvious way. To optimize this common case, there could be a second, simplified built-in:

(canon stream.splice $streamT (core func $stream.splice))

where $stream.splice is given type:

(func $stream.splice (param $readable-end i32) (param $writable-end i32))

where $readable-end and $writable-end are transferred out of the calling component instance's handle table to the host, effectively splicing the calling component instance out of the stream chain, thereby allowing the calling component instance to be more-eagerly torn down by the runtime (than with stream.forward), which is an important optimization in various proxy scenarios.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions