Skip to content

Commit 247f0fc

Browse files
committed
Add promise hooks
Lets embedders track promise chains. Closely modelled after the V8 API of the same name. Fixes: #1030
1 parent a9a8ece commit 247f0fc

File tree

3 files changed

+242
-6
lines changed

3 files changed

+242
-6
lines changed

api-test.c

+168-4
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ static int timeout_interrupt_handler(JSRuntime *rt, void *opaque)
1818

1919
static void sync_call(void)
2020
{
21-
const char *code =
21+
static const char code[] =
2222
"(function() { \
2323
try { \
2424
while (true) {} \
@@ -43,7 +43,7 @@ static void sync_call(void)
4343

4444
static void async_call(void)
4545
{
46-
const char *code =
46+
static const char code[] =
4747
"(async function() { \
4848
const loop = async () => { \
4949
await Promise.resolve(); \
@@ -85,7 +85,7 @@ static JSValue save_value(JSContext *ctx, JSValueConst this_val,
8585

8686
static void async_call_stack_overflow(void)
8787
{
88-
const char *code =
88+
static const char code[] =
8989
"(async function() { \
9090
const f = () => f(); \
9191
try { \
@@ -199,7 +199,7 @@ static JSModuleDef *loader(JSContext *ctx, const char *name, void *opaque)
199199
static void module_serde(void)
200200
{
201201
JSRuntime *rt = JS_NewRuntime();
202-
JS_SetDumpFlags(rt, JS_DUMP_MODULE_RESOLVE);
202+
//JS_SetDumpFlags(rt, JS_DUMP_MODULE_RESOLVE);
203203
JS_SetModuleLoaderFunc(rt, NULL, loader, NULL);
204204
JSContext *ctx = JS_NewContext(rt);
205205
static const char code[] = "import {f} from 'b'; f()";
@@ -311,6 +311,169 @@ function addItem() { \
311311
JS_FreeRuntime(rt);
312312
}
313313

314+
struct {
315+
int hook_type_call_count[4];
316+
} promise_hook_state;
317+
318+
static void promise_hook_cb(JSContext *ctx, JSPromiseHookType type,
319+
JSValueConst promise, JSValueConst parent_promise,
320+
void *opaque)
321+
{
322+
assert(type == JS_PROMISE_HOOK_INIT ||
323+
type == JS_PROMISE_HOOK_BEFORE ||
324+
type == JS_PROMISE_HOOK_AFTER ||
325+
type == JS_PROMISE_HOOK_RESOLVE);
326+
promise_hook_state.hook_type_call_count[type]++;
327+
assert(opaque == (void *)&promise_hook_state);
328+
if (!JS_IsUndefined(parent_promise)) {
329+
JSValue global_object = JS_GetGlobalObject(ctx);
330+
JS_SetPropertyStr(ctx, global_object, "actual",
331+
JS_DupValue(ctx, parent_promise));
332+
JS_FreeValue(ctx, global_object);
333+
}
334+
}
335+
336+
static void promise_hook(void)
337+
{
338+
int *cc = promise_hook_state.hook_type_call_count;
339+
JSContext *unused;
340+
JSRuntime *rt = JS_NewRuntime();
341+
//JS_SetDumpFlags(rt, JS_DUMP_PROMISE);
342+
JS_SetPromiseHook(rt, promise_hook_cb, &promise_hook_state);
343+
JSContext *ctx = JS_NewContext(rt);
344+
JSValue global_object = JS_GetGlobalObject(ctx);
345+
{
346+
// empty module; creates an outer and inner module promise;
347+
// JS_Eval returns the outer promise
348+
JSValue ret = JS_Eval(ctx, "", 0, "<input>", JS_EVAL_TYPE_MODULE);
349+
assert(!JS_IsException(ret));
350+
assert(JS_IsPromise(ret));
351+
assert(JS_PROMISE_FULFILLED == JS_PromiseState(ctx, ret));
352+
JS_FreeValue(ctx, ret);
353+
assert(2 == cc[JS_PROMISE_HOOK_INIT]);
354+
assert(0 == cc[JS_PROMISE_HOOK_BEFORE]);
355+
assert(0 == cc[JS_PROMISE_HOOK_AFTER]);
356+
assert(2 == cc[JS_PROMISE_HOOK_RESOLVE]);
357+
assert(!JS_IsJobPending(rt));
358+
}
359+
memset(&promise_hook_state, 0, sizeof(promise_hook_state));
360+
{
361+
// module with unresolved promise; the outer and inner module promises
362+
// are resolved but not the user's promise
363+
static const char code[] = "new Promise(() => {})";
364+
JSValue ret = JS_Eval(ctx, code, strlen(code), "<input>", JS_EVAL_TYPE_MODULE);
365+
assert(!JS_IsException(ret));
366+
assert(JS_IsPromise(ret));
367+
assert(JS_PROMISE_FULFILLED == JS_PromiseState(ctx, ret)); // outer module promise
368+
JS_FreeValue(ctx, ret);
369+
assert(3 == cc[JS_PROMISE_HOOK_INIT]);
370+
assert(0 == cc[JS_PROMISE_HOOK_BEFORE]);
371+
assert(0 == cc[JS_PROMISE_HOOK_AFTER]);
372+
assert(2 == cc[JS_PROMISE_HOOK_RESOLVE]); // outer and inner module promise
373+
assert(!JS_IsJobPending(rt));
374+
}
375+
memset(&promise_hook_state, 0, sizeof(promise_hook_state));
376+
{
377+
// module with resolved promise
378+
static const char code[] = "new Promise((resolve,reject) => resolve())";
379+
JSValue ret = JS_Eval(ctx, code, strlen(code), "<input>", JS_EVAL_TYPE_MODULE);
380+
assert(!JS_IsException(ret));
381+
assert(JS_IsPromise(ret));
382+
assert(JS_PROMISE_FULFILLED == JS_PromiseState(ctx, ret)); // outer module promise
383+
JS_FreeValue(ctx, ret);
384+
assert(3 == cc[JS_PROMISE_HOOK_INIT]);
385+
assert(0 == cc[JS_PROMISE_HOOK_BEFORE]);
386+
assert(0 == cc[JS_PROMISE_HOOK_AFTER]);
387+
assert(3 == cc[JS_PROMISE_HOOK_RESOLVE]);
388+
assert(!JS_IsJobPending(rt));
389+
}
390+
memset(&promise_hook_state, 0, sizeof(promise_hook_state));
391+
{
392+
// module with rejected promise
393+
static const char code[] = "new Promise((resolve,reject) => reject())";
394+
JSValue ret = JS_Eval(ctx, code, strlen(code), "<input>", JS_EVAL_TYPE_MODULE);
395+
assert(!JS_IsException(ret));
396+
assert(JS_IsPromise(ret));
397+
assert(JS_PROMISE_FULFILLED == JS_PromiseState(ctx, ret)); // outer module promise
398+
JS_FreeValue(ctx, ret);
399+
assert(3 == cc[JS_PROMISE_HOOK_INIT]);
400+
assert(0 == cc[JS_PROMISE_HOOK_BEFORE]);
401+
assert(0 == cc[JS_PROMISE_HOOK_AFTER]);
402+
assert(2 == cc[JS_PROMISE_HOOK_RESOLVE]);
403+
assert(!JS_IsJobPending(rt));
404+
}
405+
memset(&promise_hook_state, 0, sizeof(promise_hook_state));
406+
{
407+
// module with promise chain
408+
static const char code[] =
409+
"globalThis.count = 0;"
410+
"globalThis.actual = undefined;" // set by promise_hook_cb
411+
"globalThis.expected = new Promise(resolve => resolve());"
412+
"expected.then(_ => count++)";
413+
JSValue ret = JS_Eval(ctx, code, strlen(code), "<input>", JS_EVAL_TYPE_MODULE);
414+
assert(!JS_IsException(ret));
415+
assert(JS_IsPromise(ret));
416+
assert(JS_PROMISE_FULFILLED == JS_PromiseState(ctx, ret)); // outer module promise
417+
JS_FreeValue(ctx, ret);
418+
assert(4 == cc[JS_PROMISE_HOOK_INIT]);
419+
assert(0 == cc[JS_PROMISE_HOOK_BEFORE]);
420+
assert(0 == cc[JS_PROMISE_HOOK_AFTER]);
421+
assert(3 == cc[JS_PROMISE_HOOK_RESOLVE]);
422+
JSValue v = JS_GetPropertyStr(ctx, global_object, "count");
423+
assert(!JS_IsException(v));
424+
int32_t count;
425+
assert(0 == JS_ToInt32(ctx, &count, v));
426+
assert(0 == count);
427+
JS_FreeValue(ctx, v);
428+
assert(JS_IsJobPending(rt));
429+
assert(1 == JS_ExecutePendingJob(rt, &unused));
430+
assert(!JS_HasException(ctx));
431+
assert(4 == cc[JS_PROMISE_HOOK_INIT]);
432+
assert(0 == cc[JS_PROMISE_HOOK_BEFORE]);
433+
assert(0 == cc[JS_PROMISE_HOOK_AFTER]);
434+
assert(4 == cc[JS_PROMISE_HOOK_RESOLVE]);
435+
assert(!JS_IsJobPending(rt));
436+
v = JS_GetPropertyStr(ctx, global_object, "count");
437+
assert(!JS_IsException(v));
438+
assert(0 == JS_ToInt32(ctx, &count, v));
439+
assert(1 == count);
440+
JS_FreeValue(ctx, v);
441+
JSValue actual = JS_GetPropertyStr(ctx, global_object, "actual");
442+
JSValue expected = JS_GetPropertyStr(ctx, global_object, "expected");
443+
assert(!JS_IsException(actual));
444+
assert(!JS_IsException(expected));
445+
assert(JS_IsSameValue(ctx, actual, expected));
446+
JS_FreeValue(ctx, actual);
447+
JS_FreeValue(ctx, expected);
448+
}
449+
memset(&promise_hook_state, 0, sizeof(promise_hook_state));
450+
{
451+
// module with thenable; fires before and after hooks
452+
static const char code[] =
453+
"new Promise(resolve => resolve({then(resolve){ resolve() }}))";
454+
JSValue ret = JS_Eval(ctx, code, strlen(code), "<input>", JS_EVAL_TYPE_MODULE);
455+
assert(!JS_IsException(ret));
456+
assert(JS_IsPromise(ret));
457+
assert(JS_PROMISE_FULFILLED == JS_PromiseState(ctx, ret)); // outer module promise
458+
JS_FreeValue(ctx, ret);
459+
assert(3 == cc[JS_PROMISE_HOOK_INIT]);
460+
assert(0 == cc[JS_PROMISE_HOOK_BEFORE]);
461+
assert(0 == cc[JS_PROMISE_HOOK_AFTER]);
462+
assert(2 == cc[JS_PROMISE_HOOK_RESOLVE]);
463+
assert(JS_IsJobPending(rt));
464+
assert(1 == JS_ExecutePendingJob(rt, &unused));
465+
assert(!JS_HasException(ctx));
466+
assert(3 == cc[JS_PROMISE_HOOK_INIT]);
467+
assert(1 == cc[JS_PROMISE_HOOK_BEFORE]);
468+
assert(1 == cc[JS_PROMISE_HOOK_AFTER]);
469+
assert(3 == cc[JS_PROMISE_HOOK_RESOLVE]);
470+
assert(!JS_IsJobPending(rt));
471+
}
472+
JS_FreeValue(ctx, global_object);
473+
JS_FreeContext(ctx);
474+
JS_FreeRuntime(rt);
475+
}
476+
314477
int main(void)
315478
{
316479
sync_call();
@@ -321,5 +484,6 @@ int main(void)
321484
module_serde();
322485
two_byte_string();
323486
weak_map_gc_check();
487+
promise_hook();
324488
return 0;
325489
}

quickjs.c

+57-2
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,11 @@ typedef struct JSRuntimeFinalizerState {
237237
void *arg;
238238
} JSRuntimeFinalizerState;
239239

240+
typedef struct JSValueLink {
241+
struct JSValueLink *next;
242+
JSValueConst value;
243+
} JSValueLink;
244+
240245
struct JSRuntime {
241246
JSMallocFunctions mf;
242247
JSMallocState malloc_state;
@@ -284,6 +289,12 @@ struct JSRuntime {
284289
JSInterruptHandler *interrupt_handler;
285290
void *interrupt_opaque;
286291

292+
JSPromiseHook *promise_hook;
293+
void *promise_hook_opaque;
294+
// for smuggling the parent promise from js_promise_then
295+
// to js_promise_constructor
296+
JSValueLink *parent_promise;
297+
287298
JSHostPromiseRejectionTracker *host_promise_rejection_tracker;
288299
void *host_promise_rejection_tracker_opaque;
289300

@@ -50199,6 +50210,12 @@ static JSValue promise_reaction_job(JSContext *ctx, int argc,
5019950210
return res2;
5020050211
}
5020150212

50213+
void JS_SetPromiseHook(JSRuntime *rt, JSPromiseHook promise_hook, void *opaque)
50214+
{
50215+
rt->promise_hook = promise_hook;
50216+
rt->promise_hook_opaque = opaque;
50217+
}
50218+
5020250219
void JS_SetHostPromiseRejectionTracker(JSRuntime *rt,
5020350220
JSHostPromiseRejectionTracker *cb,
5020450221
void *opaque)
@@ -50222,6 +50239,14 @@ static void fulfill_or_reject_promise(JSContext *ctx, JSValueConst promise,
5022250239

5022350240
promise_trace(ctx, "fulfill_or_reject_promise: is_reject=%d\n", is_reject);
5022450241

50242+
if (s->promise_state == JS_PROMISE_FULFILLED) {
50243+
JSRuntime *rt = ctx->rt;
50244+
if (rt->promise_hook) {
50245+
rt->promise_hook(ctx, JS_PROMISE_HOOK_RESOLVE, promise,
50246+
JS_UNDEFINED, rt->promise_hook_opaque);
50247+
}
50248+
}
50249+
5022550250
if (s->promise_state == JS_PROMISE_REJECTED && !s->is_handled) {
5022650251
JSRuntime *rt = ctx->rt;
5022750252
if (rt->host_promise_rejection_tracker) {
@@ -50260,6 +50285,7 @@ static JSValue js_promise_resolve_thenable_job(JSContext *ctx,
5026050285
{
5026150286
JSValueConst promise, thenable, then;
5026250287
JSValue args[2], res;
50288+
JSRuntime *rt;
5026350289

5026450290
promise_trace(ctx, "js_promise_resolve_thenable_job\n");
5026550291

@@ -50269,7 +50295,16 @@ static JSValue js_promise_resolve_thenable_job(JSContext *ctx,
5026950295
then = argv[2];
5027050296
if (js_create_resolving_functions(ctx, args, promise) < 0)
5027150297
return JS_EXCEPTION;
50298+
rt = ctx->rt;
50299+
if (rt->promise_hook) {
50300+
rt->promise_hook(ctx, JS_PROMISE_HOOK_BEFORE, promise, JS_UNDEFINED,
50301+
rt->promise_hook_opaque);
50302+
}
5027250303
res = JS_Call(ctx, then, thenable, 2, vc(args));
50304+
if (rt->promise_hook) {
50305+
rt->promise_hook(ctx, JS_PROMISE_HOOK_AFTER, promise, JS_UNDEFINED,
50306+
rt->promise_hook_opaque);
50307+
}
5027350308
if (JS_IsException(res)) {
5027450309
JSValue error = JS_GetException(ctx);
5027550310
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,
5045250487
JSValueConst executor;
5045350488
JSValue obj;
5045450489
JSPromiseData *s;
50490+
JSRuntime *rt;
5045550491
JSValue args[2], ret;
5045650492
int i;
5045750493

@@ -50472,6 +50508,14 @@ static JSValue js_promise_constructor(JSContext *ctx, JSValueConst new_target,
5047250508
JS_SetOpaqueInternal(obj, s);
5047350509
if (js_create_resolving_functions(ctx, args, obj))
5047450510
goto fail;
50511+
rt = ctx->rt;
50512+
if (rt->promise_hook) {
50513+
JSValueConst parent_promise = JS_UNDEFINED;
50514+
if (rt->parent_promise)
50515+
parent_promise = rt->parent_promise->value;
50516+
rt->promise_hook(ctx, JS_PROMISE_HOOK_INIT, obj, parent_promise,
50517+
rt->promise_hook_opaque);
50518+
}
5047550519
ret = JS_Call(ctx, executor, JS_UNDEFINED, 2, vc(args));
5047650520
if (JS_IsException(ret)) {
5047750521
JSValue ret2, error;
@@ -50529,8 +50573,7 @@ static JSValue js_new_promise_capability(JSContext *ctx,
5052950573

5053050574
executor = js_promise_executor_new(ctx);
5053150575
if (JS_IsException(executor))
50532-
return executor;
50533-
50576+
return JS_EXCEPTION;
5053450577
if (JS_IsUndefined(ctor)) {
5053550578
result_promise = js_promise_constructor(ctx, ctor, 1, vc(&executor));
5053650579
} else {
@@ -51005,7 +51048,10 @@ static JSValue js_promise_then(JSContext *ctx, JSValueConst this_val,
5100551048
int argc, JSValueConst *argv)
5100651049
{
5100751050
JSValue ctor, result_promise, resolving_funcs[2];
51051+
bool have_promise_hook;
51052+
JSValueLink link;
5100851053
JSPromiseData *s;
51054+
JSRuntime *rt;
5100951055
int i, ret;
5101051056

5101151057
s = JS_GetOpaque2(ctx, this_val, JS_CLASS_PROMISE);
@@ -51015,7 +51061,16 @@ static JSValue js_promise_then(JSContext *ctx, JSValueConst this_val,
5101551061
ctor = JS_SpeciesConstructor(ctx, this_val, JS_UNDEFINED);
5101651062
if (JS_IsException(ctor))
5101751063
return ctor;
51064+
rt = ctx->rt;
51065+
// always restore, even if js_new_promise_capability callee removes hook
51066+
have_promise_hook = (rt->promise_hook != NULL);
51067+
if (have_promise_hook) {
51068+
link = (JSValueLink){rt->parent_promise, this_val};
51069+
rt->parent_promise = &link;
51070+
}
5101851071
result_promise = js_new_promise_capability(ctx, resolving_funcs, ctor);
51072+
if (have_promise_hook)
51073+
rt->parent_promise = link.next;
5101951074
JS_FreeValue(ctx, ctor);
5102051075
if (JS_IsException(result_promise))
5102151076
return result_promise;

quickjs.h

+17
Original file line numberDiff line numberDiff line change
@@ -1003,6 +1003,23 @@ JS_EXTERN bool JS_IsPromise(JSValueConst val);
10031003

10041004
JS_EXTERN JSValue JS_NewSymbol(JSContext *ctx, const char *description, bool is_global);
10051005

1006+
typedef enum JSPromiseHookType {
1007+
JS_PROMISE_HOOK_INIT, // emitted when a new promise is created
1008+
JS_PROMISE_HOOK_BEFORE, // runs right before promise.then is invoked
1009+
JS_PROMISE_HOOK_AFTER, // runs right after promise.then is invoked
1010+
JS_PROMISE_HOOK_RESOLVE, // not emitted for rejected promises
1011+
} JSPromiseHookType;
1012+
1013+
// parent_promise is only passed in when type == JS_PROMISE_HOOK_INIT and
1014+
// is then either a promise object or JS_UNDEFINED if the new promise does
1015+
// not have a parent promise; only promises created with promise.then have
1016+
// a parent promise
1017+
typedef void JSPromiseHook(JSContext *ctx, JSPromiseHookType type,
1018+
JSValueConst promise, JSValueConst parent_promise,
1019+
void *opaque);
1020+
JS_EXTERN void JS_SetPromiseHook(JSRuntime *rt, JSPromiseHook promise_hook,
1021+
void *opaque);
1022+
10061023
/* is_handled = true means that the rejection is handled */
10071024
typedef void JSHostPromiseRejectionTracker(JSContext *ctx, JSValueConst promise,
10081025
JSValueConst reason,

0 commit comments

Comments
 (0)