blob: 266850799a4eecb22b341149208622ec4b9056dc [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Implementation of functions that are shared between ReadableStream and
// WritableStream.
(function(global, binding, v8) {
'use strict';
// Common private symbols. These correspond directly to internal slots in the
// standard. "[[X]]" in the standard is spelt _X here.
const _queue = v8.createPrivateSymbol('[[queue]]');
const _queueTotalSize = v8.createPrivateSymbol('[[queueTotalSize]]');
// Javascript functions. It is important to use these copies for security and
// robustness. See "V8 Extras Design Doc", section "Security Considerations".
// https://docs.google.com/document/d/1AT5-T0aHGp7Lt29vPWFr2-qG8r3l9CByyvKwEuA8Ec0/edit#heading=h.9yixony1a18r
const Boolean = global.Boolean;
const Number = global.Number;
const Number_isFinite = Number.isFinite;
const Number_isNaN = Number.isNaN;
const RangeError = global.RangeError;
const TypeError = global.TypeError;
const TypeError_prototype = TypeError.prototype;
const hasOwnProperty = v8.uncurryThis(global.Object.hasOwnProperty);
const getPrototypeOf = global.Object.getPrototypeOf.bind(global.Object);
const getOwnPropertyDescriptor =
global.Object.getOwnPropertyDescriptor.bind(global.Object);
const thenPromise = v8.uncurryThis(Promise.prototype.then);
const JSON_parse = global.JSON.parse.bind(global.JSON);
const JSON_stringify = global.JSON.stringify.bind(global.JSON);
function hasOwnPropertyNoThrow(x, property) {
// The cast of |x| to Boolean will eliminate undefined and null, which would
// cause hasOwnProperty to throw a TypeError, as well as some other values
// that can't be objects and so will fail the check anyway.
return Boolean(x) && hasOwnProperty(x, property);
}
//
// Assert is not normally enabled, to avoid the space and time overhead. To
// enable, uncomment this definition and then in the file you wish to enable
// asserts for, uncomment the assert statements and add this definition:
// const assert = pred => binding.SimpleAssert(pred);
//
// binding.SimpleAssert = pred => {
// if (pred) {
// return;
// }
// v8.log('\n\n\n *** ASSERTION FAILURE ***\n\n');
// v8.logStackTrace();
// v8.log('**************************************************\n\n');
// class StreamsAssertionError extends Error {}
// throw new StreamsAssertionError('Streams Assertion Failure');
// };
//
// Promise-manipulation functions
//
// Not exported.
function streamInternalError() {
throw new RangeError('Stream API Internal Error');
}
function rejectPromise(p, reason) {
if (!v8.isPromise(p)) {
streamInternalError();
}
v8.rejectPromise(p, reason);
}
function resolvePromise(p, value) {
if (!v8.isPromise(p)) {
streamInternalError();
}
v8.resolvePromise(p, value);
}
function markPromiseAsHandled(p) {
if (!v8.isPromise(p)) {
streamInternalError();
}
v8.markPromiseAsHandled(p);
}
function promiseState(p) {
if (!v8.isPromise(p)) {
streamInternalError();
}
return v8.promiseState(p);
}
//
// Queue-with-Sizes Operations
//
function DequeueValue(container) {
// assert(
// hasOwnProperty(container, _queue) &&
// hasOwnProperty(container, _queueTotalSize),
// '_container_ has [[queue]] and [[queueTotalSize]] internal slots.');
// assert(container[_queue].length !== 0,
// '_container_.[[queue]] is not empty.');
const pair = container[_queue].shift();
container[_queueTotalSize] -= pair.size;
if (container[_queueTotalSize] < 0) {
container[_queueTotalSize] = 0;
}
return pair.value;
}
function EnqueueValueWithSize(container, value, size) {
// assert(
// hasOwnProperty(container, _queue) &&
// hasOwnProperty(container, _queueTotalSize),
// '_container_ has [[queue]] and [[queueTotalSize]] internal 'slots.');
size = Number(size);
if (!IsFiniteNonNegativeNumber(size)) {
throw new RangeError(binding.streamErrors.invalidSize);
}
container[_queue].push({value, size});
container[_queueTotalSize] += size;
}
function PeekQueueValue(container) {
// assert(
// hasOwnProperty(container, _queue) &&
// hasOwnProperty(container, _queueTotalSize),
// '_container_ has [[queue]] and [[queueTotalSize]] internal slots.');
// assert(container[_queue].length !== 0,
// '_container_.[[queue]] is not empty.');
const pair = container[_queue].peek();
return pair.value;
}
function ResetQueue(container) {
// assert(
// hasOwnProperty(container, _queue) &&
// hasOwnProperty(container, _queueTotalSize),
// '_container_ has [[queue]] and [[queueTotalSize]] internal slots.');
container[_queue] = new binding.SimpleQueue();
container[_queueTotalSize] = 0;
}
// Not exported.
function IsFiniteNonNegativeNumber(v) {
return Number_isFinite(v) && v >= 0;
}
function ValidateAndNormalizeHighWaterMark(highWaterMark) {
highWaterMark = Number(highWaterMark);
if (Number_isNaN(highWaterMark)) {
throw new RangeError(binding.streamErrors.invalidHWM);
}
if (highWaterMark < 0) {
throw new RangeError(binding.streamErrors.invalidHWM);
}
return highWaterMark;
}
// Unlike the version in the standard, this implementation returns the
// original function as-is if it is set. This means users of the return value
// need to be careful to explicitly set |this| when calling it.
function MakeSizeAlgorithmFromSizeFunction(size) {
if (size === undefined) {
return () => 1;
}
if (typeof size !== 'function') {
throw new TypeError(binding.streamErrors.sizeNotAFunction);
}
return size;
}
//
// Invoking functions.
// These differ from the Invoke versions in the spec in that they take a fixed
// number of arguments rather than a list, and also take a name to be used for
// the function on error.
//
// Internal utility functions. Not exported.
const callFunction = v8.uncurryThis(global.Function.prototype.call);
const errTmplMustBeFunctionOrUndefined = name =>
`${name} must be a function or undefined`;
const Promise_resolve = Promise.resolve.bind(Promise);
const Promise_reject = Promise.reject.bind(Promise);
const Function_bind = v8.uncurryThis(global.Function.prototype.bind);
function resolveMethod(O, P, nameForError) {
const method = O[P];
if (typeof method !== 'function' && typeof method !== 'undefined') {
throw new TypeError(errTmplMustBeFunctionOrUndefined(nameForError));
}
return method;
}
function CreateAlgorithmFromUnderlyingMethod(
underlyingObject, methodName, algoArgCount, methodNameForError) {
// assert(underlyingObject !== undefined,
// 'underlyingObject is not undefined.');
// assert(IsPropertyKey(methodName),
// '! IsPropertyKey(methodName) is true.');
// assert(algoArgCount === 0 || algoArgCount === 1,
// 'algoArgCount is 0 or 1.');
// assert(
// typeof methodNameForError === 'string',
// 'methodNameForError is a string');
const method =
resolveMethod(underlyingObject, methodName, methodNameForError);
// The implementation uses bound functions rather than lambdas where
// possible to give the compiler the maximum opportunity to optimise.
if (method === undefined) {
return () => Promise_resolve();
}
if (algoArgCount === 0) {
return Function_bind(PromiseCall0, undefined, method, underlyingObject);
}
return Function_bind(PromiseCall1, undefined, method, underlyingObject);
}
function CreateAlgorithmFromUnderlyingMethodPassingController(
underlyingObject, methodName, algoArgCount, controller,
methodNameForError) {
// assert(underlyingObject !== undefined,
// 'underlyingObject is not undefined.');
// assert(IsPropertyKey(methodName),
// '! IsPropertyKey(methodName) is true.');
// assert(algoArgCount === 0 || algoArgCount === 1,
// 'algoArgCount is 0 or 1.');
// assert(typeof controller === 'object', 'controller is an object');
// assert(
// typeof methodNameForError === 'string',
// 'methodNameForError is a string');
const method =
resolveMethod(underlyingObject, methodName, methodNameForError);
if (method === undefined) {
return () => Promise_resolve();
}
if (algoArgCount === 0) {
return Function_bind(
PromiseCall1, undefined, method, underlyingObject, controller);
}
return arg => PromiseCall2(method, underlyingObject, arg, controller);
}
// Modified from InvokeOrNoop in spec. Takes 1 argument.
function CallOrNoop1(O, P, arg0, nameForError) {
const method = resolveMethod(O, P, nameForError);
if (method === undefined) {
return undefined;
}
return callFunction(method, O, arg0);
}
function PromiseCall0(F, V) {
// assert(typeof F === 'function', 'IsCallable(F) is true.');
// assert(V !== undefined, 'V is not undefined.');
try {
return Promise_resolve(callFunction(F, V));
} catch (e) {
return Promise_reject(e);
}
}
function PromiseCall1(F, V, arg0) {
// assert(typeof F === 'function', 'IsCallable(F) is true.');
// assert(V !== undefined, 'V is not undefined.');
try {
return Promise_resolve(callFunction(F, V, arg0));
} catch (e) {
return Promise_reject(e);
}
}
function PromiseCall2(F, V, arg0, arg1) {
// assert(typeof F === 'function', 'IsCallable(F) is true.');
// assert(V !== undefined, 'V is not undefined.');
try {
return Promise_resolve(callFunction(F, V, arg0, arg1));
} catch (e) {
return Promise_reject(e);
}
}
// Functions for transferable streams. See design doc
// https://docs.google.com/document/d/1_KuZzg5c3pncLJPFa8SuVm23AP4tft6mzPCL5at3I9M/edit
const kPull = 1;
const kCancel = 2;
const kChunk = 3;
const kClose = 4;
const kAbort = 5;
const kError = 6;
function isATypeError(object) {
// There doesn't appear to be a 100% reliable way to identify a TypeError
// from JS.
return getPrototypeOf(object) === TypeError_prototype;
}
function isADOMException(object) {
try {
callFunction(binding.DOMException_name_get, object);
return true;
} catch (e) {
return false;
}
}
// We'd like to able to transfer TypeError exceptions, but we can't, so we
// hack around it. packReason() is guaranteed not to throw and the object
// produced is guaranteed to be serializable by postMessage().
function packReason(reason) {
switch (typeof reason) {
case 'string':
case 'number':
return {encoder: 'json', string: JSON_stringify(reason)};
case 'object':
try {
if (isATypeError(reason)) {
// "message" on TypeError is a normal property, meaning that if it
// is set, it is set on the object itself. We can take advantage of
// this to avoid executing user JavaScript in the case when the
// TypeError was generated internally.
let message;
const descriptor = getOwnPropertyDescriptor(reason, 'message');
if (descriptor) {
message = descriptor.value;
if (typeof message !== 'string') {
message = undefined;
}
}
return {encoder: 'typeerror', string: message};
}
if (isADOMException(reason)) {
const message =
callFunction(binding.DOMException_message_get, reason);
const name = callFunction(binding.DOMException_name_get, reason);
return {
encoder: 'domexception',
string: JSON_stringify({message, name})
};
}
// JSON_stringify() is lossy, but it will serialise things that
// postMessage() won't.
return {encoder: 'json', string: JSON_stringify(reason)};
} catch (e) {
return {encoder: 'typeerror', string: 'Cannot transfer message'};
}
default:
return {encoder: 'undefined', string: undefined};
}
}
function unpackReason(packedReason) {
const {encoder, string} = packedReason;
switch (encoder) {
case 'json':
return JSON_parse(string);
case 'typeerror':
return new TypeError(string);
case 'domexception':
const {message, name} = JSON_parse(string);
return new binding.DOMException(message, name);
case 'undefined':
return undefined;
}
}
function CreateCrossRealmTransformWritable(port) {
let backpressurePromise = v8.createPromise();
callFunction(binding.EventTarget_addEventListener, port, 'message', evt => {
const {type, value} = callFunction(binding.MessageEvent_data_get, evt);
// assert(type === kPull || type === kCancel || type === kError);
switch (type) {
case kPull:
// assert(backPressurePromise !== undefined);
resolvePromise(backpressurePromise);
backpressurePromise = undefined;
break;
case kCancel:
case kError:
binding.WritableStreamDefaultControllerErrorIfNeeded(
controller, unpackReason(value));
if (backpressurePromise !== undefined) {
resolvePromise(backpressurePromise);
backpressurePromise = undefined;
}
break;
}
});
callFunction(
binding.EventTarget_addEventListener, port, 'messageerror', () => {
const error = new binding.DOMException('chunk could not be cloned',
'DataCloneError');
callFunction(binding.MessagePort_postMessage, port,
{type: kError, value: packReason(error)});
callFunction(binding.MessagePort_close, port);
binding.WritableStreamDefaultControllerErrorIfNeeded(controller,
error);
});
callFunction(binding.MessagePort_start, port);
function doWrite(chunk) {
backpressurePromise = v8.createPromise();
try {
callFunction(
binding.MessagePort_postMessage, port,
{type: kChunk, value: chunk});
} catch (e) {
callFunction(
binding.MessagePort_postMessage, port,
{type: kError, value: packReason(e)});
callFunction(binding.MessagePort_close, port);
throw e;
}
}
const stream = binding.CreateWritableStream(
() => undefined,
chunk => {
if (!backpressurePromise) {
return PromiseCall1(doWrite, null, chunk);
}
return thenPromise(backpressurePromise, () => doWrite(chunk));
},
() => {
callFunction(
binding.MessagePort_postMessage, port,
{type: kClose, value: undefined});
callFunction(binding.MessagePort_close, port);
return Promise_resolve();
},
reason => {
callFunction(
binding.MessagePort_postMessage, port,
{type: kAbort, value: packReason(reason)});
callFunction(binding.MessagePort_close, port);
return Promise_resolve();
});
const controller = binding.getWritableStreamController(stream);
return stream;
}
function CreateCrossRealmTransformReadable(port) {
let backpressurePromise = v8.createPromise();
let finished = false;
callFunction(binding.EventTarget_addEventListener, port, 'message', evt => {
const {type, value} = callFunction(binding.MessageEvent_data_get, evt);
// assert(type === kChunk || type === kClose || type === kAbort ||
// type=kError);
switch (type) {
case kChunk:
if (finished) {
return;
}
binding.ReadableStreamDefaultControllerEnqueue(controller, value);
resolvePromise(backpressurePromise);
backpressurePromise = v8.createPromise();
break;
case kClose:
if (finished) {
return;
}
finished = true;
binding.ReadableStreamDefaultControllerClose(controller);
callFunction(binding.MessagePort_close, port);
break;
case kAbort:
case kError:
if (finished) {
return;
}
finished = true;
binding.ReadableStreamDefaultControllerError(
controller, unpackReason(value));
callFunction(binding.MessagePort_close, port);
break;
}
});
callFunction(
binding.EventTarget_addEventListener, port, 'messageerror', () => {
const error = new binding.DOMException('chunk could not be cloned',
'DataCloneError');
callFunction(binding.MessagePort_postMessage, port,
{type: kError, value: packReason(error)});
callFunction(binding.MessagePort_close, port);
binding.ReadableStreamDefaultControllerError(controller, error);
});
callFunction(binding.MessagePort_start, port);
const stream = binding.CreateReadableStream(
() => undefined,
() => {
callFunction(
binding.MessagePort_postMessage, port,
{type: kPull, value: undefined});
return backpressurePromise;
},
reason => {
finished = true;
callFunction(
binding.MessagePort_postMessage, port,
{type: kCancel, value: packReason(reason)});
callFunction(binding.MessagePort_close, port);
return Promise_resolve();
},
/* highWaterMark = */ 0);
const controller = binding.getReadableStreamController(stream);
return stream;
}
binding.streamOperations = {
_queue,
_queueTotalSize,
hasOwnPropertyNoThrow,
rejectPromise,
resolvePromise,
markPromiseAsHandled,
promiseState,
CreateAlgorithmFromUnderlyingMethod,
CreateAlgorithmFromUnderlyingMethodPassingController,
CreateCrossRealmTransformWritable,
CreateCrossRealmTransformReadable,
DequeueValue,
EnqueueValueWithSize,
PeekQueueValue,
ResetQueue,
ValidateAndNormalizeHighWaterMark,
MakeSizeAlgorithmFromSizeFunction,
CallOrNoop1,
PromiseCall2
};
});