Bug 1508813 - ReadableStream subclassing. r=arai
authorJason Orendorff <jorendorff@mozilla.com>
Thu, 22 Nov 2018 13:43:53 +0000
changeset 504209 7bc182b1c57fe2ecd470c7e3a32d3d2ee20ed8db
parent 504208 1df56440bdb4a75c59c5527c20ae9139b85655a6
child 504210 d3b4650e49e13b1a15f642c54368b1576d974094
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersarai
bugs1508813
milestone65.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 1508813 - ReadableStream subclassing. r=arai This modifies the four user-visible stream-related constructors that we implement, that do not unconditionally throw, to support subclassing by creating the new object with a prototype chain based on NewTarget. The Strategy constructors are also updated to follow the spec in other minor details. Differential Revision: https://phabricator.services.mozilla.com/D12474
js/src/builtin/Stream.cpp
js/src/tests/non262/ReadableStream/subclassing.js
--- a/js/src/builtin/Stream.cpp
+++ b/js/src/builtin/Stream.cpp
@@ -554,18 +554,24 @@ ReadableStream::constructor(JSContext* c
     if (strategy.isUndefined()) {
         JSObject* emptyObj = NewBuiltinClassInstance<PlainObject>(cx);
         if (!emptyObj) {
             return false;
         }
         strategy = ObjectValue(*emptyObj);
     }
 
+    // Implicit in the spec: Set this to
+    //     OrdinaryCreateFromConstructor(NewTarget, ...).
     // Step 1: Perform ! InitializeReadableStream(this).
-    Rooted<ReadableStream*> stream(cx, ReadableStream::create(cx));
+    RootedObject proto(cx);
+    if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) {
+        return false;
+    }
+    Rooted<ReadableStream*> stream(cx, ReadableStream::create(cx, proto));
     if (!stream) {
         return false;
     }
 
     // Step 2: Let size be ? GetV(strategy, "size").
     RootedValue size(cx);
     if (!GetProperty(cx, strategy, cx->names().size, &size)) {
         return false;
@@ -693,17 +699,18 @@ ReadableStream_cancel(JSContext* cx, uns
     }
     args.rval().setObject(*cancelPromise);
     return true;
 }
 
 static MOZ_MUST_USE ReadableStreamDefaultReader*
 CreateReadableStreamDefaultReader(JSContext* cx,
                                   Handle<ReadableStream*> unwrappedStream,
-                                  ForAuthorCodeBool forAuthorCode = ForAuthorCodeBool::No);
+                                  ForAuthorCodeBool forAuthorCode = ForAuthorCodeBool::No,
+                                  HandleObject proto = nullptr);
 
 /**
  * Streams spec, 3.2.5.3. getReader()
  */
 static bool
 ReadableStream_getReader(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
@@ -1779,20 +1786,21 @@ ReadableStreamReaderGenericInitialize(JS
 
 /**
  * Stream spec, 3.5.3. new ReadableStreamDefaultReader ( stream )
  * Steps 2-4.
  */
 static MOZ_MUST_USE ReadableStreamDefaultReader*
 CreateReadableStreamDefaultReader(JSContext* cx,
                                   Handle<ReadableStream*> unwrappedStream,
-                                  ForAuthorCodeBool forAuthorCode)
+                                  ForAuthorCodeBool forAuthorCode,
+                                  HandleObject proto /* = nullptr */)
 {
     Rooted<ReadableStreamDefaultReader*> reader(cx,
-        NewBuiltinClassInstance<ReadableStreamDefaultReader>(cx));
+        NewObjectWithClassProto<ReadableStreamDefaultReader>(cx, proto));
     if (!reader) {
         return nullptr;
     }
 
     // Step 2: If ! IsReadableStreamLocked(stream) is true, throw a TypeError
     //         exception.
     if (unwrappedStream->locked()) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_READABLESTREAM_LOCKED);
@@ -1819,28 +1827,35 @@ bool
 ReadableStreamDefaultReader::constructor(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     if (!ThrowIfNotConstructing(cx, args, "ReadableStreamDefaultReader")) {
         return false;
     }
 
+    // Implicit in the spec: Find the prototype object to use.
+    RootedObject proto(cx);
+    if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) {
+        return false;
+    }
+
     // Step 1: If ! IsReadableStream(stream) is false, throw a TypeError
-    //         exception.
+    // exception.
     Rooted<ReadableStream*> unwrappedStream(cx,
         UnwrapAndTypeCheckArgument<ReadableStream>(cx,
                                                    args,
                                                    "ReadableStreamDefaultReader constructor",
                                                    0));
     if (!unwrappedStream) {
         return false;
     }
 
-    RootedObject reader(cx, CreateReadableStreamDefaultReader(cx, unwrappedStream));
+    RootedObject reader(cx,
+        CreateReadableStreamDefaultReader(cx, unwrappedStream, ForAuthorCodeBool::Yes, proto));
     if (!reader) {
         return false;
     }
 
     args.rval().setObject(*reader);
     return true;
 }
 
@@ -3814,38 +3829,63 @@ ReadableByteStreamControllerInvalidateBY
 
 // Streams spec, 3.12.25.
 //      ReadableByteStreamControllerShouldCallPull ( controller )
 // Unified with 3.9.3 above.
 
 
 /*** 6.1. Queuing strategies ************************************************/
 
+/**
+ * ECMA-262 7.3.4 CreateDataProperty(O, P, V)
+ */
+static MOZ_MUST_USE bool
+CreateDataProperty(JSContext* cx, HandleObject obj, HandlePropertyName key, HandleValue value,
+                   ObjectOpResult& result)
+{
+    RootedId id(cx, NameToId(key));
+    Rooted<PropertyDescriptor> desc(cx);
+    desc.setDataDescriptor(value, JSPROP_ENUMERATE);
+    return DefineProperty(cx, obj, id, desc, result);
+}
+
 // Streams spec, 6.1.2.2. new ByteLengthQueuingStrategy({ highWaterMark })
 bool
 js::ByteLengthQueuingStrategy::constructor(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
-    RootedObject strategy(cx, NewBuiltinClassInstance<ByteLengthQueuingStrategy>(cx));
+    if (!ThrowIfNotConstructing(cx, args, "ByteLengthQueuingStrategy")) {
+        return false;
+    }
+
+    // Implicit in the spec: Create the new strategy object.
+    RootedObject proto(cx);
+    if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) {
+        return false;
+    }
+    RootedObject strategy(cx, NewObjectWithClassProto<ByteLengthQueuingStrategy>(cx, proto));
     if (!strategy) {
         return false;
     }
 
+    // Implicit in the spec: Argument destructuring.
     RootedObject argObj(cx, ToObject(cx, args.get(0)));
     if (!argObj) {
         return false;
     }
-
     RootedValue highWaterMark(cx);
     if (!GetProperty(cx, argObj, argObj, cx->names().highWaterMark, &highWaterMark)) {
         return false;
     }
 
-    if (!SetProperty(cx, strategy, cx->names().highWaterMark, highWaterMark)) {
+    // Step 1: Perform ! CreateDataProperty(this, "highWaterMark",
+    //                                      highWaterMark).
+    ObjectOpResult ignored;
+    if (!CreateDataProperty(cx, strategy, cx->names().highWaterMark, highWaterMark, ignored)) {
         return false;
     }
 
     args.rval().setObject(*strategy);
     return true;
 }
 
 // Streams spec 6.1.2.3.1. size ( chunk )
@@ -3870,32 +3910,44 @@ static const JSFunctionSpec ByteLengthQu
 CLASS_SPEC(ByteLengthQueuingStrategy, 1, 0, 0, 0, JS_NULL_CLASS_OPS);
 
 // Streams spec, 6.1.3.2. new CountQueuingStrategy({ highWaterMark })
 bool
 js::CountQueuingStrategy::constructor(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
-    Rooted<CountQueuingStrategy*> strategy(cx, NewBuiltinClassInstance<CountQueuingStrategy>(cx));
+    if (!ThrowIfNotConstructing(cx, args, "CountQueuingStrategy")) {
+        return false;
+    }
+
+    // Implicit in the spec: Create the new strategy object.
+    RootedObject proto(cx);
+    if (!GetPrototypeFromBuiltinConstructor(cx, args, &proto)) {
+        return false;
+    }
+    Rooted<CountQueuingStrategy*> strategy(cx,
+        NewObjectWithClassProto<CountQueuingStrategy>(cx, proto));
     if (!strategy) {
         return false;
     }
 
+    // Implicit in the spec: Argument destructuring.
     RootedObject argObj(cx, ToObject(cx, args.get(0)));
     if (!argObj) {
         return false;
     }
-
     RootedValue highWaterMark(cx);
     if (!GetProperty(cx, argObj, argObj, cx->names().highWaterMark, &highWaterMark)) {
         return false;
     }
 
-    if (!SetProperty(cx, strategy, cx->names().highWaterMark, highWaterMark)) {
+    // Step 1: Perform ! CreateDataProperty(this, "highWaterMark", highWaterMark).
+    ObjectOpResult ignored;
+    if (!CreateDataProperty(cx, strategy, cx->names().highWaterMark, highWaterMark, ignored)) {
         return false;
     }
 
     args.rval().setObject(*strategy);
     return true;
 }
 
 // Streams spec 6.2.3.3.1. size ( chunk )
new file mode 100644
--- /dev/null
+++ b/js/src/tests/non262/ReadableStream/subclassing.js
@@ -0,0 +1,105 @@
+// Spot-check subclassing of stream constructors.
+
+// ReadableStream can be subclassed.
+class PartyStreamer extends ReadableStream {}
+
+// The base class constructor is called.
+let started = false;
+let stream = new PartyStreamer({
+    // (The ReadableStream constructor calls this start method.)
+    start(c) { started = true; }
+});
+drainJobQueue();
+assertEq(started, true);
+
+// The instance's prototype chain is correct.
+assertEq(stream.__proto__, PartyStreamer.prototype);
+assertEq(stream.__proto__.__proto__, ReadableStream.prototype);
+assertEq(stream.__proto__.__proto__.__proto__, Object.prototype);
+assertEq(stream.__proto__.__proto__.__proto__.__proto__, null);
+assertEq(stream instanceof ReadableStream, true);
+
+// Non-generic methods can be called on the resulting stream.
+let reader = stream.getReader();
+assertEq(stream.locked, true);
+
+
+// CountQueuingStrategy can be subclassed.         
+class PixelStrategy extends CountQueuingStrategy {}
+assertEq(new PixelStrategy({highWaterMark: 4}).__proto__, PixelStrategy.prototype);
+
+// The base class constructor is called.
+assertThrowsInstanceOf(() => new PixelStrategy, TypeError);
+assertEq(new PixelStrategy({highWaterMark: -1}).highWaterMark, -1);
+
+
+// VerySmartStrategy can be subclassed.
+class VerySmartStrategy extends ByteLengthQueuingStrategy {
+    size(chunk) {
+        return super.size(chunk) * 8;
+    }
+}
+let vss = new VerySmartStrategy({highWaterMark: 12});
+assertEq(vss.size(new ArrayBuffer(8)), 64);
+assertEq(vss.__proto__, VerySmartStrategy.prototype);
+
+
+// Even ReadableStreamDefaultReader can be subclassed.
+async function readerTest() {
+    const ReadableStreamDefaultReader = new ReadableStream().getReader().constructor;
+    class MindReader extends ReadableStreamDefaultReader {
+        async read() {
+            let foretold = {value: "death", done: false};
+            let actual = await super.read();
+            actual = foretold; // ZOMG I WAS RIGHT, EXACTLY AS FORETOLD they should call me a righter
+            return actual;
+        }
+    }
+
+    let stream = new ReadableStream({
+        start(c) { c.enqueue("one"); c.enqueue("two"); },
+        pull(c) { c.close(); }
+    });
+    let reader = new MindReader(stream);
+    let result = await reader.read();
+    assertEq(result.value, "death");
+    reader.releaseLock();
+
+    reader = stream.getReader();
+    result = await reader.read();
+    assertEq(result.done, false);
+    assertEq(result.value, "two");
+    result = await reader.read();
+    assertEq(result.done, true);
+    assertEq(result.value, undefined);
+}
+runAsyncTest(readerTest);
+
+
+// Even ReadableStreamDefaultController, which can't be constructed,
+// can be subclassed.
+let ReadableStreamDefaultController;
+new ReadableStream({
+    start(c) {
+        ReadableStreamDefaultController = c.constructor;
+    }
+});
+class MasterController extends ReadableStreamDefaultController {
+    constructor() {
+        // don't call super, it'll just throw
+        return Object.create(MasterController.prototype);
+    }
+}
+let c = new MasterController();
+
+// The prototype chain is per spec.
+assertEq(c instanceof ReadableStreamDefaultController, true);
+
+// But the instance does not have the internal slots of a
+// ReadableStreamDefaultController, so the non-generic methods can't be used.
+assertThrowsInstanceOf(() => c.enqueue("horse"), TypeError);
+
+
+if (typeof reportCompare === 'function') {
+    reportCompare(0, 0);
+}