[Streams API] Add support for chunks with customized sizes
[WebKit-https.git] / Source / WebCore / bindings / js / ReadableJSStream.cpp
1 /*
2  * Copyright (C) 2015 Canon Inc.
3  * Copyright (C) 2015 Igalia S.L.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted, provided that the following conditions
7  * are required to be met:
8  *
9  * 1.  Redistributions of source code must retain the above copyright
10  *     notice, this list of conditions and the following disclaimer.
11  * 2.  Redistributions in binary form must reproduce the above copyright
12  *     notice, this list of conditions and the following disclaimer in the
13  *     documentation and/or other materials provided with the distribution.
14  * 3.  Neither the name of Canon Inc. nor the names of
15  *     its contributors may be used to endorse or promote products derived
16  *     from this software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY CANON INC. AND ITS CONTRIBUTORS "AS IS" AND ANY
19  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21  * DISCLAIMED. IN NO EVENT SHALL CANON INC. AND ITS CONTRIBUTORS BE LIABLE FOR
22  * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28  */
29
30 #include "config.h"
31 #include "ReadableJSStream.h"
32
33 #if ENABLE(STREAMS_API)
34
35 #include "DOMWrapperWorld.h"
36 #include "ExceptionCode.h"
37 #include "JSDOMPromise.h"
38 #include "JSReadableStream.h"
39 #include "JSReadableStreamController.h"
40 #include "ScriptExecutionContext.h"
41 #include <runtime/Error.h>
42 #include <runtime/Exception.h>
43 #include <runtime/JSCJSValueInlines.h>
44 #include <runtime/JSPromise.h>
45 #include <runtime/JSString.h>
46 #include <runtime/StructureInlines.h>
47
48 using namespace JSC;
49
50 namespace WebCore {
51
52 static inline JSValue getPropertyFromObject(ExecState& exec, JSObject& object, const char* identifier)
53 {
54     return object.get(&exec, Identifier::fromString(&exec, identifier));
55 }
56
57 static inline JSValue callFunction(ExecState& exec, JSValue jsFunction, JSValue thisValue, const ArgList& arguments)
58 {
59     CallData callData;
60     CallType callType = getCallData(jsFunction, callData);
61     return call(&exec, jsFunction, callType, callData, thisValue, arguments);
62 }
63
64 JSPromise* ReadableJSStream::invoke(ExecState& state, const char* propertyName, JSValue parameter)
65 {
66     JSValue function = getPropertyFromObject(state, *m_source.get(), propertyName);
67     if (state.hadException())
68         return nullptr;
69
70     if (!function.isFunction()) {
71         if (!function.isUndefined())
72             throwVMError(&state, createTypeError(&state, ASCIILiteral("ReadableStream trying to call a property that is not callable")));
73         return nullptr;
74     }
75
76     MarkedArgumentBuffer arguments;
77     arguments.append(parameter);
78
79     JSPromise* promise = jsDynamicCast<JSPromise*>(callFunction(state, function, m_source.get(), arguments));
80
81     ASSERT(!(promise && state.hadException()));
82     return promise;
83 }
84
85 static void thenPromise(ExecState& state, JSPromise* deferredPromise, JSValue fullfilFunction, JSValue rejectFunction)
86 {
87     JSValue thenValue = deferredPromise->get(&state, state.vm().propertyNames->then);
88     if (state.hadException())
89         return;
90
91     MarkedArgumentBuffer arguments;
92     arguments.append(fullfilFunction);
93     arguments.append(rejectFunction);
94
95     callFunction(state, thenValue, deferredPromise, arguments);
96 }
97
98 JSDOMGlobalObject* ReadableJSStream::globalObject()
99 {
100     return jsDynamicCast<JSDOMGlobalObject*>(m_source->globalObject());
101 }
102
103 static inline JSFunction* createStartResultFulfilledFunction(ExecState& state, ReadableStream& readableStream)
104 {
105     RefPtr<ReadableStream> stream = &readableStream;
106     return JSFunction::create(state.vm(), state.callee()->globalObject(), 1, String(), [stream](ExecState*) {
107         stream->start();
108         return JSValue::encode(jsUndefined());
109     });
110 }
111
112 static inline void startReadableStreamAsync(ReadableStream& stream)
113 {
114     RefPtr<ReadableStream> protectedStream = &stream;
115     stream.scriptExecutionContext()->postTask([protectedStream](ScriptExecutionContext&) {
116         protectedStream->start();
117     });
118 }
119
120 void ReadableJSStream::doStart(ExecState& exec)
121 {
122     JSLockHolder lock(&exec);
123
124     JSPromise* promise = invoke(exec, "start", jsController(exec, globalObject()));
125
126     if (exec.hadException())
127         return;
128
129     if (!promise) {
130         startReadableStreamAsync(*this);
131         return;
132     }
133
134     thenPromise(exec, promise, createStartResultFulfilledFunction(exec, *this), m_errorFunction.get());
135 }
136
137 static inline JSFunction* createPullResultFulfilledFunction(ExecState& exec, ReadableJSStream& stream)
138 {
139     RefPtr<ReadableJSStream> protectedStream = &stream;
140     return JSFunction::create(exec.vm(), exec.callee()->globalObject(), 0, String(), [protectedStream](ExecState*) {
141         protectedStream->finishPulling();
142         return JSValue::encode(jsUndefined());
143     });
144 }
145
146 bool ReadableJSStream::doPull()
147 {
148     ExecState& state = *globalObject()->globalExec();
149     JSLockHolder lock(&state);
150
151     JSPromise* promise = invoke(state, "pull", jsController(state, globalObject()));
152
153     if (promise)
154         thenPromise(state, promise, createPullResultFulfilledFunction(state, *this), m_errorFunction.get());
155
156     if (state.hadException()) {
157         storeException(state);
158         ASSERT(!state.hadException());
159         return true;
160     }
161
162     return !promise;
163 }
164
165 static JSFunction* createCancelResultFulfilledFunction(ExecState& exec, ReadableJSStream& stream)
166 {
167     RefPtr<ReadableJSStream> protectedStream = &stream;
168     return JSFunction::create(exec.vm(), exec.callee()->globalObject(), 1, String(), [protectedStream](ExecState*) {
169         protectedStream->notifyCancelSucceeded();
170         return JSValue::encode(jsUndefined());
171     });
172 }
173
174 static JSFunction* createCancelResultRejectedFunction(ExecState& exec, ReadableJSStream& stream)
175 {
176     RefPtr<ReadableJSStream> protectedStream = &stream;
177     return JSFunction::create(exec.vm(), exec.callee()->globalObject(), 1, String(), [protectedStream](ExecState* exec) {
178         protectedStream->storeError(*exec, exec->argument(0));
179         protectedStream->notifyCancelFailed();
180         return JSValue::encode(jsUndefined());
181     });
182 }
183
184 bool ReadableJSStream::doCancel(JSValue reason)
185 {
186     ExecState& exec = *globalObject()->globalExec();
187     JSLockHolder lock(&exec);
188
189     JSPromise* promise = invoke(exec, "cancel", reason);
190
191     if (promise)
192         thenPromise(exec, promise, createCancelResultFulfilledFunction(exec, *this), createCancelResultRejectedFunction(exec, *this));
193
194     if (exec.hadException()) {
195         storeException(exec);
196         ASSERT(!exec.hadException());
197         return true;
198     }
199     return !promise;
200 }
201
202 static inline double normalizeHighWaterMark(ExecState& exec, JSObject& strategy)
203 {
204     JSValue jsHighWaterMark = getPropertyFromObject(exec, strategy, "highWaterMark");
205
206     if (exec.hadException())
207         return 0;
208
209     if (jsHighWaterMark.isUndefined())
210         return 1;
211
212     double highWaterMark = jsHighWaterMark.toNumber(&exec);
213
214     if (exec.hadException())
215         return 0;
216
217     if (std::isnan(highWaterMark)) {
218         throwVMError(&exec, createTypeError(&exec, ASCIILiteral("Value is NaN")));
219         return 0;
220     }
221     if (highWaterMark < 0) {
222         throwVMError(&exec, createRangeError(&exec, ASCIILiteral("Not a positive value")));
223         return 0;
224     }
225     return highWaterMark;
226 }
227
228 RefPtr<ReadableJSStream> ReadableJSStream::create(ExecState& state, ScriptExecutionContext& scriptExecutionContext)
229 {
230     // FIXME: We should consider reducing the binding code herei (using Dictionary/regular binding constructor and/or improving the IDL generator). 
231     JSObject* jsSource;
232     JSValue value = state.argument(0);
233     if (value.isObject())
234         jsSource = value.getObject();
235     else if (!value.isUndefined()) {
236         throwVMError(&state, createTypeError(&state, ASCIILiteral("First argument, if any, should be an object")));
237         return nullptr;
238     } else
239         jsSource = JSFinalObject::create(state.vm(), JSFinalObject::createStructure(state.vm(), state.callee()->globalObject(), jsNull(), 1));
240
241     double highWaterMark = 1;
242     JSFunction* sizeFunction = nullptr;
243     value = state.argument(1);
244     if (value.isObject()) {
245         JSObject& strategyObject = *value.getObject();
246         highWaterMark = normalizeHighWaterMark(state, strategyObject);
247         if (state.hadException())
248             return nullptr;
249
250         if (!(sizeFunction = jsDynamicCast<JSFunction*>(getPropertyFromObject(state, strategyObject, "size")))) {
251             if (!state.hadException())
252                 throwVMError(&state, createTypeError(&state, ASCIILiteral("size parameter should be a function")));
253             return nullptr;
254         }
255         
256     } else if (!value.isUndefined()) {
257         throwVMError(&state, createTypeError(&state, ASCIILiteral("Second argument, if any, should be an object")));
258         return nullptr;
259     }
260
261     RefPtr<ReadableJSStream> readableStream = adoptRef(*new ReadableJSStream(scriptExecutionContext, state, jsSource, highWaterMark, sizeFunction));
262     readableStream->doStart(state);
263
264     if (state.hadException())
265         return nullptr;
266
267     return readableStream;
268 }
269
270 ReadableJSStream::ReadableJSStream(ScriptExecutionContext& scriptExecutionContext, ExecState& state, JSObject* source, double highWaterMark, JSFunction* sizeFunction)
271     : ReadableStream(scriptExecutionContext)
272     , m_highWaterMark(highWaterMark)
273 {
274     m_source.set(state.vm(), source);
275     // We do not take a Ref to the stream as this would cause a Ref cycle.
276     // The resolution callback used jointly with m_errorFunction as promise callbacks should protect the stream instead.
277     m_errorFunction.set(state.vm(), JSFunction::create(state.vm(), state.callee()->globalObject(), 1, String(), [this](ExecState* state) {
278         storeError(*state, state->argument(0));
279         return JSValue::encode(jsUndefined());
280     }));
281     if (sizeFunction)
282         m_sizeFunction.set(state.vm(), sizeFunction);
283 }
284
285 JSValue ReadableJSStream::jsController(ExecState& exec, JSDOMGlobalObject* globalObject)
286 {
287     if (!m_controller)
288         m_controller = std::make_unique<ReadableStreamController>(*this);
289     return toJS(&exec, globalObject, m_controller.get());
290 }
291
292 void ReadableJSStream::close(ExceptionCode& ec)
293 {
294     if (isCloseRequested() || isErrored()) {
295         ec = TypeError;
296         return;
297     }
298     changeStateToClosed();
299 }
300
301 void ReadableJSStream::storeException(JSC::ExecState& state)
302 {
303     JSValue exception = state.exception()->value();
304     state.clearException();
305     storeError(state, exception);
306 }
307
308 void ReadableJSStream::storeError(JSC::ExecState& exec, JSValue error)
309 {
310     if (m_error)
311         return;
312     m_error.set(exec.vm(), error);
313
314     changeStateToErrored();
315 }
316
317 bool ReadableJSStream::hasValue() const
318 {
319     return m_chunkQueue.size();
320 }
321
322 JSValue ReadableJSStream::read()
323 {
324     ASSERT(hasValue());
325
326     Chunk chunk = m_chunkQueue.takeFirst();
327     m_totalQueueSize -= chunk.size;
328
329     return chunk.value.get();
330 }
331
332 void ReadableJSStream::enqueue(ExecState& state)
333 {
334     ASSERT(!isCloseRequested());
335
336     if (!isReadable())
337         return;
338
339     JSValue chunk = state.argument(0);
340     if (resolveReadCallback(chunk)) {
341         pull();
342         return;
343     }
344
345     double size = retrieveChunkSize(state, chunk);
346     if (state.hadException()) {
347         storeError(state, state.exception()->value());
348         return;
349     }
350
351     m_chunkQueue.append({ JSC::Strong<JSC::Unknown>(state.vm(), chunk), size });
352     m_totalQueueSize += size;
353
354     pull();
355 }
356
357 double ReadableJSStream::retrieveChunkSize(ExecState& state, JSValue chunk)
358 {
359     if (!m_sizeFunction)
360         return 1;
361
362     MarkedArgumentBuffer arguments;
363     arguments.append(chunk);
364
365     JSValue sizeValue = callFunction(state, m_sizeFunction.get(), jsUndefined(), arguments);
366     if (state.hadException())
367         return 0;
368
369     double size = sizeValue.toNumber(&state);
370     if (state.hadException())
371         return 0;
372
373     if (!std::isfinite(size)) {
374         throwVMError(&state, createRangeError(&state, ASCIILiteral("Incorrect double value")));
375         return 0;
376     }
377
378     return size;
379 }
380
381 } // namespace WebCore
382
383 #endif