(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:
- a
stream.new to create a fresh (readable, writable) end pair
- passing out the fresh readable end
- 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.
(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 thei32index 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:stream<T>needs to be called before the caller has received the readable end that they'd like to forward fromThese cases will end up requiring:
stream.newto create a fresh (readable, writable) end pairSteps 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:
where
$stream.forwardis given type:where
$readable-endand$writable-endlogically 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 (ifasyncwas 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 awaitable-setorstream.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.forwardaddresses 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:where
$stream.spliceis given type:where
$readable-endand$writable-endare 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 withstream.forward), which is an important optimization in various proxy scenarios.