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,