We are in the process of transitioning this specification from a GitHub README into something a bit more palatable. The official-lookin' version is developed in the index.bs file and then deployed to the gh-pages branch; you can see it at https://whatwg.github.io/streams/.
Right now, we've transferred over most of the concepts and text, but none of the algorithms or APIs. We'll be iterating on the APIs a bit more here, in Markdown format, until we feel confident in them. In the meantime, please check out the rendered spec for all of the interesting stage-setting text.
By the way, this transition is being tracked as #62.
class ReadableStream {
constructor({
function start = () => {},
function pull = () => {},
function cancel = () => {},
object strategy = new CountQueuingStrategy({ highWaterMark: 1 }),
})
// Reading data from the underlying source
any read()
Promise<undefined> wait()
get ReadableStreamState state
// Composing with other streams
WritableStream pipeTo(WritableStream dest, { ToBoolean close = true } = {})
ReadableStream pipeThrough({ WritableStream in, ReadableStream out }, options)
// Stop accumulating data
Promise<undefined> cancel(any reason)
// Useful helper
get Promise<undefined> closed
// Internal slots
[[queue]]
[[started]] = false
[[draining]] = false
[[pulling]] = false
[[state]] = "waiting"
[[storedError]]
[[waitPromise]]
[[closedPromise]]
[[startedPromise]]
// Holders for stuff given by the underlying source
[[onCancel]]
[[onPull]]
[[strategy]]
// Internal methods for use by the underlying source
[[enqueue]](any chunk)
[[close]]()
[[error]](any e)
// Other internal helper methods
[[callOrSchedulePull]]()
[[callPull]]()
}
enum ReadableStreamState {
"readable" // the queue has something in it; read at will
"waiting" // the source is not ready or the queue is empty; you should call wait
"closed" // all data has been read from both the source and the queue
"errored" // the source errored so the stream is now dead
}
The constructor is passed several functions, all optional:
start(enqueue, close, error)is typically used to adapt a push-based data source, as it is called immediately so it can set up any relevant event listeners, or to acquire access to a pull-based data source. If this process is asynchronous, it can return a promise to signal success or failure.pull(enqueue, close, error)is typically used to adapt a pull-based data source, as it is called in reaction toreadcalls, or to start the flow of data in push-based data sources. Once it is called, it will not be called again until its passedenqueuefunction is called.cancel(reason)is called when the readable stream is canceled, and should perform whatever source-specific steps are necessary to clean up and stop reading. If this process is asynchronous, it can return a promise to signal success or failure.
Both start and pull are given the ability to manipulate the stream's internal queue and state by being passed the this.[[enqueue]], this.[[close]], and this.[[error]] functions.
- Set
this.[[onCancel]]tocancel. - Set
this.[[onPull]]topull. - Set
this.[[strategy]]tostrategy. - Let
this.[[waitPromise]]be a new promise. - Let
this.[[closedPromise]]be a new promise. - Let
this.[[queue]]be a new empty List. - Let startResult be the result of
start(this.[[enqueue]], this.[[close]], this.[[error]]). - ReturnIfAbrupt(startResult).
- Let
this.[[startedPromise]]be the result of resolving startResult as a promise. - Upon fulfillment of
this.[[startedPromise]], setthis.[[started]]to true. - Upon rejection of
this.[[startedPromise]]with reasonr, callthis.[[error]](r).
- Return
this.[[state]].
- If
this.[[state]]is"waiting"or"closed", throw a TypeError exception. - If
this.[[state]]is"errored", throwthis.[[storedError]]. - Assert:
this.[[state]]is"readable". - Assert:
this.[[queue]]is not empty. - Let
chunkbe DequeueValue(this.[[queue]]). - If
this.[[queue]]is now empty,- If
this.[[draining]]is true,- Set
this.[[state]]to"closed". - Resolve
this.[[closedPromise]]with undefined.
- Set
- If
this.[[draining]]is false,- Set
this.[[state]]to"waiting". - Let
this.[[waitPromise]]be a new promise. - Call
this.[[callOrSchedulePull]]().
- Set
- If
- Return
chunk.
- If
this.[[state]]is"waiting",- Call
this.[[callOrSchedulePull]]().
- Call
- Return
this.[[waitPromise]].
- If
this.[[state]]is"closed", return a new promise resolved with undefined. - If
this.[[state]]is"errored", return a new promise rejected withthis.[[storedError]]. - If
this.[[state]]is"waiting", resolvethis.[[waitPromise]]with undefined. - Let
this.[[queue]]be a new empty List. - Set
this.[[state]]to"closed". - Resolve
this.[[closedPromise]]with undefined. - Let cancelPromise be a new promise.
- Let sourceCancelPromise be the result of promise-calling this.[[onCancel]](reason).
- Upon fulfillment of sourceCancelPromise, resolve cancelPromise with undefined.
- Upon rejection of sourceCancelPromise with reason r, reject cancelPromise with r.
- Return cancelPromise.
- Return
this.[[closedPromise]].
The pipeTo method is one of the more complex methods, and is undergoing some revision and edge-case bulletproofing before we write it up in prose.
For now, please consider the reference implementation normative: reference-implementation/lib/readable-stream.js, look for the pipeTo method.
- If Type(writable) is not Object, then throw a TypeError exception.
- If Type(readable) is not Object, then throw a TypeError exception.
- Let stream be the this value.
- Let result be Invoke(stream,
"pipeTo", (writable, options)). - ReturnIfAbrupt(result).
- Return readable.
- If
this.[[state]]is"errored"or"closed", return false. - If
this.[[draining]] is true, throw a TypeError exception. - Let chunkSize be Invoke(
this.[[strategy]],"size", (chunk)). - If chunkSize is an abrupt completion,
- Call
this.[[error]](chunkSize.[[value]]). - Return false.
- Call
- EnqueueValueWithSize(
this.[[queue]],chunk, chunkSize.[[value]]). - Set
this.[[pulling]]to false. - Let queueSize be GetTotalQueueSize(
this.[[queue]]). - Let shouldApplyBackpressure be ToBoolean(Invoke(
this.[[strategy]],"shouldApplyBackpressure", (queueSize))). - If shouldApplyBackpressure is an abrupt completion,
- Call
this.[[error]](shouldApplyBackpressure.[[value]]). - Return false.
- Call
- If
this.[[state]]is"waiting",- Set
this.[[state]]to"readable". - Resolve
this.[[waitPromise]]with undefined.
- Set
- If shouldApplyBackpressure.[[value]] is true, return false.
- Return true.
- If
this.[[state]]is"waiting",- Resolve
this.[[waitPromise]]with undefined. - Resolve
this.[[closedPromise]]with undefined. - Set
this.[[state]]to"closed".
- Resolve
- If
this.[[state]]is"readable",- Set
this.[[draining]]to true.
- Set
- If
this.[[state]]is"waiting",- Set
this.[[state]]to"errored". - Set
this.[[storedError]]toe. - Reject
this.[[waitPromise]]withe. - Reject
this.[[closedPromise]]withe.
- Set
- If
this.[[state]]is"readable",- Let
this.[[queue]]be a new empty List. - Set
this.[[state]]to"errored". - Set
this.[[storedError]]toe. - Let
this.[[waitPromise]]be a new promise rejected withe. - Reject
this.[[closedPromise]]withe.
- Let
- If
this.[[pulling]]is true, return. - Set
this.[[pulling]]to true. - If
this.[[started]]is false,- Upon fulfillment of
this.[[startedPromise]], callthis.[[callPull]].
- Upon fulfillment of
- If
this.[[started]]is true, callthis.[[callPull]].
- Let
pullResultbe the result ofthis.[[onPull]](this.[[enqueue]], this.[[close]], this.[[error]]). - If
pullResultis an abrupt completion, callthis.[[error]](pullResult.[[value]]).
class WritableStream {
constructor({
function start = () => {},
function write = () => {},
function close = () => {},
function abort = close,
object strategy = new CountQueuingStrategy({ highWaterMark: 0 })
})
// Writing data to the underlying sink
Promise<undefined> write(any chunk)
Promise<undefined> wait()
get WritableStreamState state
// Close off the underlying sink gracefully; we are done.
Promise<undefined> close()
// Close off the underlying sink forcefully; everything written so far is suspect.
Promise<undefined> abort(any reason)
// Useful helpers
get Promise<undefined> closed
// Internal methods
[[error]](any e)
[[callOrScheduleAdvanceQueue]]()
[[advanceQueue]]()
[[syncStateWithQueue]]()
[[doClose]]()
// Internal slots
[[queue]]
[[started]] = false
[[writing]] = false
[[state]] = "writable"
[[storedError]]
[[writablePromise]]
[[closedPromise]]
[[startedPromise]]
// Holders for stuff given by the underlying sink
[[onWrite]]
[[onClose]]
[[onAbort]]
[[strategy]]
}
enum WritableStreamState {
"writable" // the sink is ready and the queue is not yet full; write at will
"waiting" // the sink is not ready or the queue is full; you should call wait
"closing" // the sink is being closed; no more writing
"closed" // the sink has been closed
"errored" // the sink errored so the stream is now dead
}
The constructor is passed several functions, all optional:
start(error)is called when the writable stream is created, and should open the underlying writable sink. If this process is asynchronous, it can return a promise to signal success or failure.write(chunk)should writechunkto the underlying sink. It can return a promise to signal success or failure of the write operation to the underlying sink. The stream implementation guarantees that this function will be called only after previous writes have succeeded, and never aftercloseorabortis called.close()should close the underlying sink. If this process is asynchronous, it can return a promise to signal success or failure. The stream implementation guarantees that this function will be called only after all queued-up writes have succeeded.abort()is an abrupt close, signaling that all data written so far is suspect. It should clean up underlying resources, much likeclose, but perhaps with some custom handling. Unlikeclose,abortwill be called even if writes are queued up, throwing away those chunks. If this process is asynchronous, it can return a promise to signal success or failure.
In reaction to calls to the stream's .write() method, the write constructor option is given a chunk from the internal queue, along with the means to signal that the chunk has been successfully or unsuccessfully written.
- Set
this.[[onWrite]]towrite. - Set
this.[[onClose]]toclose. - Set
this.[[onAbort]]toabort. - Set
this.[[strategy]]tostrategy. - Let
this.[[writablePromise]]be a new promise resolved with undefined. - Let
this.[[closedPromise]]be a new promise. - Let
this.[[queue]]be a new empty List. - Let startResult be the result of
start(this.[[error]]). - ReturnIfAbrupt(startResult).
- Let
this.[[startedPromise]]be the result of resolving startResult as a promise. - Upon fulfillment of
this.[[startedPromise]], setthis.[[started]]to true. - Upon rejection of
this.[[startedPromise]]with reasonr, callthis.[[error]](r).
- Return
this.[[closedPromise]].
- Return
this.[[state]].
- If
this.[[state]]is"waiting"or"writable",- Let chunkSize be Invoke(
this.[[strategy]],"size", (chunk)). - ReturnIfAbrupt(chunkSize).
- Let
promisebe a new promise. - EnqueueValueWithSize(
this.[[queue]], Record{[[type]]:"chunk", [[promise]]:promise, [[chunk]]:chunk}, chunkSize). - Let syncResult be
this.[[syncStateWithQueue]](). - If syncResult is an abrupt completion,
- Call
this.[[error]](syncResult.[[value]]). - Return
promise.
- Call
- Call
this.[[callOrScheduleAdvanceQueue]](). - Return
promise.
- Let chunkSize be Invoke(
- If
this.[[state]]is"closing"or"closed",- Return a promise rejected with a TypeError exception.
- If
this.[[state]]is"errored",- Return a promise rejected with
this.[[storedError]].
- Return a promise rejected with
- If
this.[[state]]is"closing"or"closed", return a promise rejected with a TypeError exception. - If
this.[[state]]is"errored", return a promise rejected withthis.[[storedError]]. - If
this.[[state]]is"writable",- Set
this.[[writablePromise]]to a new promise rejected with a TypeError exception.
- Set
- If
this.[[state]]is"waiting",- Reject
this.[[writablePromise]]with a TypeError exception.
- Reject
- Set
this.[[state]]to"closing". - EnqueueValueWithSize(
this.[[queue]], Record{[[type]]:"close", [[promise]]: undefined, [[chunk]]: undefined}, 0). - Call
this.[[callOrScheduleAdvanceQueue]](). - Return
this.[[closedPromise]].
- If
this.[[state]]is"closed", return a new promise resolved with undefined. - If
this.[[state]]is"errored", return a new promise rejected withthis.[[storedError]]. - Call
this.[[error]](reason). - Let abortPromise be a new promise.
- Let sinkAbortPromise be the result of promise-calling this.[[onAbort]](reason).
- Upon fulfillment of sinkAbortPromise, resolve abortPromise with undefined.
- Upon rejection of sinkAbortPromise with reason r, reject abortPromise with r.
- Return abortPromise.
- Return
this.[[writablePromise]].
- If
this.[[state]]is"closed"or"errored", return. - Repeat while
this.[[queue]]is not empty:- Let writeRecord be DequeueValue(
this.[[queue]]). - If writeRecord.[[type]] is
"write", reject writeRecord.[[promise]] withe.
- Let writeRecord be DequeueValue(
- Set
this.[[storedError]]toe. - If
this.[[state]]is"writable"or"closing", setthis.[[writablePromise]]to a new promise rejected withe. - If
this.[[state]]is"waiting", rejectthis.[[writablePromise]]withe. - Reject
this.[[closedPromise]]withe. - Set
this.[[state]]to"errored".
- If
this.[[started]]is false,- Upon fulfillment of
this.[[startedPromise]], callthis.[[advanceQueue]].
- Upon fulfillment of
- If
this.[[started]]is true, callthis.[[advanceQueue]].
- If
this.[[queue]]is empty, orthis.[[writing]]is true, return. - Let
writeRecordbe PeekQueueValue(this.[[queue]]). - If
writeRecord.[[type]]is"close",- Assert:
this.[[state]]is"closing". - DequeueValue(
this.[[queue]]). - Assert:
this.[[queue]]is empty. - Call
this.[[doClose]]().
- Assert:
- Otherwise,
- Assert:
writeRecord.[[type]]is"chunk". - Set
this.[[writing]]to true. - Let writeResult be the result of promise-calling
this.[[onWrite]](writeRecord.[[chunk]]). - Upon fulfillment of writeResult,
- If
this.[[state]]is"errored", return. - Set
this.[[writing]]to false. - Resolve
writeRecord.[[promise]]with undefined. - DequeueValue(
this.[[queue]]). - Let syncResult be
this.[[syncStateWithQueue]](). - If syncResult is an abrupt completion, then
- Call
this.[[error]](syncResult.[[value]]). - Return.
- Call
- Call
this.[[advanceQueue]]().
- If
- Upon rejection of writeResult with reason r, call
this.[[error]](r).
- Assert:
Note: the early-exit clause in the fulfillment handler will be hit if the constructor's write option returns a promise that settles after the stream has been aborted.
Note: the peeking-then-dequeuing dance is necessary so that during the call to the user-supplied function, this.[[onWrite]], the queue and corresponding public state property correctly reflect the ongoing write. The write record only leaves the queue after the chunk has been successfully written to the underlying sink, and we can advance the queue.
- If
this.[[state]]is"closing", return. - Assert:
this.[[state]]is either"writable"or"waiting". - If
this.[[state]]is"waiting"andthis.[[queue]]is empty,- Set
this.[[state]]to"writable". - Resolve
this.[[writablePromise]]with undefined. - Return.
- Set
- Let queueSize be GetTotalQueueSize(
this.[[queue]]). - Let shouldApplyBackpressure be ToBoolean(Invoke(
this.[[strategy]],"shouldApplyBackpressure", (queueSize))). - ReturnIfAbrupt(shouldApplyBackpressure).
- If shouldApplyBackpressure is true and
this.[[state]]is"writable",- Set
this.[[state]]to"waiting". - Set
this.[[writablePromise]]to a new promise.
- Set
- If shouldApplyBackpressure is false and
this.[[state]]is"waiting",- Set
this.[[state]]to"writable". - Resolve
this.[[writablePromise]]with undefined.
- Set
- Assert:
this.[[state]]is"closing". - Let sinkClosePromise be the result of promise-calling
this.[[onClose]](). - Upon fulfillment of sinkClosePromise,
- If this.[[state]] is
"errored", return. - Assert: this.[[state]] is
"closing". - Resolve
this.[[closedPromise]]with undefined. - Set this.[[state]] to
"closed".
- If this.[[state]] is
- Upon rejection of sinkClosePromise with reason r,
- Call
this.[[error]](r).
- Call
Transform streams have been developed in the testable implementation, but not yet re-encoded in spec language. We are waiting to validate their design before doing so. In the meantime, see reference-implementation/lib/transform-stream.js.
A "tee stream" is a writable stream which, when written to, itself writes to multiple destinations. It aggregates backpressure and abort signals from those destinations, propagating the appropriate aggregate signals backward.
class TeeStream extends WritableStream {
constructor() {
this.[[outputs]] = [];
super({
write(chunk) {
return Promise.all(this.[[outputs]].map(o => o.dest.write(chunk)));
},
close() {
const outputsToClose = this.[[outputs]].filter(o => o.close);
return Promise.all(outputsToClose.map(o => o.dest.close()));
},
abort(reason) {
return Promise.all(this.[[outputs]].map(o => o.dest.abort(reason)));
}
});
}
addOut(dest, { close = true } = {}) {
this.[[outputs]].push({ dest, close });
}
}A common queuing strategy when dealing with binary data is to wait until the accumulated byteLength properties of the incoming data reaches a specified highWaterMark. As such, this is provided as a built-in helper along with the stream APIs.
class ByteLengthQueuingStrategy {
constructor({ highWaterMark }) {
this.highWaterMark = Number(highWaterMark);
if (Number.isNaN(this.highWaterMark) || this.highWaterMark < 0) {
throw new RangeError("highWaterMark must be a nonnegative number.");
}
}
size(chunk) {
return chunk.byteLength;
}
shouldApplyBackpressure(queueSize) {
return queueSize > this.highWaterMark;
}
}A common queuing strategy when dealing with object streams is to simply count the number of objects that have been accumulated so far, waiting until this number reaches a specified highWaterMark. As such, this strategy is also provided as a built-in helper.
class CountQueuingStrategy {
constructor({ highWaterMark }) {
this.highWaterMark = Number(highWaterMark);
if (Number.isNaN(this.highWaterMark) || this.highWaterMark < 0) {
throw new RangeError("highWaterMark must be a nonnegative number.");
}
}
size(chunk) {
return 1;
}
shouldApplyBackpressure(queueSize) {
return queueSize > this.highWaterMark;
}
}The streams in this specification use a "queue-with-sizes" data structure to store queued up values, along with their determined sizes. A queue-with-sizes is a List of records with [[value]] and [[size]] fields (although in implementations it would of course be backed by a more efficient data structure).
A number of operations are used to make working with queues-with-sizes more pleasant:
- Let size be ToNumber(size).
- ReturnIfAbrupt(size).
- If size is NaN, +∞, or −∞, throw a RangeError exception.
- Append Record{[[value]]: value, [[size]]: size} as the last element of queue.
- Assert: queue is not empty.
- Let pair be the first element of queue.
- Remove pair from queue, shifting all other elements downward (so that the second becomes the first, and so on).
- Return pair.[[value]].
- Assert: queue is not empty.
- Let pair be the first element of queue.
- Return pair.[[value]].
- Let totalSize be 0.
- Repeat for each Record{[[value]], [[size]]} pair that is an element of queue,
- Assert: pair.[[size]] is a valid, non-NaN number.
- Add pair.[[size]] to totalSize.
- Return totalSize.