From 9ab28828eeb75372d96bc8b479c65785f8fca85a Mon Sep 17 00:00:00 2001 From: theMackabu Date: Tue, 6 Jan 2026 20:05:36 -0800 Subject: [PATCH] add navigator module and tests --- examples/spec/navigator.js | 96 +++++++++ include/modules/navigator.h | 6 + meson.build | 2 +- src/main.c | 2 + src/modules/navigator.c | 377 ++++++++++++++++++++++++++++++++++++ 5 files changed, 482 insertions(+), 1 deletion(-) create mode 100644 examples/spec/navigator.js create mode 100644 include/modules/navigator.h create mode 100644 src/modules/navigator.c diff --git a/examples/spec/navigator.js b/examples/spec/navigator.js new file mode 100644 index 0000000..c879483 --- /dev/null +++ b/examples/spec/navigator.js @@ -0,0 +1,96 @@ +import { test, testDeep, summary } from './helpers.js'; + +console.log('Navigator Tests\n'); + +test('navigator exists', typeof navigator, 'object'); +test('navigator toStringTag', Object.prototype.toString.call(navigator), '[object Navigator]'); + +test('navigator.hardwareConcurrency exists', typeof navigator.hardwareConcurrency, 'number'); +test('navigator.hardwareConcurrency is positive', navigator.hardwareConcurrency >= 1, true); +test('navigator.hardwareConcurrency is integer', Number.isInteger(navigator.hardwareConcurrency), true); + +test('navigator.language exists', typeof navigator.language, 'string'); +test('navigator.language is en-US', navigator.language, 'en-US'); + +test('navigator.languages exists', Array.isArray(navigator.languages), true); +test('navigator.languages length', navigator.languages.length >= 1, true); +test('navigator.languages contains language', navigator.languages.includes(navigator.language), true); + +test('navigator.platform exists', typeof navigator.platform, 'string'); +test('navigator.platform is non-empty', navigator.platform.length > 0, true); + +test('navigator.userAgent exists', typeof navigator.userAgent, 'string'); +test('navigator.userAgent starts with Ant', navigator.userAgent.startsWith('Ant/'), true); + +console.log('\nLockManager Tests\n'); + +test('navigator.locks exists', typeof navigator.locks, 'object'); +test('navigator.locks toStringTag', Object.prototype.toString.call(navigator.locks), '[object LockManager]'); +test('navigator.locks.request exists', typeof navigator.locks.request, 'function'); +test('navigator.locks.query exists', typeof navigator.locks.query, 'function'); + +(async () => { + let exclusiveLockAcquired = false; + let exclusiveLockName = null; + let exclusiveLockMode = null; + let exclusiveLockReleased = false; + + await navigator.locks + .request('test_resource', async lock => { + exclusiveLockAcquired = true; + exclusiveLockName = lock.name; + exclusiveLockMode = lock.mode; + return 'done'; + }) + .then(() => { + exclusiveLockReleased = true; + }); + + test('exclusive lock was acquired', exclusiveLockAcquired, true); + test('exclusive lock has correct name', exclusiveLockName, 'test_resource'); + test('exclusive lock has correct mode', exclusiveLockMode, 'exclusive'); + test('exclusive lock was released', exclusiveLockReleased, true); + + let sharedLockMode = null; + + await navigator.locks.request('shared_resource', { mode: 'shared' }, async lock => { + sharedLockMode = lock.mode; + }); + + test('shared lock has correct mode', sharedLockMode, 'shared'); + + let ifAvailableResult = null; + + await navigator.locks.request('available_resource', { ifAvailable: true }, async lock => { + ifAvailableResult = lock ? 'acquired' : 'not available'; + }); + + test('ifAvailable lock was acquired', ifAvailableResult, 'acquired'); + + const queryResult = await navigator.locks.query(); + + test('query returns object', typeof queryResult, 'object'); + test('query has held array', Array.isArray(queryResult.held), true); + test('query has pending array', Array.isArray(queryResult.pending), true); + + let lockOrder = []; + + await navigator.locks.request('order_test', async lock => { + lockOrder.push(1); + }); + + await navigator.locks.request('order_test', async lock => { + lockOrder.push(2); + }); + + testDeep('locks execute in order', lockOrder, [1, 2]); + + let lockToStringTag = null; + await navigator.locks.request('tostring_test', async lock => { + lockToStringTag = Object.prototype.toString.call(lock); + }); + + test('Lock toStringTag', lockToStringTag, '[object Lock]'); + + summary(); +})(); diff --git a/include/modules/navigator.h b/include/modules/navigator.h new file mode 100644 index 0000000..18a26b0 --- /dev/null +++ b/include/modules/navigator.h @@ -0,0 +1,6 @@ +#ifndef NAVIGATOR_H +#define NAVIGATOR_H + +void init_navigator_module(void); + +#endif diff --git a/meson.build b/meson.build index 012e84f..60d87a6 100644 --- a/meson.build +++ b/meson.build @@ -96,7 +96,7 @@ endif build_date = run_command('date', '+%Y-%m-%d', check: true).stdout().strip() version_conf = configuration_data() -version_conf.set('ANT_VERSION', '0.3.2.24') +version_conf.set('ANT_VERSION', '0.3.2.25') version_conf.set('ANT_GIT_HASH', git_hash) version_conf.set('ANT_BUILD_DATE', build_date) diff --git a/src/main.c b/src/main.c index 244d4c4..bedb74b 100644 --- a/src/main.c +++ b/src/main.c @@ -38,6 +38,7 @@ #include "modules/textcodec.h" #include "modules/sessionstorage.h" #include "modules/localstorage.h" +#include "modules/navigator.h" int js_result = EXIT_SUCCESS; @@ -210,6 +211,7 @@ int main(int argc, char *argv[]) { init_textcodec_module(); init_sessionstorage_module(); init_localstorage_module(); + init_navigator_module(); ant_register_library(shell_library, "ant:shell", NULL); ant_register_library(ffi_library, "ant:ffi", NULL); diff --git a/src/modules/navigator.c b/src/modules/navigator.c new file mode 100644 index 0000000..f36e7ce --- /dev/null +++ b/src/modules/navigator.c @@ -0,0 +1,377 @@ +#include +#include +#include +#include +#include + +#ifdef __APPLE__ +#include +#elif defined(__linux__) +#include +#endif + +#include "ant.h" +#include "config.h" +#include "runtime.h" +#include "modules/navigator.h" +#include "modules/symbol.h" + +typedef enum { + LOCK_MODE_EXCLUSIVE, + LOCK_MODE_SHARED +} lock_mode_t; + +typedef struct lock_entry { + char *name; + lock_mode_t mode; + int shared_count; + UT_hash_handle hh; +} lock_entry_t; + +typedef struct lock_request { + struct js *js; + char *name; + lock_mode_t mode; + jsval_t callback; + jsval_t promise; + struct lock_request *next; +} lock_request_t; + +static lock_entry_t *locks = NULL; +static lock_request_t *pending_requests = NULL; + +static int get_hardware_concurrency(void) { +#ifdef __APPLE__ + int count; + size_t size = sizeof(count); + if (sysctlbyname("hw.ncpu", &count, &size, NULL, 0) == 0) { + return count; + } + return 1; +#elif defined(__linux__) + int count = (int)sysconf(_SC_NPROCESSORS_ONLN); + return count > 0 ? count : 1; +#elif defined(_WIN32) || defined(_WIN64) + SYSTEM_INFO sysinfo; + GetSystemInfo(&sysinfo); + return (int)sysinfo.dwNumberOfProcessors; +#else + return 1; +#endif +} + +static const char *get_platform_string(void) { +#if defined(__APPLE__) + return "MacIntel"; +#elif defined(__linux__) + return "Linux x86_64"; +#elif defined(_WIN32) || defined(_WIN64) + return "Win32"; +#elif defined(__FreeBSD__) + return "FreeBSD"; +#else + return "Unknown"; +#endif +} + +static lock_entry_t *find_lock(const char *name) { + lock_entry_t *entry = NULL; + HASH_FIND_STR(locks, name, entry); + return entry; +} + +static lock_entry_t *create_lock(const char *name, lock_mode_t mode) { + lock_entry_t *entry = calloc(1, sizeof(lock_entry_t)); + if (!entry) return NULL; + + entry->name = strdup(name); + entry->mode = mode; + entry->shared_count = (mode == LOCK_MODE_SHARED) ? 1 : 0; + + HASH_ADD_STR(locks, name, entry); + return entry; +} + +static bool can_acquire_lock(const char *name, lock_mode_t mode) { + lock_entry_t *entry = find_lock(name); + if (!entry) return true; + if (mode == LOCK_MODE_SHARED && entry->mode == LOCK_MODE_SHARED) return true; + return false; +} + +static void release_lock(const char *name) { + lock_entry_t *entry = find_lock(name); + if (!entry) return; + + if (entry->mode == LOCK_MODE_SHARED) { + entry->shared_count--; + if (entry->shared_count > 0) return; + } + + HASH_DEL(locks, entry); + free(entry->name); + free(entry); +} + +static void process_pending_requests(struct js *js); + +static jsval_t lock_then_handler(struct js *js, jsval_t *args, int nargs) { + jsval_t current_func = js_getcurrentfunc(js); + jsval_t lock_name_val = js_get(js, current_func, "_lockName"); + jsval_t outer_promise = js_get(js, current_func, "_outerPromise"); + jsval_t result_val = (nargs > 0) ? args[0] : js_mkundef(); + + size_t name_len; + char *name = js_getstr(js, lock_name_val, &name_len); + + if (name) release_lock(name); + + js_resolve_promise(js, outer_promise, result_val); + process_pending_requests(js); + + return js_mkundef(); +} + +static jsval_t lock_catch_handler(struct js *js, jsval_t *args, int nargs) { + jsval_t current_func = js_getcurrentfunc(js); + jsval_t lock_name_val = js_get(js, current_func, "_lockName"); + jsval_t outer_promise = js_get(js, current_func, "_outerPromise"); + jsval_t error_val = (nargs > 0) ? args[0] : js_mkundef(); + + size_t name_len; + char *name = js_getstr(js, lock_name_val, &name_len); + + if (name) release_lock(name); + + js_reject_promise(js, outer_promise, error_val); + process_pending_requests(js); + + return js_mkundef(); +} + +static void execute_lock_callback(struct js *js, const char *name, lock_mode_t mode, jsval_t callback, jsval_t outer_promise) { + jsval_t lock_obj = js_mkobj(js); + js_set(js, lock_obj, "name", js_mkstr(js, name, strlen(name))); + js_set(js, lock_obj, "mode", js_mkstr(js, mode == LOCK_MODE_EXCLUSIVE ? "exclusive" : "shared", mode == LOCK_MODE_EXCLUSIVE ? 9 : 6)); + js_set(js, lock_obj, get_toStringTag_sym_key(), js_mkstr(js, "Lock", 4)); + + jsval_t result = js_call(js, callback, &lock_obj, 1); + + if (js_type(result) == JS_ERR) { + release_lock(name); + js_reject_promise(js, outer_promise, result); + process_pending_requests(js); + return; + } + + if (js_type(result) == JS_PROMISE) { + jsval_t then_fn = js_get(js, result, "then"); + + if (js_type(then_fn) == JS_FUNC) { + jsval_t name_str = js_mkstr(js, name, strlen(name)); + + jsval_t on_resolve = js_mkfun(lock_then_handler); + js_set(js, on_resolve, "_lockName", name_str); + js_set(js, on_resolve, "_outerPromise", outer_promise); + + jsval_t on_reject = js_mkfun(lock_catch_handler); + js_set(js, on_reject, "_lockName", name_str); + js_set(js, on_reject, "_outerPromise", outer_promise); + + jsval_t then_args[2] = { on_resolve, on_reject }; + js_call_with_this(js, then_fn, result, then_args, 2); + return; + } + } + + release_lock(name); + js_resolve_promise(js, outer_promise, result); + process_pending_requests(js); +} + +static void process_pending_requests(struct js *js) { + lock_request_t *prev = NULL; + lock_request_t *req = pending_requests; + + while (req) { + if (can_acquire_lock(req->name, req->mode)) { + lock_entry_t *entry = find_lock(req->name); + + if (entry && entry->mode == LOCK_MODE_SHARED && req->mode == LOCK_MODE_SHARED) { + entry->shared_count++; + } else { + create_lock(req->name, req->mode); + } + + lock_request_t *to_process = req; + + if (prev) { + prev->next = req->next; + } else { + pending_requests = req->next; + } + req = req->next; + + execute_lock_callback(js, to_process->name, to_process->mode, to_process->callback, to_process->promise); + free(to_process->name); + free(to_process); + return; + } else { + prev = req; + req = req->next; + } + } +} + +static jsval_t locks_request(struct js *js, jsval_t *args, int nargs) { + if (nargs < 2) { + return js_mkerr_typed(js, JS_ERR_TYPE, "locks.request requires at least 2 arguments"); + } + + size_t name_len; + char *name = js_getstr(js, args[0], &name_len); + if (!name) { + return js_mkerr_typed(js, JS_ERR_TYPE, "First argument must be a string"); + } + + jsval_t options = js_mkundef(); + jsval_t callback; + lock_mode_t mode = LOCK_MODE_EXCLUSIVE; + bool if_available = false; + + if (nargs == 2) { + callback = args[1]; + } else { + options = args[1]; + callback = args[2]; + + if (js_type(options) == JS_OBJ) { + jsval_t mode_val = js_get(js, options, "mode"); + if (js_type(mode_val) == JS_STR) { + size_t mode_len; + char *mode_str = js_getstr(js, mode_val, &mode_len); + if (mode_str && strcmp(mode_str, "shared") == 0) mode = LOCK_MODE_SHARED; + } + + jsval_t if_avail_val = js_get(js, options, "ifAvailable"); + if (js_type(if_avail_val) == JS_TRUE) if_available = true; + } + } + + if (js_type(callback) != JS_FUNC) { + return js_mkerr_typed(js, JS_ERR_TYPE, "Callback must be a function"); + } + + jsval_t promise = js_mkpromise(js); + + if (if_available && !can_acquire_lock(name, mode)) { + jsval_t null_val = js_mknull(); + jsval_t result = js_call(js, callback, &null_val, 1); + + if (js_type(result) == JS_PROMISE) { + jsval_t then_fn = js_get(js, result, "then"); + if (js_type(then_fn) == JS_FUNC) { + jsval_t on_resolve = js_mkfun(lock_then_handler); + js_set(js, on_resolve, "_lockName", js_mkstr(js, "", 0)); + js_set(js, on_resolve, "_outerPromise", promise); + js_call_with_this(js, then_fn, result, &on_resolve, 1); + return promise; + } + } + js_resolve_promise(js, promise, result); + return promise; + } + + if (can_acquire_lock(name, mode)) { + lock_entry_t *entry = find_lock(name); + + if (entry && entry->mode == LOCK_MODE_SHARED && mode == LOCK_MODE_SHARED) { + entry->shared_count++; + } else { + create_lock(name, mode); + } + + execute_lock_callback(js, name, mode, callback, promise); + } else { + lock_request_t *req = calloc(1, sizeof(lock_request_t)); + req->js = js; + req->name = strdup(name); + req->mode = mode; + req->callback = callback; + req->promise = promise; + req->next = NULL; + + lock_request_t *tail = pending_requests; + if (!tail) { + pending_requests = req; + } else { + while (tail->next) tail = tail->next; + tail->next = req; + } + } + + return promise; +} + +static jsval_t locks_query(struct js *js, jsval_t *args, int nargs) { + (void)args; + (void)nargs; + + jsval_t result = js_mkobj(js); + jsval_t held_arr = js_mkarr(js); + jsval_t pending_arr = js_mkarr(js); + + lock_entry_t *entry, *tmp; + HASH_ITER(hh, locks, entry, tmp) { + jsval_t lock_info = js_mkobj(js); + js_set(js, lock_info, "name", js_mkstr(js, entry->name, strlen(entry->name))); + js_set(js, lock_info, "mode", js_mkstr(js, entry->mode == LOCK_MODE_EXCLUSIVE ? "exclusive" : "shared", entry->mode == LOCK_MODE_EXCLUSIVE ? 9 : 6)); + js_arr_push(js, held_arr, lock_info); + } + + lock_request_t *req = pending_requests; + while (req) { + jsval_t req_info = js_mkobj(js); + js_set(js, req_info, "name", js_mkstr(js, req->name, strlen(req->name))); + js_set(js, req_info, "mode", js_mkstr(js, req->mode == LOCK_MODE_EXCLUSIVE ? "exclusive" : "shared", req->mode == LOCK_MODE_EXCLUSIVE ? 9 : 6)); + js_arr_push(js, pending_arr, req_info); + req = req->next; + } + + js_set(js, result, "held", held_arr); + js_set(js, result, "pending", pending_arr); + + jsval_t promise = js_mkpromise(js); + js_resolve_promise(js, promise, result); + + return promise; +} + +void init_navigator_module(void) { + struct js *js = rt->js; + + jsval_t navigator_obj = js_mkobj(js); + + js_set(js, navigator_obj, "hardwareConcurrency", js_mknum((double)get_hardware_concurrency())); + js_set(js, navigator_obj, "language", js_mkstr(js, "en-US", 5)); + + jsval_t languages_arr = js_mkarr(js); + js_arr_push(js, languages_arr, js_mkstr(js, "en-US", 5)); + js_set(js, navigator_obj, "languages", languages_arr); + + const char *platform = get_platform_string(); + js_set(js, navigator_obj, "platform", js_mkstr(js, platform, strlen(platform))); + + char user_agent[64]; + snprintf(user_agent, sizeof(user_agent), "Ant/%s", ANT_VERSION); + js_set(js, navigator_obj, "userAgent", js_mkstr(js, user_agent, strlen(user_agent))); + + jsval_t locks_obj = js_mkobj(js); + js_set(js, locks_obj, "request", js_mkfun(locks_request)); + js_set(js, locks_obj, "query", js_mkfun(locks_query)); + js_set(js, locks_obj, get_toStringTag_sym_key(), js_mkstr(js, "LockManager", 11)); + js_set(js, navigator_obj, "locks", locks_obj); + + js_set(js, navigator_obj, get_toStringTag_sym_key(), js_mkstr(js, "Navigator", 9)); + js_set(js, js_glob(js), "navigator", navigator_obj); +} -- 2.51.2