#include "root.h" #include "BunWritableStream.h" #include "BunWritableStreamDefaultController.h" #include "BunWritableStreamDefaultWriter.h" #include "BunStreamStructures.h" #include "BunStreamInlines.h" namespace Bun { using namespace JSC; // JSWritableStreamPrototype bindings JSC_DEFINE_HOST_FUNCTION(jsWritableStreamPrototypeFunction_abort, (JSGlobalObject * globalObject, CallFrame* callFrame)) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); JSWritableStream* stream = jsDynamicCast(callFrame->thisValue()); if (!stream) return throwVMTypeError(globalObject, scope, "WritableStream.prototype.abort called on non-WritableStream object"_s); JSValue reason = callFrame->argument(0); return JSValue::encode(stream->abort(globalObject, reason)); } JSC_DEFINE_HOST_FUNCTION(jsWritableStreamPrototypeFunction_close, (JSGlobalObject * globalObject, CallFrame* callFrame)) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); JSWritableStream* stream = jsDynamicCast(callFrame->thisValue()); if (!stream) return throwVMTypeError(globalObject, scope, "WritableStream.prototype.close called on non-WritableStream object"_s); return JSValue::encode(stream->close(globalObject)); } JSC_DEFINE_HOST_FUNCTION(jsWritableStreamPrototypeFunction_getWriter, (JSGlobalObject * globalObject, CallFrame* callFrame)) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); JSWritableStream* stream = jsDynamicCast(callFrame->thisValue()); if (!stream) return throwVMTypeError(globalObject, scope, "WritableStream.prototype.getWriter called on non-WritableStream object"_s); if (stream->isLocked()) return throwVMTypeError(globalObject, scope, "Cannot get writer for locked WritableStream"_s); Structure* writerStructure = globalObject->writableStreamDefaultWriterStructure(); auto* writer = JSWritableStreamDefaultWriter::create(vm, globalObject, writerStructure, stream); RETURN_IF_EXCEPTION(scope, {}); stream->setWriter(vm, writer); return JSValue::encode(writer); } JSC_DEFINE_CUSTOM_GETTER(jsWritableStreamPrototypeLockedGetter, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); JSWritableStream* stream = jsDynamicCast(JSValue::decode(thisValue)); if (!stream) return throwVMTypeError(globalObject, scope, "WritableStream.prototype.locked called on non-WritableStream object"_s); return JSValue::encode(jsBoolean(stream->isLocked())); } // Static hash table of properties static const HashTableValue JSWritableStreamPrototypeTableValues[] = { { "abort"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWritableStreamPrototypeFunction_abort, 1 } }, { "close"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWritableStreamPrototypeFunction_close, 0 } }, { "getWriter"_s, static_cast(PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWritableStreamPrototypeFunction_getWriter, 0 } }, { "locked"_s, static_cast(PropertyAttribute::CustomAccessor | PropertyAttribute::ReadOnly), NoIntrinsic, { HashTableValue::GetterSetterType, jsWritableStreamPrototypeLockedGetter, nullptr } } }; class JSWritableStreamPrototype final : public JSNonFinalObject { public: using Base = JSNonFinalObject; static JSWritableStreamPrototype* create(VM& vm, JSGlobalObject* globalObject, Structure* structure); static Structure* createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) { auto* structure = Base::createStructure(vm, globalObject, prototype); structure->setMayBePrototype(true); return structure; } DECLARE_INFO; template static GCClient::IsoSubspace* subspaceFor(VM& vm) { STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSWritableStreamPrototype, Base); return &vm.plainObjectSpace(); } private: JSWritableStreamPrototype(VM& vm, Structure* structure) : Base(vm, structure) { } void finishCreation(VM& vm, JSGlobalObject* globalObject); }; class JSWritableStreamConstructor final : public InternalFunction { public: using Base = InternalFunction; static constexpr unsigned StructureFlags = Base::StructureFlags; static JSWritableStreamConstructor* create(VM&, JSGlobalObject*, Structure*, JSWritableStreamPrototype*); DECLARE_INFO; template static GCClient::IsoSubspace* subspaceFor(VM& vm) { if constexpr (mode == SubspaceAccess::Concurrently) return nullptr; return WebCore::subspaceForImpl( vm, [](auto& spaces) { return spaces.m_clientSubspaceForConstructor.get(); }, [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForConstructor = std::forward(space); }, [](auto& spaces) { return spaces.m_subspaceForConstructor.get(); }, [](auto& spaces, auto&& space) { spaces.m_subspaceForConstructor = std::forward(space); }); } static Structure* createStructure(VM&, JSGlobalObject*, JSValue prototype); static EncodedJSValue JSC_HOST_CALL_ATTRIBUTES construct(JSGlobalObject*, CallFrame*); static EncodedJSValue JSC_HOST_CALL_ATTRIBUTES call(JSGlobalObject*, CallFrame*); private: JSWritableStreamConstructor(VM& vm, Structure* structure); void finishCreation(VM& vm, JSGlobalObject* globalObject, JSWritableStreamPrototype* prototype) { Base::finishCreation(vm, 1, "WritableStream"_s, PropertyAdditionMode::WithStructureTransition); this->putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, 0); } }; // Prototype Implementation const ClassInfo JSWritableStreamPrototype::s_info = { "WritableStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWritableStreamPrototype) }; JSWritableStreamPrototype* JSWritableStreamPrototype::create(VM& vm, JSGlobalObject* globalObject, Structure* structure) { auto* prototype = new (NotNull, allocateCell(vm)) JSWritableStreamPrototype(vm, structure); prototype->finishCreation(vm, globalObject); return prototype; } void JSWritableStreamPrototype::finishCreation(VM& vm, JSGlobalObject* globalObject) { Base::finishCreation(vm); reifyStaticProperties(vm, JSWritableStream::info(), JSWritableStreamPrototypeTableValues, *this); JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); } // Constructor Implementation const ClassInfo JSWritableStreamConstructor::s_info = { "Function"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWritableStreamConstructor) }; JSWritableStreamConstructor::JSWritableStreamConstructor(VM& vm, Structure* structure) : Base(vm, structure, call, construct) { } JSWritableStreamConstructor* JSWritableStreamConstructor::create(VM& vm, JSGlobalObject* globalObject, Structure* structure, JSWritableStreamPrototype* prototype) { JSWritableStreamConstructor* constructor = new (NotNull, allocateCell(vm)) JSWritableStreamConstructor(vm, structure); constructor->finishCreation(vm, globalObject, prototype); return constructor; } Structure* JSWritableStreamConstructor::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) { return Structure::create(vm, globalObject, prototype, TypeInfo(InternalFunctionType, StructureFlags), info()); } JSC_DEFINE_HOST_FUNCTION(jsWritableStreamConstructor, (JSGlobalObject * globalObject, CallFrame* callFrame)) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); JSValue newTarget = callFrame->newTarget(); if (newTarget.isUndefined()) return throwVMTypeError(globalObject, scope, "WritableStream constructor must be called with 'new'"_s); JSObject* underlyingSink = callFrame->argument(0).getObject(); JSValue strategy = callFrame->argument(1); JSObject* constructor = asObject(newTarget); Structure* structure = JSC::InternalFunction::createSubclassStructure(globalObject, newTarget, globalObject->writableStreamStructure()); RETURN_IF_EXCEPTION(scope, {}); JSWritableStream* stream = JSWritableStream::create(vm, globalObject, structure); RETURN_IF_EXCEPTION(scope, {}); // Initialize with underlying sink if provided if (underlyingSink) { // Set up controller with underlying sink... auto controller = JSWritableStreamDefaultController::create(vm, globalObject, stream, underlyingSink); RETURN_IF_EXCEPTION(scope, {}); stream->setController(controller); } return JSValue::encode(stream); } JSC_DEFINE_HOST_FUNCTION(jsWritableStreamPrivateConstructor, (JSGlobalObject * globalObject, CallFrame* callFrame)) { // Similar to above but for internal usage VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); Structure* structure = globalObject->writableStreamStructure(); JSWritableStream* stream = JSWritableStream::create(vm, globalObject, structure); RETURN_IF_EXCEPTION(scope, {}); return JSValue::encode(stream); } // WritableStream implementation const ClassInfo JSWritableStream::s_info = { "WritableStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWritableStream) }; JSWritableStream::JSWritableStream(VM& vm, Structure* structure) : Base(vm, structure) { } void JSWritableStream::finishCreation(VM& vm) { Base::finishCreation(vm); ASSERT(inherits(info())); } JSWritableStream* JSWritableStream::create(VM& vm, JSGlobalObject* globalObject, Structure* structure) { JSWritableStream* stream = new (NotNull, allocateCell(vm)) JSWritableStream(vm, structure); stream->finishCreation(vm); return stream; } void JSWritableStream::destroy(JSCell* cell) { static_cast(cell)->JSWritableStream::~JSWritableStream(); } template void JSWritableStream::visitChildrenImpl(JSCell* cell, Visitor& visitor) { auto* thisObject = jsCast(cell); ASSERT_GC_OBJECT_INHERITS(thisObject, info()); Base::visitChildren(thisObject, visitor); visitor.append(thisObject->m_controller); visitor.append(thisObject->m_writer); visitor.append(thisObject->m_closeRequest); visitor.append(thisObject->m_inFlightWriteRequest); visitor.append(thisObject->m_inFlightCloseRequest); visitor.append(thisObject->m_storedError); } DEFINE_VISIT_CHILDREN(JSWritableStream); Structure* JSWritableStream::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) { return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); } bool JSWritableStream::isLocked() const { return !!m_writer; } JSValue JSWritableStream::error(JSGlobalObject* globalObject, JSValue error) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); if (m_state != State::Writable) return jsUndefined(); m_state = State::Errored; m_storedError.set(vm, this, error); if (m_writer) m_writer->error(globalObject, error); RELEASE_AND_RETURN(scope, jsUndefined()); } namespace Operations { // WritableStreamDefaultControllerErrorSteps(stream.[[writableStreamController]]). void WritableStreamDefaultControllerErrorSteps(JSWritableStreamDefaultController* controller) { // 1. Let stream be controller.[[controlledWritableStream]]. ASSERT(stream); // 2. Assert: stream.[[state]] is "writable". ASSERT(stream->state() == JSWritableStream::State::Writable); // 3. Perform ! WritableStreamStartErroring(stream, controller.[[signal]].[[error]]). WritableStreamStartErroring(stream, controller->signalError()); } JSC_DEFINE_HOST_FUNCTION(jsFunctionResolveAbortPromiseWithUndefined, (JSGlobalObject * globalObject, CallFrame* callFrame)) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); JSPromise* promise = jsDynamicCast(callFrame->argument(1)); promise->fulfillWithNonPromise(globalObject, jsUndefined()); return JSValue::encode(jsUndefined()); } JSC_DEFINE_HOST_FUNCTION(jsFunctionRejectAbortPromiseWithReason, (JSGlobalObject * globalObject, CallFrame* callFrame)) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); JSPromise* promise = jsDynamicCast(callFrame->argument(1)); promise->reject(globalObject, callFrame->argument(0)); return JSValue::encode(jsUndefined()); } static void WritableStreamStartErroring(JSWritableStream* stream, JSValue reason) { // 1. Assert: stream.[[storedError]] is undefined. ASSERT(!stream->storedError() || stream->storedError().isUndefined()); // 2. Assert: stream.[[state]] is "writable". ASSERT(stream->state() == JSWritableStream::State::Writable); // 3. Let controller be stream.[[writableStreamController]]. auto* controller = stream->controller(); ASSERT(controller); // 4. Set stream.[[state]] to "erroring". stream->setState(JSWritableStream::State::Erroring); // 5. Set stream.[[storedError]] to reason. stream->setStoredError(reason); // 6. Let writer be stream.[[writer]]. auto* writer = stream->writer(); // 7. If writer is not undefined, perform ! WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, reason). if (writer) WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, reason); // 8. If ! WritableStreamHasOperationMarkedInFlight(stream) is false and controller.[[started]] is true, // perform ! WritableStreamFinishErroring(stream). if (!stream->hasOperationMarkedInFlight() && controller->started()) WritableStreamFinishErroring(stream); } static void WritableStreamFinishErroring(JSWritableStream* stream) { // 1. Assert: stream.[[state]] is "erroring". ASSERT(stream->state() == JSWritableStream::State::Erroring); // 2. Assert: ! WritableStreamHasOperationMarkedInFlight(stream) is false. ASSERT(!stream->hasOperationMarkedInFlight()); // 3. Set stream.[[state]] to "errored". stream->setState(JSWritableStream::State::Errored); // 4. Perform ! WritableStreamDefaultControllerErrorSteps(stream.[[writableStreamController]]). stream->controller()->errorSteps(); JSValue storedError = stream->storedError(); // 5. Let writer be stream.[[writer]]. auto* writer = stream->writer(); // 6. If writer is not undefined, if (writer) { // a. Let writeRequests be writer.[[writeRequests]]. // b. Set writer.[[writeRequests]] to an empty List. // c. For each writeRequest of writeRequests, // 1. Reject writeRequest with stream.[[storedError]]. writer->rejectWriteRequests(storedError); } JSPromise* abortPromise = stream->pendingAbortRequestPromise(); // 7. Let pendingAbortRequest be stream.[[pendingAbortRequest]]. // 8. If pendingAbortRequest is undefined, return. if (!abortPromise) return; // 9. Set stream.[[pendingAbortRequest]] to undefined. JSValue abortReason = stream->pendingAbortRequestReason(); bool wasAlreadyErroring = stream->wasAlreadyErroring(); stream->clearPendingAbortRequest(); // 10. If pendingAbortRequest.[[wasAlreadyErroring]] is true, if (wasAlreadyErroring) { // a. Reject pendingAbortRequest.[[promise]] with pendingAbortRequest.[[reason]]. abortPromise->(abortReason); // b. Return. return; } // 11. Let abortAlgorithm be stream.[[writableStreamController]].[[abortAlgorithm]]. // 12. Let result be the result of performing abortAlgorithm with argument pendingAbortRequest.[[reason]]. JSValue result = stream->controller()->performAbortAlgorithm(abortReason); // 13. Upon fulfillment of result, // a. Resolve pendingAbortRequest.[[promise]] with undefined. // 14. Upon rejection of result with reason r, // a. Reject pendingAbortRequest.[[promise]] with r. if (JSPromise* resultPromise = jsDynamicCast(result)) { Bun::performPromiseThen(vm, globalObject, resultPromise, jsFunctionResolveAbortPromiseWithUndefined, jsFunctionRejectAbortPromiseWithReason); } else { // If not a promise, treat as fulfilled abortPromise->resolve(jsUndefined()); } } static JSValue WritableStreamAbort(JSGlobalObject* globalObject, JSWritableStream* stream, JSValue reason) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); // 1. Let state be stream.[[state]]. const auto state = stream->state(); // 2. If state is "closed" or state is "errored", return a promise resolved with undefined. if (state == JSWritableStream::State::Closed || state == JSWritableStream::State::Errored) { return JSPromise::resolvedPromise(globalObject, jsUndefined()); } // 3. If stream.[[pendingAbortRequest]] is not undefined, return stream.[[pendingAbortRequest]].[[promise]]. if (auto promise = stream->pendingAbortRequestPromise()) return promise; // 4. Assert: state is "writable" or state is "erroring". ASSERT(state == JSWritableStream::State::Writable || state == JSWritableStream::State::Erroring); // 5. Let wasAlreadyErroring be false. bool wasAlreadyErroring = false; // 6. If state is "erroring", if (state == JSWritableStream::State::Erroring) { // a. Set wasAlreadyErroring to true. wasAlreadyErroring = true; // b. Set reason to undefined. reason = jsUndefined(); } // 7. Let promise be a new promise. JSPromise* promise = JSPromise::create(vm, globalObject->promiseStructure()); // 8. Set stream.[[pendingAbortRequest]] to record {[[promise]]: promise, [[reason]]: reason, // [[wasAlreadyErroring]]: wasAlreadyErroring}. stream->setPendingAbortRequest(vm, promise, reason, wasAlreadyErroring); // 9. If wasAlreadyErroring is false, perform ! WritableStreamStartErroring(stream, reason). if (!wasAlreadyErroring) { WritableStreamStartErroring(stream, reason); RETURN_IF_EXCEPTION(scope, {}); } // 10. If stream.[[state]] is "errored", perform ! WritableStreamFinishErroring(stream). if (stream->state() == JSWritableStream::State::Errored) { WritableStreamFinishErroring(stream); RETURN_IF_EXCEPTION(scope, {}); } // 11. Return promise. return promise; } } JSValue JSWritableStream::abort(JSGlobalObject* globalObject, JSValue reason) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); // 1. If ! IsWritableStreamLocked(this) is true, return a promise rejected with a TypeError exception. if (isLocked()) return JSPromise::rejectedPromise(globalObject, createTypeError(globalObject, "Cannot abort a locked WritableStream"_s)); // 2. Let state be this.[[state]]. const auto state = m_state; // 3. If state is "closed" or state is "errored", return a promise resolved with undefined. if (state == State::Closed || state == State::Errored) return JSPromise::resolvedPromise(globalObject, jsUndefined()); // 4. If this.[[pendingAbortRequest]] is not undefined, return this.[[pendingAbortRequest]].[[promise]]. if (auto promise = m_pendingAbortRequestPromise.get()) return promise; // 5. Assert: state is "writable" or state is "erroring". ASSERT(state == State::Writable || state == State::Erroring); // 6. Let wasAlreadyErroring be false. bool wasAlreadyErroring = false; // 7. If state is "erroring", if (state == State::Erroring) { // a. Set wasAlreadyErroring to true. wasAlreadyErroring = true; // b. Set reason to undefined. reason = jsUndefined(); } // 8. Let promise be a new promise. JSPromise* promise = JSPromise::create(vm, globalObject->promiseStructure()); // 9. Set this.[[pendingAbortRequest]] to record {[[promise]]: promise, [[reason]]: reason, [[wasAlreadyErroring]]: wasAlreadyErroring}. m_pendingAbortRequestPromise.set(vm, this, promise); m_pendingAbortRequestReason.set(vm, this, reason); m_wasAlreadyErroring = wasAlreadyErroring; // 10. If wasAlreadyErroring is false, perform ! WritableStreamStartErroring(this, reason). if (!wasAlreadyErroring) Operations::WritableStreamStartErroring(this, reason); // 11. If this.[[state]] is "errored", perform ! WritableStreamFinishErroring(this). if (m_state == State::Errored) Operations::WritableStreamFinishErroring(this); // 12. Return promise. return promise; } JSValue JSWritableStream::close(JSGlobalObject* globalObject) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); // 1. If ! IsWritableStreamLocked(this) is true, return a promise rejected with a TypeError exception. if (isLocked()) return JSPromise::rejectedPromise(globalObject, createTypeError(globalObject, "Cannot close a locked WritableStream"_s)); // 2. If ! WritableStreamCloseQueuedOrInFlight(this) is true, return a promise rejected with a TypeError exception. if (m_closeRequest || m_inFlightCloseRequest) return JSPromise::rejectedPromise(globalObject, createTypeError(globalObject, "Cannot close an already closing stream"_s)); // 3. Let state be this.[[state]]. const auto state = m_state; // 4. If state is "closed", return a promise rejected with a TypeError exception. if (state == State::Closed) return JSPromise::rejectedPromise(globalObject, createTypeError(globalObject, "Cannot close an already closed stream"_s)); // 5. If state is "errored", return a promise rejected with this.[[storedError]]. if (state == State::Errored) return JSPromise::rejectedPromise(globalObject, m_storedError.get()); // 6. If state is "erroring", return a promise rejected with this.[[storedError]]. if (state == State::Erroring) return JSPromise::rejectedPromise(globalObject, m_storedError.get()); // 7. Assert: state is "writable". ASSERT(state == State::Writable); // 8. Let closeRequest be ! WritableStreamCreateCloseRequest(this). JSPromise* closeRequest = JSPromise::create(vm, globalObject->promiseStructure()); m_closeRequest.set(vm, this, closeRequest); // 9. Perform ! WritableStreamDefaultControllerClose(this.[[controller]]). m_controller->close(globalObject); // 10. Return closeRequest.[[promise]]. return closeRequest; } void JSWritableStream::finishInFlightClose() { VM& vm = m_controller->vm(); JSGlobalObject* globalObject = m_controller->globalObject(); // 1. Assert: this.[[inFlightCloseRequest]] is not undefined. ASSERT(m_inFlightCloseRequest); // 2. Resolve this.[[inFlightCloseRequest]] with undefined. m_inFlightCloseRequest->resolve(globalObject, jsUndefined()); // 3. Set this.[[inFlightCloseRequest]] to undefined. m_inFlightCloseRequest.clear(); // 4. Set this.[[state]] to "closed". m_state = State::Closed; // 5. Let writer be this.[[writer]]. auto* writer = m_writer.get(); // 6. If writer is not undefined, if (writer) { // a. Resolve writer.[[closedPromise]] with undefined. writer->resolveClosedPromise(globalObject, jsUndefined()); } } void JSWritableStream::finishInFlightCloseWithError(JSValue error) { VM& vm = m_controller->vm(); JSGlobalObject* globalObject = m_controller->globalObject(); // 1. Assert: this.[[inFlightCloseRequest]] is not undefined. ASSERT(m_inFlightCloseRequest); // 2. Reject this.[[inFlightCloseRequest]] with error. m_inFlightCloseRequest->reject(globalObject, error); // 3. Set this.[[inFlightCloseRequest]] to undefined. m_inFlightCloseRequest.clear(); // 4. Set this.[[state]] to "errored". m_state = State::Errored; // 5. Set this.[[storedError]] to error. m_storedError.set(vm, this, error); // 6. Let writer be this.[[writer]]. auto* writer = m_writer.get(); // 7. If writer is not undefined, if (writer) { // a. Reject writer.[[closedPromise]] with error. writer->rejectClosedPromise(globalObject, error); } } }