Skip to content

Commit 1fd19a0

Browse files
authored
Support transferable streams (postMessage)
Make readable, writable, and transform streams transferable via postMessage(stream, [stream]). The streams themselves must be transferred, but the chunks written or read from the streams are cloned, not transferred. Support for transferring chunks will require API changes and is expected to be added in a future update. There is no reference implementation of this functionality as jsdom does not support transferrable objects, and so it wouldn't be testable. Closes #276.
1 parent 1975208 commit 1fd19a0

File tree

1 file changed

+284
-3
lines changed

1 file changed

+284
-3
lines changed

index.bs

Lines changed: 284 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ Markup Shorthands: markdown yes
1515
spec:webidl; type:dfn; text:resolve
1616
spec:webidl; type:dfn; text:new
1717
spec:infra; type:dfn; text:list
18+
spec:html; type:dfn; text:entangle
19+
spec:html; type:dfn; text:message port post message steps
20+
spec:html; type:dfn; text:port message queue
1821
</pre>
1922

2023
<pre class="anchors">
@@ -28,10 +31,12 @@ urlPrefix: https://tc39.es/ecma262/; spec: ECMASCRIPT
2831
text: DataView; url: #sec-dataview-objects
2932
text: Number; url: #sec-ecmascript-language-types-number-type
3033
text: Uint8Array; url: #sec-typedarray-objects
34+
text: %Object.prototype%; url: #sec-properties-of-the-object-prototype-object
3135
type: dfn
3236
text: abstract operation; url: #sec-algorithm-conventions-abstract-operations
3337
text: completion record; url: #sec-completion-record-specification-type
3438
text: internal slot; url: #sec-object-internal-methods-and-internal-slots
39+
text: realm; url: #sec-code-realms
3540
text: the current Realm; url: #current-realm
3641
text: the typed array constructors table; url: #table-49
3742
text: typed array; url: #sec-typedarray-objects
@@ -41,11 +46,14 @@ urlPrefix: https://tc39.es/ecma262/; spec: ECMASCRIPT
4146
text: CopyDataBlockBytes; url: #sec-copydatablockbytes
4247
text: CreateArrayFromList; url: #sec-createarrayfromlist
4348
text: CreateBuiltinFunction; url: #sec-createbuiltinfunction
49+
text: CreateDataProperty; url: #sec-createdataproperty
4450
text: Construct; url: #sec-construct
4551
text: DetachArrayBuffer; url: #sec-detacharraybuffer
52+
text: Get; url: #sec-get-o-p
4653
text: GetV; url: #sec-getv
4754
text: IsDetachedBuffer; url: #sec-isdetachedbuffer
4855
text: IsInteger; url: #sec-isinteger
56+
text: OrdinaryObjectCreate; url: #sec-ordinaryobjectcreate
4957
text: SetFunctionLength; url: #sec-setfunctionlength
5058
text: SetFunctionName; url: #sec-setfunctionname
5159
text: Type; url: #sec-ecmascript-data-types-and-values
@@ -451,7 +459,7 @@ by the [=underlying source=] but not yet read by any consumer.
451459
The Web IDL definition for the {{ReadableStream}} class is given as follows:
452460

453461
<xmp class="idl">
454-
[Exposed=(Window,Worker,Worklet)]
462+
[Exposed=(Window,Worker,Worklet), Transferable]
455463
interface ReadableStream {
456464
constructor(optional object underlyingSource, optional QueuingStrategy strategy = {});
457465

@@ -507,6 +515,10 @@ table:
507515
<td class="non-normative">A {{ReadableStreamDefaultController}} or
508516
{{ReadableByteStreamController}} created with the ability to control the state and queue of this
509517
stream
518+
<tr>
519+
<td><!-- TODO(ricea): Style this as <dfn unused> when that is supported.
520+
See https://github.com/tabatkins/bikeshed/issues/1747 --><b>\[[detached]]</b>
521+
<td class="non-normative">A boolean flag set to true when the stream is transferred
510522
<tr>
511523
<td><dfn>\[[disturbed]]</dfn>
512524
<td class="non-normative">A boolean flag set to true when the stream has been read from or
@@ -1010,6 +1022,43 @@ default-reader-asynciterator-prototype-internal-slots">Asynchronous iteration</h
10101022
1. Return [=a promise resolved with=] undefined.
10111023
</div>
10121024

1025+
<h4 id="rs-transfer">Transfer via `postMessage()`</h4>
1026+
1027+
<dl class="domintro">
1028+
<dt><code>destination.postMessage(rs, { transfer: [rs] });</code>
1029+
<dd>
1030+
<p>Sends a {{ReadableStream}} to another frame, window, or worker.
1031+
1032+
<p>The transferred stream can be used exactly like the original. The original will become
1033+
[=locked to a reader|locked=] and no longer directly usable.
1034+
</dd>
1035+
</dl>
1036+
1037+
<div algorithm="ReadableStream transfer steps">
1038+
{{ReadableStream}} objects are [=transferable objects=]. Their [=transfer steps=], given |value|
1039+
and |dataHolder|, are:
1040+
1041+
1. If ! [$IsReadableStreamLocked$](|value|) is true, throw a "{{DataCloneError}}" {{DOMException}}.
1042+
1. Let |port1| be a [=new=] {{MessagePort}} in [=the current Realm=].
1043+
1. Let |port2| be a [=new=] {{MessagePort}} in [=the current Realm=].
1044+
1. [=Entangle=] |port1| and |port2|.
1045+
1. Let |writable| be a [=new=] {{WritableStream}} in [=the current Realm=].
1046+
1. Perform ! [$SetUpCrossRealmTransformWritable$](|writable|, |port1|).
1047+
1. Let |promise| be ! [$ReadableStreamPipeTo$](|value|, |writable|, false, false, false).
1048+
1. Set |promise|.\[[PromiseIsHandled]] to true.
1049+
1. Set |dataHolder|.\[[port]] to ! [$StructuredSerializeWithTransfer$](|port2|, « |port2| »).
1050+
</div>
1051+
1052+
<div algorithm="ReadableStream transfer-receiving steps">
1053+
Their [=transfer-receiving steps=], given |dataHolder| and |value|, are:
1054+
1055+
1. Let |deserializedRecord| be ! [$StructuredDeserializeWithTransfer$](|dataHolder|.\[[port]],
1056+
[=the current Realm=]).
1057+
1. Let |port| be |deserializedRecord|.\[[Deserialized]].
1058+
1. Perform ! [$SetUpCrossRealmTransformReadable$](|value|, |port|).
1059+
1060+
</div>
1061+
10131062
<h3 id="generic-reader-mixin">The {{ReadableStreamGenericReader}} mixin</h3>
10141063

10151064
The {{ReadableStreamGenericReader}} mixin defines common internal slots, getters and methods that
@@ -3457,7 +3506,7 @@ The {{WritableStream}} represents a [=writable stream=].
34573506
The Web IDL definition for the {{WritableStream}} class is given as follows:
34583507

34593508
<xmp class="idl">
3460-
[Exposed=(Window,Worker,Worklet)]
3509+
[Exposed=(Window,Worker,Worklet), Transferable]
34613510
interface WritableStream {
34623511
constructor(optional object underlyingSink, optional QueuingStrategy strategy = {});
34633512

@@ -3491,6 +3540,9 @@ table:
34913540
<td><dfn>\[[controller]]</dfn>
34923541
<td class="non-normative">A {{WritableStreamDefaultController}} created with the ability to
34933542
control the state and queue of this stream
3543+
<tr>
3544+
<td><!-- TODO(ricea): Style this as <dfn unused> when that is supported --><b>\[[detached]]</b>
3545+
<td class="non-normative">A boolean flag set to true when the stream is transferred
34943546
<tr>
34953547
<td><dfn>\[[inFlightWriteRequest]]</dfn>
34963548
<td class="non-normative">A slot set to the promise for the current in-flight write operation
@@ -3746,6 +3798,42 @@ as seen for example in [[#example-ws-no-backpressure]].
37463798
1. Return ? [$AcquireWritableStreamDefaultWriter$]([=this=]).
37473799
</div>
37483800

3801+
<h4 id="ws-transfer">Transfer via `postMessage()`</h4>
3802+
3803+
<dl class="domintro">
3804+
<dt><code>destination.postMessage(ws, { transfer: [ws] });</code>
3805+
<dd>
3806+
<p>Sends a {{WritableStream}} to another frame, window, or worker.
3807+
3808+
<p>The transferred stream can be used exactly like the original. The original will become
3809+
[=locked to a writer|locked=] and no longer directly usable.
3810+
</dd>
3811+
</dl>
3812+
3813+
<div algorithm="WritableStream transfer steps">
3814+
{{WritableStream}} objects are [=transferable objects=]. Their [=transfer steps=], given |value|
3815+
and |dataHolder|, are:
3816+
3817+
1. If ! [$IsWritableStreamLocked$](|value|) is true, throw a "{{DataCloneError}}" {{DOMException}}.
3818+
1. Let |port1| be a [=new=] {{MessagePort}} in [=the current Realm=].
3819+
1. Let |port2| be a [=new=] {{MessagePort}} in [=the current Realm=].
3820+
1. [=Entangle=] |port1| and |port2|.
3821+
1. Let |readable| be a [=new=] {{ReadableStream}} in [=the current Realm=].
3822+
1. Perform ! [$SetUpCrossRealmTransformReadable$](|readable|, |port1|).
3823+
1. Let |promise| be ! [$ReadableStreamPipeTo$](|readable|, |value|, false, false, false).
3824+
1. Set |promise|.\[[PromiseIsHandled]] to true.
3825+
1. Set |dataHolder|.\[[port]] to ! [$StructuredSerializeWithTransfer$](|port2|, « |port2| »).
3826+
</div>
3827+
3828+
<div algorithm="WritableStream transfer-receiving steps">
3829+
Their [=transfer-receiving steps=], given |dataHolder| and |value|, are:
3830+
3831+
1. Let |deserializedRecord| be ! [$StructuredDeserializeWithTransfer$](|dataHolder|.\[[port]],
3832+
[=the current Realm=]).
3833+
1. Let |port| be a |deserializedRecord|.\[[Deserialized]].
3834+
1. Perform ! [$SetUpCrossRealmTransformWritable$](|value|, |port|).
3835+
</div>
3836+
37493837
<h3 id="default-writer-class">The {{WritableStreamDefaultWriter}} class</h3>
37503838

37513839
The {{WritableStreamDefaultWriter}} class represents a [=writable stream writer=] designed to be
@@ -4884,7 +4972,7 @@ The {{TransformStream}} class is a concrete instance of the general [=transform
48844972
The Web IDL definition for the {{TransformStream}} class is given as follows:
48854973

48864974
<xmp class="idl">
4887-
[Exposed=(Window,Worker,Worklet)]
4975+
[Exposed=(Window,Worker,Worklet), Transferable]
48884976
interface TransformStream {
48894977
constructor(optional object transformer,
48904978
optional QueuingStrategy writableStrategy = {},
@@ -4918,6 +5006,9 @@ table:
49185006
<td><dfn>\[[controller]]</dfn>
49195007
<td class="non-normative">A {{TransformStreamDefaultController}} created with the ability to
49205008
control [=TransformStream/[[readable]]=] and [=TransformStream/[[writable]]=]
5009+
<tr>
5010+
<td><!-- TODO(ricea): Style this as <dfn unused> when that is supported --><b>\[[detached]]</b>
5011+
<td class="non-normative">A boolean flag set to true when the stream is transferred
49215012
<tr>
49225013
<td><dfn>\[[readable]]</dfn>
49235014
<td class="non-normative">The {{ReadableStream}} instance controlled by this object
@@ -5090,6 +5181,52 @@ side=], or to terminate or error the stream.
50905181
1. Return [=this=].[=TransformStream/[[writable]]=].
50915182
</div>
50925183

5184+
<h4 id="ts-transfer">Transfer via `postMessage()`</h4>
5185+
5186+
<dl class="domintro">
5187+
<dt><code>destination.postMessage(ts, { transfer: [ts] });</code>
5188+
<dd>
5189+
<p>Sends a {{TransformStream}} to another frame, window, or worker.
5190+
5191+
<p>The transferred stream can be used exactly like the original. Its [=readable side|readable=]
5192+
and [=writable sides=] will become locked and no longer directly usable.
5193+
</dd>
5194+
</dl>
5195+
5196+
<div algorithm="TransformStream transfer steps">
5197+
{{TransformStream}} objects are [=transferable objects=]. Their [=transfer steps=], given |value|
5198+
and |dataHolder|, are:
5199+
5200+
1. Let |readable| be |value|.[=TransformStream/[[readable]]=].
5201+
1. Let |writable| be |value|.[=TransformStream/[[writable]]=].
5202+
1. If ! [$IsReadableStreamLocked$](|readable|) is true, throw a "{{DataCloneError}}"
5203+
{{DOMException}}.
5204+
1. If ! [$IsWritableStreamLocked$](|writable|) is true, throw a "{{DataCloneError}}"
5205+
{{DOMException}}.
5206+
1. Set |dataHolder|.\[[readable]] to ! [$StructuredSerializeWithTransfer$](|readable|,
5207+
« |readable| »).
5208+
1. Set |dataHolder|.\[[writable]] to ! [$StructuredSerializeWithTransfer$](|writable|,
5209+
« |writable| »).
5210+
</div>
5211+
5212+
<div algorithm="TransformStream transfer-receiving steps">
5213+
Their [=transfer-receiving steps=], given |dataHolder| and |value|, are:
5214+
5215+
1. Let |readableRecord| be ! [$StructuredDeserializeWithTransfer$](|dataHolder|.\[[readable]],
5216+
[=the current Realm=]).
5217+
1. Let |writableRecord| be ! [$StructuredDeserializeWithTransfer$](|dataHolder|.\[[writable]],
5218+
[=the current Realm=]).
5219+
1. Set |value|.[=TransformStream/[[readable]]=] to |readableRecord|.\[[Deserialized]].
5220+
1. Set |value|.[=TransformStream/[[writable]]=] to |writableRecord|.\[[Deserialized]].
5221+
1. Set |value|.[=TransformStream/[[backpressure]]=],
5222+
|value|.[=TransformStream/[[backpressureChangePromise]]=], and
5223+
|value|.[=TransformStream/[[controller]]=] to undefined.
5224+
5225+
<p class="note">The [=TransformStream/[[backpressure]]=],
5226+
[=TransformStream/[[backpressureChangePromise]]=], and [=TransformStream/[[controller]]=] slots are
5227+
not used in a transferred {{TransformStream}}.</p>
5228+
</div>
5229+
50935230
<h3 id="ts-default-controller-class">The {{TransformStreamDefaultController}} class</h3>
50945231

50955232
The {{TransformStreamDefaultController}} class has methods that allow manipulation of the
@@ -5900,6 +6037,150 @@ for="value-with-size">value</dfn> and <dfn for="value-with-size">size</dfn>.
59006037
1. Set |container|.\[[queueTotalSize]] to 0.
59016038
</div>
59026039

6040+
<h3 id="transferrable-streams">Transferable streams</h3>
6041+
6042+
Transferable streams are implemented using a special kind of identity transform which has the
6043+
[=writable side=] in one [=realm=] and the [=readable side=] in another realm. The following
6044+
abstract operations are used to implement these "cross-realm transforms".
6045+
6046+
<div algorithm>
6047+
<dfn abstract-op lt="CrossRealmTransformSendError">CrossRealmTransformSendError(|port|,
6048+
|error|)</dfn> performs the following steps:
6049+
6050+
1. Perform [$PackAndPostMessage$](|port|, "`error`", |error|), discarding the result.
6051+
6052+
<p class="note">As we are already in an errored state when this abstract operation is performed, we
6053+
cannot handle further errors, so we just discard them.</p>
6054+
</div>
6055+
6056+
<div algorithm>
6057+
<dfn abstract-op lt="PackAndPostMessage">PackAndPostMessage(|port|, |type|, |value|)</dfn> performs
6058+
the following steps:
6059+
6060+
1. Let |message| be [$OrdinaryObjectCreate$](null).
6061+
1. Perform ! [$CreateDataProperty$](|message|, "`type`", |type|).
6062+
1. Perform ! [$CreateDataProperty$](|message|, "`value`", |value|).
6063+
1. Let |targetPort| be the port with which |port| is entangled, if any; otherwise let it be null.
6064+
1. Let |options| be «[ "`transfer`" → « » ]».
6065+
1. Run the [=message port post message steps=] providing |targetPort|, |message|, and |options|.
6066+
6067+
<p class="note">A JavaScript object is used for transfer to avoid having to duplicate the [=message
6068+
port post message steps=]. The prototype of the object is set to null to avoid interference from
6069+
{{%Object.prototype%}}.</p>
6070+
</div>
6071+
6072+
<div algorithm>
6073+
<dfn abstract-op lt="PackAndPostMessageHandlingError">PackAndPostMessageHandlingError(|port|,
6074+
|type|, |value|)</dfn> performs the following steps:
6075+
6076+
1. Let |result| be [$PackAndPostMessage$](|port|, |type|, |value|).
6077+
1. If |result| is an abrupt completion,
6078+
1. Perform ! [$CrossRealmTransformSendError$](|port|, |result|.\[[Value]]).
6079+
1. Return |result| as a completion record.
6080+
</div>
6081+
6082+
<div algorithm>
6083+
<dfn abstract-op lt="SetUpCrossRealmTransformReadable">SetUpCrossRealmTransformReadable(|stream|,
6084+
|port|)</dfn> performs the following steps:
6085+
6086+
1. Perform ! [$InitializeReadableStream$](|stream|).
6087+
1. Let |controller| be a [=new=] {{ReadableStreamDefaultController}}.
6088+
1. Add a handler for |port|'s {{MessagePort/message}} event with the following steps:
6089+
1. Let |data| be the data of the message.
6090+
1. Assert: [$Type$](|data|) is Object.
6091+
1. Let |type| be ! [$Get$](|data|, "`type`").
6092+
1. Let |value| be ! [$Get$](|data|, "`value`").
6093+
1. Assert: [$Type$](|type|) is String.
6094+
1. If |type| is "`chunk`",
6095+
1. Perform ! [$ReadableStreamDefaultControllerEnqueue$](|controller|, |value|).
6096+
1. Otherwise, if |type| is "`close`",
6097+
1. Perform ! [$ReadableStreamDefaultControllerClose$](|controller|).
6098+
1. Disentangle |port|.
6099+
1. Otherwise, if |type| is "`error`",
6100+
1. Perform ! [$ReadableStreamDefaultControllerError$](|controller|, |value|).
6101+
1. Disentangle |port|.
6102+
1. Add a handler for |port|'s {{MessagePort/messageerror}} event with the following steps:
6103+
1. Let |error| be a new "{{DataCloneError}}" {{DOMException}}.
6104+
1. Perform ! [$CrossRealmTransformSendError$](|port|, |error|).
6105+
1. Perform ! [$ReadableStreamDefaultControllerError$](|controller|, |error|).
6106+
1. Disentangle |port|.
6107+
1. Enable |port|'s [=port message queue=].
6108+
1. Let |startAlgorithm| be an algorithm that returns undefined.
6109+
1. Let |pullAlgorithm| be the following steps:
6110+
1. Perform ! [$PackAndPostMessage$](|port|, "`pull`", undefined).
6111+
1. Return [=a promise resolved with=] undefined.
6112+
1. Let |cancelAlgorithm| be the following steps, taking a |reason| argument:
6113+
1. Let |result| be [$PackAndPostMessageHandlingError$](|port|, "`error`", |reason|).
6114+
1. Disentangle |port|.
6115+
1. If |result| is an abrupt completion, return [=a promise rejected with=] |result|.\[[Value]].
6116+
1. Otherwise, return [=a promise resolved with=] undefined.
6117+
1. Let |sizeAlgorithm| be an algorithm that returns 1.
6118+
1. Perform ! [$SetUpReadableStreamDefaultController$](|stream|, |controller|, |startAlgorithm|,
6119+
|pullAlgorithm|, |cancelAlgorithm|, 0, |sizeAlgorithm|).
6120+
6121+
<p class="note">Implementations are encouraged to explicitly handle failures from the asserts in
6122+
this algorithm, as the input might come from an untrusted context. Failure to do so could lead to
6123+
security issues.</p>
6124+
</div>
6125+
6126+
<div algorithm>
6127+
<dfn abstract-op lt="SetUpCrossRealmTransformWritable">SetUpCrossRealmTransformWritable(|stream|,
6128+
|port|)</dfn> performs the following steps:
6129+
6130+
1. Perform ! [$InitializeWritableStream$](|stream|).
6131+
1. Let |controller| be a [=new=] {{WritableStreamDefaultController}}.
6132+
1. Let |backpressurePromise| be [=a new promise=].
6133+
1. Add a handler for |port|'s {{MessagePort/message}} event with the following steps:
6134+
1. Let |data| be the data of the message.
6135+
1. Assert: [$Type$](|data|) is Object.
6136+
1. Let |type| be ! [$Get$](|data|, "`type`").
6137+
1. Let |value| be ! [$Get$](|data|, "`value`").
6138+
1. Assert: [$Type$](|type|) is String.
6139+
1. If |type| is "`pull`",
6140+
1. If |backpressurePromise| is not undefined,
6141+
1. [=Resolve=] |backpressurePromise| with undefined.
6142+
1. Set |backpressurePromise| to undefined.
6143+
1. Otherwise, if |type| is "`error`",
6144+
1. Perform ! [$WritableStreamDefaultControllerErrorIfNeeded$](|controller|, |value|).
6145+
1. If |backpressurePromise| is not undefined,
6146+
1. [=Resolve=] |backpressurePromise| with undefined.
6147+
1. Set |backpressurePromise| to undefined.
6148+
1. Add a handler for |port|'s {{MessagePort/messageerror}} event with the following steps:
6149+
1. Let |error| be a new "{{DataCloneError}}" {{DOMException}}.
6150+
1. Perform ! [$CrossRealmTransformSendError$](|port|, |error|).
6151+
1. Perform ! [$WritableStreamDefaultControllerError$](|controller|, |error|).
6152+
1. Disentangle |port|.
6153+
1. Enable |port|'s [=port message queue=].
6154+
1. Let |startAlgorithm| be an algorithm that returns undefined.
6155+
1. Let |writeAlgorithm| be the following steps, taking a |chunk| argument:
6156+
1. If |backpressurePromise| is undefined, set |backpressurePromise| to
6157+
[=a promise resolved with=] undefined.
6158+
1. Return the result of [=reacting=] to |backpressurePromise| with the following
6159+
fulfillment steps:
6160+
1. Set |backpressurePromise| to [=a new promise=].
6161+
1. Let |result| be [$PackAndPostMessageHandlingError$](|port|, "`chunk`", |chunk|).
6162+
1. If |result| is an abrupt completion,
6163+
1. Disentangle |port|.
6164+
1. Return [=a promise rejected with=] |result|.\[[Value]].
6165+
1. Otherwise, return [=a promise resolved with=] undefined.
6166+
1. Let |closeAlgorithm| be the folowing steps:
6167+
1. Perform ! [$PackAndPostMessage$](|port|, "`close`", undefined).
6168+
1. Disentangle |port|.
6169+
1. Return [=a promise resolved with=] undefined.
6170+
1. Let |abortAlgorithm| be the following steps, taking a |reason| argument:
6171+
1. Let |result| be [$PackAndPostMessageHandlingError$](|port|, "`error`", |reason|).
6172+
1. Disentangle |port|.
6173+
1. If |result| is an abrupt completion, return [=a promise rejected with=] |result|.\[[Value]].
6174+
1. Otherwise, return [=a promise resolved with=] undefined.
6175+
1. Let |sizeAlgorithm| be an algorithm that returns 1.
6176+
1. Perform ! [$SetUpWritableStreamDefaultController$](|stream|, |controller|, |startAlgorithm|,
6177+
|writeAlgorithm|, |closeAlgorithm|, |abortAlgorithm|, 1, |sizeAlgorithm|).
6178+
6179+
<p class="note">Implementations are encouraged to explicitly handle failures from the asserts in
6180+
this algorithm, as the input might come from an untrusted context. Failure to do so could lead to
6181+
security issues.</p>
6182+
</div>
6183+
59036184
<h3 id="misc-abstract-ops">Miscellaneous</h4>
59046185

59056186
The following abstract operations are a grab-bag of utilities.

0 commit comments

Comments
 (0)