Commit ef0fc520 authored by Adam Rice's avatar Adam Rice Committed by Commit Bot

Mark all streams-created promises with isSettled flag

Wrap v8.createPromise() in a createPromise() function which always sets
the isSettled flag to false. Make resolvePromise() and rejectPromise()
ignore promises that don't have the isSettled flag present. Replace
Promise_resolve() and Promise_reject() with createResolvedPromise() and
createRejectedPromise() functions that also set the isSettled flag.

Strictly speaking, the createR*Promise() functions only need to be used
when a promise is stored and might be resolved or rejected later. But to
avoid confusion about where they are needed, always use them in place of
Promise_resolve() and Promise_reject().

BUG=931640

Change-Id: I5472cb4a78f7b970ab256084b819ae28ffda6a33
Reviewed-on: https://chromium-review.googlesource.com/c/1481178Reviewed-by: default avatarYutaka Hirano <yhirano@chromium.org>
Commit-Queue: Adam Rice <ricea@chromium.org>
Cr-Commit-Position: refs/heads/master@{#635449}
parent afc1d6a7
......@@ -73,12 +73,27 @@
throw new RangeError('Stream API Internal Error');
}
// For safety, this must always be used in place of calling v8.createPromise()
// directly.
function createPromise() {
const p = v8.createPromise();
p[_isSettled] = false;
return p;
}
// Calling v8.rejectPromise() directly is very dangerous. Always use this
// wrapper.
function rejectPromise(p, reason) {
if (!v8.isPromise(p)) {
streamInternalError();
}
if (p[_isSettled]) {
// assert(typeof p[_isSettled] === 'boolean',
// 'Type(p.[[isSettled]]) is `"boolean"`');
// Note that this makes the function a no-op for promises that were not
// created via createPromise(). This is critical for security.
if (p[_isSettled] !== false) {
return;
}
p[_isSettled] = true;
......@@ -86,12 +101,27 @@
v8.rejectPromise(p, reason);
}
// This must always be used instead of Promise.reject().
function createRejectedPromise(reason) {
const p = createPromise();
rejectPromise(p, reason);
return p;
}
// Calling v8.resolvePromise() directly is very dangerous. Always use this
// wrapper. If |value| is an object this will look up Object.prototype.then
// and so may be re-entrant.
function resolvePromise(p, value) {
if (!v8.isPromise(p)) {
streamInternalError();
}
if (p[_isSettled]) {
// assert(typeof p[_isSettled] === 'boolean',
// 'Type(p.[[isSettled]]) is `"boolean"`');
// Note that this makes the function a no-op for promises that were not
// created via createPromise(). This is critical for security.
if (p[_isSettled] !== false) {
return;
}
p[_isSettled] = true;
......@@ -99,6 +129,20 @@
v8.resolvePromise(p, value);
}
// This must always be used instead of Promise.resolve(). If |value| is an
// object this will look up Object.prototype.then and so may be re-entrant.
function createResolvedPromise(value) {
if (v8.isPromise(value)) {
// This case applies when an underlying method returns a promise. Promises
// that are passed through in this way are not used with resolvePromise()
// or rejectPromise().
return value;
}
const p = createPromise();
resolvePromise(p, value);
return p;
}
function markPromiseAsHandled(p) {
if (!v8.isPromise(p)) {
streamInternalError();
......@@ -207,8 +251,6 @@
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) {
......@@ -237,7 +279,7 @@
// 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();
return () => createResolvedPromise();
}
if (algoArgCount === 0) {
......@@ -263,7 +305,7 @@
const method =
resolveMethod(underlyingObject, methodName, methodNameForError);
if (method === undefined) {
return () => Promise_resolve();
return () => createResolvedPromise();
}
if (algoArgCount === 0) {
......@@ -288,9 +330,9 @@
// assert(typeof F === 'function', 'IsCallable(F) is true.');
// assert(V !== undefined, 'V is not undefined.');
try {
return Promise_resolve(callFunction(F, V));
return createResolvedPromise(callFunction(F, V));
} catch (e) {
return Promise_reject(e);
return createRejectedPromise(e);
}
}
......@@ -298,9 +340,9 @@
// assert(typeof F === 'function', 'IsCallable(F) is true.');
// assert(V !== undefined, 'V is not undefined.');
try {
return Promise_resolve(callFunction(F, V, arg0));
return createResolvedPromise(callFunction(F, V, arg0));
} catch (e) {
return Promise_reject(e);
return createRejectedPromise(e);
}
}
......@@ -308,9 +350,9 @@
// assert(typeof F === 'function', 'IsCallable(F) is true.');
// assert(V !== undefined, 'V is not undefined.');
try {
return Promise_resolve(callFunction(F, V, arg0, arg1));
return createResolvedPromise(callFunction(F, V, arg0, arg1));
} catch (e) {
return Promise_reject(e);
return createRejectedPromise(e);
}
}
......@@ -408,7 +450,7 @@
}
function CreateCrossRealmTransformWritable(port) {
let backpressurePromise = v8.createPromise();
let backpressurePromise = createPromise();
callFunction(binding.EventTarget_addEventListener, port, 'message', evt => {
const {type, value} = callFunction(binding.MessageEvent_data_get, evt);
......@@ -446,7 +488,7 @@
callFunction(binding.MessagePort_start, port);
function doWrite(chunk) {
backpressurePromise = v8.createPromise();
backpressurePromise = createPromise();
try {
callFunction(
binding.MessagePort_postMessage, port,
......@@ -473,14 +515,14 @@
binding.MessagePort_postMessage, port,
{type: kClose, value: undefined});
callFunction(binding.MessagePort_close, port);
return Promise_resolve();
return createResolvedPromise();
},
reason => {
callFunction(
binding.MessagePort_postMessage, port,
{type: kAbort, value: packReason(reason)});
callFunction(binding.MessagePort_close, port);
return Promise_resolve();
return createResolvedPromise();
});
const controller = binding.getWritableStreamController(stream);
......@@ -488,7 +530,7 @@
}
function CreateCrossRealmTransformReadable(port) {
let backpressurePromise = v8.createPromise();
let backpressurePromise = createPromise();
let finished = false;
callFunction(binding.EventTarget_addEventListener, port, 'message', evt => {
......@@ -502,7 +544,7 @@
case kChunk:
binding.ReadableStreamDefaultControllerEnqueue(controller, value);
resolvePromise(backpressurePromise);
backpressurePromise = v8.createPromise();
backpressurePromise = createPromise();
break;
case kClose:
......@@ -547,7 +589,7 @@
binding.MessagePort_postMessage, port,
{type: kCancel, value: packReason(reason)});
callFunction(binding.MessagePort_close, port);
return Promise_resolve();
return createResolvedPromise();
},
/* highWaterMark = */ 0);
......@@ -558,6 +600,9 @@
binding.streamOperations = {
_queue,
_queueTotalSize,
createPromise,
createRejectedPromise,
createResolvedPromise,
hasOwnPropertyNoThrow,
rejectPromise,
resolvePromise,
......
......@@ -63,13 +63,14 @@
const Promise = global.Promise;
const thenPromise = v8.uncurryThis(Promise.prototype.then);
const Promise_resolve = Promise.resolve.bind(Promise);
const Promise_reject = Promise.reject.bind(Promise);
// From CommonOperations.js
const {
_queue,
_queueTotalSize,
createPromise,
createRejectedPromise,
createResolvedPromise,
hasOwnPropertyNoThrow,
rejectPromise,
resolvePromise,
......@@ -175,7 +176,7 @@
const reader = AcquireReadableStreamDefaultReader(readable);
const writer = binding.AcquireWritableStreamDefaultWriter(dest);
let shuttingDown = false;
const promise = v8.createPromise();
const promise = createPromise();
let reading = false;
let lastWrite;
......@@ -366,7 +367,7 @@
// rejects.
return thenPromise(lastWrite, () => undefined, () => undefined);
}
return Promise_resolve(undefined);
return createResolvedPromise(undefined);
}
return promise;
......@@ -430,7 +431,7 @@
let canceled2 = false;
let reason1;
let reason2;
const cancelPromise = v8.createPromise();
const cancelPromise = createPromise();
function pullAlgorithm() {
return thenPromise(
......@@ -515,7 +516,7 @@
//
function ReadableStreamAddReadRequest(stream, forAuthorCode) {
const promise = v8.createPromise();
const promise = createPromise();
stream[_reader][_readRequests].push({promise, forAuthorCode});
return promise;
}
......@@ -525,10 +526,10 @@
const state = ReadableStreamGetState(stream);
if (state === STATE_CLOSED) {
return Promise_resolve(undefined);
return createResolvedPromise(undefined);
}
if (state === STATE_ERRORED) {
return Promise_reject(stream[_storedError]);
return createRejectedPromise(stream[_storedError]);
}
ReadableStreamClose(stream);
......@@ -631,7 +632,8 @@
get closed() {
if (IsReadableStreamDefaultReader(this) === false) {
return Promise_reject(new TypeError(streamErrors.illegalInvocation));
return createRejectedPromise(
new TypeError(streamErrors.illegalInvocation));
}
return this[_closedPromise];
......@@ -639,11 +641,12 @@
cancel(reason) {
if (IsReadableStreamDefaultReader(this) === false) {
return Promise_reject(new TypeError(streamErrors.illegalInvocation));
return createRejectedPromise(
new TypeError(streamErrors.illegalInvocation));
}
if (this[_ownerReadableStream] === undefined) {
return Promise_reject(new TypeError(errCancelReleasedReader));
return createRejectedPromise(new TypeError(errCancelReleasedReader));
}
return ReadableStreamReaderGenericCancel(this, reason);
......@@ -651,11 +654,12 @@
read() {
if (IsReadableStreamDefaultReader(this) === false) {
return Promise_reject(new TypeError(streamErrors.illegalInvocation));
return createRejectedPromise(
new TypeError(streamErrors.illegalInvocation));
}
if (this[_ownerReadableStream] === undefined) {
return Promise_reject(new TypeError(errReadReleasedReader));
return createRejectedPromise(new TypeError(errReadReleasedReader));
}
return ReadableStreamDefaultReaderRead(this, true);
......@@ -707,13 +711,13 @@
switch (ReadableStreamGetState(stream)) {
case STATE_READABLE:
reader[_closedPromise] = v8.createPromise();
reader[_closedPromise] = createPromise();
break;
case STATE_CLOSED:
reader[_closedPromise] = Promise_resolve(undefined);
reader[_closedPromise] = createResolvedPromise(undefined);
break;
case STATE_ERRORED:
reader[_closedPromise] = Promise_reject(stream[_storedError]);
reader[_closedPromise] = createRejectedPromise(stream[_storedError]);
markPromiseAsHandled(reader[_closedPromise]);
break;
}
......@@ -738,7 +742,7 @@
new TypeError(errReleasedReaderClosedPromise));
} else {
reader[_closedPromise] =
Promise_reject(new TypeError(errReleasedReaderClosedPromise));
createRejectedPromise(new TypeError(errReleasedReaderClosedPromise));
}
markPromiseAsHandled(reader[_closedPromise]);
......@@ -752,11 +756,11 @@
switch (ReadableStreamGetState(stream)) {
case STATE_CLOSED:
return Promise_resolve(ReadableStreamCreateReadResult(undefined, true,
forAuthorCode));
return createResolvedPromise(
ReadableStreamCreateReadResult(undefined, true, forAuthorCode));
case STATE_ERRORED:
return Promise_reject(stream[_storedError]);
return createRejectedPromise(stream[_storedError]);
default:
return ReadableStreamDefaultControllerPull(stream[_controller],
......@@ -854,8 +858,8 @@
ReadableStreamDefaultControllerCallPullIfNeeded(controller);
}
return Promise_resolve(ReadableStreamCreateReadResult(chunk, false,
forAuthorCode));
return createResolvedPromise(
ReadableStreamCreateReadResult(chunk, false, forAuthorCode));
}
const pendingPromise = ReadableStreamAddReadRequest(stream, forAuthorCode);
......@@ -1008,7 +1012,7 @@
controller[_cancelAlgorithm] = cancelAlgorithm;
stream[_controller] = controller;
thenPromise(Promise_resolve(startAlgorithm()), () => {
thenPromise(createResolvedPromise(startAlgorithm()), () => {
controller[_readableStreamDefaultControllerBits] |= STARTED;
ReadableStreamDefaultControllerCallPullIfNeeded(controller);
}, r => ReadableStreamDefaultControllerError(controller, r));
......
......@@ -42,11 +42,12 @@
const Promise = global.Promise;
const thenPromise = v8.uncurryThis(Promise.prototype.then);
const Promise_resolve = Promise.resolve.bind(Promise);
const Promise_reject = Promise.reject.bind(Promise);
// From CommonOperations.js
const {
createPromise,
createRejectedPromise,
createResolvedPromise,
hasOwnPropertyNoThrow,
resolvePromise,
CreateAlgorithmFromUnderlyingMethod,
......@@ -103,7 +104,7 @@
readableHighWaterMark =
ValidateAndNormalizeHighWaterMark(readableHighWaterMark);
const startPromise = v8.createPromise();
const startPromise = createPromise();
InitializeTransformStream(
this, startPromise, writableHighWaterMark, writableSizeAlgorithm,
readableHighWaterMark, readableSizeAlgorithm);
......@@ -159,7 +160,7 @@
// readableHighWaterMark >= 0,
// '! IsNonNegativeNumber(_readableHighWaterMark_) is true');
const stream = ObjectCreate(TransformStream_prototype);
const startPromise = v8.createPromise();
const startPromise = createPromise();
InitializeTransformStream(
stream, startPromise, writableHighWaterMark, writableSizeAlgorithm,
readableHighWaterMark, readableSizeAlgorithm);
......@@ -188,7 +189,7 @@
TransformStreamDefaultSourcePullAlgorithm(stream);
const cancelAlgorithm = reason => {
TransformStreamErrorWritableAndUnblockWrite(stream, reason);
return Promise_resolve(undefined);
return createResolvedPromise(undefined);
};
stream[_readable] = binding.CreateReadableStream(
startAlgorithm, pullAlgorithm, cancelAlgorithm, readableHighWaterMark,
......@@ -234,7 +235,7 @@
resolvePromise(stream[_backpressureChangePromise], undefined);
}
stream[_backpressureChangePromise] = v8.createPromise();
stream[_backpressureChangePromise] = createPromise();
stream[_backpressure] = backpressure;
}
......@@ -316,9 +317,9 @@
transformAlgorithm = chunk => {
try {
TransformStreamDefaultControllerEnqueue(controller, chunk);
return Promise_resolve();
return createResolvedPromise();
} catch (resultValue) {
return Promise_reject(resultValue);
return createRejectedPromise(resultValue);
}
};
}
......@@ -415,7 +416,7 @@
function TransformStreamDefaultSinkAbortAlgorithm(stream, reason) {
TransformStreamError(stream, reason);
return Promise_resolve();
return createResolvedPromise();
}
function TransformStreamDefaultSinkCloseAlgorithm(stream) {
......@@ -458,7 +459,7 @@
// blink::TransformStream needs. |transformAlgorithm| and |flushAlgorithm| are
// passed the controller, unlike in the standard.
function createTransformStreamSimple(transformAlgorithm, flushAlgorithm) {
return CreateTransformStream(() => Promise_resolve(),
return CreateTransformStream(() => createResolvedPromise(),
transformAlgorithm, flushAlgorithm);
}
function createTransformStream(
......
......@@ -73,13 +73,14 @@
const Promise = global.Promise;
const thenPromise = v8.uncurryThis(Promise.prototype.then);
const Promise_resolve = Promise.resolve.bind(Promise);
const Promise_reject = Promise.reject.bind(Promise);
// From CommonOperations.js
const {
_queue,
_queueTotalSize,
createPromise,
createRejectedPromise,
createResolvedPromise,
hasOwnPropertyNoThrow,
rejectPromise,
resolvePromise,
......@@ -211,7 +212,7 @@
function WritableStreamAbort(stream, reason) {
const state = stream[_stateAndFlags] & STATE_MASK;
if (state === CLOSED || state === ERRORED) {
return Promise_resolve(undefined);
return createResolvedPromise(undefined);
}
if (stream[_pendingAbortRequest] !== undefined) {
return stream[_pendingAbortRequest].promise;
......@@ -225,7 +226,7 @@
reason = undefined;
}
const promise = v8.createPromise();
const promise = createPromise();
stream[_pendingAbortRequest] = {promise, reason, wasAlreadyErroring};
if (!wasAlreadyErroring) {
......@@ -241,7 +242,7 @@
// '! IsWritableStreamLocked(writer) is true.');
// assert((stream[_stateAndFlags] & STATE_MASK) === WRITABLE,
// 'stream.[[state]] is "writable".');
const promise = v8.createPromise();
const promise = createPromise();
stream[_writeRequests].push(promise);
return promise;
}
......@@ -448,7 +449,7 @@
if (writer !== undefined &&
backpressure !== Boolean(stream[_stateAndFlags] & BACKPRESSURE_FLAG)) {
if (backpressure) {
writer[_readyPromise] = v8.createPromise();
writer[_readyPromise] = createPromise();
} else {
// assert(!backpressure, '_backpressure_ is *false*.');
resolvePromise(writer[_readyPromise], undefined);
......@@ -552,33 +553,33 @@
case WRITABLE: {
if (!WritableStreamCloseQueuedOrInFlight(stream) &&
stream[_stateAndFlags] & BACKPRESSURE_FLAG) {
this[_readyPromise] = v8.createPromise();
this[_readyPromise] = createPromise();
} else {
this[_readyPromise] = Promise_resolve(undefined);
this[_readyPromise] = createResolvedPromise(undefined);
}
this[_closedPromise] = v8.createPromise();
this[_closedPromise] = createPromise();
break;
}
case ERRORING: {
this[_readyPromise] = Promise_reject(stream[_storedError]);
this[_readyPromise] = createRejectedPromise(stream[_storedError]);
markPromiseAsHandled(this[_readyPromise]);
this[_closedPromise] = v8.createPromise();
this[_closedPromise] = createPromise();
break;
}
case CLOSED: {
this[_readyPromise] = Promise_resolve(undefined);
this[_closedPromise] = Promise_resolve(undefined);
this[_readyPromise] = createResolvedPromise(undefined);
this[_closedPromise] = createResolvedPromise(undefined);
break;
}
default: {
// assert(state === ERRORED, '_state_ is `"errored"`.');
const storedError = stream[_storedError];
this[_readyPromise] = Promise_reject(storedError);
this[_readyPromise] = createRejectedPromise(storedError);
markPromiseAsHandled(this[_readyPromise]);
this[_closedPromise] = Promise_reject(storedError);
this[_closedPromise] = createRejectedPromise(storedError);
markPromiseAsHandled(this[_closedPromise]);
break;
}
......@@ -587,7 +588,8 @@
get closed() {
if (!IsWritableStreamDefaultWriter(this)) {
return Promise_reject(new TypeError(streamErrors.illegalInvocation));
return createRejectedPromise(
new TypeError(streamErrors.illegalInvocation));
}
return this[_closedPromise];
}
......@@ -604,31 +606,36 @@
get ready() {
if (!IsWritableStreamDefaultWriter(this)) {
return Promise_reject(new TypeError(streamErrors.illegalInvocation));
return createRejectedPromise(
new TypeError(streamErrors.illegalInvocation));
}
return this[_readyPromise];
}
abort(reason) {
if (!IsWritableStreamDefaultWriter(this)) {
return Promise_reject(new TypeError(streamErrors.illegalInvocation));
return createRejectedPromise(
new TypeError(streamErrors.illegalInvocation));
}
if (this[_ownerWritableStream] === undefined) {
return Promise_reject(createWriterLockReleasedError(verbAborted));
return createRejectedPromise(
createWriterLockReleasedError(verbAborted));
}
return WritableStreamDefaultWriterAbort(this, reason);
}
close() {
if (!IsWritableStreamDefaultWriter(this)) {
return Promise_reject(new TypeError(streamErrors.illegalInvocation));
return createRejectedPromise(
new TypeError(streamErrors.illegalInvocation));
}
const stream = this[_ownerWritableStream];
if (stream === undefined) {
return Promise_reject(createWriterLockReleasedError(verbClosed));
return createRejectedPromise(createWriterLockReleasedError(verbClosed));
}
if (WritableStreamCloseQueuedOrInFlight(stream)) {
return Promise_reject(new TypeError(errCloseCloseRequestedStream));
return createRejectedPromise(
new TypeError(errCloseCloseRequestedStream));
}
return WritableStreamDefaultWriterClose(this);
}
......@@ -648,10 +655,12 @@
write(chunk) {
if (!IsWritableStreamDefaultWriter(this)) {
return Promise_reject(new TypeError(streamErrors.illegalInvocation));
return createRejectedPromise(
new TypeError(streamErrors.illegalInvocation));
}
if (this[_ownerWritableStream] === undefined) {
return Promise_reject(createWriterLockReleasedError(verbWrittenTo));
return createRejectedPromise(
createWriterLockReleasedError(verbWrittenTo));
}
return WritableStreamDefaultWriterWrite(this, chunk);
}
......@@ -675,7 +684,7 @@
// assert(stream !== undefined, 'stream is not undefined.');
const state = stream[_stateAndFlags] & STATE_MASK;
if (state === CLOSED || state === ERRORED) {
return Promise_reject(
return createRejectedPromise(
createCannotActionOnStateStreamError('close', state));
}
......@@ -683,7 +692,7 @@
// '_state_ is `"writable"` or `"erroring"`.');
// assert(!WritableStreamCloseQueuedOrInFlight(stream),
// '! WritableStreamCloseQueuedOrInFlight(_stream_) is *false*.');
const promise = v8.createPromise();
const promise = createPromise();
stream[_closeRequest] = promise;
if ((stream[_stateAndFlags] & BACKPRESSURE_FLAG) && state === WRITABLE) {
......@@ -698,10 +707,10 @@
// assert(stream !== undefined, 'stream is not undefined.');
const state = stream[_stateAndFlags] & STATE_MASK;
if (WritableStreamCloseQueuedOrInFlight(stream) || state === CLOSED) {
return Promise_resolve(undefined);
return createResolvedPromise(undefined);
}
if (state === ERRORED) {
return Promise_reject(stream[_storedError]);
return createRejectedPromise(stream[_storedError]);
}
// assert(state === WRITABLE || state === ERRORING,
......@@ -715,7 +724,7 @@
if (promiseState(writer[_closedPromise]) === v8.kPROMISE_PENDING) {
rejectPromise(writer[_closedPromise], error);
} else {
writer[_closedPromise] = Promise_reject(error);
writer[_closedPromise] = createRejectedPromise(error);
}
markPromiseAsHandled(writer[_closedPromise]);
}
......@@ -726,7 +735,7 @@
if (promiseState(writer[_readyPromise]) === v8.kPROMISE_PENDING) {
rejectPromise(writer[_readyPromise], error);
} else {
writer[_readyPromise] = Promise_reject(error);
writer[_readyPromise] = createRejectedPromise(error);
}
markPromiseAsHandled(writer[_readyPromise]);
}
......@@ -766,22 +775,23 @@
const chunkSize =
WritableStreamDefaultControllerGetChunkSize(controller, chunk);
if (stream !== writer[_ownerWritableStream]) {
return Promise_reject(createWriterLockReleasedError(verbWrittenTo));
return createRejectedPromise(
createWriterLockReleasedError(verbWrittenTo));
}
const state = stream[_stateAndFlags] & STATE_MASK;
if (state === ERRORED) {
return Promise_reject(stream[_storedError]);
return createRejectedPromise(stream[_storedError]);
}
if (WritableStreamCloseQueuedOrInFlight(stream)) {
return Promise_reject(new TypeError(
return createRejectedPromise(new TypeError(
templateErrorCannotActionOnStateStream('write to', 'closing')));
}
if (state === CLOSED) {
return Promise_reject(
return createRejectedPromise(
createCannotActionOnStateStreamError('write to', CLOSED));
}
if (state === ERRORING) {
return Promise_reject(stream[_storedError]);
return createRejectedPromise(stream[_storedError]);
}
// assert(state === WRITABLE, '_state_ is `"writable"`');
const promise = WritableStreamAddWriteRequest(stream);
......@@ -871,7 +881,7 @@
WritableStreamDefaultControllerGetBackpressure(controller);
WritableStreamUpdateBackpressure(stream, backpressure);
const startResult = startAlgorithm();
const startPromise = Promise_resolve(startResult);
const startPromise = createResolvedPromise(startResult);
thenPromise(
startPromise,
() => {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment