aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--packages/bun-types/globals.d.ts79
-rw-r--r--src/bun.js/bindings/ZigGlobalObject.cpp51
-rw-r--r--src/js/builtins/EventSource.ts500
-rw-r--r--src/js/out/WebCoreJSBuiltins.cpp18
-rw-r--r--src/js/out/WebCoreJSBuiltins.h81
-rw-r--r--src/js/out/modules/thirdparty/detect-libc.linux.js4
-rw-r--r--test/js/bun/eventsource/eventsource.test.ts153
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();
+ };
+ });
+ });
+});