From 81024c4c099bbab82299ba6a9668f5b60d30b9c2 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Sat, 6 Jun 2026 10:55:10 -0500 Subject: [PATCH] Require async --- assets/init.js | 25 +-- assets/testscene.js | 100 +++++----- src/dusk/script/module/CMakeLists.txt | 1 + src/dusk/script/module/event/CMakeLists.txt | 9 + src/dusk/script/module/event/moduleevent.c | 112 +++++++++++ src/dusk/script/module/event/moduleevent.h | 36 ++++ src/dusk/script/module/modulelist.c | 3 + .../script/module/require/modulerequire.c | 184 ++++++++++++++---- 8 files changed, 362 insertions(+), 108 deletions(-) create mode 100644 src/dusk/script/module/event/CMakeLists.txt create mode 100644 src/dusk/script/module/event/moduleevent.c create mode 100644 src/dusk/script/module/event/moduleevent.h diff --git a/assets/init.js b/assets/init.js index 611446af..230eb3a6 100644 --- a/assets/init.js +++ b/assets/init.js @@ -10,24 +10,11 @@ const platformNames = { Console.print('Platform: ' + (platformNames[System.platform] || 'Unknown')); -// Testing async/await -async function testAsync() { - Console.print('Testing async/await...'); - await frame(); - Console.print('First await done!'); - await timeout(1000); - Console.print('Async/await works!'); +async function testRequireAsync() { + Console.print('Loading testscene.js...'); + const scene = await requireAsync('testscene.js'); + Console.print('Loaded!'); + Console.print(scene.test()); } -testAsync(); - -// Scene.set('testscene.js'); -// Console.print('Loading scene...'); -// requireAsync('./testscene.js', function(scene) { -// throw "test"; -// Console.print('Initializing scene...'); -// var batch = scene.load(); -// batch.lock(); -// batch.requireLoaded(); -// scene.init(); -// }); \ No newline at end of file +testRequireAsync(); \ No newline at end of file diff --git a/assets/testscene.js b/assets/testscene.js index 0717d51a..b9fe17ba 100644 --- a/assets/testscene.js +++ b/assets/testscene.js @@ -1,50 +1,58 @@ -var scene = { - 'test': 'teststring' -}; +Console.print('testscene.js is loaded'); -var assets = AssetBatch([ - { path: 'test.png', type: Asset.TYPE_TEXTURE, format: Texture.FORMAT_RGBA } -]); - -var cam; -var camPos; -var testEntity; -var testPos; -var testRenderable; -var texEntry; - -scene.init = function() { - assets.lock(); - assets.onLoaded[0] = scene.loaded; -}; - -scene.loaded = function() { - texEntry = assets.entry(0); - - // Camera at (3, 3, 3) looking at origin - cam = Entity.create(); - camPos = cam.add(Component.POSITION); - cam.add(Component.CAMERA); - camPos.localPosition = new Vec3(3, 3, 3); - camPos.lookAt(new Vec3(0, 0, 0)); - - // Test entity with textured quad at origin - testEntity = Entity.create(); - testPos = testEntity.add(Component.POSITION); - testRenderable = testEntity.add(Component.RENDERABLE); - - testRenderable.texture = texEntry.texture; - testRenderable.sprites = [ - [0, 0, 1, 1, 0, 1, 1, 0] - ]; - testPos.localPosition = new Vec3(0, 0, 0); +module.exports = { + test: function() { + return 'Hello string'; + } } -scene.dispose = function() { - Console.print('Scene Dispose'); - Entity.dispose(cam); - Entity.dispose(testEntity); - assets.unlock(); -}; +// var scene = { +// 'test': 'teststring' +// }; -module.exports = scene; +// var assets = AssetBatch([ +// { path: 'test.png', type: Asset.TYPE_TEXTURE, format: Texture.FORMAT_RGBA } +// ]); + +// var cam; +// var camPos; +// var testEntity; +// var testPos; +// var testRenderable; +// var texEntry; + +// scene.init = function() { +// assets.lock(); +// assets.onLoaded[0] = scene.loaded; +// }; + +// scene.loaded = function() { +// texEntry = assets.entry(0); + +// // Camera at (3, 3, 3) looking at origin +// cam = Entity.create(); +// camPos = cam.add(Component.POSITION); +// cam.add(Component.CAMERA); +// camPos.localPosition = new Vec3(3, 3, 3); +// camPos.lookAt(new Vec3(0, 0, 0)); + +// // Test entity with textured quad at origin +// testEntity = Entity.create(); +// testPos = testEntity.add(Component.POSITION); +// testRenderable = testEntity.add(Component.RENDERABLE); + +// testRenderable.texture = texEntry.texture; +// testRenderable.sprites = [ +// [0, 0, 1, 1, 0, 1, 1, 0] +// ]; +// testPos.localPosition = new Vec3(0, 0, 0); +// } + +// scene.dispose = function() { +// Console.print('Scene Dispose'); +// Entity.dispose(cam); +// Entity.dispose(testEntity); +// assets.unlock(); +// }; + +// module.exports = scene; diff --git a/src/dusk/script/module/CMakeLists.txt b/src/dusk/script/module/CMakeLists.txt index dadd3eb7..5f82f17f 100644 --- a/src/dusk/script/module/CMakeLists.txt +++ b/src/dusk/script/module/CMakeLists.txt @@ -10,4 +10,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} ) # Subdirs +add_subdirectory(event) add_subdirectory(require) \ No newline at end of file diff --git a/src/dusk/script/module/event/CMakeLists.txt b/src/dusk/script/module/event/CMakeLists.txt new file mode 100644 index 00000000..5bb1fd1b --- /dev/null +++ b/src/dusk/script/module/event/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + moduleevent.c +) diff --git a/src/dusk/script/module/event/moduleevent.c b/src/dusk/script/module/event/moduleevent.c new file mode 100644 index 00000000..23608390 --- /dev/null +++ b/src/dusk/script/module/event/moduleevent.c @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "moduleevent.h" +#include "util/memory.h" +#include "assert/assert.h" + +#define MODULE_EVENT_PENDING_MAX 32 + +typedef struct { + event_t *event; + jerry_value_t promise; +} moduleeventpending_t; + +scriptproto_t MODULE_EVENT_PROTO; + +static moduleeventpending_t MODULE_EVENT_PENDING[MODULE_EVENT_PENDING_MAX]; +static uint32_t MODULE_EVENT_PENDING_COUNT = 0; + +/** + * Single shared C callback subscribed to any event that has JS awaits. + * Resolves all pending promises for the fired event, then unsubscribes. + * The user pointer is the event_t * so we can look up and unsubscribe. + */ +static void moduleEventFireCallback(void *params, void *user) { + (void)params; + event_t *event = (event_t *)user; + + uint32_t i = 0; + while(i < MODULE_EVENT_PENDING_COUNT) { + if(MODULE_EVENT_PENDING[i].event != event) { i++; continue; } + jerry_value_t ret = jerry_promise_resolve( + MODULE_EVENT_PENDING[i].promise, jerry_undefined() + ); + jerry_value_free(ret); + jerry_value_free(MODULE_EVENT_PENDING[i].promise); + MODULE_EVENT_PENDING_COUNT--; + if(i < MODULE_EVENT_PENDING_COUNT) { + MODULE_EVENT_PENDING[i] = MODULE_EVENT_PENDING[MODULE_EVENT_PENDING_COUNT]; + } + } + + eventUnsubscribe(event, moduleEventFireCallback); +} + +static jerry_value_t moduleEventWait( + const jerry_call_info_t *callInfo, + const jerry_value_t args[], + const jerry_length_t argc +) { + (void)args; (void)argc; + + jsevent_t *ev = scriptProtoGetValue(&MODULE_EVENT_PROTO, callInfo->this_value); + if(!ev) return moduleBaseThrow("Event.wait: invalid this"); + + if(MODULE_EVENT_PENDING_COUNT >= MODULE_EVENT_PENDING_MAX) { + return moduleBaseThrow("Event.wait: too many pending awaits"); + } + + // Only subscribe once per event — check if we already have a pending await + bool_t subscribed = false; + for(uint32_t i = 0; i < MODULE_EVENT_PENDING_COUNT; i++) { + if(MODULE_EVENT_PENDING[i].event == ev->event) { subscribed = true; break; } + } + + if(!subscribed) { + if(ev->event->count >= ev->event->size) { + return moduleBaseThrow("Event.wait: event subscriber capacity exceeded"); + } + eventSubscribe(ev->event, moduleEventFireCallback, (void *)ev->event); + } + + jerry_value_t promise = jerry_promise(); + MODULE_EVENT_PENDING[MODULE_EVENT_PENDING_COUNT].event = ev->event; + MODULE_EVENT_PENDING[MODULE_EVENT_PENDING_COUNT].promise = jerry_value_copy(promise); + MODULE_EVENT_PENDING_COUNT++; + return promise; +} + +jerry_value_t moduleEventCreate(event_t *event) { + assertNotNull(event, "moduleEventCreate: event must not be NULL"); + jsevent_t ev = { .event = event }; + return scriptProtoCreateValue(&MODULE_EVENT_PROTO, &ev); +} + +void moduleEventInit(void) { + MODULE_EVENT_PENDING_COUNT = 0; + scriptProtoInit(&MODULE_EVENT_PROTO, NULL, sizeof(jsevent_t), NULL); + scriptProtoDefineFunc(&MODULE_EVENT_PROTO, "wait", moduleEventWait); +} + +void moduleEventDispose(void) { + // Unsubscribe from each distinct event still in the pending list + for(uint32_t i = 0; i < MODULE_EVENT_PENDING_COUNT; i++) { + bool_t alreadyUnsub = false; + for(uint32_t j = 0; j < i; j++) { + if(MODULE_EVENT_PENDING[j].event == MODULE_EVENT_PENDING[i].event) { + alreadyUnsub = true; break; + } + } + if(!alreadyUnsub) { + eventUnsubscribe(MODULE_EVENT_PENDING[i].event, moduleEventFireCallback); + } + jerry_value_free(MODULE_EVENT_PENDING[i].promise); + } + MODULE_EVENT_PENDING_COUNT = 0; + scriptProtoDispose(&MODULE_EVENT_PROTO); +} diff --git a/src/dusk/script/module/event/moduleevent.h b/src/dusk/script/module/event/moduleevent.h new file mode 100644 index 00000000..18fd6e2d --- /dev/null +++ b/src/dusk/script/module/event/moduleevent.h @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "script/module/modulebase.h" +#include "script/scriptproto.h" +#include "event/event.h" + +/** C struct wrapped by every Event JS instance. */ +typedef struct { + event_t *event; +} jsevent_t; + +extern scriptproto_t MODULE_EVENT_PROTO; + +/** + * Wraps a C event_t pointer in a JS Event object. + * + * @param event The event to wrap. Must outlive the returned JS value. + * @return A new JS Event instance. + */ +jerry_value_t moduleEventCreate(event_t *event); + +/** + * Initializes the Event module and registers the global Event prototype. + */ +void moduleEventInit(void); + +/** + * Disposes the Event module, rejecting any pending awaits and cleaning up. + */ +void moduleEventDispose(void); diff --git a/src/dusk/script/module/modulelist.c b/src/dusk/script/module/modulelist.c index 8c19ed34..2e27342e 100644 --- a/src/dusk/script/module/modulelist.c +++ b/src/dusk/script/module/modulelist.c @@ -12,6 +12,7 @@ #include "script/module/display/modulescreen.h" #include "script/module/engine/moduleengine.h" #include "script/module/engine/moduleframe.h" +#include "script/module/event/moduleevent.h" #include "script/module/engine/moduletimeout.h" #include "script/module/entity/component/modulecomponentlist.h" #include "script/module/entity/modulecomponent.h" @@ -24,6 +25,7 @@ void moduleListInit(void) { + moduleEventInit(); moduleTextureInit(); moduleColorInit(); moduleAssetInit(); @@ -64,4 +66,5 @@ void moduleListDispose(void) { moduleAssetDispose(); moduleColorDispose(); moduleTextureDispose(); + moduleEventDispose(); } diff --git a/src/dusk/script/module/require/modulerequire.c b/src/dusk/script/module/require/modulerequire.c index 3f13bfc5..dd5eff48 100644 --- a/src/dusk/script/module/require/modulerequire.c +++ b/src/dusk/script/module/require/modulerequire.c @@ -10,7 +10,75 @@ #include "util/memory.h" #include "util/string.h" #include "assert/assert.h" -#include "console/console.h" + +#define MODULE_REQUIRE_ASYNC_MAX 16 + +typedef struct { + assetentry_t *entry; + jerry_value_t promise; +} modulerequireasyncpending_t; + +static modulerequireasyncpending_t MODULE_REQUIRE_ASYNC_PENDING[MODULE_REQUIRE_ASYNC_MAX]; +static uint32_t MODULE_REQUIRE_ASYNC_PENDING_COUNT = 0; + +/* Resolves or rejects every promise associated with entry, then unsubscribes. */ +static void moduleRequireAsyncOnError(void *params, void *user); + +static void moduleRequireAsyncOnLoaded(void *params, void *user) { + assetentry_t *entry = (assetentry_t *)params; + (void)user; + + eventUnsubscribe(&entry->onLoaded, moduleRequireAsyncOnLoaded); + eventUnsubscribe(&entry->onError, moduleRequireAsyncOnError); + + jerry_value_t exports = jerry_value_is_undefined(entry->data.script.exports) + ? jerry_undefined() + : jerry_value_copy(entry->data.script.exports); + + uint32_t i = 0; + while(i < MODULE_REQUIRE_ASYNC_PENDING_COUNT) { + if(MODULE_REQUIRE_ASYNC_PENDING[i].entry != entry) { i++; continue; } + assetUnlockEntry(entry); + jerry_value_t copy = jerry_value_copy(exports); + jerry_value_t ret = jerry_promise_resolve(MODULE_REQUIRE_ASYNC_PENDING[i].promise, copy); + jerry_value_free(ret); + jerry_value_free(copy); + jerry_value_free(MODULE_REQUIRE_ASYNC_PENDING[i].promise); + MODULE_REQUIRE_ASYNC_PENDING_COUNT--; + if(i < MODULE_REQUIRE_ASYNC_PENDING_COUNT) { + MODULE_REQUIRE_ASYNC_PENDING[i] = MODULE_REQUIRE_ASYNC_PENDING[MODULE_REQUIRE_ASYNC_PENDING_COUNT]; + } + } + + jerry_value_free(exports); +} + +static void moduleRequireAsyncOnError(void *params, void *user) { + assetentry_t *entry = (assetentry_t *)params; + (void)user; + + eventUnsubscribe(&entry->onLoaded, moduleRequireAsyncOnLoaded); + eventUnsubscribe(&entry->onError, moduleRequireAsyncOnError); + + jerry_value_t errStr = jerry_string_sz("Module load failed"); + + uint32_t i = 0; + while(i < MODULE_REQUIRE_ASYNC_PENDING_COUNT) { + if(MODULE_REQUIRE_ASYNC_PENDING[i].entry != entry) { i++; continue; } + assetUnlockEntry(entry); + jerry_value_t copy = jerry_value_copy(errStr); + jerry_value_t ret = jerry_promise_reject(MODULE_REQUIRE_ASYNC_PENDING[i].promise, copy); + jerry_value_free(ret); + jerry_value_free(copy); + jerry_value_free(MODULE_REQUIRE_ASYNC_PENDING[i].promise); + MODULE_REQUIRE_ASYNC_PENDING_COUNT--; + if(i < MODULE_REQUIRE_ASYNC_PENDING_COUNT) { + MODULE_REQUIRE_ASYNC_PENDING[i] = MODULE_REQUIRE_ASYNC_PENDING[MODULE_REQUIRE_ASYNC_PENDING_COUNT]; + } + } + + jerry_value_free(errStr); +} jerry_value_t moduleRequireFunc( const jerry_call_info_t *callInfo, @@ -23,45 +91,30 @@ jerry_value_t moduleRequireFunc( if(argc < 1 || !jerry_value_is_string(args[0])) { return moduleBaseThrow("Expected a string argument for module name."); } - - // Is filename too long? - if( - jerry_string_size(args[0], JERRY_ENCODING_UTF8) >= ASSET_FILE_NAME_MAX - ) { + if(jerry_string_size(args[0], JERRY_ENCODING_UTF8) >= ASSET_FILE_NAME_MAX) { return moduleBaseThrow("Module name too long."); } - // Get C string char_t moduleName[ASSET_FILE_NAME_MAX]; moduleBaseToString(args[0], moduleName, sizeof(moduleName)); - // Lock and load the asset. assetloaderinput_t input; input.script.nothing = NULL; - - assetentry_t *entry = assetLock( - moduleName, - ASSET_LOADER_TYPE_SCRIPT, - &input - ); + assetentry_t *entry = assetLock(moduleName, ASSET_LOADER_TYPE_SCRIPT, &input); errorret_t err = assetRequireLoaded(entry); - if(errorIsNotOk(err)) { assetUnlockEntry(entry); return moduleBaseThrowError(err); } - // Now the module is loaded, copy it before unlocking. if(jerry_value_is_undefined(entry->data.script.exports)) { assetUnlockEntry(entry); return jerry_undefined(); } - jerry_value_t exportsCopy = jerry_value_copy( - entry->data.script.exports - ); - assetUnlockEntry(entry);// Frees entry->data.script.exports + jerry_value_t exportsCopy = jerry_value_copy(entry->data.script.exports); + assetUnlockEntry(entry); return exportsCopy; } @@ -73,47 +126,92 @@ jerry_value_t moduleRequireAsyncFunc( assertNotNull(callInfo, "callInfo must not be null."); assertNotNull(args, "args must not be null."); - // Filename, required if(argc < 1 || !jerry_value_is_string(args[0])) { - return moduleBaseThrow( - "requireAsync expects filename." - ); + return moduleBaseThrow("requireAsync expects a filename string."); } - - // Callback, optional. - if(argc >= 2 && !jerry_value_is_function(args[1])) { - return moduleBaseThrow( - "requireAsync callback must be a function." - ); - } - - // Is filename too long? - if( - jerry_string_size(args[0], JERRY_ENCODING_UTF8) >= ASSET_FILE_NAME_MAX - ) { + if(jerry_string_size(args[0], JERRY_ENCODING_UTF8) >= ASSET_FILE_NAME_MAX) { return moduleBaseThrow("Module name too long."); } + if(MODULE_REQUIRE_ASYNC_PENDING_COUNT >= MODULE_REQUIRE_ASYNC_MAX) { + return moduleBaseThrow("Too many pending requireAsync calls."); + } - // Get C string char_t moduleName[ASSET_FILE_NAME_MAX]; moduleBaseToString(args[0], moduleName, sizeof(moduleName)); - // Lock asset assetloaderinput_t input; input.script.nothing = NULL; + assetentry_t *entry = assetLock(moduleName, ASSET_LOADER_TYPE_SCRIPT, &input); - assetentry_t *entry = assetLock( - moduleName, - ASSET_LOADER_TYPE_SCRIPT, - &input - ); + jerry_value_t promise = jerry_promise(); + // Already loaded — resolve immediately. + if(entry->state == ASSET_ENTRY_STATE_LOADED) { + jerry_value_t exports = jerry_value_is_undefined(entry->data.script.exports) + ? jerry_undefined() + : jerry_value_copy(entry->data.script.exports); + assetUnlockEntry(entry); + jerry_value_t ret = jerry_promise_resolve(promise, exports); + jerry_value_free(ret); + jerry_value_free(exports); + return promise; + } + + // Already errored — reject immediately. + if(entry->state == ASSET_ENTRY_STATE_ERROR) { + assetUnlockEntry(entry); + jerry_value_t errStr = jerry_string_sz("Module load failed"); + jerry_value_t ret = jerry_promise_reject(promise, errStr); + jerry_value_free(ret); + jerry_value_free(errStr); + return promise; + } + + // Subscribe to entry events only if this is the first pending await for it. + bool_t subscribed = false; + for(uint32_t i = 0; i < MODULE_REQUIRE_ASYNC_PENDING_COUNT; i++) { + if(MODULE_REQUIRE_ASYNC_PENDING[i].entry == entry) { subscribed = true; break; } + } + if(!subscribed) { + if(entry->onLoaded.count >= entry->onLoaded.size) { + assetUnlockEntry(entry); + jerry_value_free(promise); + return moduleBaseThrow("requireAsync: onLoaded event capacity exceeded."); + } + if(entry->onError.count >= entry->onError.size) { + assetUnlockEntry(entry); + jerry_value_free(promise); + return moduleBaseThrow("requireAsync: onError event capacity exceeded."); + } + eventSubscribe(&entry->onLoaded, moduleRequireAsyncOnLoaded, NULL); + eventSubscribe(&entry->onError, moduleRequireAsyncOnError, NULL); + } + + MODULE_REQUIRE_ASYNC_PENDING[MODULE_REQUIRE_ASYNC_PENDING_COUNT].entry = entry; + MODULE_REQUIRE_ASYNC_PENDING[MODULE_REQUIRE_ASYNC_PENDING_COUNT].promise = jerry_value_copy(promise); + MODULE_REQUIRE_ASYNC_PENDING_COUNT++; + return promise; } void moduleRequireInit(void) { + MODULE_REQUIRE_ASYNC_PENDING_COUNT = 0; moduleBaseDefineGlobalMethod("require", moduleRequireFunc); moduleBaseDefineGlobalMethod("requireAsync", moduleRequireAsyncFunc); } void moduleRequireDispose(void) { + for(uint32_t i = 0; i < MODULE_REQUIRE_ASYNC_PENDING_COUNT; i++) { + assetentry_t *entry = MODULE_REQUIRE_ASYNC_PENDING[i].entry; + bool_t alreadyUnsub = false; + for(uint32_t j = 0; j < i; j++) { + if(MODULE_REQUIRE_ASYNC_PENDING[j].entry == entry) { alreadyUnsub = true; break; } + } + if(!alreadyUnsub) { + eventUnsubscribe(&entry->onLoaded, moduleRequireAsyncOnLoaded); + eventUnsubscribe(&entry->onError, moduleRequireAsyncOnError); + } + assetUnlockEntry(entry); + jerry_value_free(MODULE_REQUIRE_ASYNC_PENDING[i].promise); + } + MODULE_REQUIRE_ASYNC_PENDING_COUNT = 0; }