Require async
This commit is contained in:
@@ -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
|
||||
)
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user