remote-web-streams
Version:
Web streams that work across web workers and iframes.
144 lines (140 loc) • 4.04 kB
JavaScript
function fromReadablePort(port) {
return new ReadableStream(new MessagePortSource(port));
}
class MessagePortSource {
constructor(_port) {
this._port = _port;
this._port.onmessage = (event) => this._onMessage(event.data);
}
start(controller) {
this._controller = controller;
}
pull(controller) {
const message = {
type: 'pull'
};
this._port.postMessage(message);
}
cancel(reason) {
const message = {
type: 'error',
reason
};
this._port.postMessage(message);
this._port.close();
}
_onMessage(message) {
switch (message.type) {
case 'write':
// enqueue() will call pull() if needed when there's no backpressure
this._controller.enqueue(message.chunk);
break;
case 'abort':
this._controller.error(message.reason);
this._port.close();
break;
case 'close':
this._controller.close();
this._port.close();
break;
}
}
}
function fromWritablePort(port, options) {
return new WritableStream(new MessagePortSink(port, options));
}
class MessagePortSink {
constructor(_port, options = {}) {
this._port = _port;
this._transferChunk = options.transferChunk;
this._resetReady();
this._port.onmessage = (event) => this._onMessage(event.data);
}
start(controller) {
this._controller = controller;
// Apply initial backpressure
return this._readyPromise;
}
write(chunk, controller) {
const message = {
type: 'write',
chunk
};
// Send chunk, optionally transferring its contents
let transferList = this._transferChunk ? this._transferChunk(chunk) : [];
if (transferList.length) {
this._port.postMessage(message, transferList);
}
else {
this._port.postMessage(message);
}
// Assume backpressure after every write, until sender pulls
this._resetReady();
// Apply backpressure
return this._readyPromise;
}
close() {
const message = {
type: 'close'
};
this._port.postMessage(message);
this._port.close();
}
abort(reason) {
const message = {
type: 'abort',
reason
};
this._port.postMessage(message);
this._port.close();
}
_onMessage(message) {
switch (message.type) {
case 'pull':
this._resolveReady();
break;
case 'error':
this._onError(message.reason);
break;
}
}
_onError(reason) {
this._controller.error(reason);
this._rejectReady(reason);
this._port.close();
}
_resetReady() {
this._readyPromise = new Promise((resolve, reject) => {
this._readyResolve = resolve;
this._readyReject = reject;
});
this._readyPending = true;
}
_resolveReady() {
this._readyResolve();
this._readyPending = false;
}
_rejectReady(reason) {
if (!this._readyPending) {
this._resetReady();
}
this._readyPromise.catch(() => { });
this._readyReject(reason);
this._readyPending = false;
}
}
class RemoteReadableStream {
constructor() {
const channel = new MessageChannel();
this.writablePort = channel.port1;
this.readable = fromReadablePort(channel.port2);
}
}
class RemoteWritableStream {
constructor(options) {
const channel = new MessageChannel();
this.readablePort = channel.port1;
this.writable = fromWritablePort(channel.port2, options);
}
}
export { RemoteReadableStream, RemoteWritableStream, fromReadablePort, fromWritablePort };