#include #include #include #include "errors.h" #include "cfunc.h" #include "internal.h" #include "silver/call.h" #include "descriptors.h" #include "gc/roots.h" #include "modules/observable.h" static void observable_report_error(ant_t *js, ant_value_t result, const char *where) { GC_ROOT_SAVE(root_mark, js); ant_value_t reason = js_take_thrown(js, result); GC_ROOT_PIN(js, reason); char prefix[80]; snprintf(prefix, sizeof(prefix), "Error in %s: ", where); print_error_value(js, reason, js_mkundef(), prefix); if (Ant_Exception_Pending(js)) js_take_thrown(js, js_mkundef()); GC_ROOT_RESTORE(js, root_mark); } static bool subscription_closed(ant_t *js, ant_value_t subscription) { ant_value_t observer = js_get_slot(subscription, SLOT_SUBSCRIPTION_OBSERVER); return vtype(observer) == kTypeUndefined; } static void cleanup_subscription(ant_t *js, ant_value_t subscription) { ant_value_t cleanup = js_get_slot(subscription, SLOT_SUBSCRIPTION_CLEANUP); if (vtype(cleanup) == kTypeUndefined) return; if (!is_callable(cleanup)) return; js_set_slot(subscription, SLOT_SUBSCRIPTION_CLEANUP, js_mkundef()); ant_value_t result = sv_vm_call(js->vm, js, cleanup, js_mkundef(), NULL, 0, NULL, js_mkundef()); if (is_err(result)) observable_report_error(js, result, "subscription cleanup"); } static ant_value_t create_subscription(ant_t *js, ant_value_t observer) { ant_value_t subscription = Ant_Object_CreateBare(js); js_set_slot(subscription, SLOT_SUBSCRIPTION_OBSERVER, observer); js_set_slot(subscription, SLOT_SUBSCRIPTION_CLEANUP, js_mkundef()); js_set_tostringtag(js, subscription, js_mkstr(js, "Subscription", 12)); return subscription; } static ant_value_t js_subscription_get_closed(ant_params_t) { (void)args; (void)nargs; ant_value_t subscription = js_getthis(js); if (!is_special_object(subscription)) { return js_mkerr_typed(js, JS_ERR_TYPE, "Subscription.closed getter called on non-object"); } return js_bool(subscription_closed(js, subscription)); } static ant_value_t js_subscription_unsubscribe(ant_params_t) { (void)args; (void)nargs; ant_value_t subscription = js_getthis(js); if (!is_special_object(subscription)) { return js_mkerr_typed(js, JS_ERR_TYPE, "Subscription.unsubscribe called on non-object"); } if (subscription_closed(js, subscription)) return js_mkundef(); js_set_slot(subscription, SLOT_SUBSCRIPTION_OBSERVER, js_mkundef()); cleanup_subscription(js, subscription); return js_mkundef(); } static void setup_subscription_methods(ant_t *js, ant_value_t subscription) { js_set(js, subscription, "unsubscribe", Ant_Function_Create(js_subscription_unsubscribe)); ant_value_t closed_getter = Ant_Function_Create(js_subscription_get_closed); js_set_getter_desc(js, subscription, "closed", 6, closed_getter, JS_DESC_E | JS_DESC_C); } static ant_value_t js_subobs_get_closed(ant_params_t) { (void)args; (void)nargs; ant_value_t O = js_getthis(js); if (!is_special_object(O)) { return js_mkerr_typed(js, JS_ERR_TYPE, "SubscriptionObserver.closed getter called on non-object"); } ant_value_t subscription = js_get_slot(O, SLOT_DATA); if (!is_special_object(subscription)) { return js_mkerr_typed(js, JS_ERR_TYPE, "Invalid SubscriptionObserver"); } return js_bool(subscription_closed(js, subscription)); } static ant_value_t js_subobs_next(ant_params_t) { ant_value_t O = js_getthis(js); if (!is_special_object(O)) return js_mkerr_typed(js, JS_ERR_TYPE, "SubscriptionObserver.next called on non-object"); ant_value_t subscription = js_get_slot(O, SLOT_DATA); if (!is_special_object(subscription)) return js_mkerr_typed(js, JS_ERR_TYPE, "Invalid SubscriptionObserver"); if (subscription_closed(js, subscription)) return js_mkundef(); ant_value_t observer = js_get_slot(subscription, SLOT_SUBSCRIPTION_OBSERVER); if (!is_special_object(observer)) return js_mkundef(); ant_value_t nextMethod = js_get(js, observer, "next"); if (is_err(nextMethod)) { observable_report_error(js, nextMethod, "observer.next"); return js_mkundef(); } if (is_callable(nextMethod)) { ant_value_t value = (nargs > 0) ? args[0] : js_mkundef(); ant_value_t call_args[1] = {value}; ant_value_t result = sv_vm_call(js->vm, js, nextMethod, observer, call_args, 1, NULL, js_mkundef()); if (is_err(result)) observable_report_error(js, result, "observer.next"); } return js_mkundef(); } static ant_value_t js_subobs_error(ant_params_t) { ant_value_t O = js_getthis(js); if (!is_special_object(O)) { return js_mkerr_typed(js, JS_ERR_TYPE, "SubscriptionObserver.error called on non-object"); } ant_value_t subscription = js_get_slot(O, SLOT_DATA); if (!is_special_object(subscription)) { return js_mkerr_typed(js, JS_ERR_TYPE, "Invalid SubscriptionObserver"); } if (subscription_closed(js, subscription)) return js_mkundef(); ant_value_t observer = js_get_slot(subscription, SLOT_SUBSCRIPTION_OBSERVER); js_set_slot(subscription, SLOT_SUBSCRIPTION_OBSERVER, js_mkundef()); if (is_special_object(observer)) { ant_value_t errorMethod = js_get(js, observer, "error"); if (is_err(errorMethod)) observable_report_error(js, errorMethod, "observer.error"); if (is_callable(errorMethod)) { ant_value_t exception = (nargs > 0) ? args[0] : js_mkundef(); ant_value_t call_args[1] = {exception}; ant_value_t result = sv_vm_call(js->vm, js, errorMethod, observer, call_args, 1, NULL, js_mkundef()); if (is_err(result)) observable_report_error(js, result, "observer.error"); } } cleanup_subscription(js, subscription); return js_mkundef(); } static ant_value_t js_subobs_complete(ant_params_t) { (void)args; (void)nargs; ant_value_t O = js_getthis(js); if (!is_special_object(O)) { return js_mkerr_typed(js, JS_ERR_TYPE, "SubscriptionObserver.complete called on non-object"); } ant_value_t subscription = js_get_slot(O, SLOT_DATA); if (!is_special_object(subscription)) { return js_mkerr_typed(js, JS_ERR_TYPE, "Invalid SubscriptionObserver"); } if (subscription_closed(js, subscription)) return js_mkundef(); ant_value_t observer = js_get_slot(subscription, SLOT_SUBSCRIPTION_OBSERVER); js_set_slot(subscription, SLOT_SUBSCRIPTION_OBSERVER, js_mkundef()); if (is_special_object(observer)) { ant_value_t completeMethod = js_get(js, observer, "complete"); if (is_err(completeMethod)) observable_report_error(js, completeMethod, "observer.complete"); if (is_callable(completeMethod)) { ant_value_t result = sv_vm_call(js->vm, js, completeMethod, observer, NULL, 0, NULL, js_mkundef()); if (is_err(result)) observable_report_error(js, result, "observer.complete"); } } cleanup_subscription(js, subscription); return js_mkundef(); } static ant_value_t create_subscription_observer(ant_t *js, ant_value_t subscription) { ant_value_t subobs = Ant_Object_CreateBare(js); js_set_slot(subobs, SLOT_DATA, subscription); js_set(js, subobs, "next", Ant_Function_Create(js_subobs_next)); js_set(js, subobs, "error", Ant_Function_Create(js_subobs_error)); js_set(js, subobs, "complete", Ant_Function_Create(js_subobs_complete)); js_set_tostringtag(js, subobs, js_mkstr(js, "SubscriptionObserver", 20)); ant_value_t closed_getter = Ant_Function_Create(js_subobs_get_closed); js_set_getter_desc(js, subobs, "closed", 6, closed_getter, JS_DESC_E | JS_DESC_C); return subobs; } static ant_value_t js_cleanup_fn(ant_params_t) { (void)args; (void)nargs; ant_value_t F = js_getcurrentfunc(js); ant_value_t subscription = js_get_slot(F, SLOT_DATA); if (!is_special_object(subscription)) return js_mkundef(); ant_value_t unsubscribe = js_get(js, subscription, "unsubscribe"); if (is_callable(unsubscribe)) { return sv_vm_call(js->vm, js, unsubscribe, subscription, NULL, 0, NULL, js_mkundef()); } return js_mkundef(); } static ant_value_t execute_subscriber(ant_t *js, ant_value_t subscriber, ant_value_t observer) { ant_value_t call_args[1] = {observer}; ant_value_t subscriberResult = sv_vm_call(js->vm, js, subscriber, js_mkundef(), call_args, 1, NULL, js_mkundef()); if (vtype(subscriberResult) == kTypeError) return subscriberResult; if (vtype(subscriberResult) == kTypeNull || vtype(subscriberResult) == kTypeUndefined) return js_mkundef(); if (is_callable(subscriberResult)) return subscriberResult; if (is_special_object(subscriberResult)) { ant_value_t result = js_get(js, subscriberResult, "unsubscribe"); if (is_err(result)) return result; if (vtype(result) == kTypeUndefined) return js_mkerr_typed(js, JS_ERR_TYPE, "Subscriber return value must have an unsubscribe method"); ant_value_t cleanupFunction = Ant_Object_CreateBare(js); js_set_slot(cleanupFunction, SLOT_DATA, subscriberResult); js_set_slot(cleanupFunction, SLOT_CFUNC, Ant_Function_Create(js_cleanup_fn)); return js_obj_to_func(js, cleanupFunction); } return js_mkerr_typed(js, JS_ERR_TYPE, "Subscriber must return a function, an object with unsubscribe, or undefined"); } static ant_value_t js_observable_subscribe(ant_params_t) { ant_value_t O = js_getthis(js); if (!is_special_object(O)) { return js_mkerr_typed(js, JS_ERR_TYPE, "Observable.prototype.subscribe called on non-object"); } ant_value_t subscriber = js_get_slot(O, SLOT_OBSERVABLE_SUBSCRIBER); if (!is_callable(subscriber)) { return js_mkerr_typed(js, JS_ERR_TYPE, "Observable has no [[Subscriber]] internal slot"); } ant_value_t observer; if (nargs > 0 && is_callable(args[0])) { ant_value_t nextCallback = args[0]; ant_value_t errorCallback = (nargs > 1) ? args[1] : js_mkundef(); ant_value_t completeCallback = (nargs > 2) ? args[2] : js_mkundef(); observer = Ant_Object_CreateBare(js); js_set(js, observer, "next", nextCallback); js_set(js, observer, "error", errorCallback); js_set(js, observer, "complete", completeCallback); } else if (nargs > 0 && is_special_object(args[0])) { observer = args[0]; } else observer = Ant_Object_CreateBare(js); ant_value_t subscription = create_subscription(js, observer); setup_subscription_methods(js, subscription); ant_value_t start = js_get(js, observer, "start"); if (is_err(start)) observable_report_error(js, start, "observer.start"); if (is_callable(start)) { ant_value_t start_args[1] = {subscription}; ant_value_t result = sv_vm_call(js->vm, js, start, observer, start_args, 1, NULL, js_mkundef()); if (vtype(result) == kTypeError) observable_report_error(js, result, "observer.start"); if (subscription_closed(js, subscription)) return subscription; } ant_value_t subscriptionObserver = create_subscription_observer(js, subscription); ant_value_t subscriberResult = execute_subscriber(js, subscriber, subscriptionObserver); if (vtype(subscriberResult) == kTypeError) { GC_ROOT_SAVE(root_mark, js); ant_value_t thrown_error = js_take_thrown(js, subscriberResult); GC_ROOT_PIN(js, thrown_error); ant_value_t error_args[1] = {thrown_error}; ant_value_t error_method = js_get(js, subscriptionObserver, "error"); if (is_callable(error_method)) sv_vm_call(js->vm, js, error_method, subscriptionObserver, error_args, 1, NULL, js_mkundef()); GC_ROOT_RESTORE(js, root_mark); } else js_set_slot_wb(js, subscription, SLOT_SUBSCRIPTION_CLEANUP, subscriberResult); if (subscription_closed(js, subscription)) cleanup_subscription(js, subscription); return subscription; } static ant_value_t js_observable_symbol_observable(ant_params_t) { (void)args; (void)nargs; return js_getthis(js); } static ant_value_t js_observable_constructor(ant_params_t) { if (nargs < 1) { return js_mkerr_typed(js, JS_ERR_TYPE, "Observable constructor requires a subscriber function"); } ant_value_t subscriber = args[0]; if (!is_callable(subscriber)) { return js_mkerr_typed(js, JS_ERR_TYPE, "Observable subscriber must be a function"); } ant_value_t proto = js->builtins.observable_proto; ant_value_t observable = Ant_Object_CreateBare(js); js_set_proto_init(observable, proto); js_set_slot(observable, SLOT_OBSERVABLE_SUBSCRIBER, subscriber); return observable; } static ant_value_t js_of_subscriber(ant_params_t) { ant_value_t F = js_getcurrentfunc(js); ant_value_t items = js_get_slot(F, SLOT_DATA); if (nargs < 1) return js_mkundef(); ant_value_t observer = args[0]; ant_value_t subscription = js_get_slot(observer, SLOT_DATA); ant_value_t length_val = js_get(js, items, "length"); int length = (vtype(length_val) == kTypeNumber) ? (int)js_getnum(length_val) : 0; for (int i = 0; i < length; i++) { char key[16]; snprintf(key, sizeof(key), "%d", i); ant_value_t value = js_get(js, items, key); ant_value_t next = js_get(js, observer, "next"); if (is_callable(next)) { ant_value_t next_args[1] = {value}; sv_vm_call(js->vm, js, next, observer, next_args, 1, NULL, js_mkundef()); } if (is_special_object(subscription) && subscription_closed(js, subscription)) return js_mkundef(); } ant_value_t complete = js_get(js, observer, "complete"); if (is_callable(complete)) sv_vm_call(js->vm, js, complete, observer, NULL, 0, NULL, js_mkundef()); return js_mkundef(); } static ant_value_t js_observable_of(ant_params_t) { ant_value_t items = js_mkarr(js); for (int i = 0; i < nargs; i++) js_arr_push(js, items, args[i]); ant_value_t subscriber_func = js_heavy_mkfun(js, js_of_subscriber, items); ant_value_t ctor_args[1] = {subscriber_func}; return js_observable_constructor(js, ctor_args, 1, js_mkundef()); } static ant_value_t js_from_delegating(ant_params_t) { ant_value_t F = js_getcurrentfunc(js); ant_value_t observable = js_get_slot(F, SLOT_DATA); if (!is_special_object(observable)) return js_mkundef(); ant_value_t subscribe = js_get(js, observable, "subscribe"); if (is_callable(subscribe)) { return sv_vm_call(js->vm, js, subscribe, observable, args, nargs, NULL, js_mkundef()); } return js_mkundef(); } static ant_value_t js_from_iteration(ant_params_t) { ant_value_t F = js_getcurrentfunc(js); ant_value_t data = js_get_slot(F, SLOT_DATA); ant_value_t iterable = js_get(js, data, "iterable"); ant_value_t iteratorMethod = js_get(js, data, "iteratorMethod"); if (nargs < 1) return js_mkundef(); ant_value_t observer = args[0]; ant_value_t subscription = js_get_slot(observer, SLOT_DATA); if (!is_callable(iteratorMethod)) return js_mkerr_typed(js, JS_ERR_TYPE, "Object is not iterable"); ant_value_t iterator = sv_vm_call(js->vm, js, iteratorMethod, iterable, NULL, 0, NULL, js_mkundef()); if (is_err(iterator)) return iterator; if (!is_special_object(iterator)) return js_mkerr_typed(js, JS_ERR_TYPE, "Iterator must return an object"); ant_value_t nextMethod = js_getprop_fallback(js, iterator, "next"); if (!is_callable(nextMethod)) { return js_mkerr_typed(js, JS_ERR_TYPE, "Iterator must have a next method"); } while (true) { ant_value_t next = sv_vm_call(js->vm, js, nextMethod, iterator, NULL, 0, NULL, js_mkundef()); if (vtype(next) == kTypeError) return next; ant_value_t done = js_get(js, next, "done"); if (js_truthy(js, done)) { ant_value_t complete = js_get(js, observer, "complete"); if (is_callable(complete)) sv_vm_call(js->vm, js, complete, observer, NULL, 0, NULL, js_mkundef()); return js_mkundef(); } ant_value_t nextValue = js_get(js, next, "value"); ant_value_t obs_next = js_get(js, observer, "next"); if (is_callable(obs_next)) { ant_value_t next_args[1] = {nextValue}; sv_vm_call(js->vm, js, obs_next, observer, next_args, 1, NULL, js_mkundef()); } if (is_special_object(subscription) && subscription_closed(js, subscription)) { ant_value_t returnMethod = js_getprop_fallback(js, iterator, "return"); if (is_callable(returnMethod)) sv_vm_call(js->vm, js, returnMethod, iterator, NULL, 0, NULL, js_mkundef()); return js_mkundef(); } } } static ant_value_t js_observable_from(ant_params_t) { if (nargs < 1) return js_mkerr_typed(js, JS_ERR_TYPE, "Observable.from requires an argument"); ant_value_t x = args[0]; if (vtype(x) == kTypeNull || vtype(x) == kTypeUndefined) { return js_mkerr_typed(js, JS_ERR_TYPE, "Cannot convert null or undefined to observable"); } ant_value_t observableMethod = js_get_sym(js, x, js->sym.observable_sym); if (is_callable(observableMethod)) { ant_value_t observable = sv_vm_call(js->vm, js, observableMethod, x, NULL, 0, NULL, js_mkundef()); if (is_err(observable)) return observable; if (!is_special_object(observable)) { return js_mkerr_typed(js, JS_ERR_TYPE, "@@observable must return an object"); } ant_value_t existing_subscriber = js_get_slot(observable, SLOT_OBSERVABLE_SUBSCRIBER); if (is_callable(existing_subscriber)) return observable; ant_value_t subscriber_func = js_heavy_mkfun(js, js_from_delegating, observable); ant_value_t ctor_args[1] = {subscriber_func}; return js_observable_constructor(js, ctor_args, 1, js_mkundef()); } ant_value_t iteratorMethod = js_get_sym(js, x, js->sym.iterator_sym); if (!is_callable(iteratorMethod)) return js_mkerr_typed(js, JS_ERR_TYPE, "Object is not observable or iterable"); ant_value_t data = Ant_Object_CreateBare(js); js_set(js, data, "iterable", x); js_set(js, data, "iteratorMethod", iteratorMethod); ant_value_t subscriber_func = js_heavy_mkfun(js, js_from_iteration, data); ant_value_t ctor_args[1] = {subscriber_func}; return js_observable_constructor(js, ctor_args, 1, js_mkundef()); } void init_observable_module(ant_t *js) { ant_value_t observable_ctor = Ant_Object_CreateBare(js); ant_value_t observable_proto = Ant_Object_CreateBare(js); js->builtins.observable_proto = observable_proto; defmethod(js, observable_proto, "subscribe", 9, Ant_Function_Create(js_observable_subscribe)); defmethod_sym(js, observable_proto, js->sym.observable_sym, Ant_Function_Create(js_observable_symbol_observable)); js_set_tostringtag(js, observable_proto, js_mkstr(js, "Observable", 10)); js_set_slot(observable_ctor, SLOT_CFUNC, Ant_Function_CreateWithArity(js_observable_constructor, 1)); js_setprop_readonly_nonconfigurable(js, observable_ctor, "prototype", 9, observable_proto); js_set_function_name(js, observable_ctor, "Observable", sizeof("Observable") - 1); defmethod(js, observable_ctor, "of", 2, Ant_Function_Create(js_observable_of)); defmethod(js, observable_ctor, "from", 4, Ant_Function_Create(js_observable_from)); ant_value_t Observable = js_obj_to_func(js, observable_ctor); defhidden(js, observable_proto, "constructor", 11, Observable); js_set_global_builtin(js, "Observable", Observable); }