diff --git a/src/lib/byte-length-queuing-strategy.ts b/src/lib/byte-length-queuing-strategy.ts index c650548..b5a9075 100644 --- a/src/lib/byte-length-queuing-strategy.ts +++ b/src/lib/byte-length-queuing-strategy.ts @@ -1,3 +1,5 @@ +declare function markUncloneable(value: object, transferable?: boolean): void; + import type { QueuingStrategy, QueuingStrategyInit } from './queuing-strategy'; import { setFunctionName, typeIsObject } from './helpers/miscellaneous'; import { assertRequiredArgument } from './validators/basic'; @@ -19,6 +21,7 @@ export default class ByteLengthQueuingStrategy implements QueuingStrategy { readonly _countQueuingStrategyHighWaterMark!: number; constructor(options: QueuingStrategyInit) { + markUncloneable(this); assertRequiredArgument(options, 1, 'CountQueuingStrategy'); options = convertQueuingStrategyInit(options, 'First parameter'); this._countQueuingStrategyHighWaterMark = options.highWaterMark; diff --git a/src/lib/helpers/webidl.ts b/src/lib/helpers/webidl.ts index bce6b50..0984ee5 100644 --- a/src/lib/helpers/webidl.ts +++ b/src/lib/helpers/webidl.ts @@ -1,10 +1,17 @@ -import { rethrowAssertionErrorRejection } from './miscellaneous'; import assert from '../../stub/assert'; -const originalPromise = Promise; -const originalPromiseResolve = Promise.resolve.bind(originalPromise); -const originalPromiseThen = Promise.prototype.then; -const originalPromiseReject = Promise.reject.bind(originalPromise); +declare function markPromiseHandled(promise: Promise): void; + +declare function safeNewPromise(executor: (resolve: (value: T | PromiseLike) => void, reject: (reason?: any) => void) => void): Promise; +declare const safePromiseResolve: typeof Promise.resolve; +declare const safePromiseReject: typeof Promise.reject; +declare const then: (promise: Promise, fulfilled?: (value: T) => R1 | PromiseLike, rejected?: (reason: any) => R2 | PromiseLike) => Promise; +declare const apply: typeof Reflect.apply; + +const originalPromise = safeNewPromise; +const originalPromiseResolve = safePromiseResolve; +const originalPromiseThen = then; +const originalPromiseReject = safePromiseReject; export const promiseResolve = originalPromiseResolve; @@ -13,7 +20,7 @@ export function newPromise(executor: ( resolve: (value: T | PromiseLike) => void, reject: (reason?: any) => void ) => void): Promise { - return new originalPromise(executor); + return originalPromise(executor); } // https://webidl.spec.whatwg.org/#a-promise-resolved-with @@ -21,6 +28,15 @@ export function promiseResolvedWith(value: T | PromiseLike): Promise { return newPromise(resolve => resolve(value)); } +const resolvedUndefinedPromise = originalPromise(resolve => resolve(undefined)); +// Shadowing constructor would invalidate V8's global promise-species protector. +Object.freeze(resolvedUndefinedPromise); + +// Only for internal reactions, not public results or promise assimilation. +export function promiseResolvedWithUndefined(): Promise { + return resolvedUndefinedPromise; +} + // https://webidl.spec.whatwg.org/#a-promise-rejected-with export function promiseRejectedWith(reason: any): Promise { return originalPromiseReject(reason); @@ -33,7 +49,7 @@ export function PerformPromiseThen( ): Promise { // There doesn't appear to be any way to correctly emulate the behaviour from JavaScript, so this is just an // approximation. - return originalPromiseThen.call(promise, onFulfilled, onRejected) as Promise; + return originalPromiseThen(promise, onFulfilled, onRejected); } // Bluebird logs a warning when a promise is created within a fulfillment handler, but then isn't returned @@ -44,11 +60,7 @@ export function uponPromise( onFulfilled?: (value: T) => null | PromiseLike, onRejected?: (reason: any) => null | PromiseLike ): void { - PerformPromiseThen( - PerformPromiseThen(promise, onFulfilled, onRejected), - undefined, - rethrowAssertionErrorRejection - ); + setPromiseIsHandledToTrue(PerformPromiseThen(promise, onFulfilled, onRejected)); } export function uponFulfillment(promise: Promise, onFulfilled: (value: T) => null | PromiseLike): void { @@ -68,7 +80,7 @@ export function transformPromiseWith( } export function setPromiseIsHandledToTrue(promise: Promise): void { - PerformPromiseThen(promise, undefined, rethrowAssertionErrorRejection); + markPromiseHandled(promise); } let _queueMicrotask: (callback: () => void) => void = (callback) => { @@ -87,7 +99,7 @@ export function reflectCall(F: (this: T, ...fnArgs: A) => if (typeof F !== 'function') { throw new TypeError('Argument is not a function'); } - return Function.prototype.apply.call(F, V, args); + return apply(F, V, args); } export function promiseCall( diff --git a/src/lib/readable-stream.ts b/src/lib/readable-stream.ts index 95a0fb1..f420dfa 100644 --- a/src/lib/readable-stream.ts +++ b/src/lib/readable-stream.ts @@ -1,3 +1,5 @@ +declare function markUncloneable(value: object, transferable?: boolean): void; + import assert from '../stub/assert'; import { promiseRejectedWith, @@ -408,6 +410,7 @@ export function CreateReadableStream( InitializeReadableStream(stream); const controller: ReadableStreamDefaultController = Object.create(ReadableStreamDefaultController.prototype); + markUncloneable(controller); SetUpReadableStreamDefaultController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, sizeAlgorithm); return stream; @@ -423,12 +426,14 @@ export function CreateReadableByteStream( InitializeReadableStream(stream); const controller: ReadableByteStreamController = Object.create(ReadableByteStreamController.prototype); + markUncloneable(controller); SetUpReadableByteStreamController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, 0, undefined); return stream; } function InitializeReadableStream(stream: ReadableStream) { + markUncloneable(stream, true); stream._state = 'readable'; stream._reader = undefined; stream._storedError = undefined; diff --git a/src/lib/readable-stream/async-iterator.ts b/src/lib/readable-stream/async-iterator.ts index 503472d..c697e80 100644 --- a/src/lib/readable-stream/async-iterator.ts +++ b/src/lib/readable-stream/async-iterator.ts @@ -1,3 +1,5 @@ +declare function markUncloneable(value: object, transferable?: boolean): void; + import { ReadableStream } from '../readable-stream'; import { AcquireReadableStreamDefaultReader, @@ -58,7 +60,7 @@ export class ReadableStreamAsyncIteratorImpl { private _nextSteps(): Promise> { if (this._isFinished) { - return Promise.resolve({ value: undefined, done: true }); + return promiseResolvedWith({ value: undefined, done: true }); } const reader = this._reader; @@ -71,7 +73,7 @@ export class ReadableStreamAsyncIteratorImpl { private _returnSteps(value: any): Promise> { if (this._isFinished) { - return Promise.resolve({ value, done: true }); + return promiseResolvedWith({ value, done: true }); } this._isFinished = true; @@ -173,6 +175,7 @@ export function AcquireReadableStreamAsyncIterator( const reader = AcquireReadableStreamDefaultReader(stream); const impl = new ReadableStreamAsyncIteratorImpl(reader, preventCancel); const iterator: ReadableStreamAsyncIteratorInstance = Object.create(ReadableStreamAsyncIteratorPrototype); + markUncloneable(iterator); iterator._asyncIteratorImpl = impl; return iterator; } diff --git a/src/lib/readable-stream/byob-reader.ts b/src/lib/readable-stream/byob-reader.ts index dfc103f..5458438 100644 --- a/src/lib/readable-stream/byob-reader.ts +++ b/src/lib/readable-stream/byob-reader.ts @@ -1,3 +1,5 @@ +declare function markUncloneable(value: object, transferable?: boolean): void; + import assert from '../../stub/assert'; import { SimpleQueue } from '../simple-queue'; import { @@ -119,6 +121,7 @@ export class ReadableStreamBYOBReader { _readIntoRequests: SimpleQueue>; constructor(stream: ReadableStream) { + markUncloneable(this); assertRequiredArgument(stream, 1, 'ReadableStreamBYOBReader'); assertReadableStream(stream, 'First parameter'); diff --git a/src/lib/readable-stream/byte-stream-controller.ts b/src/lib/readable-stream/byte-stream-controller.ts index e435b76..c44d980 100644 --- a/src/lib/readable-stream/byte-stream-controller.ts +++ b/src/lib/readable-stream/byte-stream-controller.ts @@ -1,3 +1,5 @@ +declare function markUncloneable(value: object, transferable?: boolean): void; + import assert from '../../stub/assert'; import { SimpleQueue } from '../simple-queue'; import { ResetQueue } from '../abstract-ops/queue-with-sizes'; @@ -34,7 +36,7 @@ import { TransferArrayBuffer } from '../abstract-ops/ecmascript'; import { CancelSteps, CanPullSyncSteps, PullSteps, ReleaseSteps } from '../abstract-ops/internal-methods'; -import { promiseResolvedWith, uponPromise } from '../helpers/webidl'; +import { promiseResolvedWith, promiseResolvedWithUndefined, uponPromise } from '../helpers/webidl'; import { assertRequiredArgument, convertUnsignedLongLongWithEnforceRange } from '../validators/basic'; import { type ArrayBufferViewConstructor, @@ -1016,6 +1018,7 @@ export function ReadableByteStreamControllerGetBYOBRequest(controller: ReadableB ); const byobRequest: ReadableStreamBYOBRequest = Object.create(ReadableStreamBYOBRequest.prototype); + markUncloneable(byobRequest); SetUpReadableStreamBYOBRequest(byobRequest, controller, view as Uint8Array); controller._byobRequest = byobRequest; } @@ -1161,6 +1164,7 @@ export function SetUpReadableByteStreamControllerFromUnderlyingSource( highWaterMark: number ) { const controller: ReadableByteStreamController = Object.create(ReadableByteStreamController.prototype); + markUncloneable(controller); let startAlgorithm: () => void | PromiseLike; let pullAlgorithm: () => Promise; @@ -1174,12 +1178,12 @@ export function SetUpReadableByteStreamControllerFromUnderlyingSource( if (underlyingByteSource.pull !== undefined) { pullAlgorithm = () => underlyingByteSource.pull!(controller); } else { - pullAlgorithm = () => promiseResolvedWith(undefined); + pullAlgorithm = promiseResolvedWithUndefined; } if (underlyingByteSource.cancel !== undefined) { cancelAlgorithm = reason => underlyingByteSource.cancel!(reason); } else { - cancelAlgorithm = () => promiseResolvedWith(undefined); + cancelAlgorithm = promiseResolvedWithUndefined; } const autoAllocateChunkSize = underlyingByteSource.autoAllocateChunkSize; diff --git a/src/lib/readable-stream/default-controller.ts b/src/lib/readable-stream/default-controller.ts index 11eb3be..0f9086e 100644 --- a/src/lib/readable-stream/default-controller.ts +++ b/src/lib/readable-stream/default-controller.ts @@ -1,3 +1,5 @@ +declare function markUncloneable(value: object, transferable?: boolean): void; + import type { QueuingStrategySizeCallback } from '../queuing-strategy'; import assert from '../../stub/assert'; import { DequeueValue, EnqueueValueWithSize, type QueuePair, ResetQueue } from '../abstract-ops/queue-with-sizes'; @@ -12,7 +14,7 @@ import { IsReadableStreamLocked, ReadableStream, ReadableStreamClose, ReadableSt import type { ValidatedUnderlyingSource } from './underlying-source'; import { setFunctionName, typeIsObject } from '../helpers/miscellaneous'; import { CancelSteps, CanPullSyncSteps, PullSteps, ReleaseSteps } from '../abstract-ops/internal-methods'; -import { promiseResolvedWith, uponPromise } from '../helpers/webidl'; +import { promiseResolvedWith, promiseResolvedWithUndefined, uponPromise } from '../helpers/webidl'; /** * Allows control of a {@link ReadableStream | readable stream}'s state and internal queue. @@ -387,6 +389,7 @@ export function SetUpReadableStreamDefaultControllerFromUnderlyingSource( sizeAlgorithm: QueuingStrategySizeCallback ) { const controller: ReadableStreamDefaultController = Object.create(ReadableStreamDefaultController.prototype); + markUncloneable(controller); let startAlgorithm: () => void | PromiseLike; let pullAlgorithm: () => Promise; @@ -400,12 +403,12 @@ export function SetUpReadableStreamDefaultControllerFromUnderlyingSource( if (underlyingSource.pull !== undefined) { pullAlgorithm = () => underlyingSource.pull!(controller); } else { - pullAlgorithm = () => promiseResolvedWith(undefined); + pullAlgorithm = promiseResolvedWithUndefined; } if (underlyingSource.cancel !== undefined) { cancelAlgorithm = reason => underlyingSource.cancel!(reason); } else { - cancelAlgorithm = () => promiseResolvedWith(undefined); + cancelAlgorithm = promiseResolvedWithUndefined; } SetUpReadableStreamDefaultController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, sizeAlgorithm); diff --git a/src/lib/readable-stream/default-reader.ts b/src/lib/readable-stream/default-reader.ts index b0a432c..8d8007c 100644 --- a/src/lib/readable-stream/default-reader.ts +++ b/src/lib/readable-stream/default-reader.ts @@ -1,3 +1,5 @@ +declare function markUncloneable(value: object, transferable?: boolean): void; + import assert from '../../stub/assert'; import { SimpleQueue } from '../simple-queue'; import { @@ -103,6 +105,7 @@ export class ReadableStreamDefaultReader { _readRequests: SimpleQueue>; constructor(stream: ReadableStream) { + markUncloneable(this); assertRequiredArgument(stream, 1, 'ReadableStreamDefaultReader'); assertReadableStream(stream, 'First parameter'); diff --git a/src/lib/readable-stream/from.ts b/src/lib/readable-stream/from.ts index d745178..deceee7 100644 --- a/src/lib/readable-stream/from.ts +++ b/src/lib/readable-stream/from.ts @@ -6,7 +6,7 @@ import { } from './readable-stream-like'; import { ReadableStreamDefaultControllerClose, ReadableStreamDefaultControllerEnqueue } from './default-controller'; import { GetIterator, GetMethod, IteratorNext } from '../abstract-ops/ecmascript'; -import { promiseCall, promiseRejectedWith, promiseResolvedWith, transformPromiseWith } from '../helpers/webidl'; +import { promiseCall, promiseRejectedWith, promiseResolvedWith, promiseResolvedWithUndefined, transformPromiseWith } from '../helpers/webidl'; import { typeIsObject } from '../helpers/miscellaneous'; import { noop } from '../../utils'; @@ -54,7 +54,7 @@ export function ReadableStreamFromIterable(asyncIterable: Iterable | Async return promiseRejectedWith(e); } if (returnMethod === undefined) { - return promiseResolvedWith(undefined); + return promiseResolvedWithUndefined(); } const returnPromise = promiseCall(returnMethod, iterator, [reason]); return transformPromiseWith(returnPromise, (iterResult) => { diff --git a/src/lib/readable-stream/pipe.ts b/src/lib/readable-stream/pipe.ts index 8044696..e0a4810 100644 --- a/src/lib/readable-stream/pipe.ts +++ b/src/lib/readable-stream/pipe.ts @@ -23,6 +23,7 @@ import { newPromise, PerformPromiseThen, promiseResolvedWith, + promiseResolvedWithUndefined, setPromiseIsHandledToTrue, uponFulfillment, uponPromise, @@ -32,6 +33,8 @@ import { noop } from '../../utils'; import { type AbortSignal, isAbortSignal } from '../abort-signal'; import { DOMException } from '../../stub/dom-exception'; +declare const safePromiseAll: typeof Promise.all; + export function ReadableStreamPipeTo( source: ReadableStream, dest: WritableStream, @@ -79,7 +82,13 @@ export function ReadableStreamPipeTo( return promiseResolvedWith(undefined); }); } - shutdownWithAction(() => Promise.all(actions.map(action => action())), true, error); + shutdownWithAction(() => { + const promises = []; + for (let index = 0; index < actions.length; index++) { + promises[index] = actions[index](); + } + return safePromiseAll(promises); + }, true, error); }; if (signal.aborted) { @@ -234,7 +243,7 @@ class PipeState { _shuttingDown = false; // This is used to keep track of the spec's requirement that we wait for ongoing writes during shutdown. - _currentWrite = promiseResolvedWith(undefined); + _currentWrite = promiseResolvedWithUndefined(); constructor(readonly _writer: WritableStreamDefaultWriter) {} diff --git a/src/lib/readable-stream/tee.ts b/src/lib/readable-stream/tee.ts index 5b05eb1..3a82830 100644 --- a/src/lib/readable-stream/tee.ts +++ b/src/lib/readable-stream/tee.ts @@ -22,7 +22,7 @@ import { type ReadIntoRequest } from './byob-reader'; import assert from '../../stub/assert'; -import { newPromise, promiseResolvedWith, queueMicrotask, uponRejection } from '../helpers/webidl'; +import { newPromise, promiseResolvedWithUndefined, queueMicrotask, uponRejection } from '../helpers/webidl'; import { ReadableStreamDefaultControllerClose, ReadableStreamDefaultControllerEnqueue, @@ -79,7 +79,7 @@ export function ReadableStreamDefaultTee( function pullAlgorithm(): Promise { if (reading) { readAgain = true; - return promiseResolvedWith(undefined); + return promiseResolvedWithUndefined(); } reading = true; @@ -132,7 +132,7 @@ export function ReadableStreamDefaultTee( }; ReadableStreamDefaultReaderRead(reader, readRequest); - return promiseResolvedWith(undefined); + return promiseResolvedWithUndefined(); } function cancel1Algorithm(reason: any): Promise { @@ -369,7 +369,7 @@ export function ReadableByteStreamTee(stream: ReadableByteStream): [ReadableByte function pull1Algorithm(): Promise { if (reading) { readAgainForBranch1 = true; - return promiseResolvedWith(undefined); + return promiseResolvedWithUndefined(); } reading = true; @@ -381,13 +381,13 @@ export function ReadableByteStreamTee(stream: ReadableByteStream): [ReadableByte pullWithBYOBReader(byobRequest._view!, false); } - return promiseResolvedWith(undefined); + return promiseResolvedWithUndefined(); } function pull2Algorithm(): Promise { if (reading) { readAgainForBranch2 = true; - return promiseResolvedWith(undefined); + return promiseResolvedWithUndefined(); } reading = true; @@ -399,7 +399,7 @@ export function ReadableByteStreamTee(stream: ReadableByteStream): [ReadableByte pullWithBYOBReader(byobRequest._view!, true); } - return promiseResolvedWith(undefined); + return promiseResolvedWithUndefined(); } function cancel1Algorithm(reason: any): Promise { diff --git a/src/lib/transform-stream.ts b/src/lib/transform-stream.ts index f800382..1775e8d 100644 --- a/src/lib/transform-stream.ts +++ b/src/lib/transform-stream.ts @@ -1,8 +1,11 @@ +declare function markUncloneable(value: object, transferable?: boolean): void; + import assert from '../stub/assert'; import { newPromise, promiseRejectedWith, promiseResolvedWith, + promiseResolvedWithUndefined, setPromiseIsHandledToTrue, transformPromiseWith, uponPromise @@ -171,6 +174,7 @@ export function CreateTransformStream( ); const controller: TransformStreamDefaultController = Object.create(TransformStreamDefaultController.prototype); + markUncloneable(controller); SetUpTransformStreamDefaultController(stream, controller, transformAlgorithm, flushAlgorithm, cancelAlgorithm); @@ -187,6 +191,7 @@ function InitializeTransformStream( readableHighWaterMark: number, readableSizeAlgorithm: QueuingStrategySizeCallback ) { + markUncloneable(stream, true); function startAlgorithm(): Promise { return startPromise; } @@ -411,6 +416,7 @@ function SetUpTransformStreamDefaultControllerFromTransformer( transformer: ValidatedTransformer ) { const controller: TransformStreamDefaultController = Object.create(TransformStreamDefaultController.prototype); + markUncloneable(controller); let transformAlgorithm: (chunk: I) => Promise; let flushAlgorithm: () => Promise; @@ -422,7 +428,7 @@ function SetUpTransformStreamDefaultControllerFromTransformer( transformAlgorithm = (chunk) => { try { TransformStreamDefaultControllerEnqueue(controller, chunk as unknown as O); - return promiseResolvedWith(undefined); + return promiseResolvedWithUndefined(); } catch (transformResultE) { return promiseRejectedWith(transformResultE); } @@ -432,13 +438,13 @@ function SetUpTransformStreamDefaultControllerFromTransformer( if (transformer.flush !== undefined) { flushAlgorithm = () => transformer.flush!(controller); } else { - flushAlgorithm = () => promiseResolvedWith(undefined); + flushAlgorithm = promiseResolvedWithUndefined; } if (transformer.cancel !== undefined) { cancelAlgorithm = reason => transformer.cancel!(reason); } else { - cancelAlgorithm = () => promiseResolvedWith(undefined); + cancelAlgorithm = promiseResolvedWithUndefined; } SetUpTransformStreamDefaultController(stream, controller, transformAlgorithm, flushAlgorithm, cancelAlgorithm); diff --git a/src/lib/writable-stream.ts b/src/lib/writable-stream.ts index 08966c9..bc98b62 100644 --- a/src/lib/writable-stream.ts +++ b/src/lib/writable-stream.ts @@ -1,8 +1,11 @@ +declare function markUncloneable(value: object, transferable?: boolean): void; + import assert from '../stub/assert'; import { newPromise, promiseRejectedWith, promiseResolvedWith, + promiseResolvedWithUndefined, setPromiseIsHandledToTrue, uponPromise } from './helpers/webidl'; @@ -237,6 +240,7 @@ function CreateWritableStream( InitializeWritableStream(stream); const controller: WritableStreamDefaultController = Object.create(WritableStreamDefaultController.prototype); + markUncloneable(controller); SetUpWritableStreamDefaultController( stream, controller, startAlgorithm, writeAlgorithm, closeAlgorithm, @@ -246,6 +250,7 @@ function CreateWritableStream( } function InitializeWritableStream(stream: WritableStream) { + markUncloneable(stream, true); stream._state = 'writable'; // The error that will be reported by new method calls once the state becomes errored. Only set when [[state]] is @@ -616,6 +621,7 @@ export class WritableStreamDefaultWriter { _readyPromiseState!: 'pending' | 'fulfilled' | 'rejected'; constructor(stream: WritableStream) { + markUncloneable(this); assertRequiredArgument(stream, 1, 'WritableStreamDefaultWriter'); assertWritableStream(stream, 'First parameter'); @@ -843,7 +849,7 @@ function WritableStreamDefaultWriterCloseWithErrorPropagation(writer: WritableSt const state = stream._state; if (WritableStreamCloseQueuedOrInFlight(stream) || state === 'closed') { - return promiseResolvedWith(undefined); + return promiseResolvedWithUndefined(); } if (state === 'errored') { @@ -1124,6 +1130,7 @@ function SetUpWritableStreamDefaultControllerFromUnderlyingSink( sizeAlgorithm: QueuingStrategySizeCallback ) { const controller = Object.create(WritableStreamDefaultController.prototype); + markUncloneable(controller); let startAlgorithm: () => void | PromiseLike; let writeAlgorithm: (chunk: W) => Promise; @@ -1138,17 +1145,17 @@ function SetUpWritableStreamDefaultControllerFromUnderlyingSink( if (underlyingSink.write !== undefined) { writeAlgorithm = chunk => underlyingSink.write!(chunk, controller); } else { - writeAlgorithm = () => promiseResolvedWith(undefined); + writeAlgorithm = promiseResolvedWithUndefined; } if (underlyingSink.close !== undefined) { closeAlgorithm = () => underlyingSink.close!(); } else { - closeAlgorithm = () => promiseResolvedWith(undefined); + closeAlgorithm = promiseResolvedWithUndefined; } if (underlyingSink.abort !== undefined) { abortAlgorithm = reason => underlyingSink.abort!(reason); } else { - abortAlgorithm = () => promiseResolvedWith(undefined); + abortAlgorithm = promiseResolvedWithUndefined; } SetUpWritableStreamDefaultController(stream, controller, startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, highWaterMark, sizeAlgorithm);