Skip to content

Add promise hooks #1033

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 168 additions & 4 deletions api-test.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {} \
Expand All @@ -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(); \
Expand Down Expand Up @@ -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 { \
Expand Down Expand Up @@ -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()";
Expand Down Expand Up @@ -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, "<input>", 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), "<input>", 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), "<input>", 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), "<input>", 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), "<input>", 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), "<input>", 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();
Expand All @@ -321,5 +484,6 @@ int main(void)
module_serde();
two_byte_string();
weak_map_gc_check();
promise_hook();
return 0;
}
59 changes: 57 additions & 2 deletions quickjs.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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)
Expand All @@ -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) {
Expand Down Expand Up @@ -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");

Expand All @@ -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));
Expand Down Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down
17 changes: 17 additions & 0 deletions quickjs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down