diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | packages/bun-types/globals.d.ts | 79 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGlobalObject.cpp | 51 | ||||
-rw-r--r-- | src/js/builtins/EventSource.ts | 500 | ||||
-rw-r--r-- | src/js/out/WebCoreJSBuiltins.cpp | 18 | ||||
-rw-r--r-- | src/js/out/WebCoreJSBuiltins.h | 81 | ||||
-rw-r--r-- | src/js/out/modules/thirdparty/detect-libc.linux.js | 4 | ||||
-rw-r--r-- | test/js/bun/eventsource/eventsource.test.ts | 153 |
8 files changed, 886 insertions, 2 deletions
diff --git a/.gitignore b/.gitignore index 263b52a61..3b0e270d8 100644 --- a/.gitignore +++ b/.gitignore @@ -121,3 +121,5 @@ cold-jsc-start cold-jsc-start.d /test.ts + +src/js/out/modules_dev diff --git a/packages/bun-types/globals.d.ts b/packages/bun-types/globals.d.ts index 4ca1a8d3e..a5d011b52 100644 --- a/packages/bun-types/globals.d.ts +++ b/packages/bun-types/globals.d.ts @@ -3197,3 +3197,82 @@ declare module "*.txt" { var text: string; export = text; } + +interface EventSourceEventMap { + error: Event; + message: MessageEvent; + open: Event; +} + +interface EventSource extends EventTarget { + onerror: ((this: EventSource, ev: ErrorEvent) => any) | null; + onmessage: ((this: EventSource, ev: MessageEvent) => any) | null; + onopen: ((this: EventSource, ev: Event) => any) | null; + /** Returns the state of this EventSource object's connection. It can have the values described below. */ + readonly readyState: number; + /** Returns the URL providing the event stream. */ + readonly url: string; + /** Returns true if the credentials mode for connection requests to the URL providing the event stream is set to "include", and false otherwise. + * + * Not supported in Bun + * + */ + readonly withCredentials: boolean; + /** Aborts any instances of the fetch algorithm started for this EventSource object, and sets the readyState attribute to CLOSED. */ + close(): void; + readonly CLOSED: number; + readonly CONNECTING: number; + readonly OPEN: number; + addEventListener<K extends keyof EventSourceEventMap>( + type: K, + listener: (this: EventSource, ev: EventSourceEventMap[K]) => any, + options?: boolean | AddEventListenerOptions, + ): void; + addEventListener( + type: string, + listener: (this: EventSource, event: MessageEvent) => any, + options?: boolean | AddEventListenerOptions, + ): void; + addEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions, + ): void; + removeEventListener<K extends keyof EventSourceEventMap>( + type: K, + listener: (this: EventSource, ev: EventSourceEventMap[K]) => any, + options?: boolean | EventListenerOptions, + ): void; + removeEventListener( + type: string, + listener: (this: EventSource, event: MessageEvent) => any, + options?: boolean | EventListenerOptions, + ): void; + removeEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions, + ): void; + + /** + * Keep the event loop alive while connection is open or reconnecting + * + * Not available in browsers + */ + ref(): void; + + /** + * Do not keep the event loop alive while connection is open or reconnecting + * + * Not available in browsers + */ + unref(): void; +} + +declare var EventSource: { + prototype: EventSource; + new (url: string | URL, eventSourceInitDict?: EventSourceInit): EventSource; + readonly CLOSED: number; + readonly CONNECTING: number; + readonly OPEN: number; +}; diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 299ad7a8c..13bb51afd 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -3241,6 +3241,55 @@ JSC_DEFINE_CUSTOM_GETTER(functionBuildMessageGetter, (JSGlobalObject * globalObj return JSValue::encode(reinterpret_cast<Zig::GlobalObject*>(globalObject)->JSBuildMessageConstructor()); } +JSC_DEFINE_CUSTOM_GETTER( + EventSource_getter, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName property)) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + // If "this" is not the Global object, just return undefined + // you should not be able to reset the global object's EventSource if you muck around with prototypes + if (JSValue::decode(thisValue) != globalObject) + return JSValue::encode(JSC::jsUndefined()); + + JSC::JSFunction* getSourceEvent = JSC::JSFunction::create(vm, eventSourceGetEventSourceCodeGenerator(vm), globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + JSC::MarkedArgumentBuffer args; + + auto clientData = WebCore::clientData(vm); + JSC::CallData callData = JSC::getCallData(getSourceEvent); + + NakedPtr<JSC::Exception> returnedException = nullptr; + auto result = JSC::call(globalObject, getSourceEvent, callData, globalObject->globalThis(), args, returnedException); + RETURN_IF_EXCEPTION(scope, {}); + + if (returnedException) { + throwException(globalObject, scope, returnedException.get()); + } + + RETURN_IF_EXCEPTION(scope, {}); + + if (LIKELY(result)) { + globalObject->putDirect(vm, property, result, 0); + } + + RELEASE_AND_RETURN(scope, JSValue::encode(result)); +} + +JSC_DEFINE_CUSTOM_SETTER(EventSource_setter, + (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, + JSC::EncodedJSValue value, JSC::PropertyName property)) +{ + if (JSValue::decode(thisValue) != globalObject) { + return false; + } + + auto& vm = globalObject->vm(); + globalObject->putDirect(vm, property, JSValue::decode(value), 0); + return true; +} + EncodedJSValue GlobalObject::assignToStream(JSValue stream, JSValue controller) { JSC::VM& vm = this->vm(); @@ -3538,6 +3587,8 @@ void GlobalObject::addBuiltinGlobals(JSC::VM& vm) putDirectCustomAccessor(vm, JSC::Identifier::fromString(vm, "$_BunCommonJSModule_$"_s), JSC::CustomGetterSetter::create(vm, BunCommonJSModule_getter, nullptr), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly); + putDirectCustomAccessor(vm, JSC::Identifier::fromString(vm, "EventSource"_s), JSC::CustomGetterSetter::create(vm, EventSource_getter, EventSource_setter), 0); + auto bufferAccessor = JSC::CustomGetterSetter::create(vm, JSBuffer_getter, JSBuffer_setter); auto realBufferAccessor = JSC::CustomGetterSetter::create(vm, JSBuffer_privateGetter, nullptr); diff --git a/src/js/builtins/EventSource.ts b/src/js/builtins/EventSource.ts new file mode 100644 index 000000000..64179bc0d --- /dev/null +++ b/src/js/builtins/EventSource.ts @@ -0,0 +1,500 @@ +/* + * Copyright 2023 Codeblog Corp. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +export function getEventSource() { + type Socket = Awaited<ReturnType<typeof Bun.connect<EventSource>>>; + + class EventSource extends EventTarget { + #url; + #state; + #onerror; + #onmessage; + #onopen; + #is_tls = false; + #socket: Socket | null = null; + #data_buffer = ""; + #send_buffer = ""; + #lastEventID = ""; + #reconnect = true; + #content_length = 0; // 0 means chunked -1 means not informed aka no auto end + #received_length = 0; + #reconnection_time = 0; + #reconnection_timer: Timer | null = null; + + static #ConnectNextTick(self: EventSource) { + self.#connect(); + } + static #SendRequest(socket: Socket, url: URL) { + const self = socket.data; + const last_event_header = self.#lastEventID ? `Last-Event-ID: ${self.#lastEventID}\r\n` : ""; + const request = `GET ${url.pathname}${url.search} HTTP/1.1\r\nHost: bun\r\nContent-type: text/event-stream\r\nContent-length: 0\r\n${last_event_header}\r\n`; + const sended = socket.write(request); + if (sended !== request.length) { + self.#send_buffer = request.substring(sended); + } + } + + static #ProcessChunk(self: EventSource, chunks: string, offset: number) { + for (;;) { + if (offset >= chunks.length) { + return; + } + let chunk_end_idx = -1; + let start_idx = chunks.indexOf("\r\n", offset); + const chunk_start_idx = start_idx + 2; + if (start_idx > 0) { + if (self.#content_length === 0) { + const chunk_size = parseInt(chunks.substring(offset, start_idx), 16); + if (chunk_size === 0) { + // no more chunks + self.#state = 2; + self.#socket?.end(); + return; + } + chunk_end_idx = chunk_start_idx + chunk_size; + } else { + //not chunked + chunk_end_idx = chunks.length; + } + } else { + // wait for the chunk if is chunked + if (self.#data_buffer.length === 0) { + self.#data_buffer += chunks.substring(offset); + return; + } + chunk_end_idx = chunks.length; + } + + // check for chunk end + let chunk = chunks.substring(chunk_start_idx, chunk_end_idx); + offset = chunk_end_idx + 2; + let chunk_offset = 0; + // wait for data end + let event_idx = chunk.indexOf("\n\n"); + if (event_idx == -1) { + // wait for more data + self.#data_buffer += chunks.substring(chunk_start_idx); + return; + } + + // combine data + if (self.#data_buffer.length) { + self.#data_buffer += chunk; + chunk = self.#data_buffer; + self.#data_buffer = ""; + } + + let more_events = true; + while (more_events) { + const event_data = chunk.substring(chunk_offset, event_idx); + + let type; + let data = ""; + let id; + let event_line_idx = 0; + let retry = -1; + for (;;) { + let idx = event_data.indexOf("\n", event_line_idx); + if (idx === -1) { + if (event_line_idx >= event_data.length) { + break; + } + idx = event_data.length; + } + const line = event_data.substring(event_line_idx, idx); + if (line.startsWith("data:")) { + if (data.length) { + data += `\n${line.substring(5).trim()}`; + } else { + data = line.substring(5).trim(); + } + } else if (line.startsWith("event:")) { + type = line.substring(6).trim(); + } else if (line.startsWith("id:")) { + id = line.substring(3).trim(); + } else if (line.startsWith("retry:")) { + retry = parseInt(line.substring(6).trim(), 10); + if (isNaN(retry)) { + retry = -1; + } + } + event_line_idx = idx + 1; + } + self.#lastEventID = id || ""; + if (retry >= 0) { + self.#reconnection_time = retry; + } + + if (data || id || type) { + self.dispatchEvent( + new MessageEvent(type || "message", { + data: data || "", + origin: self.#url.origin, + // @ts-ignore + source: self, + lastEventId: id, + }), + ); + } + + // no more events + if (chunk.length === event_idx + 2) { + more_events = false; + break; + } + + const next_event_idx = chunk.indexOf("\n\n", event_idx + 1); + if (next_event_idx === -1) { + break; + } + chunk_offset = event_idx; + event_idx = next_event_idx; + } + } + } + static #Handlers = { + open(socket: Socket) { + const self = socket.data; + self.#socket = socket; + if (!self.#is_tls) { + EventSource.#SendRequest(socket, self.#url); + } + }, + handshake(socket: Socket, success: boolean, verifyError: Error) { + const self = socket.data; + if (success) { + EventSource.#SendRequest(socket, self.#url); + } else { + self.#state = 2; + self.dispatchEvent(new ErrorEvent("error", { error: verifyError })); + socket.end(); + } + }, + data(socket: Socket, buffer: Buffer) { + const self = socket.data; + switch (self.#state) { + case 0: { + let text = buffer.toString(); + const headers_idx = text.indexOf("\r\n\r\n"); + if (headers_idx === -1) { + // wait headers + self.#data_buffer += text; + return; + } + + if (self.#data_buffer.length) { + self.#data_buffer += text; + text = self.#data_buffer; + self.#data_buffer = ""; + } + const headers = text.substring(0, headers_idx); + const status_idx = headers.indexOf("\r\n"); + + if (status_idx === -1) { + self.#state = 2; + self.dispatchEvent(new ErrorEvent("error", { error: new Error("Invalid HTTP request") })); + socket.end(); + return; + } + const status = headers.substring(0, status_idx); + if (status !== "HTTP/1.1 200 OK") { + self.#state = 2; + self.dispatchEvent(new ErrorEvent("error", { error: new Error(status) })); + socket.end(); + return; + } + + let start_idx = status_idx + 1; + let mime_type_ok = false; + let content_length = -1; + for (;;) { + let header_idx = headers.indexOf("\r\n", start_idx); + // No text/event-stream mime type + if (header_idx === -1) { + if (start_idx >= headers.length) { + if (!mime_type_ok) { + self.#state = 2; + self.dispatchEvent( + new ErrorEvent("error", { + error: new Error( + `EventSource's response has no MIME type and "text/event-stream" is required. Aborting the connection.`, + ), + }), + ); + socket.end(); + } + return; + } + + header_idx = headers.length; + } + + const header = headers.substring(start_idx + 1, header_idx); + const header_name_idx = header.indexOf(":"); + const header_name = header.substring(0, header_name_idx); + const is_content_type = + header_name.localeCompare("content-type", undefined, { sensitivity: "accent" }) === 0; + start_idx = header_idx + 1; + + if (is_content_type) { + if (header.endsWith(" text/event-stream")) { + mime_type_ok = true; + } else { + // wrong mime type + self.#state = 2; + self.dispatchEvent( + new ErrorEvent("error", { + error: new Error( + `EventSource's response has a MIME type that is not "text/event-stream". Aborting the connection.`, + ), + }), + ); + socket.end(); + return; + } + } else { + const is_content_length = + header_name.localeCompare("content-length", undefined, { sensitivity: "accent" }) === 0; + if (is_content_length) { + content_length = parseInt(header.substring(header_name_idx + 1).trim(), 10); + if (isNaN(content_length) || content_length <= 0) { + self.dispatchEvent( + new ErrorEvent("error", { + error: new Error(`EventSource's Content-Length is invalid. Aborting the connection.`), + }), + ); + socket.end(); + return; + } + if (mime_type_ok) { + break; + } + } else { + const is_transfer_encoding = + header_name.localeCompare("transfer-encoding", undefined, { sensitivity: "accent" }) === 0; + if (is_transfer_encoding) { + if (header.substring(header_name_idx + 1).trim() !== "chunked") { + self.dispatchEvent( + new ErrorEvent("error", { + error: new Error(`EventSource's Transfer-Encoding is invalid. Aborting the connection.`), + }), + ); + socket.end(); + return; + } + content_length = 0; + if (mime_type_ok) { + break; + } + } + } + } + } + + self.#content_length = content_length; + self.#state = 1; + self.dispatchEvent(new Event("open")); + const chunks = text.substring(headers_idx + 4); + EventSource.#ProcessChunk(self, chunks, 0); + if (self.#content_length > 0) { + self.#received_length += chunks.length; + if (self.#received_length >= self.#content_length) { + self.#state = 2; + socket.end(); + } + } + return; + } + case 1: + EventSource.#ProcessChunk(self, buffer.toString(), 2); + if (self.#content_length > 0) { + self.#received_length += buffer.byteLength; + if (self.#received_length >= self.#content_length) { + self.#state = 2; + socket.end(); + } + } + return; + default: + break; + } + }, + drain(socket: Socket) { + const self = socket.data; + if (self.#state === 0) { + const request = self.#data_buffer; + if (request.length) { + const sended = socket.write(request); + if (sended !== request.length) { + socket.data.#send_buffer = request.substring(sended); + } else { + socket.data.#send_buffer = ""; + } + } + } + }, + close: EventSource.#Close, + end(socket: Socket) { + EventSource.#Close(socket).dispatchEvent( + new ErrorEvent("error", { error: new Error("Connection closed by server") }), + ); + }, + timeout(socket: Socket) { + EventSource.#Close(socket).dispatchEvent(new ErrorEvent("error", { error: new Error("Timeout") })); + }, + binaryType: "buffer", + }; + + static #Close(socket: Socket) { + const self = socket.data; + self.#socket = null; + self.#received_length = 0; + self.#state = 2; + if (self.#reconnect) { + if (self.#reconnection_timer) { + clearTimeout(self.#reconnection_timer); + } + self.#reconnection_timer = setTimeout(EventSource.#ConnectNextTick, self.#reconnection_time, self); + } + return self; + } + constructor(url: string, options = undefined) { + super(); + const uri = new URL(url); + this.#is_tls = uri.protocol === "https:"; + this.#url = uri; + this.#state = 2; + process.nextTick(EventSource.#ConnectNextTick, this); + } + + // Not web standard + ref() { + this.#reconnection_timer?.ref(); + this.#socket?.ref(); + } + + // Not web standard + unref() { + this.#reconnection_timer?.unref(); + this.#socket?.unref(); + } + + #connect() { + if (this.#state !== 2) return; + const uri = this.#url; + const is_tls = this.#is_tls; + this.#state = 0; + //@ts-ignore + Bun.connect({ + data: this, + socket: EventSource.#Handlers, + hostname: uri.hostname, + port: parseInt(uri.port || (is_tls ? "443" : "80"), 10), + tls: is_tls + ? { + requestCert: true, + rejectUnauthorized: false, + } + : false, + }).catch(err => { + super.dispatchEvent(new ErrorEvent("error", { error: err })); + if (this.#reconnect) { + if (this.#reconnection_timer) { + this.#reconnection_timer.unref?.(); + } + + this.#reconnection_timer = setTimeout(EventSource.#ConnectNextTick, 1000, this); + } + }); + } + + get url() { + return this.#url.href; + } + + get readyState() { + return this.#state; + } + + close() { + this.#reconnect = false; + this.#state = 2; + this.#socket?.unref(); + this.#socket?.end(); + } + + get onopen() { + return this.#onopen; + } + get onerror() { + return this.#onerror; + } + get onmessage() { + return this.#onmessage; + } + + set onopen(cb) { + if (this.#onopen) { + super.removeEventListener("close", this.#onopen); + } + super.addEventListener("open", cb); + this.#onopen = cb; + } + + set onerror(cb) { + if (this.#onerror) { + super.removeEventListener("error", this.#onerror); + } + super.addEventListener("error", cb); + this.#onerror = cb; + } + + set onmessage(cb) { + if (this.#onmessage) { + super.removeEventListener("message", this.#onmessage); + } + super.addEventListener("message", cb); + this.#onmessage = cb; + } + } + + Object.defineProperty(EventSource.prototype, "CONNECTING", { + enumerable: true, + value: 0, + }); + + Object.defineProperty(EventSource.prototype, "OPEN", { + enumerable: true, + value: 1, + }); + + Object.defineProperty(EventSource.prototype, "CLOSED", { + enumerable: true, + value: 2, + }); + + EventSource[Symbol.for("CommonJS")] = 0; + + return EventSource; +} diff --git a/src/js/out/WebCoreJSBuiltins.cpp b/src/js/out/WebCoreJSBuiltins.cpp index b57e346b5..55238274b 100644 --- a/src/js/out/WebCoreJSBuiltins.cpp +++ b/src/js/out/WebCoreJSBuiltins.cpp @@ -2911,6 +2911,24 @@ JSC::FunctionExecutable* codeName##Generator(JSC::VM& vm) \ WEBCORE_FOREACH_WRITABLESTREAMDEFAULTCONTROLLER_BUILTIN_CODE(DEFINE_BUILTIN_GENERATOR) #undef DEFINE_BUILTIN_GENERATOR +/* EventSource.ts */ +// getEventSource +const JSC::ConstructAbility s_eventSourceGetEventSourceCodeConstructAbility = JSC::ConstructAbility::CannotConstruct; +const JSC::ConstructorKind s_eventSourceGetEventSourceCodeConstructorKind = JSC::ConstructorKind::None; +const JSC::ImplementationVisibility s_eventSourceGetEventSourceCodeImplementationVisibility = JSC::ImplementationVisibility::Public; +const int s_eventSourceGetEventSourceCodeLength = 5476; +static const JSC::Intrinsic s_eventSourceGetEventSourceCodeIntrinsic = JSC::NoIntrinsic; +const char* const s_eventSourceGetEventSourceCode = "(function (){\"use strict\";class j extends EventTarget{#$;#j;#w;#A;#B;#F=!1;#G=null;#J=\"\";#K=\"\";#L=\"\";#M=!0;#O=0;#Q=0;#U=0;#V=null;static#W(w){w.#H()}static#X(w,A){const B=w.data,F=B.#L\?`Last-Event-ID: ${B.#L}\\r\\n`:\"\",G=`GET ${A.pathname}${A.search} HTTP/1.1\\r\\nHost: bun\\r\\nContent-type: text/event-stream\\r\\nContent-length: 0\\r\\n${F}\\r\\n`,J=w.write(G);if(J!==G.length)B.#K=G.substring(J)}static#Y(w,A,B){for(;;){if(B>=A.length)return;let F=-1,G=A.indexOf(\"\\r\\n\",B);const J=G+2;if(G>0)if(w.#O===0){const Q=parseInt(A.substring(B,G),16);if(Q===0){w.#j=2,w.#G\?.end();return}F=J+Q}else F=A.length;else{if(w.#J.length===0){w.#J+=A.substring(B);return}F=A.length}let K=A.substring(J,F);B=F+2;let L=0,M=K.indexOf(\"\\n\\n\");if(M==-1){w.#J+=A.substring(J);return}if(w.#J.length)w.#J+=K,K=w.#J,w.#J=\"\";let O=!0;while(O){const Q=K.substring(L,M);let U,V=\"\",W,X=0,Y=-1;for(;;){let z=Q.indexOf(\"\\n\",X);if(z===-1){if(X>=Q.length)break;z=Q.length}const H=Q.substring(X,z);if(H.startsWith(\"data:\"))if(V.length)V+=`\\n${H.substring(5).trim()}`;else V=H.substring(5).trim();else if(H.startsWith(\"event:\"))U=H.substring(6).trim();else if(H.startsWith(\"id:\"))W=H.substring(3).trim();else if(H.startsWith(\"retry:\")){if(Y=parseInt(H.substring(6).trim(),10),@isNaN(Y))Y=-1}X=z+1}if(w.#L=W||\"\",Y>=0)w.#U=Y;if(V||W||U)w.dispatchEvent(new MessageEvent(U||\"message\",{data:V||\"\",origin:w.#$.origin,source:w,lastEventId:W}));if(K.length===M+2){O=!1;break}const Z=K.indexOf(\"\\n\\n\",M+1);if(Z===-1)break;L=M,M=Z}}}static#Z={open(w){const A=w.data;if(A.#G=w,!A.#F)j.#X(w,A.#$)},handshake(w,A,B){const F=w.data;if(A)j.#X(w,F.#$);else F.#j=2,F.dispatchEvent(new ErrorEvent(\"error\",{error:B})),w.end()},data(w,A){const B=w.data;switch(B.#j){case 0:{let F=A.toString();const G=F.indexOf(\"\\r\\n\\r\\n\");if(G===-1){B.#J+=F;return}if(B.#J.length)B.#J+=F,F=B.#J,B.#J=\"\";const J=F.substring(0,G),K=J.indexOf(\"\\r\\n\");if(K===-1){B.#j=2,B.dispatchEvent(new ErrorEvent(\"error\",{error:new Error(\"Invalid HTTP request\")})),w.end();return}const L=J.substring(0,K);if(L!==\"HTTP/1.1 200 OK\"){B.#j=2,B.dispatchEvent(new ErrorEvent(\"error\",{error:new Error(L)})),w.end();return}let M=K+1,O=!1,Q=-1;for(;;){let V=J.indexOf(\"\\r\\n\",M);if(V===-1){if(M>=J.length){if(!O)B.#j=2,B.dispatchEvent(new ErrorEvent(\"error\",{error:new Error(`EventSource's response has no MIME type and \"text/event-stream\" is required. Aborting the connection.`)})),w.end();return}V=J.length}const W=J.substring(M+1,V),X=W.indexOf(\":\"),Y=W.substring(0,X),Z=Y.localeCompare(\"content-type\",@undefined,{sensitivity:\"accent\"})===0;if(M=V+1,Z)if(W.endsWith(\" text/event-stream\"))O=!0;else{B.#j=2,B.dispatchEvent(new ErrorEvent(\"error\",{error:new Error(`EventSource's response has a MIME type that is not \"text/event-stream\". Aborting the connection.`)})),w.end();return}else if(Y.localeCompare(\"content-length\",@undefined,{sensitivity:\"accent\"})===0){if(Q=parseInt(W.substring(X+1).trim(),10),@isNaN(Q)||Q<=0){B.dispatchEvent(new ErrorEvent(\"error\",{error:new Error(`EventSource's Content-Length is invalid. Aborting the connection.`)})),w.end();return}if(O)break}else if(Y.localeCompare(\"transfer-encoding\",@undefined,{sensitivity:\"accent\"})===0){if(W.substring(X+1).trim()!==\"chunked\"){B.dispatchEvent(new ErrorEvent(\"error\",{error:new Error(`EventSource's Transfer-Encoding is invalid. Aborting the connection.`)})),w.end();return}if(Q=0,O)break}}B.#O=Q,B.#j=1,B.dispatchEvent(new Event(\"open\"));const U=F.substring(G+4);if(j.#Y(B,U,0),B.#O>0){if(B.#Q+=U.length,B.#Q>=B.#O)B.#j=2,w.end()}return}case 1:if(j.#Y(B,A.toString(),2),B.#O>0){if(B.#Q+=A.byteLength,B.#Q>=B.#O)B.#j=2,w.end()}return;default:break}},drain(w){const A=w.data;if(A.#j===0){const B=A.#J;if(B.length){const F=w.write(B);if(F!==B.length)w.data.#K=B.substring(F);else w.data.#K=\"\"}}},close:j.#z,end(w){j.#z(w).dispatchEvent(new ErrorEvent(\"error\",{error:new Error(\"Connection closed by server\")}))},timeout(w){j.#z(w).dispatchEvent(new ErrorEvent(\"error\",{error:new Error(\"Timeout\")}))},binaryType:\"buffer\"};static#z(w){const A=w.data;if(A.#G=null,A.#Q=0,A.#j=2,A.#M){if(A.#V)clearTimeout(A.#V);A.#V=setTimeout(j.#W,A.#U,A)}return A}constructor(w,A=@undefined){super();const B=new URL(w);this.#F=B.protocol===\"https:\",this.#$=B,this.#j=2,process.nextTick(j.#W,this)}ref(){this.#V\?.ref(),this.#G\?.ref()}unref(){this.#V\?.unref(),this.#G\?.unref()}#H(){if(this.#j!==2)return;const w=this.#$,A=this.#F;this.#j=0,@Bun.connect({data:this,socket:j.#Z,hostname:w.hostname,port:parseInt(w.port||(A\?\"443\":\"80\"),10),tls:A\?{requestCert:!0,rejectUnauthorized:!1}:!1}).catch((B)=>{if(this.dispatchEvent(new ErrorEvent(\"error\",{error:B})),this.#M){if(this.#V)this.#V.unref\?.();this.#V=setTimeout(j.#W,1000,this)}})}get url(){return this.#$.href}get readyState(){return this.#j}close(){this.#M=!1,this.#j=2,this.#G\?.unref(),this.#G\?.end()}get onopen(){return this.#B}get onerror(){return this.#w}get onmessage(){return this.#A}set onopen(w){if(this.#B)super.removeEventListener(\"close\",this.#B);super.addEventListener(\"open\",w),this.#B=w}set onerror(w){if(this.#w)super.removeEventListener(\"error\",this.#w);super.addEventListener(\"error\",w),this.#w=w}set onmessage(w){if(this.#A)super.removeEventListener(\"message\",this.#A);super.addEventListener(\"message\",w),this.#A=w}}return Object.defineProperty(j.prototype,\"CONNECTING\",{enumerable:!0,value:0}),Object.defineProperty(j.prototype,\"OPEN\",{enumerable:!0,value:1}),Object.defineProperty(j.prototype,\"CLOSED\",{enumerable:!0,value:2}),j[Symbol.for(\"CommonJS\")]=0,j})\n"; + +#define DEFINE_BUILTIN_GENERATOR(codeName, functionName, overriddenName, argumentCount) \ +JSC::FunctionExecutable* codeName##Generator(JSC::VM& vm) \ +{\ + JSVMClientData* clientData = static_cast<JSVMClientData*>(vm.clientData); \ + return clientData->builtinFunctions().eventSourceBuiltins().codeName##Executable()->link(vm, nullptr, clientData->builtinFunctions().eventSourceBuiltins().codeName##Source(), std::nullopt, s_##codeName##Intrinsic); \ +} +WEBCORE_FOREACH_EVENTSOURCE_BUILTIN_CODE(DEFINE_BUILTIN_GENERATOR) +#undef DEFINE_BUILTIN_GENERATOR + JSBuiltinInternalFunctions::JSBuiltinInternalFunctions(JSC::VM& vm) diff --git a/src/js/out/WebCoreJSBuiltins.h b/src/js/out/WebCoreJSBuiltins.h index 3511c6190..e79dc1922 100644 --- a/src/js/out/WebCoreJSBuiltins.h +++ b/src/js/out/WebCoreJSBuiltins.h @@ -5400,6 +5400,84 @@ inline void WritableStreamDefaultControllerBuiltinsWrapper::exportNames() WEBCORE_FOREACH_WRITABLESTREAMDEFAULTCONTROLLER_BUILTIN_FUNCTION_NAME(EXPORT_FUNCTION_NAME) #undef EXPORT_FUNCTION_NAME } +/* EventSource.ts */ +// getEventSource +#define WEBCORE_BUILTIN_EVENTSOURCE_GETEVENTSOURCE 1 +extern const char* const s_eventSourceGetEventSourceCode; +extern const int s_eventSourceGetEventSourceCodeLength; +extern const JSC::ConstructAbility s_eventSourceGetEventSourceCodeConstructAbility; +extern const JSC::ConstructorKind s_eventSourceGetEventSourceCodeConstructorKind; +extern const JSC::ImplementationVisibility s_eventSourceGetEventSourceCodeImplementationVisibility; + +#define WEBCORE_FOREACH_EVENTSOURCE_BUILTIN_DATA(macro) \ + macro(getEventSource, eventSourceGetEventSource, 0) \ + +#define WEBCORE_FOREACH_EVENTSOURCE_BUILTIN_CODE(macro) \ + macro(eventSourceGetEventSourceCode, getEventSource, ASCIILiteral(), s_eventSourceGetEventSourceCodeLength) \ + +#define WEBCORE_FOREACH_EVENTSOURCE_BUILTIN_FUNCTION_NAME(macro) \ + macro(getEventSource) \ + +#define DECLARE_BUILTIN_GENERATOR(codeName, functionName, overriddenName, argumentCount) \ + JSC::FunctionExecutable* codeName##Generator(JSC::VM&); + +WEBCORE_FOREACH_EVENTSOURCE_BUILTIN_CODE(DECLARE_BUILTIN_GENERATOR) +#undef DECLARE_BUILTIN_GENERATOR + +class EventSourceBuiltinsWrapper : private JSC::WeakHandleOwner { +public: + explicit EventSourceBuiltinsWrapper(JSC::VM& vm) + : m_vm(vm) + WEBCORE_FOREACH_EVENTSOURCE_BUILTIN_FUNCTION_NAME(INITIALIZE_BUILTIN_NAMES) +#define INITIALIZE_BUILTIN_SOURCE_MEMBERS(name, functionName, overriddenName, length) , m_##name##Source(JSC::makeSource(StringImpl::createWithoutCopying(s_##name, length), { })) + WEBCORE_FOREACH_EVENTSOURCE_BUILTIN_CODE(INITIALIZE_BUILTIN_SOURCE_MEMBERS) +#undef INITIALIZE_BUILTIN_SOURCE_MEMBERS + { + } + +#define EXPOSE_BUILTIN_EXECUTABLES(name, functionName, overriddenName, length) \ + JSC::UnlinkedFunctionExecutable* name##Executable(); \ + const JSC::SourceCode& name##Source() const { return m_##name##Source; } + WEBCORE_FOREACH_EVENTSOURCE_BUILTIN_CODE(EXPOSE_BUILTIN_EXECUTABLES) +#undef EXPOSE_BUILTIN_EXECUTABLES + + WEBCORE_FOREACH_EVENTSOURCE_BUILTIN_FUNCTION_NAME(DECLARE_BUILTIN_IDENTIFIER_ACCESSOR) + + void exportNames(); + +private: + JSC::VM& m_vm; + + WEBCORE_FOREACH_EVENTSOURCE_BUILTIN_FUNCTION_NAME(DECLARE_BUILTIN_NAMES) + +#define DECLARE_BUILTIN_SOURCE_MEMBERS(name, functionName, overriddenName, length) \ + JSC::SourceCode m_##name##Source;\ + JSC::Weak<JSC::UnlinkedFunctionExecutable> m_##name##Executable; + WEBCORE_FOREACH_EVENTSOURCE_BUILTIN_CODE(DECLARE_BUILTIN_SOURCE_MEMBERS) +#undef DECLARE_BUILTIN_SOURCE_MEMBERS + +}; + +#define DEFINE_BUILTIN_EXECUTABLES(name, functionName, overriddenName, length) \ +inline JSC::UnlinkedFunctionExecutable* EventSourceBuiltinsWrapper::name##Executable() \ +{\ + if (!m_##name##Executable) {\ + JSC::Identifier executableName = functionName##PublicName();\ + if (overriddenName)\ + executableName = JSC::Identifier::fromString(m_vm, overriddenName);\ + m_##name##Executable = JSC::Weak<JSC::UnlinkedFunctionExecutable>(JSC::createBuiltinExecutable(m_vm, m_##name##Source, executableName, s_##name##ImplementationVisibility, s_##name##ConstructorKind, s_##name##ConstructAbility), this, &m_##name##Executable);\ + }\ + return m_##name##Executable.get();\ +} +WEBCORE_FOREACH_EVENTSOURCE_BUILTIN_CODE(DEFINE_BUILTIN_EXECUTABLES) +#undef DEFINE_BUILTIN_EXECUTABLES + +inline void EventSourceBuiltinsWrapper::exportNames() +{ +#define EXPORT_FUNCTION_NAME(name) m_vm.propertyNames->appendExternalName(name##PublicName(), name##PrivateName()); + WEBCORE_FOREACH_EVENTSOURCE_BUILTIN_FUNCTION_NAME(EXPORT_FUNCTION_NAME) +#undef EXPORT_FUNCTION_NAME +} class JSBuiltinFunctions { public: explicit JSBuiltinFunctions(JSC::VM& vm) @@ -5427,6 +5505,7 @@ public: , m_readableStreamDefaultControllerBuiltins(m_vm) , m_readableByteStreamInternalsBuiltins(m_vm) , m_writableStreamDefaultControllerBuiltins(m_vm) + , m_eventSourceBuiltins(m_vm) { m_writableStreamInternalsBuiltins.exportNames(); @@ -5458,6 +5537,7 @@ public: ReadableStreamDefaultControllerBuiltinsWrapper& readableStreamDefaultControllerBuiltins() { return m_readableStreamDefaultControllerBuiltins; } ReadableByteStreamInternalsBuiltinsWrapper& readableByteStreamInternalsBuiltins() { return m_readableByteStreamInternalsBuiltins; } WritableStreamDefaultControllerBuiltinsWrapper& writableStreamDefaultControllerBuiltins() { return m_writableStreamDefaultControllerBuiltins; } + EventSourceBuiltinsWrapper& eventSourceBuiltins() { return m_eventSourceBuiltins; } private: JSC::VM& m_vm; @@ -5484,6 +5564,7 @@ private: ReadableStreamDefaultControllerBuiltinsWrapper m_readableStreamDefaultControllerBuiltins; ReadableByteStreamInternalsBuiltinsWrapper m_readableByteStreamInternalsBuiltins; WritableStreamDefaultControllerBuiltinsWrapper m_writableStreamDefaultControllerBuiltins; + EventSourceBuiltinsWrapper m_eventSourceBuiltins; ; }; diff --git a/src/js/out/modules/thirdparty/detect-libc.linux.js b/src/js/out/modules/thirdparty/detect-libc.linux.js index 1c4e18a7a..987ae9aa6 100644 --- a/src/js/out/modules/thirdparty/detect-libc.linux.js +++ b/src/js/out/modules/thirdparty/detect-libc.linux.js @@ -2,13 +2,13 @@ function family() { return Promise.resolve(familySync()); } function familySync() { - return null; + return GLIBC; } function versionAsync() { return Promise.resolve(version()); } function version() { - return null; + return "2.29"; } function isNonGlibcLinuxSync() { return !1; diff --git a/test/js/bun/eventsource/eventsource.test.ts b/test/js/bun/eventsource/eventsource.test.ts new file mode 100644 index 000000000..2f5b7d755 --- /dev/null +++ b/test/js/bun/eventsource/eventsource.test.ts @@ -0,0 +1,153 @@ +function sse(req: Request) { + const signal = req.signal; + return new Response( + new ReadableStream({ + type: "direct", + async pull(controller) { + while (!signal.aborted) { + await controller.write(`data:Hello, World!\n\n`); + await controller.write(`event: bun\ndata: Hello, World!\n\n`); + await controller.write(`event: lines\ndata: Line 1!\ndata: Line 2!\n\n`); + await controller.write(`event: id_test\nid:1\n\n`); + await controller.flush(); + await Bun.sleep(100); + } + controller.close(); + }, + }), + { status: 200, headers: { "Content-Type": "text/event-stream" } }, + ); +} + +function sse_unstable(req: Request) { + const signal = req.signal; + let id = parseInt(req.headers.get("last-event-id") || "0", 10); + + return new Response( + new ReadableStream({ + type: "direct", + async pull(controller) { + if (!signal.aborted) { + await controller.write(`id:${++id}\ndata: Hello, World!\nretry:100\n\n`); + await controller.flush(); + } + controller.close(); + }, + }), + { status: 200, headers: { "Content-Type": "text/event-stream" } }, + ); +} + +function sseServer( + done: (err?: unknown) => void, + pathname: string, + callback: (evtSource: EventSource, done: (err?: unknown) => void) => void, +) { + const server = Bun.serve({ + port: 0, + fetch(req) { + if (new URL(req.url).pathname === "/stream") { + return sse(req); + } + if (new URL(req.url).pathname === "/unstable") { + return sse_unstable(req); + } + return new Response("Hello, World!"); + }, + }); + let evtSource: EventSource | undefined; + try { + evtSource = new EventSource(`http://localhost:${server.port}${pathname}`); + callback(evtSource, err => { + try { + done(err); + evtSource?.close(); + } catch (err) { + done(err); + } finally { + server.stop(true); + } + }); + } catch (err) { + evtSource?.close(); + server.stop(true); + done(err); + } +} + +import { describe, expect, it } from "bun:test"; + +describe("events", () => { + it("should call open", done => { + sseServer(done, "/stream", (evtSource, done) => { + evtSource.onopen = () => { + done(); + }; + evtSource.onerror = err => { + done(err); + }; + }); + }); + + it("should call message", done => { + sseServer(done, "/stream", (evtSource, done) => { + evtSource.onmessage = e => { + expect(e.data).toBe("Hello, World!"); + done(); + }; + }); + }); + + it("should call custom event", done => { + sseServer(done, "/stream", (evtSource, done) => { + evtSource.addEventListener("bun", e => { + expect(e.data).toBe("Hello, World!"); + done(); + }); + }); + }); + + it("should call event with multiple lines", done => { + sseServer(done, "/stream", (evtSource, done) => { + evtSource.addEventListener("lines", e => { + expect(e.data).toBe("Line 1!\nLine 2!"); + done(); + }); + }); + }); + + it("should receive id", done => { + sseServer(done, "/stream", (evtSource, done) => { + evtSource.addEventListener("id_test", e => { + expect(e.lastEventId).toBe("1"); + done(); + }); + }); + }); + + it("should reconnect with id", done => { + sseServer(done, "/unstable", (evtSource, done) => { + const ids: string[] = []; + evtSource.onmessage = e => { + ids.push(e.lastEventId); + if (ids.length === 2) { + for (let i = 0; i < 2; i++) { + expect(ids[i]).toBe((i + 1).toString()); + } + done(); + } + }; + }); + }); + + it("should call error", done => { + sseServer(done, "/", (evtSource, done) => { + evtSource.onerror = e => { + expect(e.error.message).toBe( + `EventSource's response has a MIME type that is not "text/event-stream". Aborting the connection.`, + ); + done(); + }; + }); + }); +}); |