/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* Writable stream writer abstract operations. */ #include "builtin/streams/WritableStreamWriterOperations.h" #include "mozilla/Assertions.h" // MOZ_ASSERT #include "mozilla/Attributes.h" // MOZ_MUST_USE #include "jsapi.h" // JS_ReportErrorNumberASCII, JS_ReportErrorASCII #include "builtin/streams/MiscellaneousOperations.h" // js::PromiseRejectedWithPendingError #include "builtin/streams/WritableStream.h" // js::WritableStream #include "builtin/streams/WritableStreamDefaultController.h" // js::WritableStream::controller #include "builtin/streams/WritableStreamDefaultControllerOperations.h" // js::WritableStreamDefaultController{Close,GetDesiredSize} #include "builtin/streams/WritableStreamDefaultWriter.h" // js::WritableStreamDefaultWriter #include "builtin/streams/WritableStreamOperations.h" // js::WritableStream{Abort,CloseQueuedOrInFlight} #include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* #include "js/Promise.h" // JS::PromiseState #include "js/Value.h" // JS::Value, JS::{Int32,Null}Value #include "vm/Compartment.h" // JS::Compartment #include "vm/Interpreter.h" // js::GetAndClearException #include "vm/JSContext.h" // JSContext #include "vm/PromiseObject.h" // js::PromiseObject, js::PromiseResolvedWithUndefined #include "builtin/Promise-inl.h" // js::SetSettledPromiseIsHandled #include "builtin/streams/MiscellaneousOperations-inl.h" // js::ResolveUnwrappedPromiseWithUndefined #include "builtin/streams/WritableStream-inl.h" // js::WritableStream::setCloseRequest #include "builtin/streams/WritableStreamDefaultWriter-inl.h" // js::UnwrapStreamFromWriter #include "vm/Compartment-inl.h" // js::UnwrapAnd{DowncastObject,TypeCheckThis} #include "vm/JSContext-inl.h" // JSContext::check #include "vm/Realm-inl.h" // js::AutoRealm using JS::Handle; using JS::Int32Value; using JS::MutableHandle; using JS::NullValue; using JS::NumberValue; using JS::Rooted; using JS::Value; using js::AutoRealm; using js::PromiseObject; using js::UnwrapAndDowncastObject; using js::WritableStreamDefaultWriter; /*** 4.6. Writable stream writer abstract operations ************************/ /** * Streams spec, 4.6.2. * WritableStreamDefaultWriterAbort ( writer, reason ) */ JSObject* js::WritableStreamDefaultWriterAbort( JSContext* cx, Handle unwrappedWriter, Handle reason) { cx->check(reason); // Step 1: Let stream be writer.[[ownerWritableStream]]. // Step 2: Assert: stream is not undefined. MOZ_ASSERT(unwrappedWriter->hasStream()); Rooted unwrappedStream( cx, UnwrapStreamFromWriter(cx, unwrappedWriter)); if (!unwrappedStream) { return nullptr; } // Step 3: Return ! WritableStreamAbort(stream, reason). return WritableStreamAbort(cx, unwrappedStream, reason); } /** * Streams spec, 4.6.3. * WritableStreamDefaultWriterClose ( writer ) */ PromiseObject* js::WritableStreamDefaultWriterClose( JSContext* cx, Handle unwrappedWriter) { // Step 1: Let stream be writer.[[ownerWritableStream]]. // Step 2: Assert: stream is not undefined. MOZ_ASSERT(unwrappedWriter->hasStream()); Rooted unwrappedStream( cx, UnwrapStreamFromWriter(cx, unwrappedWriter)); if (!unwrappedStream) { return PromiseRejectedWithPendingError(cx); } // Step 3: Let state be stream.[[state]]. // Step 4: If state is "closed" or "errored", return a promise rejected with a // TypeError exception. if (unwrappedStream->closed() || unwrappedStream->errored()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_WRITABLESTREAM_CLOSED_OR_ERRORED); return PromiseRejectedWithPendingError(cx); } // Step 5: Assert: state is "writable" or "erroring". MOZ_ASSERT(unwrappedStream->writable() ^ unwrappedStream->erroring()); // Step 6: Assert: ! WritableStreamCloseQueuedOrInFlight(stream) is false. MOZ_ASSERT(!WritableStreamCloseQueuedOrInFlight(unwrappedStream)); // Step 7: Let promise be a new promise. Rooted promise(cx, PromiseObject::createSkippingExecutor(cx)); if (!promise) { return nullptr; } // Step 8: Set stream.[[closeRequest]] to promise. { AutoRealm ar(cx, unwrappedStream); Rooted closeRequest(cx, promise); if (!cx->compartment()->wrap(cx, &closeRequest)) { return nullptr; } unwrappedStream->setCloseRequest(closeRequest); } // Step 9: If stream.[[backpressure]] is true and state is "writable", resolve // writer.[[readyPromise]] with undefined. if (unwrappedStream->backpressure() && unwrappedStream->writable()) { if (!ResolveUnwrappedPromiseWithUndefined( cx, unwrappedWriter->readyPromise())) { return nullptr; } } // Step 10: Perform // ! WritableStreamDefaultControllerClose( // stream.[[writableStreamController]]). Rooted unwrappedController( cx, unwrappedStream->controller()); if (!WritableStreamDefaultControllerClose(cx, unwrappedController)) { return nullptr; } // Step 11: Return promise. return promise; } /** * Streams spec. * WritableStreamDefaultWriterCloseWithErrorPropagation ( writer ) */ PromiseObject* js::WritableStreamDefaultWriterCloseWithErrorPropagation( JSContext* cx, Handle unwrappedWriter) { // Step 1: Let stream be writer.[[ownerWritableStream]]. // Step 2: Assert: stream is not undefined. WritableStream* unwrappedStream = UnwrapStreamFromWriter(cx, unwrappedWriter); if (!unwrappedStream) { return nullptr; } // Step 3: Let state be stream.[[state]]. // Step 4: If ! WritableStreamCloseQueuedOrInFlight(stream) is true or state // is "closed", return a promise resolved with undefined. if (WritableStreamCloseQueuedOrInFlight(unwrappedStream) || unwrappedStream->closed()) { return PromiseResolvedWithUndefined(cx); } // Step 5: If state is "errored", return a promise rejected with // stream.[[storedError]]. if (unwrappedStream->errored()) { Rooted storedError(cx, unwrappedStream->storedError()); if (!cx->compartment()->wrap(cx, &storedError)) { return nullptr; } return PromiseObject::unforgeableReject(cx, storedError); } // Step 6: Assert: state is "writable" or "erroring". MOZ_ASSERT(unwrappedStream->writable() ^ unwrappedStream->erroring()); // Step 7: Return ! WritableStreamDefaultWriterClose(writer). return WritableStreamDefaultWriterClose(cx, unwrappedWriter); } using GetField = JSObject* (WritableStreamDefaultWriter::*)() const; using SetField = void (WritableStreamDefaultWriter::*)(JSObject*); static bool EnsurePromiseRejected( JSContext* cx, Handle unwrappedWriter, GetField getField, SetField setField, Handle error) { cx->check(error); Rooted unwrappedPromise( cx, UnwrapAndDowncastObject( cx, (unwrappedWriter->*getField)())); if (!unwrappedPromise) { return false; } // 4.6.{5,6} step 1: If writer.[[]].[[PromiseState]] is "pending", // reject writer.[[]] with error. if (unwrappedPromise->state() == JS::PromiseState::Pending) { if (!RejectUnwrappedPromiseWithError(cx, unwrappedPromise, error)) { return false; } } else { // 4.6.{5,6} step 2: Otherwise, set writer.[[]] to a promise rejected // with error. Rooted rejectedWithError( cx, PromiseObject::unforgeableReject(cx, error)); if (!rejectedWithError) { return false; } { AutoRealm ar(cx, unwrappedWriter); if (!cx->compartment()->wrap(cx, &rejectedWithError)) { return false; } (unwrappedWriter->*setField)(rejectedWithError); } // Directly-unobservable rejected promises aren't collapsed like resolved // promises, and this promise is created in the current realm, so it's // always an actual Promise. unwrappedPromise = &rejectedWithError->as(); } // 4.6.{5,6} step 3: Set writer.[[]].[[PromiseIsHandled]] to true. js::SetSettledPromiseIsHandled(cx, unwrappedPromise); return true; } /** * Streams spec, 4.6.5. * WritableStreamDefaultWriterEnsureClosedPromiseRejected( writer, error ) */ MOZ_MUST_USE bool js::WritableStreamDefaultWriterEnsureClosedPromiseRejected( JSContext* cx, Handle unwrappedWriter, Handle error) { return EnsurePromiseRejected( cx, unwrappedWriter, &WritableStreamDefaultWriter::closedPromise, &WritableStreamDefaultWriter::setClosedPromise, error); } /** * Streams spec, 4.6.6. * WritableStreamDefaultWriterEnsureReadyPromiseRejected( writer, error ) */ MOZ_MUST_USE bool js::WritableStreamDefaultWriterEnsureReadyPromiseRejected( JSContext* cx, Handle unwrappedWriter, Handle error) { return EnsurePromiseRejected( cx, unwrappedWriter, &WritableStreamDefaultWriter::readyPromise, &WritableStreamDefaultWriter::setReadyPromise, error); } /** * Streams spec, 4.6.7. * WritableStreamDefaultWriterGetDesiredSize ( writer ) */ bool js::WritableStreamDefaultWriterGetDesiredSize( JSContext* cx, Handle unwrappedWriter, MutableHandle size) { // Step 1: Let stream be writer.[[ownerWritableStream]]. const WritableStream* unwrappedStream = UnwrapStreamFromWriter(cx, unwrappedWriter); if (!unwrappedStream) { return false; } // Step 2: Let state be stream.[[state]]. // Step 3: If state is "errored" or "erroring", return null. if (unwrappedStream->errored() || unwrappedStream->erroring()) { size.setNull(); } // Step 4: If state is "closed", return 0. else if (unwrappedStream->closed()) { size.setInt32(0); } // Step 5: Return // ! WritableStreamDefaultControllerGetDesiredSize( // stream.[[writableStreamController]]). else { size.setNumber(WritableStreamDefaultControllerGetDesiredSize( unwrappedStream->controller())); } return true; } /** * Streams spec, 4.6.8. * WritableStreamDefaultWriterRelease ( writer ) */ bool js::WritableStreamDefaultWriterRelease( JSContext* cx, Handle unwrappedWriter) { // Step 1: Let stream be writer.[[ownerWritableStream]]. // Step 2: Assert: stream is not undefined. MOZ_ASSERT(unwrappedWriter->hasStream()); Rooted unwrappedStream( cx, UnwrapStreamFromWriter(cx, unwrappedWriter)); if (!unwrappedStream) { return false; } // Step 3: Assert: stream.[[writer]] is writer. #ifdef DEBUG { WritableStreamDefaultWriter* unwrappedStreamWriter = UnwrapWriterFromStream(cx, unwrappedStream); if (!unwrappedStreamWriter) { return false; } MOZ_ASSERT(unwrappedStreamWriter == unwrappedWriter); } #endif // Step 4: Let releasedError be a new TypeError. Rooted releasedError(cx); JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_WRITABLESTREAM_CANT_RELEASE_ALREADY_CLOSED); if (!cx->isExceptionPending() || !GetAndClearException(cx, &releasedError)) { return false; } // Step 5: Perform // ! WritableStreamDefaultWriterEnsureReadyPromiseRejected( // writer, releasedError). if (!WritableStreamDefaultWriterEnsureReadyPromiseRejected( cx, unwrappedWriter, releasedError)) { return false; } // Step 6: Perform // ! WritableStreamDefaultWriterEnsureClosedPromiseRejected( // writer, releasedError). if (!WritableStreamDefaultWriterEnsureClosedPromiseRejected( cx, unwrappedWriter, releasedError)) { return false; } // Step 7: Set stream.[[writer]] to undefined. unwrappedStream->clearWriter(); // Step 8: Set writer.[[ownerWritableStream]] to undefined. unwrappedWriter->clearStream(); return true; } /** * Streams spec, 4.6.9. * WritableStreamDefaultWriterWrite ( writer, chunk ) */ PromiseObject* js::WritableStreamDefaultWriterWrite( JSContext* cx, Handle unwrappedWriter, Handle chunk) { cx->check(chunk); // Step 1: Let stream be writer.[[ownerWritableStream]]. // Step 2: Assert: stream is not undefined. MOZ_ASSERT(unwrappedWriter->hasStream()); Rooted unwrappedStream( cx, UnwrapStreamFromWriter(cx, unwrappedWriter)); if (!unwrappedStream) { return nullptr; } // Step 3: Let controller be stream.[[writableStreamController]]. Rooted unwrappedController( cx, unwrappedStream->controller()); // Step 4: Let chunkSize be // ! WritableStreamDefaultControllerGetChunkSize(controller, chunk). Rooted chunkSize(cx); if (!WritableStreamDefaultControllerGetChunkSize(cx, unwrappedController, chunk, &chunkSize)) { return nullptr; } cx->check(chunkSize); // Step 5: If stream is not equal to writer.[[ownerWritableStream]], return a // promise rejected with a TypeError exception. // (This is just an obscure way of saying "If step 4 caused writer's lock on // stream to be released", or concretely, "If writer.[[ownerWritableStream]] // is [now, newly] undefined".) if (!unwrappedWriter->hasStream()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_WRITABLESTREAM_RELEASED_DURING_WRITE); return PromiseRejectedWithPendingError(cx); } auto RejectWithStoredError = [](JSContext* cx, Handle unwrappedStream) -> PromiseObject* { Rooted storedError(cx, unwrappedStream->storedError()); if (!cx->compartment()->wrap(cx, &storedError)) { return nullptr; } return PromiseObject::unforgeableReject(cx, storedError); }; // Step 6: Let state be stream.[[state]]. // Step 7: If state is "errored", return a promise rejected with // stream.[[storedError]]. if (unwrappedStream->errored()) { return RejectWithStoredError(cx, unwrappedStream); } // Step 8: If ! WritableStreamCloseQueuedOrInFlight(stream) is true or state // is "closed", return a promise rejected with a TypeError exception // indicating that the stream is closing or closed. if (WritableStreamCloseQueuedOrInFlight(unwrappedStream) || unwrappedStream->closed()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_WRITABLESTREAM_WRITE_CLOSING_OR_CLOSED); return PromiseRejectedWithPendingError(cx); } // Step 9: If state is "erroring", return a promise rejected with // stream.[[storedError]]. if (unwrappedStream->erroring()) { return RejectWithStoredError(cx, unwrappedStream); } // Step 10: Assert: state is "writable". MOZ_ASSERT(unwrappedStream->writable()); // Step 11: Let promise be ! WritableStreamAddWriteRequest(stream). Rooted promise( cx, WritableStreamAddWriteRequest(cx, unwrappedStream)); if (!promise) { return nullptr; } // Step 12: Perform // ! WritableStreamDefaultControllerWrite(controller, chunk, // chunkSize). if (!WritableStreamDefaultControllerWrite(cx, unwrappedController, chunk, chunkSize)) { return nullptr; } // Step 13: Return promise. return promise; }