Require async

This commit is contained in:
2026-06-06 10:55:10 -05:00
parent 9068d96130
commit 81024c4c09
8 changed files with 362 additions and 108 deletions
+6 -19
View File
@@ -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();
// });
testRequireAsync();
+54 -46
View File
@@ -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;
+1
View File
@@ -10,4 +10,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
)
# Subdirs
add_subdirectory(event)
add_subdirectory(require)
@@ -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
)
+112
View File
@@ -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);
}
@@ -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);
+3
View File
@@ -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();
}
+141 -43
View File
@@ -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;
}