diff --git a/api-test.c b/api-test.c index d8ba2b202..370a97f2f 100644 --- a/api-test.c +++ b/api-test.c @@ -18,7 +18,7 @@ static int timeout_interrupt_handler(JSRuntime *rt, void *opaque) static void sync_call(void) { - const char *code = + static const char code[] = "(function() { \ try { \ while (true) {} \ @@ -43,7 +43,7 @@ static void sync_call(void) static void async_call(void) { - const char *code = + static const char code[] = "(async function() { \ const loop = async () => { \ await Promise.resolve(); \ @@ -85,7 +85,7 @@ static JSValue save_value(JSContext *ctx, JSValueConst this_val, static void async_call_stack_overflow(void) { - const char *code = + static const char code[] = "(async function() { \ const f = () => f(); \ try { \ @@ -199,7 +199,7 @@ static JSModuleDef *loader(JSContext *ctx, const char *name, void *opaque) static void module_serde(void) { JSRuntime *rt = JS_NewRuntime(); - JS_SetDumpFlags(rt, JS_DUMP_MODULE_RESOLVE); + //JS_SetDumpFlags(rt, JS_DUMP_MODULE_RESOLVE); JS_SetModuleLoaderFunc(rt, NULL, loader, NULL); JSContext *ctx = JS_NewContext(rt); static const char code[] = "import {f} from 'b'; f()"; @@ -311,6 +311,169 @@ function addItem() { \ JS_FreeRuntime(rt); } +struct { + int hook_type_call_count[4]; +} promise_hook_state; + +static void promise_hook_cb(JSContext *ctx, JSPromiseHookType type, + JSValueConst promise, JSValueConst parent_promise, + void *opaque) +{ + assert(type == JS_PROMISE_HOOK_INIT || + type == JS_PROMISE_HOOK_BEFORE || + type == JS_PROMISE_HOOK_AFTER || + type == JS_PROMISE_HOOK_RESOLVE); + promise_hook_state.hook_type_call_count[type]++; + assert(opaque == (void *)&promise_hook_state); + if (!JS_IsUndefined(parent_promise)) { + JSValue global_object = JS_GetGlobalObject(ctx); + JS_SetPropertyStr(ctx, global_object, "actual", + JS_DupValue(ctx, parent_promise)); + JS_FreeValue(ctx, global_object); + } +} + +static void promise_hook(void) +{ + int *cc = promise_hook_state.hook_type_call_count; + JSContext *unused; + JSRuntime *rt = JS_NewRuntime(); + //JS_SetDumpFlags(rt, JS_DUMP_PROMISE); + JS_SetPromiseHook(rt, promise_hook_cb, &promise_hook_state); + JSContext *ctx = JS_NewContext(rt); + JSValue global_object = JS_GetGlobalObject(ctx); + { + // empty module; creates an outer and inner module promise; + // JS_Eval returns the outer promise + JSValue ret = JS_Eval(ctx, "", 0, "", JS_EVAL_TYPE_MODULE); + assert(!JS_IsException(ret)); + assert(JS_IsPromise(ret)); + assert(JS_PROMISE_FULFILLED == JS_PromiseState(ctx, ret)); + JS_FreeValue(ctx, ret); + assert(2 == cc[JS_PROMISE_HOOK_INIT]); + assert(0 == cc[JS_PROMISE_HOOK_BEFORE]); + assert(0 == cc[JS_PROMISE_HOOK_AFTER]); + assert(2 == cc[JS_PROMISE_HOOK_RESOLVE]); + assert(!JS_IsJobPending(rt)); + } + memset(&promise_hook_state, 0, sizeof(promise_hook_state)); + { + // module with unresolved promise; the outer and inner module promises + // are resolved but not the user's promise + static const char code[] = "new Promise(() => {})"; + JSValue ret = JS_Eval(ctx, code, strlen(code), "", JS_EVAL_TYPE_MODULE); + assert(!JS_IsException(ret)); + assert(JS_IsPromise(ret)); + assert(JS_PROMISE_FULFILLED == JS_PromiseState(ctx, ret)); // outer module promise + JS_FreeValue(ctx, ret); + assert(3 == cc[JS_PROMISE_HOOK_INIT]); + assert(0 == cc[JS_PROMISE_HOOK_BEFORE]); + assert(0 == cc[JS_PROMISE_HOOK_AFTER]); + assert(2 == cc[JS_PROMISE_HOOK_RESOLVE]); // outer and inner module promise + assert(!JS_IsJobPending(rt)); + } + memset(&promise_hook_state, 0, sizeof(promise_hook_state)); + { + // module with resolved promise + static const char code[] = "new Promise((resolve,reject) => resolve())"; + JSValue ret = JS_Eval(ctx, code, strlen(code), "", JS_EVAL_TYPE_MODULE); + assert(!JS_IsException(ret)); + assert(JS_IsPromise(ret)); + assert(JS_PROMISE_FULFILLED == JS_PromiseState(ctx, ret)); // outer module promise + JS_FreeValue(ctx, ret); + assert(3 == cc[JS_PROMISE_HOOK_INIT]); + assert(0 == cc[JS_PROMISE_HOOK_BEFORE]); + assert(0 == cc[JS_PROMISE_HOOK_AFTER]); + assert(3 == cc[JS_PROMISE_HOOK_RESOLVE]); + assert(!JS_IsJobPending(rt)); + } + memset(&promise_hook_state, 0, sizeof(promise_hook_state)); + { + // module with rejected promise + static const char code[] = "new Promise((resolve,reject) => reject())"; + JSValue ret = JS_Eval(ctx, code, strlen(code), "", JS_EVAL_TYPE_MODULE); + assert(!JS_IsException(ret)); + assert(JS_IsPromise(ret)); + assert(JS_PROMISE_FULFILLED == JS_PromiseState(ctx, ret)); // outer module promise + JS_FreeValue(ctx, ret); + assert(3 == cc[JS_PROMISE_HOOK_INIT]); + assert(0 == cc[JS_PROMISE_HOOK_BEFORE]); + assert(0 == cc[JS_PROMISE_HOOK_AFTER]); + assert(2 == cc[JS_PROMISE_HOOK_RESOLVE]); + assert(!JS_IsJobPending(rt)); + } + memset(&promise_hook_state, 0, sizeof(promise_hook_state)); + { + // module with promise chain + static const char code[] = + "globalThis.count = 0;" + "globalThis.actual = undefined;" // set by promise_hook_cb + "globalThis.expected = new Promise(resolve => resolve());" + "expected.then(_ => count++)"; + JSValue ret = JS_Eval(ctx, code, strlen(code), "", JS_EVAL_TYPE_MODULE); + assert(!JS_IsException(ret)); + assert(JS_IsPromise(ret)); + assert(JS_PROMISE_FULFILLED == JS_PromiseState(ctx, ret)); // outer module promise + JS_FreeValue(ctx, ret); + assert(4 == cc[JS_PROMISE_HOOK_INIT]); + assert(0 == cc[JS_PROMISE_HOOK_BEFORE]); + assert(0 == cc[JS_PROMISE_HOOK_AFTER]); + assert(3 == cc[JS_PROMISE_HOOK_RESOLVE]); + JSValue v = JS_GetPropertyStr(ctx, global_object, "count"); + assert(!JS_IsException(v)); + int32_t count; + assert(0 == JS_ToInt32(ctx, &count, v)); + assert(0 == count); + JS_FreeValue(ctx, v); + assert(JS_IsJobPending(rt)); + assert(1 == JS_ExecutePendingJob(rt, &unused)); + assert(!JS_HasException(ctx)); + assert(4 == cc[JS_PROMISE_HOOK_INIT]); + assert(0 == cc[JS_PROMISE_HOOK_BEFORE]); + assert(0 == cc[JS_PROMISE_HOOK_AFTER]); + assert(4 == cc[JS_PROMISE_HOOK_RESOLVE]); + assert(!JS_IsJobPending(rt)); + v = JS_GetPropertyStr(ctx, global_object, "count"); + assert(!JS_IsException(v)); + assert(0 == JS_ToInt32(ctx, &count, v)); + assert(1 == count); + JS_FreeValue(ctx, v); + JSValue actual = JS_GetPropertyStr(ctx, global_object, "actual"); + JSValue expected = JS_GetPropertyStr(ctx, global_object, "expected"); + assert(!JS_IsException(actual)); + assert(!JS_IsException(expected)); + assert(JS_IsSameValue(ctx, actual, expected)); + JS_FreeValue(ctx, actual); + JS_FreeValue(ctx, expected); + } + memset(&promise_hook_state, 0, sizeof(promise_hook_state)); + { + // module with thenable; fires before and after hooks + static const char code[] = + "new Promise(resolve => resolve({then(resolve){ resolve() }}))"; + JSValue ret = JS_Eval(ctx, code, strlen(code), "", JS_EVAL_TYPE_MODULE); + assert(!JS_IsException(ret)); + assert(JS_IsPromise(ret)); + assert(JS_PROMISE_FULFILLED == JS_PromiseState(ctx, ret)); // outer module promise + JS_FreeValue(ctx, ret); + assert(3 == cc[JS_PROMISE_HOOK_INIT]); + assert(0 == cc[JS_PROMISE_HOOK_BEFORE]); + assert(0 == cc[JS_PROMISE_HOOK_AFTER]); + assert(2 == cc[JS_PROMISE_HOOK_RESOLVE]); + assert(JS_IsJobPending(rt)); + assert(1 == JS_ExecutePendingJob(rt, &unused)); + assert(!JS_HasException(ctx)); + assert(3 == cc[JS_PROMISE_HOOK_INIT]); + assert(1 == cc[JS_PROMISE_HOOK_BEFORE]); + assert(1 == cc[JS_PROMISE_HOOK_AFTER]); + assert(3 == cc[JS_PROMISE_HOOK_RESOLVE]); + assert(!JS_IsJobPending(rt)); + } + JS_FreeValue(ctx, global_object); + JS_FreeContext(ctx); + JS_FreeRuntime(rt); +} + int main(void) { sync_call(); @@ -321,5 +484,6 @@ int main(void) module_serde(); two_byte_string(); weak_map_gc_check(); + promise_hook(); return 0; } diff --git a/quickjs.c b/quickjs.c index 44f076317..078d38022 100644 --- a/quickjs.c +++ b/quickjs.c @@ -237,6 +237,11 @@ typedef struct JSRuntimeFinalizerState { void *arg; } JSRuntimeFinalizerState; +typedef struct JSValueLink { + struct JSValueLink *next; + JSValueConst value; +} JSValueLink; + struct JSRuntime { JSMallocFunctions mf; JSMallocState malloc_state; @@ -284,6 +289,12 @@ struct JSRuntime { JSInterruptHandler *interrupt_handler; void *interrupt_opaque; + JSPromiseHook *promise_hook; + void *promise_hook_opaque; + // for smuggling the parent promise from js_promise_then + // to js_promise_constructor + JSValueLink *parent_promise; + JSHostPromiseRejectionTracker *host_promise_rejection_tracker; void *host_promise_rejection_tracker_opaque; @@ -50199,6 +50210,12 @@ static JSValue promise_reaction_job(JSContext *ctx, int argc, return res2; } +void JS_SetPromiseHook(JSRuntime *rt, JSPromiseHook promise_hook, void *opaque) +{ + rt->promise_hook = promise_hook; + rt->promise_hook_opaque = opaque; +} + void JS_SetHostPromiseRejectionTracker(JSRuntime *rt, JSHostPromiseRejectionTracker *cb, void *opaque) @@ -50222,6 +50239,14 @@ static void fulfill_or_reject_promise(JSContext *ctx, JSValueConst promise, promise_trace(ctx, "fulfill_or_reject_promise: is_reject=%d\n", is_reject); + if (s->promise_state == JS_PROMISE_FULFILLED) { + JSRuntime *rt = ctx->rt; + if (rt->promise_hook) { + rt->promise_hook(ctx, JS_PROMISE_HOOK_RESOLVE, promise, + JS_UNDEFINED, rt->promise_hook_opaque); + } + } + if (s->promise_state == JS_PROMISE_REJECTED && !s->is_handled) { JSRuntime *rt = ctx->rt; if (rt->host_promise_rejection_tracker) { @@ -50260,6 +50285,7 @@ static JSValue js_promise_resolve_thenable_job(JSContext *ctx, { JSValueConst promise, thenable, then; JSValue args[2], res; + JSRuntime *rt; promise_trace(ctx, "js_promise_resolve_thenable_job\n"); @@ -50269,7 +50295,16 @@ static JSValue js_promise_resolve_thenable_job(JSContext *ctx, then = argv[2]; if (js_create_resolving_functions(ctx, args, promise) < 0) return JS_EXCEPTION; + rt = ctx->rt; + if (rt->promise_hook) { + rt->promise_hook(ctx, JS_PROMISE_HOOK_BEFORE, promise, JS_UNDEFINED, + rt->promise_hook_opaque); + } res = JS_Call(ctx, then, thenable, 2, vc(args)); + if (rt->promise_hook) { + rt->promise_hook(ctx, JS_PROMISE_HOOK_AFTER, promise, JS_UNDEFINED, + rt->promise_hook_opaque); + } if (JS_IsException(res)) { JSValue error = JS_GetException(ctx); res = JS_Call(ctx, args[1], JS_UNDEFINED, 1, vc(&error)); @@ -50452,6 +50487,7 @@ static JSValue js_promise_constructor(JSContext *ctx, JSValueConst new_target, JSValueConst executor; JSValue obj; JSPromiseData *s; + JSRuntime *rt; JSValue args[2], ret; int i; @@ -50472,6 +50508,14 @@ static JSValue js_promise_constructor(JSContext *ctx, JSValueConst new_target, JS_SetOpaqueInternal(obj, s); if (js_create_resolving_functions(ctx, args, obj)) goto fail; + rt = ctx->rt; + if (rt->promise_hook) { + JSValueConst parent_promise = JS_UNDEFINED; + if (rt->parent_promise) + parent_promise = rt->parent_promise->value; + rt->promise_hook(ctx, JS_PROMISE_HOOK_INIT, obj, parent_promise, + rt->promise_hook_opaque); + } ret = JS_Call(ctx, executor, JS_UNDEFINED, 2, vc(args)); if (JS_IsException(ret)) { JSValue ret2, error; @@ -50529,8 +50573,7 @@ static JSValue js_new_promise_capability(JSContext *ctx, executor = js_promise_executor_new(ctx); if (JS_IsException(executor)) - return executor; - + return JS_EXCEPTION; if (JS_IsUndefined(ctor)) { result_promise = js_promise_constructor(ctx, ctor, 1, vc(&executor)); } else { @@ -51005,7 +51048,10 @@ static JSValue js_promise_then(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { JSValue ctor, result_promise, resolving_funcs[2]; + bool have_promise_hook; + JSValueLink link; JSPromiseData *s; + JSRuntime *rt; int i, ret; s = JS_GetOpaque2(ctx, this_val, JS_CLASS_PROMISE); @@ -51015,7 +51061,16 @@ static JSValue js_promise_then(JSContext *ctx, JSValueConst this_val, ctor = JS_SpeciesConstructor(ctx, this_val, JS_UNDEFINED); if (JS_IsException(ctor)) return ctor; + rt = ctx->rt; + // always restore, even if js_new_promise_capability callee removes hook + have_promise_hook = (rt->promise_hook != NULL); + if (have_promise_hook) { + link = (JSValueLink){rt->parent_promise, this_val}; + rt->parent_promise = &link; + } result_promise = js_new_promise_capability(ctx, resolving_funcs, ctor); + if (have_promise_hook) + rt->parent_promise = link.next; JS_FreeValue(ctx, ctor); if (JS_IsException(result_promise)) return result_promise; diff --git a/quickjs.h b/quickjs.h index e39947312..daefa7665 100644 --- a/quickjs.h +++ b/quickjs.h @@ -1003,6 +1003,23 @@ JS_EXTERN bool JS_IsPromise(JSValueConst val); JS_EXTERN JSValue JS_NewSymbol(JSContext *ctx, const char *description, bool is_global); +typedef enum JSPromiseHookType { + JS_PROMISE_HOOK_INIT, // emitted when a new promise is created + JS_PROMISE_HOOK_BEFORE, // runs right before promise.then is invoked + JS_PROMISE_HOOK_AFTER, // runs right after promise.then is invoked + JS_PROMISE_HOOK_RESOLVE, // not emitted for rejected promises +} JSPromiseHookType; + +// parent_promise is only passed in when type == JS_PROMISE_HOOK_INIT and +// is then either a promise object or JS_UNDEFINED if the new promise does +// not have a parent promise; only promises created with promise.then have +// a parent promise +typedef void JSPromiseHook(JSContext *ctx, JSPromiseHookType type, + JSValueConst promise, JSValueConst parent_promise, + void *opaque); +JS_EXTERN void JS_SetPromiseHook(JSRuntime *rt, JSPromiseHook promise_hook, + void *opaque); + /* is_handled = true means that the rejection is handled */ typedef void JSHostPromiseRejectionTracker(JSContext *ctx, JSValueConst promise, JSValueConst reason,