1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2  * vim: set ts=8 sts=2 et sw=2 tw=80:
3  * This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 /* Class ReadableStream. */
8 
9 #include "builtin/streams/ReadableStream.h"
10 
11 #include "mozilla/Attributes.h"  // MOZ_MUST_USE
12 
13 #include "jsapi.h"        // JS_ReportErrorNumberASCII
14 #include "jsfriendapi.h"  // js::GetErrorMessage, JSMSG_*
15 #include "jspubtd.h"      // JSProto_ReadableStream
16 
17 #include "builtin/Array.h"                   // js::NewDenseFullyAllocatedArray
18 #include "builtin/streams/ClassSpecMacro.h"  // JS_STREAMS_CLASS_SPEC
19 #include "builtin/streams/MiscellaneousOperations.h"  // js::MakeSizeAlgorithmFromSizeFunction, js::ValidateAndNormalizeHighWaterMark, js::ReturnPromiseRejectedWithPendingError
20 #include "builtin/streams/ReadableStreamController.h"  // js::ReadableStream{,Default}Controller, js::ReadableByteStreamController
21 #include "builtin/streams/ReadableStreamDefaultControllerOperations.h"  // js::SetUpReadableStreamDefaultControllerFromUnderlyingSource
22 #include "builtin/streams/ReadableStreamInternals.h"  // js::ReadableStreamCancel
23 #include "builtin/streams/ReadableStreamOperations.h"  // js::ReadableStream{PipeTo,Tee}
24 #include "builtin/streams/ReadableStreamReader.h"  // js::CreateReadableStream{BYOB,Default}Reader, js::ForAuthorCodeBool
25 #include "builtin/streams/WritableStream.h"  // js::WritableStream
26 #include "js/CallArgs.h"                     // JS::CallArgs{,FromVp}
27 #include "js/Class.h"  // JSCLASS_PRIVATE_IS_NSISUPPORTS, JSCLASS_HAS_PRIVATE, JS_NULL_CLASS_OPS
28 #include "js/Conversions.h"  // JS::ToBoolean
29 #include "js/PropertySpec.h"  // JS{Function,Property}Spec, JS_FN, JS_PSG, JS_{FS,PS}_END
30 #include "js/RootingAPI.h"        // JS::Handle, JS::Rooted, js::CanGC
31 #include "js/Stream.h"            // JS::ReadableStream{Mode,UnderlyingSource}
32 #include "js/Value.h"             // JS::Value
33 #include "vm/JSContext.h"         // JSContext
34 #include "vm/JSObject.h"          // js::GetPrototypeFromBuiltinConstructor
35 #include "vm/ObjectOperations.h"  // js::GetProperty
36 #include "vm/PlainObject.h"       // js::PlainObject
37 #include "vm/Runtime.h"           // JSAtomState
38 #include "vm/StringType.h"        // js::EqualStrings, js::ToString
39 
40 #include "vm/Compartment-inl.h"   // js::UnwrapAndTypeCheck{Argument,This}
41 #include "vm/JSObject-inl.h"      // js::NewBuiltinClassInstance
42 #include "vm/NativeObject-inl.h"  // js::ThrowIfNotConstructing
43 
44 using js::CanGC;
45 using js::ClassSpec;
46 using js::CreateReadableStreamDefaultReader;
47 using js::EqualStrings;
48 using js::ForAuthorCodeBool;
49 using js::GetErrorMessage;
50 using js::NativeObject;
51 using js::NewBuiltinClassInstance;
52 using js::NewDenseFullyAllocatedArray;
53 using js::PlainObject;
54 using js::ReadableStream;
55 using js::ReadableStreamPipeTo;
56 using js::ReadableStreamTee;
57 using js::ReturnPromiseRejectedWithPendingError;
58 using js::ToString;
59 using js::UnwrapAndTypeCheckArgument;
60 using js::UnwrapAndTypeCheckThis;
61 using js::WritableStream;
62 
63 using JS::CallArgs;
64 using JS::CallArgsFromVp;
65 using JS::Handle;
66 using JS::ObjectValue;
67 using JS::Rooted;
68 using JS::Value;
69 
70 /*** 3.2. Class ReadableStream **********************************************/
71 
mode() const72 JS::ReadableStreamMode ReadableStream::mode() const {
73   ReadableStreamController* controller = this->controller();
74   if (controller->is<ReadableStreamDefaultController>()) {
75     return JS::ReadableStreamMode::Default;
76   }
77   return controller->as<ReadableByteStreamController>().hasExternalSource()
78              ? JS::ReadableStreamMode::ExternalSource
79              : JS::ReadableStreamMode::Byte;
80 }
81 
createExternalSourceStream(JSContext * cx,JS::ReadableStreamUnderlyingSource * source,void * nsISupportsObject_alreadyAddreffed,Handle<JSObject * > proto)82 ReadableStream* ReadableStream::createExternalSourceStream(
83     JSContext* cx, JS::ReadableStreamUnderlyingSource* source,
84     void* nsISupportsObject_alreadyAddreffed /* = nullptr */,
85     Handle<JSObject*> proto /* = nullptr */) {
86   Rooted<ReadableStream*> stream(
87       cx, create(cx, nsISupportsObject_alreadyAddreffed, proto));
88   if (!stream) {
89     return nullptr;
90   }
91 
92   if (!SetUpExternalReadableByteStreamController(cx, stream, source)) {
93     return nullptr;
94   }
95 
96   return stream;
97 }
98 
99 /**
100  * Streams spec, 3.2.3. new ReadableStream(underlyingSource = {}, strategy = {})
101  */
constructor(JSContext * cx,unsigned argc,JS::Value * vp)102 bool ReadableStream::constructor(JSContext* cx, unsigned argc, JS::Value* vp) {
103   CallArgs args = CallArgsFromVp(argc, vp);
104 
105   if (!ThrowIfNotConstructing(cx, args, "ReadableStream")) {
106     return false;
107   }
108 
109   // Implicit in the spec: argument default values.
110   Rooted<Value> underlyingSource(cx, args.get(0));
111   if (underlyingSource.isUndefined()) {
112     JSObject* emptyObj = NewBuiltinClassInstance<PlainObject>(cx);
113     if (!emptyObj) {
114       return false;
115     }
116     underlyingSource = ObjectValue(*emptyObj);
117   }
118 
119   Rooted<Value> strategy(cx, args.get(1));
120   if (strategy.isUndefined()) {
121     JSObject* emptyObj = NewBuiltinClassInstance<PlainObject>(cx);
122     if (!emptyObj) {
123       return false;
124     }
125     strategy = ObjectValue(*emptyObj);
126   }
127 
128   // Implicit in the spec: Set this to
129   //     OrdinaryCreateFromConstructor(NewTarget, ...).
130   // Step 1: Perform ! InitializeReadableStream(this).
131   Rooted<JSObject*> proto(cx);
132   if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_ReadableStream,
133                                           &proto)) {
134     return false;
135   }
136   Rooted<ReadableStream*> stream(cx,
137                                  ReadableStream::create(cx, nullptr, proto));
138   if (!stream) {
139     return false;
140   }
141 
142   // Step 2: Let size be ? GetV(strategy, "size").
143   Rooted<Value> size(cx);
144   if (!GetProperty(cx, strategy, cx->names().size, &size)) {
145     return false;
146   }
147 
148   // Step 3: Let highWaterMark be ? GetV(strategy, "highWaterMark").
149   Rooted<Value> highWaterMarkVal(cx);
150   if (!GetProperty(cx, strategy, cx->names().highWaterMark,
151                    &highWaterMarkVal)) {
152     return false;
153   }
154 
155   // Step 4: Let type be ? GetV(underlyingSource, "type").
156   Rooted<Value> type(cx);
157   if (!GetProperty(cx, underlyingSource, cx->names().type, &type)) {
158     return false;
159   }
160 
161   // Step 5: Let typeString be ? ToString(type).
162   Rooted<JSString*> typeString(cx, ToString<CanGC>(cx, type));
163   if (!typeString) {
164     return false;
165   }
166 
167   // Step 6: If typeString is "bytes",
168   bool equal;
169   if (!EqualStrings(cx, typeString, cx->names().bytes, &equal)) {
170     return false;
171   }
172   if (equal) {
173     // The rest of step 6 is unimplemented, since we don't support
174     // user-defined byte streams yet.
175     JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
176                               JSMSG_READABLESTREAM_BYTES_TYPE_NOT_IMPLEMENTED);
177     return false;
178   }
179 
180   // Step 7: Otherwise, if type is undefined,
181   if (type.isUndefined()) {
182     // Step 7.a: Let sizeAlgorithm be ? MakeSizeAlgorithmFromSizeFunction(size).
183     if (!MakeSizeAlgorithmFromSizeFunction(cx, size)) {
184       return false;
185     }
186 
187     // Step 7.b: If highWaterMark is undefined, let highWaterMark be 1.
188     double highWaterMark;
189     if (highWaterMarkVal.isUndefined()) {
190       highWaterMark = 1;
191     } else {
192       // Step 7.c: Set highWaterMark to ?
193       // ValidateAndNormalizeHighWaterMark(highWaterMark).
194       if (!ValidateAndNormalizeHighWaterMark(cx, highWaterMarkVal,
195                                              &highWaterMark)) {
196         return false;
197       }
198     }
199 
200     // Step 7.d: Perform
201     //           ? SetUpReadableStreamDefaultControllerFromUnderlyingSource(
202     //           this, underlyingSource, highWaterMark, sizeAlgorithm).
203     if (!SetUpReadableStreamDefaultControllerFromUnderlyingSource(
204             cx, stream, underlyingSource, highWaterMark, size)) {
205       return false;
206     }
207 
208     args.rval().setObject(*stream);
209     return true;
210   }
211 
212   // Step 8: Otherwise, throw a RangeError exception.
213   JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
214                             JSMSG_READABLESTREAM_UNDERLYINGSOURCE_TYPE_WRONG);
215   return false;
216 }
217 
218 /**
219  * Streams spec, 3.2.5.1. get locked
220  */
ReadableStream_locked(JSContext * cx,unsigned argc,JS::Value * vp)221 static MOZ_MUST_USE bool ReadableStream_locked(JSContext* cx, unsigned argc,
222                                                JS::Value* vp) {
223   CallArgs args = CallArgsFromVp(argc, vp);
224 
225   // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception.
226   Rooted<ReadableStream*> unwrappedStream(
227       cx, UnwrapAndTypeCheckThis<ReadableStream>(cx, args, "get locked"));
228   if (!unwrappedStream) {
229     return false;
230   }
231 
232   // Step 2: Return ! IsReadableStreamLocked(this).
233   args.rval().setBoolean(unwrappedStream->locked());
234   return true;
235 }
236 
237 /**
238  * Streams spec, 3.2.5.2. cancel ( reason )
239  */
ReadableStream_cancel(JSContext * cx,unsigned argc,JS::Value * vp)240 static MOZ_MUST_USE bool ReadableStream_cancel(JSContext* cx, unsigned argc,
241                                                JS::Value* vp) {
242   CallArgs args = CallArgsFromVp(argc, vp);
243 
244   // Step 1: If ! IsReadableStream(this) is false, return a promise rejected
245   //         with a TypeError exception.
246   Rooted<ReadableStream*> unwrappedStream(
247       cx, UnwrapAndTypeCheckThis<ReadableStream>(cx, args, "cancel"));
248   if (!unwrappedStream) {
249     return ReturnPromiseRejectedWithPendingError(cx, args);
250   }
251 
252   // Step 2: If ! IsReadableStreamLocked(this) is true, return a promise
253   //         rejected with a TypeError exception.
254   if (unwrappedStream->locked()) {
255     JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
256                               JSMSG_READABLESTREAM_LOCKED_METHOD, "cancel");
257     return ReturnPromiseRejectedWithPendingError(cx, args);
258   }
259 
260   // Step 3: Return ! ReadableStreamCancel(this, reason).
261   Rooted<JSObject*> cancelPromise(
262       cx, js::ReadableStreamCancel(cx, unwrappedStream, args.get(0)));
263   if (!cancelPromise) {
264     return false;
265   }
266   args.rval().setObject(*cancelPromise);
267   return true;
268 }
269 
270 // Streams spec, 3.2.5.3.
271 //      getIterator({ preventCancel } = {})
272 //
273 // Not implemented.
274 
275 /**
276  * Streams spec, 3.2.5.4. getReader({ mode } = {})
277  */
ReadableStream_getReader(JSContext * cx,unsigned argc,JS::Value * vp)278 static MOZ_MUST_USE bool ReadableStream_getReader(JSContext* cx, unsigned argc,
279                                                   JS::Value* vp) {
280   CallArgs args = CallArgsFromVp(argc, vp);
281 
282   // Implicit in the spec: Argument defaults and destructuring.
283   Rooted<Value> optionsVal(cx, args.get(0));
284   if (optionsVal.isUndefined()) {
285     JSObject* emptyObj = NewBuiltinClassInstance<PlainObject>(cx);
286     if (!emptyObj) {
287       return false;
288     }
289     optionsVal.setObject(*emptyObj);
290   }
291   Rooted<Value> modeVal(cx);
292   if (!GetProperty(cx, optionsVal, cx->names().mode, &modeVal)) {
293     return false;
294   }
295 
296   // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception.
297   Rooted<ReadableStream*> unwrappedStream(
298       cx, UnwrapAndTypeCheckThis<ReadableStream>(cx, args, "getReader"));
299   if (!unwrappedStream) {
300     return false;
301   }
302 
303   // Step 2: If mode is undefined, return
304   //         ? AcquireReadableStreamDefaultReader(this, true).
305   Rooted<JSObject*> reader(cx);
306   if (modeVal.isUndefined()) {
307     reader = CreateReadableStreamDefaultReader(cx, unwrappedStream,
308                                                ForAuthorCodeBool::Yes);
309   } else {
310     // Step 3: Set mode to ? ToString(mode) (implicit).
311     Rooted<JSString*> mode(cx, ToString<CanGC>(cx, modeVal));
312     if (!mode) {
313       return false;
314     }
315 
316     // Step 5: (If mode is not "byob",) Throw a RangeError exception.
317     bool equal;
318     if (!EqualStrings(cx, mode, cx->names().byob, &equal)) {
319       return false;
320     }
321     if (!equal) {
322       JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
323                                 JSMSG_READABLESTREAM_INVALID_READER_MODE);
324       return false;
325     }
326 
327     // Step 4: If mode is "byob",
328     //         return ? AcquireReadableStreamBYOBReader(this, true).
329     reader = CreateReadableStreamBYOBReader(cx, unwrappedStream,
330                                             ForAuthorCodeBool::Yes);
331   }
332 
333   // Reordered second part of steps 2 and 4.
334   if (!reader) {
335     return false;
336   }
337   args.rval().setObject(*reader);
338   return true;
339 }
340 
341 // Streams spec, 3.2.5.5.
342 //      pipeThrough({ writable, readable },
343 //                  { preventClose, preventAbort, preventCancel, signal })
344 //
345 // Not implemented.
346 
347 /**
348  * Streams spec, 3.2.5.6.
349  *      pipeTo(dest, { preventClose, preventAbort, preventCancel, signal } = {})
350  */
ReadableStream_pipeTo(JSContext * cx,unsigned argc,Value * vp)351 static bool ReadableStream_pipeTo(JSContext* cx, unsigned argc, Value* vp) {
352   CallArgs args = CallArgsFromVp(argc, vp);
353 
354   // Implicit in the spec: argument default values.
355   Rooted<Value> options(cx, args.get(1));
356   if (options.isUndefined()) {
357     JSObject* emptyObj = NewBuiltinClassInstance<PlainObject>(cx);
358     if (!emptyObj) {
359       return false;
360     }
361     options.setObject(*emptyObj);
362   }
363   // Step 3 (reordered).
364   // Implicit in the spec: get the values of the named parameters inside the
365   // second argument destructuring pattern.  But as |ToBoolean| is infallible
366   // and has no observable side effects, we may as well do step 3 here too.
367   bool preventClose, preventAbort, preventCancel;
368   Rooted<Value> signalVal(cx);
369   {
370     // (P)(Re)use the |signal| root.
371     auto& v = signalVal;
372 
373     if (!GetProperty(cx, options, cx->names().preventClose, &v)) {
374       return false;
375     }
376     preventClose = JS::ToBoolean(v);
377 
378     if (!GetProperty(cx, options, cx->names().preventAbort, &v)) {
379       return false;
380     }
381     preventAbort = JS::ToBoolean(v);
382 
383     if (!GetProperty(cx, options, cx->names().preventCancel, &v)) {
384       return false;
385     }
386     preventCancel = JS::ToBoolean(v);
387   }
388   if (!GetProperty(cx, options, cx->names().signal, &signalVal)) {
389     return false;
390   }
391 
392   // Step 1: If ! IsReadableStream(this) is false, return a promise rejected
393   //         with a TypeError exception.
394   Rooted<ReadableStream*> unwrappedThis(
395       cx, UnwrapAndTypeCheckThis<ReadableStream>(cx, args, "pipeTo"));
396   if (!unwrappedThis) {
397     return ReturnPromiseRejectedWithPendingError(cx, args);
398   }
399 
400   // Step 2: If ! IsWritableStream(dest) is false, return a promise rejected
401   //         with a TypeError exception.
402   Rooted<WritableStream*> unwrappedDest(
403       cx, UnwrapAndTypeCheckArgument<WritableStream>(cx, args, "pipeTo", 0));
404   if (!unwrappedDest) {
405     return ReturnPromiseRejectedWithPendingError(cx, args);
406   }
407 
408   // Step 3: Set preventClose to ! ToBoolean(preventClose), set preventAbort to
409   //         ! ToBoolean(preventAbort), and set preventCancel to
410   //         ! ToBoolean(preventCancel).
411   // This already happened above.
412 
413   // Step 4: If signal is not undefined, and signal is not an instance of the
414   //         AbortSignal interface, return a promise rejected with a TypeError
415   //         exception.
416   Rooted<JSObject*> signal(cx, nullptr);
417   do {
418     if (signalVal.isUndefined()) {
419       break;
420     }
421 
422     if (signalVal.isObject()) {
423       // XXX jwalden need some JSAPI hooks to detect AbortSignal instances, or
424       //             something
425 
426       signal = &signalVal.toObject();
427     }
428 
429     JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
430                               JSMSG_READABLESTREAM_PIPETO_BAD_SIGNAL);
431     return ReturnPromiseRejectedWithPendingError(cx, args);
432   } while (false);
433 
434   // Step 5: If ! IsReadableStreamLocked(this) is true, return a promise
435   //         rejected with a TypeError exception.
436   if (unwrappedThis->locked()) {
437     JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
438                               JSMSG_READABLESTREAM_LOCKED_METHOD, "pipeTo");
439     return ReturnPromiseRejectedWithPendingError(cx, args);
440   }
441 
442   // Step 6: If ! IsWritableStreamLocked(dest) is true, return a promise
443   //         rejected with a TypeError exception.
444   if (unwrappedDest->isLocked()) {
445     JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
446                               JSMSG_WRITABLESTREAM_ALREADY_LOCKED);
447     return ReturnPromiseRejectedWithPendingError(cx, args);
448   }
449 
450   // Step 7: Return
451   //         ! ReadableStreamPipeTo(this, dest, preventClose, preventAbort,
452   //                                preventCancel, signal).
453   JSObject* promise =
454       ReadableStreamPipeTo(cx, unwrappedThis, unwrappedDest, preventClose,
455                            preventAbort, preventCancel, signal);
456   if (!promise) {
457     return false;
458   }
459 
460   args.rval().setObject(*promise);
461   return true;
462 }
463 
464 /**
465  * Streams spec, 3.2.5.7. tee()
466  */
ReadableStream_tee(JSContext * cx,unsigned argc,Value * vp)467 static bool ReadableStream_tee(JSContext* cx, unsigned argc, Value* vp) {
468   CallArgs args = CallArgsFromVp(argc, vp);
469 
470   // Step 1: If ! IsReadableStream(this) is false, throw a TypeError exception.
471   Rooted<ReadableStream*> unwrappedStream(
472       cx, UnwrapAndTypeCheckThis<ReadableStream>(cx, args, "tee"));
473   if (!unwrappedStream) {
474     return false;
475   }
476 
477   // Step 2: Let branches be ? ReadableStreamTee(this, false).
478   Rooted<ReadableStream*> branch1(cx);
479   Rooted<ReadableStream*> branch2(cx);
480   if (!ReadableStreamTee(cx, unwrappedStream, false, &branch1, &branch2)) {
481     return false;
482   }
483 
484   // Step 3: Return ! CreateArrayFromList(branches).
485   Rooted<NativeObject*> branches(cx, NewDenseFullyAllocatedArray(cx, 2));
486   if (!branches) {
487     return false;
488   }
489   branches->setDenseInitializedLength(2);
490   branches->initDenseElement(0, ObjectValue(*branch1));
491   branches->initDenseElement(1, ObjectValue(*branch2));
492 
493   args.rval().setObject(*branches);
494   return true;
495 }
496 
497 // Streams spec, 3.2.5.8.
498 //      [@@asyncIterator]({ preventCancel } = {})
499 //
500 // Not implemented.
501 
502 static const JSFunctionSpec ReadableStream_methods[] = {
503     JS_FN("cancel", ReadableStream_cancel, 1, 0),
504     JS_FN("getReader", ReadableStream_getReader, 0, 0),
505     // pipeTo is only conditionally supported right now, so it must be manually
506     // added below if desired.
507     JS_FN("tee", ReadableStream_tee, 0, 0), JS_FS_END};
508 
509 static const JSPropertySpec ReadableStream_properties[] = {
510     JS_PSG("locked", ReadableStream_locked, 0), JS_PS_END};
511 
FinishReadableStreamClassInit(JSContext * cx,Handle<JSObject * > ctor,Handle<JSObject * > proto)512 static bool FinishReadableStreamClassInit(JSContext* cx, Handle<JSObject*> ctor,
513                                           Handle<JSObject*> proto) {
514   // This function and everything below should be replaced with
515   //
516   // JS_STREAMS_CLASS_SPEC(ReadableStream, 0, SlotCount, 0,
517   //                       JSCLASS_PRIVATE_IS_NSISUPPORTS | JSCLASS_HAS_PRIVATE,
518   //                       JS_NULL_CLASS_OPS);
519   //
520   // when "pipeTo" is always enabled.
521   const auto& rco = cx->realm()->creationOptions();
522   if (rco.getStreamsEnabled() && rco.getWritableStreamsEnabled() &&
523       rco.getReadableStreamPipeToEnabled()) {
524     Rooted<jsid> pipeTo(cx, NameToId(cx->names().pipeTo));
525     if (!DefineFunction(cx, proto, pipeTo, ReadableStream_pipeTo, 2,
526                         JSPROP_RESOLVING)) {
527       return false;
528     }
529   }
530 
531   return true;
532 }
533 
534 const ClassSpec ReadableStream::classSpec_ = {
535     js::GenericCreateConstructor<ReadableStream::constructor, 2,
536                                  js::gc::AllocKind::FUNCTION>,
537     js::GenericCreatePrototype<ReadableStream>,
538     nullptr,
539     nullptr,
540     ReadableStream_methods,
541     ReadableStream_properties,
542     FinishReadableStreamClassInit,
543     0};
544 
545 const JSClass ReadableStream::class_ = {
546     "ReadableStream",
547     JSCLASS_HAS_RESERVED_SLOTS(ReadableStream::SlotCount) |
548         JSCLASS_HAS_CACHED_PROTO(JSProto_ReadableStream) |
549         JSCLASS_PRIVATE_IS_NSISUPPORTS | JSCLASS_HAS_PRIVATE,
550     JS_NULL_CLASS_OPS, &ReadableStream::classSpec_};
551 
552 const JSClass ReadableStream::protoClass_ = {
553     "object", JSCLASS_HAS_CACHED_PROTO(JSProto_ReadableStream),
554     JS_NULL_CLASS_OPS, &ReadableStream::classSpec_};
555