/* -*- 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/. */ /* Class WritableStreamDefaultWriter. */ #include "builtin/streams/WritableStreamDefaultWriter-inl.h" #include "mozilla/Assertions.h" // MOZ_ASSERT #include "mozilla/Attributes.h" // MOZ_MUST_USE #include "jsapi.h" // JS_ReportErrorASCII, JS_ReportErrorNumberASCII #include "builtin/streams/ClassSpecMacro.h" // JS_STREAMS_CLASS_SPEC #include "builtin/streams/MiscellaneousOperations.h" // js::ReturnPromiseRejectedWithPendingError #include "builtin/streams/WritableStream.h" // js::WritableStream #include "builtin/streams/WritableStreamOperations.h" // js::WritableStreamCloseQueuedOrInFlight #include "builtin/streams/WritableStreamWriterOperations.h" // js::WritableStreamDefaultWriter{Abort,GetDesiredSize,Release,Write} #include "js/CallArgs.h" // JS::CallArgs{,FromVp} #include "js/Class.h" // js::ClassSpec, JS_NULL_CLASS_OPS #include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* #include "js/PropertySpec.h" // JS{Function,Property}Spec, JS_{FS,PS}_END, JS_{FN,PSG} #include "js/RootingAPI.h" // JS::Handle #include "js/Value.h" // JS::Value #include "vm/Compartment.h" // JS::Compartment #include "vm/JSContext.h" // JSContext #include "vm/PromiseObject.h" // js::PromiseObject, js::PromiseResolvedWithUndefined #include "builtin/Promise-inl.h" // js::SetSettledPromiseIsHandled #include "vm/Compartment-inl.h" // JS::Compartment::wrap, js::UnwrapAndTypeCheck{Argument,This} #include "vm/JSObject-inl.h" // js::NewObjectWithClassProto #include "vm/NativeObject-inl.h" // js::ThrowIfNotConstructing #include "vm/Realm-inl.h" // js::AutoRealm using JS::CallArgs; using JS::CallArgsFromVp; using JS::Handle; using JS::Rooted; using JS::Value; using js::ClassSpec; using js::GetErrorMessage; using js::PromiseObject; using js::ReturnPromiseRejectedWithPendingError; using js::UnwrapAndTypeCheckArgument; using js::UnwrapAndTypeCheckThis; using js::WritableStream; using js::WritableStreamCloseQueuedOrInFlight; using js::WritableStreamDefaultWriter; using js::WritableStreamDefaultWriterGetDesiredSize; using js::WritableStreamDefaultWriterRelease; using js::WritableStreamDefaultWriterWrite; /*** 4.5. Class WritableStreamDefaultWriter *********************************/ /** * Stream spec, 4.5.3. new WritableStreamDefaultWriter(stream) * Steps 3-9. */ MOZ_MUST_USE WritableStreamDefaultWriter* js::CreateWritableStreamDefaultWriter( JSContext* cx, Handle unwrappedStream, Handle proto /* = nullptr */) { Rooted writer( cx, NewObjectWithClassProto(cx, proto)); if (!writer) { return nullptr; } // Step 3: Set this.[[ownerWritableStream]] to stream. { Rooted stream(cx, unwrappedStream); if (!cx->compartment()->wrap(cx, &stream)) { return nullptr; } writer->setStream(stream); } // Step 4 is moved to the end. // Step 5: Let state be stream.[[state]]. // Step 6: If state is "writable", if (unwrappedStream->writable()) { // Step 6.a: If ! WritableStreamCloseQueuedOrInFlight(stream) is false and // stream.[[backpressure]] is true, set this.[[readyPromise]] to a // new promise. PromiseObject* promise; if (!WritableStreamCloseQueuedOrInFlight(unwrappedStream) && unwrappedStream->backpressure()) { promise = PromiseObject::createSkippingExecutor(cx); } // Step 6.b: Otherwise, set this.[[readyPromise]] to a promise resolved with // undefined. else { promise = PromiseResolvedWithUndefined(cx); } if (!promise) { return nullptr; } writer->setReadyPromise(promise); // Step 6.c: Set this.[[closedPromise]] to a new promise. promise = PromiseObject::createSkippingExecutor(cx); if (!promise) { return nullptr; } writer->setClosedPromise(promise); } // Step 8: Otherwise, if state is "closed", else if (unwrappedStream->closed()) { // Step 8.a: Set this.[[readyPromise]] to a promise resolved with undefined. PromiseObject* readyPromise = PromiseResolvedWithUndefined(cx); if (!readyPromise) { return nullptr; } writer->setReadyPromise(readyPromise); // Step 8.b: Set this.[[closedPromise]] to a promise resolved with // undefined. PromiseObject* closedPromise = PromiseResolvedWithUndefined(cx); if (!closedPromise) { return nullptr; } writer->setClosedPromise(closedPromise); } else { // Wrap stream.[[StoredError]] just once for either step 7 or step 9. Rooted storedError(cx, unwrappedStream->storedError()); if (!cx->compartment()->wrap(cx, &storedError)) { return nullptr; } // Step 7: Otherwise, if state is "erroring", if (unwrappedStream->erroring()) { // Step 7.a: Set this.[[readyPromise]] to a promise rejected with // stream.[[storedError]]. Rooted promise( cx, PromiseObject::unforgeableReject(cx, storedError)); if (!promise) { return nullptr; } writer->setReadyPromise(promise); // Step 7.b: Set this.[[readyPromise]].[[PromiseIsHandled]] to true. js::SetSettledPromiseIsHandled(cx, promise.as()); // Step 7.c: Set this.[[closedPromise]] to a new promise. JSObject* closedPromise = PromiseObject::createSkippingExecutor(cx); if (!closedPromise) { return nullptr; } writer->setClosedPromise(closedPromise); } // Step 9: Otherwise, else { // Step 9.a: Assert: state is "errored". MOZ_ASSERT(unwrappedStream->errored()); Rooted promise(cx); // Step 9.b: Let storedError be stream.[[storedError]]. // Step 9.c: Set this.[[readyPromise]] to a promise rejected with // storedError. promise = PromiseObject::unforgeableReject(cx, storedError); if (!promise) { return nullptr; } writer->setReadyPromise(promise); // Step 9.d: Set this.[[readyPromise]].[[PromiseIsHandled]] to true. js::SetSettledPromiseIsHandled(cx, promise.as()); // Step 9.e: Set this.[[closedPromise]] to a promise rejected with // storedError. promise = PromiseObject::unforgeableReject(cx, storedError); if (!promise) { return nullptr; } writer->setClosedPromise(promise); // Step 9.f: Set this.[[closedPromise]].[[PromiseIsHandled]] to true. js::SetSettledPromiseIsHandled(cx, promise.as()); } } // Step 4 (reordered): Set stream.[[writer]] to this. // Doing this last prevents a partially-initialized writer from being attached // to the stream (and possibly left there on OOM). { AutoRealm ar(cx, unwrappedStream); Rooted wrappedWriter(cx, writer); if (!cx->compartment()->wrap(cx, &wrappedWriter)) { return nullptr; } unwrappedStream->setWriter(wrappedWriter); } return writer; } /** * Streams spec, 4.5.3. * new WritableStreamDefaultWriter(stream) */ bool WritableStreamDefaultWriter::constructor(JSContext* cx, unsigned argc, Value* vp) { MOZ_ASSERT(cx->realm()->creationOptions().getWritableStreamsEnabled(), "WritableStream should be enabled in this realm if we reach here"); CallArgs args = CallArgsFromVp(argc, vp); if (!ThrowIfNotConstructing(cx, args, "WritableStreamDefaultWriter")) { return false; } // Step 1: If ! IsWritableStream(stream) is false, throw a TypeError // exception. Rooted unwrappedStream( cx, UnwrapAndTypeCheckArgument( cx, args, "WritableStreamDefaultWriter constructor", 0)); if (!unwrappedStream) { return false; } // Step 2: If ! IsWritableStreamLocked(stream) is true, throw a TypeError // exception. if (unwrappedStream->isLocked()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_WRITABLESTREAM_ALREADY_LOCKED); return false; } // Implicit in the spec: Find the prototype object to use. Rooted proto(cx); if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Null, &proto)) { return false; } // Steps 3-9. Rooted writer( cx, CreateWritableStreamDefaultWriter(cx, unwrappedStream, proto)); if (!writer) { return false; } args.rval().setObject(*writer); return true; } /** * Streams spec, 4.5.4.1. get closed */ static MOZ_MUST_USE bool WritableStreamDefaultWriter_closed(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); // Step 1: If ! IsWritableStreamDefaultWriter(this) is false, return a promise // rejected with a TypeError exception. Rooted unwrappedWriter( cx, UnwrapAndTypeCheckThis(cx, args, "get closed")); if (!unwrappedWriter) { return ReturnPromiseRejectedWithPendingError(cx, args); } // Step 2: Return this.[[closedPromise]]. Rooted closedPromise(cx, unwrappedWriter->closedPromise()); if (!cx->compartment()->wrap(cx, &closedPromise)) { return false; } args.rval().setObject(*closedPromise); return true; } /** * Streams spec, 4.5.4.2. get desiredSize */ static MOZ_MUST_USE bool WritableStreamDefaultWriter_desiredSize(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); // Step 1: If ! IsWritableStreamDefaultWriter(this) is false, throw a // TypeError exception. Rooted unwrappedWriter( cx, UnwrapAndTypeCheckThis( cx, args, "get desiredSize")); if (!unwrappedWriter) { return false; } // Step 2: If this.[[ownerWritableStream]] is undefined, throw a TypeError // exception. if (!unwrappedWriter->hasStream()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_WRITABLESTREAMWRITER_NOT_OWNED, "get desiredSize"); return false; } // Step 3: Return ! WritableStreamDefaultWriterGetDesiredSize(this). if (!WritableStreamDefaultWriterGetDesiredSize(cx, unwrappedWriter, args.rval())) { return false; } MOZ_ASSERT(args.rval().isNull() || args.rval().isNumber(), "expected a type that'll never require wrapping"); return true; } /** * Streams spec, 4.5.4.3. get ready */ static MOZ_MUST_USE bool WritableStreamDefaultWriter_ready(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); // Step 1: If ! IsWritableStreamDefaultWriter(this) is false, return a promise // rejected with a TypeError exception. Rooted unwrappedWriter( cx, UnwrapAndTypeCheckThis(cx, args, "get ready")); if (!unwrappedWriter) { return ReturnPromiseRejectedWithPendingError(cx, args); } // Step 2: Return this.[[readyPromise]]. Rooted readyPromise(cx, unwrappedWriter->readyPromise()); if (!cx->compartment()->wrap(cx, &readyPromise)) { return false; } args.rval().setObject(*readyPromise); return true; } /** * Streams spec, 4.5.4.4. abort(reason) */ static MOZ_MUST_USE bool WritableStreamDefaultWriter_abort(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); // Step 1: If ! IsWritableStreamDefaultWriter(this) is false, return a promise // rejected with a TypeError exception. Rooted unwrappedWriter( cx, UnwrapAndTypeCheckThis(cx, args, "abort")); if (!unwrappedWriter) { return ReturnPromiseRejectedWithPendingError(cx, args); } // Step 2: If this.[[ownerWritableStream]] is undefined, return a promise // rejected with a TypeError exception. if (!unwrappedWriter->hasStream()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_WRITABLESTREAMWRITER_NOT_OWNED, "abort"); return ReturnPromiseRejectedWithPendingError(cx, args); } // Step 3: Return ! WritableStreamDefaultWriterAbort(this, reason). JSObject* promise = WritableStreamDefaultWriterAbort(cx, unwrappedWriter, args.get(0)); if (!promise) { return false; } cx->check(promise); args.rval().setObject(*promise); return true; } /** * Streams spec, 4.5.4.5. close() */ static MOZ_MUST_USE bool WritableStreamDefaultWriter_close(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); // Step 1: If ! IsWritableStreamDefaultWriter(this) is false, return a promise // rejected with a TypeError exception. Rooted unwrappedWriter( cx, UnwrapAndTypeCheckThis(cx, args, "close")); if (!unwrappedWriter) { return ReturnPromiseRejectedWithPendingError(cx, args); } // Step 2: Let stream be this.[[ownerWritableStream]]. // Step 3: If stream is undefined, return a promise rejected with a TypeError // exception. if (!unwrappedWriter->hasStream()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_WRITABLESTREAMWRITER_NOT_OWNED, "write"); return ReturnPromiseRejectedWithPendingError(cx, args); } WritableStream* unwrappedStream = UnwrapStreamFromWriter(cx, unwrappedWriter); if (!unwrappedStream) { return false; } // Step 4: If ! WritableStreamCloseQueuedOrInFlight(stream) is true, return a // promise rejected with a TypeError exception. if (WritableStreamCloseQueuedOrInFlight(unwrappedStream)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_WRITABLESTREAM_CLOSE_CLOSING_OR_CLOSED); return ReturnPromiseRejectedWithPendingError(cx, args); } // Step 5: Return ! WritableStreamDefaultWriterClose(this). JSObject* promise = WritableStreamDefaultWriterClose(cx, unwrappedWriter); if (!promise) { return false; } cx->check(promise); args.rval().setObject(*promise); return true; } /** * Streams spec, 4.5.4.6. releaseLock() */ static MOZ_MUST_USE bool WritableStreamDefaultWriter_releaseLock(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); // Step 1: If ! IsWritableStreamDefaultWriter(this) is false, return a promise // rejected with a TypeError exception. Rooted unwrappedWriter( cx, UnwrapAndTypeCheckThis(cx, args, "close")); if (!unwrappedWriter) { return false; } // Step 2: Let stream be this.[[ownerWritableStream]]. // Step 3: If stream is undefined, return. if (!unwrappedWriter->hasStream()) { args.rval().setUndefined(); return true; } // Step 4: Assert: stream.[[writer]] is not undefined. #ifdef DEBUG { WritableStream* unwrappedStream = UnwrapStreamFromWriter(cx, unwrappedWriter); if (!unwrappedStream) { return false; } MOZ_ASSERT(unwrappedStream->hasWriter()); } #endif // Step 5: Perform ! WritableStreamDefaultWriterRelease(this). if (!WritableStreamDefaultWriterRelease(cx, unwrappedWriter)) { return false; } args.rval().setUndefined(); return true; } /** * Streams spec, 4.5.4.7. write(chunk) */ static MOZ_MUST_USE bool WritableStreamDefaultWriter_write(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); // Step 1: If ! IsWritableStreamDefaultWriter(this) is false, return a promise // rejected with a TypeError exception. Rooted unwrappedWriter( cx, UnwrapAndTypeCheckThis(cx, args, "write")); if (!unwrappedWriter) { return ReturnPromiseRejectedWithPendingError(cx, args); } // Step 2: If this.[[ownerWritableStream]] is undefined, return a promise // rejected with a TypeError exception. if (!unwrappedWriter->hasStream()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_WRITABLESTREAMWRITER_NOT_OWNED, "write"); return ReturnPromiseRejectedWithPendingError(cx, args); } // Step 3: Return this.[[readyPromise]]. PromiseObject* promise = WritableStreamDefaultWriterWrite(cx, unwrappedWriter, args.get(0)); if (!promise) { return false; } cx->check(promise); args.rval().setObject(*promise); return true; } static const JSPropertySpec WritableStreamDefaultWriter_properties[] = { JS_PSG("closed", WritableStreamDefaultWriter_closed, 0), JS_PSG("desiredSize", WritableStreamDefaultWriter_desiredSize, 0), JS_PSG("ready", WritableStreamDefaultWriter_ready, 0), JS_PS_END}; static const JSFunctionSpec WritableStreamDefaultWriter_methods[] = { JS_FN("abort", WritableStreamDefaultWriter_abort, 1, 0), JS_FN("close", WritableStreamDefaultWriter_close, 0, 0), JS_FN("releaseLock", WritableStreamDefaultWriter_releaseLock, 0, 0), JS_FN("write", WritableStreamDefaultWriter_write, 1, 0), JS_FS_END}; JS_STREAMS_CLASS_SPEC(WritableStreamDefaultWriter, 1, SlotCount, ClassSpec::DontDefineConstructor, 0, JS_NULL_CLASS_OPS);