ngx_js_loc_conf_t *conf);
static ngx_int_t ngx_js_init_preload_vm(njs_vm_t *vm, ngx_js_loc_conf_t *conf);
+static ngx_int_t ngx_njs_execute_pending_jobs(njs_vm_t *vm, ngx_log_t *log);
+static njs_int_t ngx_njs_await(njs_vm_t *vm, ngx_log_t *log,
+ njs_value_t *value);
+
#if (NJS_HAVE_QUICKJS)
+static ngx_int_t ngx_qjs_execute_pending_jobs(JSContext *cx, ngx_log_t *log);
+static ngx_int_t ngx_qjs_await(JSContext *cx, ngx_log_t *log,
+ JSValueConst *value);
static ngx_int_t ngx_engine_qjs_init(ngx_engine_t *engine,
ngx_engine_opts_t *opts);
static ngx_int_t ngx_engine_qjs_compile(ngx_js_loc_conf_t *conf, ngx_log_t *log,
}
+static ngx_int_t
+ngx_njs_execute_pending_jobs(njs_vm_t *vm, ngx_log_t *log)
+{
+ njs_int_t ret;
+ njs_str_t exception;
+
+ for ( ;; ) {
+ ret = njs_vm_execute_pending_job(vm);
+ if (ret <= NJS_OK) {
+ if (ret == NJS_ERROR) {
+ njs_vm_exception_string(vm, &exception);
+ ngx_log_error(NGX_LOG_ERR, log, 0, "js job exception: %V",
+ &exception);
+ return NGX_ERROR;
+ }
+
+ break;
+ }
+ }
+
+ return NGX_OK;
+}
+
+
+static njs_int_t
+ngx_njs_await(njs_vm_t *vm, ngx_log_t *log, njs_value_t *value)
+{
+ ngx_int_t ret;
+ njs_promise_type_t state;
+
+ ret = ngx_njs_execute_pending_jobs(vm, log);
+ if (ret != NGX_OK) {
+ return NGX_ERROR;
+ }
+
+ if (njs_value_is_promise(value)) {
+ state = njs_promise_state(value);
+
+ if (state == NJS_PROMISE_FULFILL) {
+ njs_value_assign(value, njs_promise_result(value));
+
+ } else if (state == NJS_PROMISE_REJECTED) {
+ njs_vm_throw(vm, njs_promise_result(value));
+ }
+ }
+
+ return NGX_OK;
+}
+
+
ngx_engine_t *
ngx_njs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external)
{
njs_vm_t *vm;
+ njs_int_t ret;
ngx_engine_t *engine;
njs_opaque_value_t retval;
engine = njs_mp_alloc(njs_vm_memory_pool(vm), sizeof(ngx_engine_t));
if (engine == NULL) {
- njs_vm_destroy(vm);
- return NULL;
+ goto destroy;
}
memcpy(engine, cf->engine, sizeof(ngx_engine_t));
if (njs_vm_start(vm, njs_value_arg(&retval)) == NJS_ERROR) {
ngx_js_log_exception(vm, ctx->log, "exception");
+ goto destroy;
+ }
- njs_vm_destroy(vm);
-
- return NULL;
+ ret = ngx_njs_await(vm, ctx->log, njs_value_arg(&retval));
+ if (ret == NGX_ERROR) {
+ goto destroy;
}
return engine;
+
+destroy:
+
+ njs_vm_destroy(vm);
+
+ return NULL;
}
return NGX_ERROR;
}
- for ( ;; ) {
- ret = njs_vm_execute_pending_job(vm);
- if (ret <= NJS_OK) {
- if (ret == NJS_ERROR) {
- ngx_js_log_exception(vm, ctx->log, "exception");
-
- return NGX_ERROR;
- }
-
- break;
- }
+ ret = ngx_njs_await(vm, ctx->log, njs_value_arg(&ctx->retval));
+ if (ret == NGX_ERROR) {
+ return NGX_ERROR;
}
return njs_rbtree_is_empty(&ctx->waiting_events) ? NGX_OK : NGX_AGAIN;
#if (NJS_HAVE_QUICKJS)
+static ngx_int_t
+ngx_qjs_execute_pending_jobs(JSContext *cx, ngx_log_t *log)
+{
+ int rc;
+ JSValue value;
+ JSContext *cx1;
+ const char *exception;
+
+ for ( ;; ) {
+ rc = JS_ExecutePendingJob(JS_GetRuntime(cx), &cx1);
+ if (rc <= 0) {
+ if (rc == 0) {
+ break;
+ }
+
+ value = JS_GetException(cx);
+ exception = JS_ToCString(cx, value);
+ JS_FreeValue(cx, value);
+
+ ngx_log_error(NGX_LOG_ERR, log, 0, "js job exception: %s",
+ exception);
+
+ JS_FreeCString(cx, exception);
+
+ return NGX_ERROR;
+ }
+ }
+
+ return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_qjs_await(JSContext *cx, ngx_log_t *log, JSValue *value)
+{
+ JSValue ret;
+ ngx_int_t rc;
+ JSPromiseStateEnum state;
+
+ rc = ngx_qjs_execute_pending_jobs(cx, log);
+ if (rc != NGX_OK) {
+ return NGX_ERROR;
+ }
+
+ state = JS_PromiseState(cx, *value);
+ if (state == JS_PROMISE_FULFILLED) {
+ ret = JS_PromiseResult(cx, *value);
+ JS_FreeValue(cx, *value);
+ *value = ret;
+
+ } else if (state == JS_PROMISE_REJECTED) {
+ ret = JS_Throw(cx, JS_PromiseResult(cx, *value));
+ JS_FreeValue(cx, *value);
+ *value = ret;
+ }
+
+ return NGX_OK;
+}
+
+
static ngx_int_t
ngx_engine_qjs_init(ngx_engine_t *engine, ngx_engine_opts_t *opts)
{
}
-static JSValue
-js_std_await(JSContext *ctx, JSValue obj)
-{
- int state, err;
- JSValue ret;
- JSContext *ctx1;
-
- for (;;) {
- state = JS_PromiseState(ctx, obj);
- if (state == JS_PROMISE_FULFILLED) {
- ret = JS_PromiseResult(ctx, obj);
- JS_FreeValue(ctx, obj);
- break;
-
- } else if (state == JS_PROMISE_REJECTED) {
- ret = JS_Throw(ctx, JS_PromiseResult(ctx, obj));
- JS_FreeValue(ctx, obj);
- break;
-
- } else if (state == JS_PROMISE_PENDING) {
- err = JS_ExecutePendingJob(JS_GetRuntime(ctx), &ctx1);
- if (err < 0) {
- /* js_std_dump_error(ctx1); */
- }
-
- } else {
- /* not a promise */
- ret = obj;
- break;
- }
- }
-
- return ret;
-}
-
-
ngx_engine_t *
ngx_qjs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external)
{
JSValue rv;
njs_mp_t *mp;
uint32_t i, length;
+ ngx_int_t rc;
JSRuntime *rt;
JSContext *cx;
ngx_engine_t *engine;
goto destroy;
}
- rv = js_std_await(cx, rv);
- if (JS_IsException(rv)) {
- ngx_qjs_log_exception(engine, ctx->log, "eval exception");
+ rc = ngx_qjs_await(cx, ctx->log, &rv);
+ JS_FreeValue(cx, rv);
+ if (rc == NGX_ERROR) {
+ ngx_qjs_log_exception(engine, ctx->log, "await exception");
goto destroy;
}
- JS_FreeValue(cx, rv);
-
return engine;
destroy:
ngx_engine_qjs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname,
njs_opaque_value_t *args, njs_uint_t nargs)
{
- int rc;
JSValue fn, val;
- JSRuntime *rt;
- JSContext *cx, *cx1;
+ ngx_int_t rc;
+ JSContext *cx;
cx = ctx->engine->u.qjs.ctx;
JS_FreeValue(cx, ngx_qjs_arg(ctx->retval));
ngx_qjs_arg(ctx->retval) = val;
- rt = JS_GetRuntime(cx);
-
- for ( ;; ) {
- rc = JS_ExecutePendingJob(rt, &cx1);
- if (rc <= 0) {
- if (rc == -1) {
- ngx_qjs_log_exception(ctx->engine, ctx->log, "job exception");
-
- return NGX_ERROR;
- }
-
- break;
- }
+ rc = ngx_qjs_await(cx, ctx->log, &ngx_qjs_arg(ctx->retval));
+ if (rc == NGX_ERROR) {
+ ngx_qjs_log_exception(ctx->engine, ctx->log, "await exception");
+ return NGX_ERROR;
}
return njs_rbtree_is_empty(&ctx->waiting_events) ? NGX_OK : NGX_AGAIN;
ngx_int_t
ngx_qjs_call(JSContext *cx, JSValue fn, JSValue *argv, int argc)
{
- int rc;
JSValue ret;
- JSRuntime *rt;
- JSContext *cx1;
+ ngx_int_t rc;
ngx_js_ctx_t *ctx;
ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx));
JS_FreeValue(cx, ret);
- rt = JS_GetRuntime(cx);
-
- for ( ;; ) {
- rc = JS_ExecutePendingJob(rt, &cx1);
- if (rc <= 0) {
- if (rc == -1) {
- ngx_qjs_log_exception(ctx->engine, ctx->log, "job exception");
-
- return NGX_ERROR;
- }
-
- break;
- }
+ rc = ngx_qjs_execute_pending_jobs(cx, ctx->log);
+ if (rc != NGX_OK) {
+ return NGX_ERROR;
}
return NGX_OK;
njs_uint_t nargs)
{
njs_int_t ret;
+ ngx_js_ctx_t *ctx;
ngx_connection_t *c;
ret = njs_vm_call(vm, func, njs_value_arg(args), nargs);
return NGX_ERROR;
}
- for ( ;; ) {
- ret = njs_vm_execute_pending_job(vm);
- if (ret <= NJS_OK) {
- c = ngx_external_connection(vm, njs_vm_external_ptr(vm));
-
- if (ret == NJS_ERROR) {
- ngx_js_log_exception(vm, c->log, "job exception");
-
- return NGX_ERROR;
- }
+ ctx = ngx_external_ctx(vm, njs_vm_external_ptr(vm));
- break;
- }
+ ret = ngx_njs_execute_pending_jobs(vm, ctx->log);
+ if (ret != NGX_OK) {
+ return NGX_ERROR;
}
return NGX_OK;
http {
%%TEST_GLOBALS_HTTP%%
- js_set $test_async test.set_timeout;
- js_set $context_var test.context_var;
- js_set $test_set_rv_var test.set_rv_var;
+ js_set $test_async test.set_timeout;
+ js_set $context_var test.context_var;
+ js_set $test_set_rv_var test.set_rv_var;
+ js_set $test_promise_var test.set_promise_var;
js_import test.js;
return 200 $test_set_rv_var;
}
+ location /promise_var {
+ return 200 $test_promise_var;
+ }
+
location /await_reject {
js_content test.await_reject;
}
r.setReturnValue(`retval: \${a1 + a2}`);
}
+ async function set_promise_var(r) {
+ const a1 = await pr(10);
+ const a2 = await pr(20);
+
+ return `retval: \${a1 + a2}`;
+ }
+
async function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => {
export default {njs:test_njs, set_timeout, set_timeout_data,
set_timeout_many, context_var, shared_ctx, limit_rate,
- async_content, set_rv_var, await_reject};
+ async_content, set_rv_var, set_promise_var, await_reject};
EOF
-$t->try_run('no njs available')->plan(10);
+$t->try_run('no njs available')->plan(11);
###############################################################################
like(http_get('/async_content'), qr/retval: AB/, 'async content');
like(http_get('/set_rv_var'), qr/retval: 30/, 'set return value variable');
+like(http_get('/promise_var'), qr/retval: 30/, 'fulfilled promise variable');
http_get('/async_var');
http_get('/await_reject');
--- /dev/null
+#!/usr/bin/perl
+
+# (C) F5, Inc.
+# Vadim Zhestikov
+
+# Tests for top-level await of promises in QuickJS engine.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/http rewrite/)
+ ->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+http {
+ %%TEST_GLOBALS_HTTP%%
+
+ js_import test.js;
+ js_import fulfilled_test.js;
+
+ server {
+ listen 127.0.0.1:8080;
+ server_name localhost;
+
+ location /resolved {
+ js_content test.test;
+ }
+
+ location /fulfilled {
+ js_content fulfilled_test.test;
+ }
+ }
+}
+
+EOF
+
+$t->write_file('test.js', <<'EOF');
+ var globalResult = await Promise.resolve("resolved value");
+
+ function test(r) {
+ r.return(200, "global result: " + globalResult);
+ }
+
+ export default {test};
+
+EOF
+
+$t->write_file('fulfilled_test.js', <<'EOF');
+ var globalResult = await new Promise((resolve) => {
+ Promise.resolve().then(() => {
+ resolve("fulfilled value");
+ });
+ });
+
+ function test(r) {
+ r.return(200, "fulfilled result: " + globalResult);
+ }
+
+ export default {test};
+
+EOF
+
+$t->try_run('no top-level await support')->plan(2);
+
+###############################################################################
+
+like(http_get('/resolved'), qr/global result: resolved value/,
+ 'basic global await works');
+like(http_get('/fulfilled'), qr/fulfilled result: fulfilled value/,
+ 'fulfilled promise via microtask works');
+
+###############################################################################
stream {
%%TEST_GLOBALS_STREAM%%
- js_set $js_addr test.addr;
- js_set $js_var test.variable;
- js_set $js_log test.log;
- js_set $js_unk test.unk;
- js_set $js_req_line test.req_line;
- js_set $js_sess_unk test.sess_unk;
- js_set $js_async test.asyncf;
- js_set $js_buffer test.buffer;
+ js_set $js_addr test.addr;
+ js_set $js_var test.variable;
+ js_set $js_log test.log;
+ js_set $js_unk test.unk;
+ js_set $js_req_line test.req_line;
+ js_set $js_sess_unk test.sess_unk;
+ js_set $js_async test.asyncf;
+ js_set $js_async_direct test.asyncf_direct;
+ js_set $js_buffer test.buffer;
js_import test.js;
return $js_async;
}
+ server {
+ listen 127.0.0.1:8102;
+ return $js_async_direct;
+ }
+
server {
listen 127.0.0.1:8101;
return $js_buffer;
s.setReturnValue(`retval: \${a1 + a2}`);
}
+ async function asyncf_direct(s) {
+ const a1 = await pr(10);
+ const a2 = await pr(20);
+
+ return `retval: \${a1 + a2}`;
+ }
+
export default {njs:test_njs, addr, variable, sess_unk, log, access_step,
preread_step, filter_step, access_undecided, access_allow,
access_deny, preread_async, preread_data, preread_req_line,
req_line, filter_empty, filter_header_inject, filter_search,
access_except, preread_except, filter_except, asyncf,
- buffer};
+ asyncf_direct, buffer};
EOF
$t->run_daemon(\&stream_daemon, port(8090));
-$t->try_run('no stream njs available')->plan(25);
+$t->try_run('no stream njs available')->plan(26);
$t->waitforsocket('127.0.0.1:' . port(8090));
###############################################################################
stream('127.0.0.1:' . port(8098))->io('x');
stream('127.0.0.1:' . port(8099))->io('x');
-is(stream('127.0.0.1:' . port(8100))->read(), 'retval: 30', 'asyncf');
+is(stream('127.0.0.1:' . port(8100))->read(), 'retval: 30',
+ 'async var handler setReturnValue');
+is(stream('127.0.0.1:' . port(8102))->read(), 'retval: 30',
+ 'async var handler direct');
TODO: {
local $TODO = 'not yet' unless has_version('0.8.3');