Something went wrong. Try again.
javascript for 🐜's, a tiny runtime with big ambitions antjs.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465#include <compat.h> // IWYU pragma: keep
#include <stdbool.h>#include <stddef.h>#include <stdint.h>#include <stdlib.h>#include <string.h>#include <strings.h>#include <uv.h>
#include "ant.h"#include "cfunc.h"#include "list.h"#include "loop.h"#include "ptr.h"#include "common.h"#include "errors.h"#include "internal.h"#include "inspector.h"
#include "http/eventsource.h"#include "modules/events.h"#include "modules/eventsource.h"#include "modules/http.h"#include "modules/timer.h"#include "modules/url.h"
typedef struct eventsource_timer_s { uv_timer_t handle; struct eventsource_state_s *source;} eventsource_timer_t;
struct eventsource_state_s { ant_value_t obj; ant_http_request_t *request; eventsource_timer_t *timer; uint64_t network_request_id; size_t network_encoded_length; ant_sse_parser_t parser; char *url; char *last_event_id; eventsource_state_t *next; eventsource_state_t **pprev; uint32_t retry_ms; uint8_t ready_state; bool with_credentials : 1; bool closed : 1; bool opened_request : 1; bool error_emitted_for_request : 1;};
static constexpr uint32_t ES_NATIVE_TAG = Ant_NativeTag("EVSR");enum { ES_CONNECTING = 0, ES_OPEN = 1, ES_CLOSED = 2 };
static eventsource_state_t *eventsource_data(ant_value_t value) { return (eventsource_state_t *)js_get_native(value, ES_NATIVE_TAG);}
static void eventsource_maybe_unroot(eventsource_state_t *es) { if (!es || !es->closed) return; if (es->request || es->timer) return; Ant_List_Remove(es);}
static void eventsource_sync_state(ant_t *js, eventsource_state_t *es) { if (!es || !is_object_type(es->obj)) return; js_set(js, es->obj, "readyState", js_mknum(es->ready_state));}
static void eventsource_emit(ant_t *js, eventsource_state_t *es, ant_value_t event) { Ant_EventTarget_Dispatch(js, es->obj, event); process_microtasks(js);}
static void eventsource_emit_simple(ant_t *js, eventsource_state_t *es, const char *type) { ant_value_t event = Ant_EventTarget_CreateEvent(js, es->obj, js->builtins.event_proto, ANT_EVENT_BASE, type); if (is_object_type(event)) eventsource_emit(js, es, event);}
static void eventsource_emit_error_once(ant_t *js, eventsource_state_t *es) { if (!es || es->closed || es->error_emitted_for_request) return; es->error_emitted_for_request = true; eventsource_emit_simple(js, es, "error");}
static bool eventsource_content_type_ok(const char *value) { static const char expected[] = "text/event-stream"; size_t expected_len = sizeof(expected) - 1; if (!value) return false; while (*value == ' ' || *value == '\t') value++; return strncasecmp(value, expected, expected_len) == 0;}
static bool eventsource_is_http_url(const char *url) { return url && (strncmp(url, "http://", 7) == 0 || strncmp(url, "https://", 8) == 0);}
static const char *eventsource_find_header(const ant_http_header_t *headers, const char *name) { for (const ant_http_header_t *entry = headers; entry; entry = entry->next) { if (entry->name && name && strcasecmp(entry->name, name) == 0) return entry->value; } return NULL;}
static void eventsource_start_request(ant_t *js, eventsource_state_t *es);
static void eventsource_timer_close_cb(uv_handle_t *handle) { free(handle);}
static void eventsource_reconnect_cb(uv_timer_t *handle) { eventsource_timer_t *timer = (eventsource_timer_t *)handle; eventsource_state_t *es = timer->source; if (es && es->timer == timer) es->timer = NULL; uv_timer_stop(handle); uv_close((uv_handle_t *)handle, eventsource_timer_close_cb);
if (!es || es->closed) { eventsource_maybe_unroot(es); return; } eventsource_start_request(Ant_Loop_Isolate(handle->loop), es);}
static void eventsource_cancel_timer(eventsource_state_t *es) { eventsource_timer_t *timer = es ? es->timer : NULL; if (!timer) return; es->timer = NULL; uv_timer_stop(&timer->handle); uv_close((uv_handle_t *)&timer->handle, eventsource_timer_close_cb);}
static void eventsource_schedule_reconnect(ant_t *js, eventsource_state_t *es) { if (!es || es->closed || es->timer) return; eventsource_timer_t *timer = calloc(1, sizeof(*timer)); if (!timer) { es->closed = true; es->ready_state = ES_CLOSED; eventsource_sync_state(js, es); eventsource_maybe_unroot(es); return; } timer->source = es; uv_timer_init(js->loop, &timer->handle); es->timer = timer; es->ready_state = ES_CONNECTING; eventsource_sync_state(js, es); uv_timer_start(&timer->handle, eventsource_reconnect_cb, es->retry_ms, 0);}
typedef struct { ant_t *js; eventsource_state_t *es;} eventsource_feed_t;
static bool eventsource_on_message(const ant_sse_message_t *message, void *user_data) { eventsource_feed_t *feed = (eventsource_feed_t *)user_data; eventsource_state_t *es = feed->es; ant_t *js = feed->js; const char *type = (message->event && message->event[0]) ? message->event : "message";
if (message->id) { free(es->last_event_id); es->last_event_id = strdup(message->id); } else if (es->parser.last_event_id) { free(es->last_event_id); es->last_event_id = strdup(es->parser.last_event_id); }
if (!es->last_event_id) es->last_event_id = strdup(""); ant_value_t event = Ant_EventTarget_CreateEvent(js, es->obj, js->builtins.message_event_proto, ANT_EVENT_MESSAGE, type); if (!is_object_type(event)) return !es->closed;
Ant_Event_SetField( js, event, ANT_EVENT_FIELD_MESSAGE_data, js_mkstr(js, message->data ? message->data : "", message->data ? strlen(message->data) : 0) );
Ant_Event_SetField( js, event, ANT_EVENT_FIELD_MESSAGE_lastEventId, js_mkstr(js, es->last_event_id ? es->last_event_id : "", es->last_event_id ? strlen(es->last_event_id) : 0) ); Ant_Event_SetField(js, event, ANT_EVENT_FIELD_MESSAGE_origin, js_mkstr(js, "", 0)); eventsource_emit(js, es, event); return !es->closed;}
static void eventsource_http_on_response(ant_http_request_t *req, const ant_http_response_t *resp, void *user_data) { eventsource_state_t *es = (eventsource_state_t *)user_data; ant_t *js = Ant_Loop_Isolate(Ant_Http_RequestLoop(req)); const char *content_type = eventsource_find_header(resp->headers, "content-type");
if (!es || es->closed) return; ant_inspector_network_response( es->network_request_id, es->url, resp->status, resp->status_text, content_type ? content_type : "text/event-stream", "EventSource", resp->headers ); if (resp->status != 200 || !eventsource_content_type_ok(content_type)) { eventsource_emit_error_once(js, es); ant_http_request_cancel(req); return; }
es->opened_request = true; es->ready_state = ES_OPEN; eventsource_sync_state(js, es); eventsource_emit_simple(js, es, "open");}
static void eventsource_http_on_body(ant_http_request_t *req, const uint8_t *chunk, size_t len, void *user_data) { eventsource_state_t *es = (eventsource_state_t *)user_data; if (!es || es->closed || !es->opened_request) return; es->network_encoded_length += len; ant_inspector_network_append_response_body(es->network_request_id, chunk, len); eventsource_feed_t feed = { Ant_Loop_Isolate(Ant_Http_RequestLoop(req)), es }; if (!ant_sse_parser_feed(&es->parser, (const char *)chunk, len, eventsource_on_message, &feed)) { eventsource_emit_error_once(feed.js, es); ant_http_request_cancel(req); return; } if (es->parser.has_retry) es->retry_ms = es->parser.retry;}
static void eventsource_http_on_complete( ant_http_request_t *req, ant_http_result_t result, int error_code, const char *error_message, void *user_data) { eventsource_state_t *es = (eventsource_state_t *)user_data; ant_t *js = Ant_Loop_Isolate(Ant_Http_RequestLoop(req));
if (!es) return; es->request = NULL; es->opened_request = false; if (es->network_request_id) { if (result == ANT_HTTP_RESULT_OK && error_code == 0) ant_inspector_network_finish( es->network_request_id, es->network_encoded_length ); else ant_inspector_network_fail( es->network_request_id, error_message ? error_message : (es->closed ? "EventSource closed" : "EventSource connection failed"), es->closed || result == ANT_HTTP_RESULT_ABORTED, "EventSource" ); es->network_request_id = 0; es->network_encoded_length = 0; }
if (!es->closed) { if (result != ANT_HTTP_RESULT_ABORTED) eventsource_emit_error_once(js, es); es->error_emitted_for_request = false; ant_sse_parser_free(&es->parser); ant_sse_parser_init(&es->parser); eventsource_schedule_reconnect(js, es); } eventsource_maybe_unroot(es);}
static void eventsource_start_request(ant_t *js, eventsource_state_t *es) { ant_http_header_t last_id = {0}; ant_http_header_t accept = { "Accept", "text/event-stream", NULL }; ant_http_request_options_t options = {0}; int rc = 0;
if (!es || es->closed || es->request) return; es->ready_state = ES_CONNECTING; es->error_emitted_for_request = false; eventsource_sync_state(js, es);
if (es->last_event_id && es->last_event_id[0]) { last_id.name = "Last-Event-ID"; last_id.value = es->last_event_id; accept.next = &last_id; }
es->network_encoded_length = 0; es->network_request_id = ant_inspector_network_request( "GET", es->url, "EventSource", "EventSource", false, &accept );
options.method = "GET"; options.url = es->url; options.headers = &accept; if (!js->http_pool) js->http_pool = ant_http_pool_create(js->loop); options.pool = js->http_pool;
rc = ant_http_request_start( js->loop, &options, eventsource_http_on_response, eventsource_http_on_body, eventsource_http_on_complete, es, &es->request ); if (rc != 0) { ant_inspector_network_fail(es->network_request_id, uv_strerror(rc), false, "EventSource"); es->network_request_id = 0; es->request = NULL; eventsource_emit_error_once(js, es); es->error_emitted_for_request = false; eventsource_schedule_reconnect(js, es); }}
static void eventsource_close(ant_t *js, eventsource_state_t *es) { if (!es || es->closed) return; es->closed = true; es->ready_state = ES_CLOSED; eventsource_sync_state(js, es); eventsource_cancel_timer(es); if (es->request) ant_http_request_cancel(es->request); eventsource_maybe_unroot(es);}
static void eventsource_finalize(ant_t *js, ant_object_t *obj) { ant_value_t value = js_obj_from_ptr(obj); eventsource_state_t *es = eventsource_data(value); if (!es) return; js_clear_native(value, ES_NATIVE_TAG); if (es->request) ant_http_request_cancel(es->request); eventsource_cancel_timer(es); Ant_List_Remove(es); ant_sse_parser_free(&es->parser); free(es->url); free(es->last_event_id); free(es);}
static ant_value_t eventsource_create_object(ant_t *js) { ant_value_t obj = Ant_Object_CreateBare(js); if (is_object_type(js->builtins.eventsource_proto)) js_set_proto_init(obj, js->builtins.eventsource_proto); js_set_slot(obj, SLOT_BRAND, js_mknum(BRAND_EVENTTARGET)); js_set_finalizer(obj, eventsource_finalize); return obj;}
static ant_value_t js_eventsource_ctor(ant_params_t) { if (vtype(call_new_target) == kTypeUndefined) return js_mkerr_typed(js, JS_ERR_TYPE, "EventSource constructor requires 'new'"); if (nargs < 1) return js_mkerr_typed(js, JS_ERR_TYPE, "EventSource URL is required");
ant_value_t url_val = js_tostring_val(js, args[0]); if (is_err(url_val)) return url_val; const char *input = js_getstr(js, url_val, NULL); url_state_t parsed = {0}; if (!input || Ant_URL_ParseDocumentRelative(js, input, &parsed) != 0) { url_state_clear(&parsed); return js_mkerr_typed(js, JS_ERR_SYNTAX, "Failed to construct 'EventSource': Cannot open an EventSource to '%s'. The URL is invalid.", input ? input : ""); } char *href = build_href(&parsed); url_state_clear(&parsed); if (!href) return js_mkerr(js, "oom"); if (!eventsource_is_http_url(href)) { free(href); return js_mkerr_typed(js, JS_ERR_SYNTAX, "EventSource URL must use http: or https:"); } url_val = js_mkstr(js, href, strlen(href)); free(href); const char *url = js_getstr(js, url_val, NULL);
ant_value_t obj = eventsource_create_object(js); eventsource_state_t *es = calloc(1, sizeof(*es)); if (!es) return js_mkerr_typed(js, JS_ERR_TYPE, "Out of memory"); es->obj = obj; es->url = strdup(url); es->last_event_id = strdup(""); es->retry_ms = 3000; es->ready_state = ES_CONNECTING; if (nargs > 1 && is_object_type(args[1])) { ant_value_t with_credentials = js_get(js, args[1], "withCredentials"); if (vtype(with_credentials) != kTypeUndefined) es->with_credentials = js_truthy(js, with_credentials); } if (!es->url || !es->last_event_id) { free(es->url); free(es->last_event_id); free(es); return js_mkerr_typed(js, JS_ERR_TYPE, "Out of memory"); }
ant_sse_parser_init(&es->parser); js_set_native(obj, es, ES_NATIVE_TAG); js_set(js, obj, "url", url_val); js_set(js, obj, "withCredentials", js_bool(es->with_credentials)); eventsource_sync_state(js, es); Ant_List_Insert(&js->active_eventsources, es); eventsource_start_request(js, es); return obj;}
static ant_value_t js_eventsource_close(ant_params_t) { eventsource_state_t *es = eventsource_data(js_getthis(js)); if (!es) return js_mkerr_typed(js, JS_ERR_TYPE, "Invalid EventSource"); eventsource_close(js, es); return js_mkundef();}
static ant_value_t eventsource_invalid_this(ant_t *js, ant_value_t this_obj) { return eventsource_data(this_obj) ? 0 : js_mkerr_typed(js, JS_ERR_TYPE, "Invalid EventSource");}
ANT_EVENT_HANDLER_ACCESSORS(eventsource, open, eventsource_invalid_this)ANT_EVENT_HANDLER_ACCESSORS(eventsource, message, eventsource_invalid_this)ANT_EVENT_HANDLER_ACCESSORS(eventsource, error, eventsource_invalid_this)
void init_eventsource_module(ant_t *js) { ant_value_t eventtarget_proto = js->builtins.eventtarget_proto; js->builtins.eventsource_proto = Ant_Object_CreateBare(js); if (is_object_type(eventtarget_proto)) js_set_proto_init(js->builtins.eventsource_proto, eventtarget_proto); js_set(js, js->builtins.eventsource_proto, "close", Ant_Function_Create(js_eventsource_close)); ANT_EVENT_HANDLER_DEFINE(js->builtins.eventsource_proto, eventsource, open); ANT_EVENT_HANDLER_DEFINE(js->builtins.eventsource_proto, eventsource, message); ANT_EVENT_HANDLER_DEFINE(js->builtins.eventsource_proto, eventsource, error); js_set(js, js->builtins.eventsource_proto, "CONNECTING", js_mknum(ES_CONNECTING)); js_set(js, js->builtins.eventsource_proto, "OPEN", js_mknum(ES_OPEN)); js_set(js, js->builtins.eventsource_proto, "CLOSED", js_mknum(ES_CLOSED)); js_set_tostringtag(js, js->builtins.eventsource_proto, js_mkstr(js, "EventSource", 11));
js->builtins.eventsource_ctor = Ant_Function_CreateIntrinsicConstructor(js, js_eventsource_ctor, js->builtins.eventsource_proto, "EventSource", 1); js_set(js, js->builtins.eventsource_ctor, "CONNECTING", js_mknum(ES_CONNECTING)); js_set(js, js->builtins.eventsource_ctor, "OPEN", js_mknum(ES_OPEN)); js_set(js, js->builtins.eventsource_ctor, "CLOSED", js_mknum(ES_CLOSED)); js_set_global_builtin(js, "EventSource", js->builtins.eventsource_ctor);}
void cleanup_eventsource_module(ant_t *js) { for (eventsource_state_t *es = js->active_eventsources, *next; es; es = next) { next = es->next; eventsource_close(js, es); }}
void gc_mark_eventsource(ant_t *js, gc_mark_fn mark) { for (eventsource_state_t *es = js->active_eventsources; es; es = es->next) mark(js, es->obj);}