Bug 1582348 - Implement WritableStreamClose and WritableStream.prototype.close. r=arai
authorJeff Walden <jwalden@mit.edu>
Thu, 07 Nov 2019 00:19:32 +0000
changeset 501030 875c829c57ceb711fbd0f6ed36613cff83380fb1
parent 501029 eadda7d0287cc25ef6d5eb332a98b0d7b6142eee
child 501031 979d77ef56928092343868192f4d632ea074a892
push id99940
push userjwalden@mit.edu
push dateThu, 07 Nov 2019 00:41:19 +0000
treeherderautoland@8caeb3eb603c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersarai
bugs1582348
milestone72.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1582348 - Implement WritableStreamClose and WritableStream.prototype.close. r=arai Differential Revision: https://phabricator.services.mozilla.com/D51965
js/src/builtin/streams/WritableStream.cpp
js/src/builtin/streams/WritableStreamOperations.cpp
js/src/builtin/streams/WritableStreamOperations.h
js/src/js.msg
--- a/js/src/builtin/streams/WritableStream.cpp
+++ b/js/src/builtin/streams/WritableStream.cpp
@@ -14,17 +14,17 @@
 #include "jsapi.h"    // JS_ReportErrorNumberASCII
 #include "jsfriendapi.h"  // js::GetErrorMessage, JSMSG_*
 #include "jspubtd.h"  // JSProto_WritableStream
 
 #include "builtin/streams/ClassSpecMacro.h"  // JS_STREAMS_CLASS_SPEC
 #include "builtin/streams/MiscellaneousOperations.h"  // js::MakeSizeAlgorithmFromSizeFunction, js::ReturnPromiseRejectedWithPendingError, js::ValidateAndNormalizeHighWaterMark
 #include "builtin/streams/WritableStreamDefaultControllerOperations.h"  // js::SetUpWritableStreamDefaultControllerFromUnderlyingSink
 #include "builtin/streams/WritableStreamDefaultWriter.h"  // js::CreateWritableStreamDefaultWriter
-#include "builtin/streams/WritableStreamOperations.h"  // js::WritableStreamAbort
+#include "builtin/streams/WritableStreamOperations.h"  // js::WritableStream{Abort,Close{,QueuedOrInFlight}}
 #include "js/CallArgs.h"  // JS::CallArgs{,FromVp}
 #include "js/Class.h"  // JS{Function,Property}Spec, JS_{FS,PS}_END, JSCLASS_PRIVATE_IS_NSISUPPORTS, JSCLASS_HAS_PRIVATE, JS_NULL_CLASS_OPS
 #include "js/RealmOptions.h"      // JS::RealmCreationOptions
 #include "js/RootingAPI.h"        // JS::Handle, JS::Rooted
 #include "js/Value.h"             // JS::{,Object}Value
 #include "vm/JSContext.h"         // JSContext
 #include "vm/JSObject.h"          // js::GetPrototypeFromBuiltinConstructor
 #include "vm/NativeObject.h"      // js::PlainObject
@@ -37,16 +37,18 @@
 #include "vm/NativeObject-inl.h"  // js::ThrowIfNotConstructing
 
 using js::CreateWritableStreamDefaultWriter;
 using js::GetErrorMessage;
 using js::ReturnPromiseRejectedWithPendingError;
 using js::UnwrapAndTypeCheckThis;
 using js::WritableStream;
 using js::WritableStreamAbort;
+using js::WritableStreamClose;
+using js::WritableStreamCloseQueuedOrInFlight;
 
 using JS::CallArgs;
 using JS::CallArgsFromVp;
 using JS::Handle;
 using JS::ObjectValue;
 using JS::Rooted;
 using JS::Value;
 
@@ -184,33 +186,73 @@ static bool WritableStream_abort(JSConte
   if (!unwrappedStream) {
     return false;
   }
 
   // Step 2: If ! IsWritableStreamLocked(this) is true, return a promise
   //         rejected with a TypeError exception.
   if (unwrappedStream->isLocked()) {
     JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
-                              JSMSG_CANT_ABORT_LOCKED_WRITABLESTREAM);
+                              JSMSG_CANT_USE_LOCKED_WRITABLESTREAM, "abort");
     return ReturnPromiseRejectedWithPendingError(cx, args);
   }
 
   // Step 3: Return ! WritableStreamAbort(this, reason).
   JSObject* promise = WritableStreamAbort(cx, unwrappedStream, args.get(0));
   if (!promise) {
     return false;
   }
   cx->check(promise);
 
   args.rval().setObject(*promise);
   return true;
 }
 
 /**
- * Streams spec, 4.2.5.3. getWriter()
+ * Streams spec, 4.2.5.3. close()
+ */
+static bool WritableStream_close(JSContext* cx, unsigned argc, Value* vp) {
+  CallArgs args = CallArgsFromVp(argc, vp);
+
+  // Step 1: If ! IsWritableStream(this) is false, return a promise rejected
+  //         with a TypeError exception.
+  Rooted<WritableStream*> unwrappedStream(
+      cx, UnwrapAndTypeCheckThis<WritableStream>(cx, args, "close"));
+  if (!unwrappedStream) {
+    return ReturnPromiseRejectedWithPendingError(cx, args);
+  }
+
+  // Step 2: If ! IsWritableStreamLocked(this) is true, return a promise
+  //         rejected with a TypeError exception.
+  if (unwrappedStream->isLocked()) {
+    JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
+                              JSMSG_CANT_USE_LOCKED_WRITABLESTREAM, "close");
+    return ReturnPromiseRejectedWithPendingError(cx, args);
+  }
+
+  // Step 3: If ! WritableStreamCloseQueuedOrInFlight(this) 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 4: Return ! WritableStreamClose(this).
+  JSObject* promise = WritableStreamClose(cx, unwrappedStream);
+  if (!promise) {
+    return false;
+  }
+
+  args.rval().setObject(*promise);
+  return true;
+}
+
+/**
+ * Streams spec, 4.2.5.4. getWriter()
  */
 static bool WritableStream_getWriter(JSContext* cx, unsigned argc, Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
 
   // Step 1: If ! WritableStream(this) is false, throw a TypeError exception.
   Rooted<WritableStream*> unwrappedStream(
       cx, UnwrapAndTypeCheckThis<WritableStream>(cx, args, "getWriter"));
   if (!unwrappedStream) {
@@ -223,16 +265,17 @@ static bool WritableStream_getWriter(JSC
   }
 
   args.rval().setObject(*writer);
   return true;
 }
 
 static const JSFunctionSpec WritableStream_methods[] = {
     JS_FN("abort", WritableStream_abort, 1, 0),
+    JS_FN("close", WritableStream_close, 0, 0),
     JS_FN("getWriter", WritableStream_getWriter, 0, 0), JS_FS_END};
 
 static const JSPropertySpec WritableStream_properties[] = {
     JS_PSG("locked", WritableStream_locked, 0), JS_PS_END};
 
 JS_STREAMS_CLASS_SPEC(WritableStream, 0, SlotCount, 0,
                       JSCLASS_PRIVATE_IS_NSISUPPORTS | JSCLASS_HAS_PRIVATE,
                       JS_NULL_CLASS_OPS);
--- a/js/src/builtin/streams/WritableStreamOperations.cpp
+++ b/js/src/builtin/streams/WritableStreamOperations.cpp
@@ -9,20 +9,22 @@
 #include "builtin/streams/WritableStreamOperations.h"
 
 #include "mozilla/Assertions.h"  // MOZ_ASSERT
 #include "mozilla/Attributes.h"  // MOZ_MUST_USE
 
 #include <stdint.h>  // uint32_t
 
 #include "jsapi.h"  // JS_ReportErrorASCII, JS_SetPrivate
+#include "jsfriendapi.h"  // js::GetErrorMessage, JSMSG_*
 
 #include "builtin/Promise.h"                 // js::PromiseObject
+#include "builtin/streams/MiscellaneousOperations.h"  // js::PromiseRejectedWithPendingError
 #include "builtin/streams/WritableStream.h"  // js::WritableStream
-#include "builtin/streams/WritableStreamDefaultController.h"  // js::WritableStreamDefaultController, js::WritableStream::controller
+#include "builtin/streams/WritableStreamDefaultController.h"  // js::WritableStreamDefaultController{,Close}, js::WritableStream::controller
 #include "builtin/streams/WritableStreamDefaultControllerOperations.h"  // js::WritableStreamControllerErrorSteps
 #include "builtin/streams/WritableStreamWriterOperations.h"  // js::WritableStreamDefaultWriterEnsureReadyPromiseRejected
 #include "js/CallArgs.h"     // JS::CallArgs{,FromVp}
 #include "js/Promise.h"      // JS::{Reject,Resolve}Promise
 #include "js/RootingAPI.h"   // JS::Handle, JS::Rooted
 #include "js/Value.h"  // JS::Value, JS::ObjecValue, JS::UndefinedHandleValue
 #include "vm/Compartment.h"  // JS::Compartment
 #include "vm/JSContext.h"    // JSContext
@@ -192,16 +194,88 @@ JSObject* js::WritableStreamAbort(JSCont
       return nullptr;
     }
   }
 
   // Step 10: Return promise.
   return promise;
 }
 
+/**
+ * Streams spec, 4.3.7.
+ *      WritableStreamClose ( stream )
+ *
+ * Note: The object (a promise) returned by this function is in the current
+ *       compartment and does not require special wrapping to be put to use.
+ */
+JSObject* js::WritableStreamClose(JSContext* cx,
+                                  Handle<WritableStream*> unwrappedStream) {
+  // Step 1: Let state be stream.[[state]].
+  // Step 2: 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 3: Assert: state is "writable" or "erroring".
+  MOZ_ASSERT(unwrappedStream->writable() ^ unwrappedStream->erroring());
+
+  // Step 4: Assert: ! WritableStreamCloseQueuedOrInFlight(stream) is false.
+  MOZ_ASSERT(!WritableStreamCloseQueuedOrInFlight(unwrappedStream));
+
+  // Step 5: Let promise be a new promise.
+  Rooted<PromiseObject*> promise(cx, PromiseObject::createSkippingExecutor(cx));
+  if (!promise) {
+    return nullptr;
+  }
+
+  // Step 6: Set stream.[[closeRequest]] to promise.
+  {
+    AutoRealm ar(cx, unwrappedStream);
+    Rooted<JSObject*> wrappedPromise(cx, promise);
+    if (!cx->compartment()->wrap(cx, &wrappedPromise)) {
+      return nullptr;
+    }
+
+    unwrappedStream->setCloseRequest(promise);
+  }
+
+  // Step 7: Let writer be stream.[[writer]].
+  // Step 8: If writer is not undefined, and stream.[[backpressure]] is true,
+  //         and state is "writable", resolve writer.[[readyPromise]] with
+  //         undefined.
+  if (unwrappedStream->hasWriter() && unwrappedStream->backpressure() &&
+      unwrappedStream->writable()) {
+    Rooted<WritableStreamDefaultWriter*> unwrappedWriter(
+        cx, UnwrapWriterFromStream(cx, unwrappedStream));
+    if (!unwrappedWriter) {
+      return nullptr;
+    }
+
+    if (!ResolveUnwrappedPromiseWithUndefined(
+            cx, unwrappedWriter->readyPromise())) {
+      return nullptr;
+    }
+  }
+
+  // Step 9: Perform
+  //         ! WritableStreamDefaultControllerClose(
+  //               stream.[[writableStreamController]]).
+  Rooted<WritableStreamDefaultController*> unwrappedController(
+      cx, unwrappedStream->controller());
+  if (!WritableStreamDefaultControllerClose(cx, unwrappedController)) {
+    return nullptr;
+  }
+
+  // Step 10: Return promise.
+  return promise;
+}
+
 /*** 4.4. Writable stream abstract operations used by controllers ***********/
 
 /**
  * Streams spec, 4.4.1.
  *      WritableStreamAddWriteRequest ( stream )
  */
 MOZ_MUST_USE PromiseObject* js::WritableStreamAddWriteRequest(
     JSContext* cx, Handle<WritableStream*> unwrappedStream) {
--- a/js/src/builtin/streams/WritableStreamOperations.h
+++ b/js/src/builtin/streams/WritableStreamOperations.h
@@ -21,16 +21,19 @@ namespace js {
 
 class PromiseObject;
 class WritableStream;
 
 extern JSObject* WritableStreamAbort(
     JSContext* cx, JS::Handle<WritableStream*> unwrappedStream,
     JS::Handle<JS::Value> reason);
 
+extern JSObject* WritableStreamClose(
+    JSContext* cx, JS::Handle<WritableStream*> unwrappedStream);
+
 extern MOZ_MUST_USE PromiseObject* WritableStreamAddWriteRequest(
     JSContext* cx, JS::Handle<WritableStream*> unwrappedStream);
 
 extern MOZ_MUST_USE bool WritableStreamDealWithRejection(
     JSContext* cx, JS::Handle<WritableStream*> unwrappedStream,
     JS::Handle<JS::Value> error);
 
 extern MOZ_MUST_USE bool WritableStreamStartErroring(
--- a/js/src/js.msg
+++ b/js/src/js.msg
@@ -684,17 +684,17 @@ MSG_DEF(JSMSG_READABLESTREAMBYOBREQUEST_
 MSG_DEF(JSMSG_READABLESTREAM_METHOD_NOT_IMPLEMENTED,     1, JSEXN_TYPEERR, "ReadableStream method {0} not yet implemented")
 
 // WritableStream
 MSG_DEF(JSMSG_READABLESTREAM_UNDERLYINGSINK_TYPE_WRONG,  0, JSEXN_RANGEERR,"'underlyingSink.type' must be undefined.")
 MSG_DEF(JSMSG_WRITABLESTREAMWRITER_NOT_OWNED,            1, JSEXN_TYPEERR, "the WritableStream writer method '{0}' may only be called on a writer owned by a stream")
 MSG_DEF(JSMSG_WRITABLESTREAM_CLOSED_OR_ERRORED,          0, JSEXN_TYPEERR, "writable stream is already closed or errored")
 MSG_DEF(JSMSG_WRITABLESTREAM_RELEASED_DURING_WRITE,      0, JSEXN_TYPEERR, "writer's lock on the stream was released before writing completed")
 MSG_DEF(JSMSG_WRITABLESTREAM_WRITE_CLOSING_OR_CLOSED,    0, JSEXN_TYPEERR, "can't write to a stream that's currently closing or already closed")
-MSG_DEF(JSMSG_CANT_ABORT_LOCKED_WRITABLESTREAM,          0, JSEXN_TYPEERR, "can't abort a WritableStream that's locked to a writer")
+MSG_DEF(JSMSG_CANT_USE_LOCKED_WRITABLESTREAM,            1, JSEXN_TYPEERR, "can't {0} a WritableStream that's locked to a writer")
 MSG_DEF(JSMSG_WRITABLESTREAM_CLOSE_CLOSING_OR_CLOSED,    0, JSEXN_TYPEERR, "can't close a stream that's currently closing or already closed")
 MSG_DEF(JSMSG_WRITABLESTREAM_CANT_RELEASE_ALREADY_CLOSED,0, JSEXN_TYPEERR, "writer has already been released and can't be closed")
 MSG_DEF(JSMSG_WRITABLESTREAM_ALREADY_LOCKED,             0, JSEXN_TYPEERR, "writable stream is already locked by another writer")
 MSG_DEF(JSMSG_READABLESTREAM_NYI,                        0, JSEXN_ERR,     "full WritableStream support is not yet implemented")
 
 // Other Stream-related
 MSG_DEF(JSMSG_STREAM_INVALID_HIGHWATERMARK,              0, JSEXN_RANGEERR, "'highWaterMark' must be a non-negative, non-NaN number.")
 MSG_DEF(JSMSG_STREAM_CONSUME_ERROR,                      0, JSEXN_TYPEERR,  "error consuming stream body")