http subrequest() method.

Creates an nginx's subrequest with the specified arguments and
registers a finalization callback.

req.subrequest(<uri>[, <options>[, <callback>]]):
    uri - string.
    options - string | object.
        string value - uri arguments.
        object value can contain:
            args, body, method all are string values.
    callback - function with the following argument:
        reply - the result object with the following properties:
            uri, method, status, contentType, contentLength,
            headers, args, body, parent.
diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c
index 367edb7..453b257 100644
--- a/nginx/ngx_http_js_module.c
+++ b/nginx/ngx_http_js_module.c
@@ -24,6 +24,7 @@
     ngx_str_t            content;
     const njs_extern_t  *req_proto;
     const njs_extern_t  *res_proto;
+    const njs_extern_t  *rep_proto;
 } ngx_http_js_loc_conf_t;
 
 
@@ -31,6 +32,7 @@
     njs_vm_t            *vm;
     ngx_log_t           *log;
     njs_opaque_value_t   args[2];
+    ngx_uint_t           done;
 } ngx_http_js_ctx_t;
 
 
@@ -107,6 +109,17 @@
     void *obj, uintptr_t data);
 static njs_ret_t ngx_http_js_ext_get_response(njs_vm_t *vm, njs_value_t *value,
     void *obj, uintptr_t data);
+static njs_ret_t ngx_http_js_ext_subrequest(njs_vm_t *vm, njs_value_t *args,
+    nxt_uint_t nargs, njs_index_t unused);
+static ngx_int_t ngx_http_js_subrequest(ngx_http_request_t *r,
+    nxt_str_t *uri_arg, nxt_str_t *args_arg, njs_function_t *callback,
+    ngx_http_request_t **sr);
+static ngx_int_t ngx_http_js_subrequest_done(ngx_http_request_t *r,
+    void *data, ngx_int_t rc);
+static njs_ret_t ngx_http_js_ext_get_parent(njs_vm_t *vm, njs_value_t *value,
+    void *obj, uintptr_t data);
+static njs_ret_t ngx_http_js_ext_get_body(njs_vm_t *vm, njs_value_t *value,
+    void *obj, uintptr_t data);
 
 static njs_host_event_t ngx_http_js_set_timer(njs_external_ptr_t external,
     uint64_t delay, njs_vm_event_t vm_event);
@@ -114,7 +127,7 @@
     njs_host_event_t event);
 static void ngx_http_js_timer_handler(ngx_event_t *ev);
 static void ngx_http_js_handle_event(ngx_http_request_t *r,
-    njs_vm_event_t vm_event);
+    njs_vm_event_t vm_event, njs_opaque_value_t *args, nxt_uint_t nargs);
 
 static char *ngx_http_js_include(ngx_conf_t *cf, ngx_command_t *cmd,
     void *conf);
@@ -381,6 +394,118 @@
       NULL,
       NULL,
       0 },
+
+    { nxt_string("subrequest"),
+      NJS_EXTERN_METHOD,
+      NULL,
+      0,
+      NULL,
+      NULL,
+      NULL,
+      NULL,
+      NULL,
+      ngx_http_js_ext_subrequest,
+      0 },
+};
+
+
+static njs_external_t  ngx_http_js_ext_reply[] = {
+
+    { nxt_string("headers"),
+      NJS_EXTERN_OBJECT,
+      NULL,
+      0,
+      ngx_http_js_ext_get_header_out,
+      NULL,
+      NULL,
+      ngx_http_js_ext_foreach_header_out,
+      ngx_http_js_ext_next_header,
+      NULL,
+      0 },
+
+    { nxt_string("status"),
+      NJS_EXTERN_PROPERTY,
+      NULL,
+      0,
+      ngx_http_js_ext_get_status,
+      NULL,
+      NULL,
+      NULL,
+      NULL,
+      NULL,
+      offsetof(ngx_http_request_t, headers_out.status) },
+
+    { nxt_string("uri"),
+      NJS_EXTERN_PROPERTY,
+      NULL,
+      0,
+      ngx_http_js_ext_get_string,
+      NULL,
+      NULL,
+      NULL,
+      NULL,
+      NULL,
+      offsetof(ngx_http_request_t, uri) },
+
+    { nxt_string("method"),
+      NJS_EXTERN_PROPERTY,
+      NULL,
+      0,
+      ngx_http_js_ext_get_string,
+      NULL,
+      NULL,
+      NULL,
+      NULL,
+      NULL,
+      offsetof(ngx_http_request_t, method_name) },
+
+    { nxt_string("contentType"),
+      NJS_EXTERN_PROPERTY,
+      NULL,
+      0,
+      ngx_http_js_ext_get_string,
+      NULL,
+      NULL,
+      NULL,
+      NULL,
+      NULL,
+      offsetof(ngx_http_request_t, headers_out.content_type) },
+
+    { nxt_string("contentLength"),
+      NJS_EXTERN_PROPERTY,
+      NULL,
+      0,
+      ngx_http_js_ext_get_content_length,
+      NULL,
+      NULL,
+      NULL,
+      NULL,
+      NULL,
+      0 },
+
+    { nxt_string("parent"),
+      NJS_EXTERN_PROPERTY,
+      NULL,
+      0,
+      ngx_http_js_ext_get_parent,
+      NULL,
+      NULL,
+      NULL,
+      NULL,
+      NULL,
+      0 },
+
+    { nxt_string("body"),
+      NJS_EXTERN_PROPERTY,
+      NULL,
+      0,
+      ngx_http_js_ext_get_body,
+      NULL,
+      NULL,
+      NULL,
+      NULL,
+      NULL,
+      0 },
 };
 
 
@@ -409,6 +534,18 @@
       NULL,
       NULL,
       0 },
+
+    { nxt_string("reply"),
+      NJS_EXTERN_OBJECT,
+      ngx_http_js_ext_reply,
+      nxt_nitems(ngx_http_js_ext_reply),
+      NULL,
+      NULL,
+      NULL,
+      NULL,
+      NULL,
+      NULL,
+      0 },
 };
 
 
@@ -916,20 +1053,13 @@
 ngx_http_js_ext_get_status(njs_vm_t *vm, njs_value_t *value, void *obj,
     uintptr_t data)
 {
-    size_t               len;
-    u_char              *p;
     ngx_http_request_t  *r;
 
     r = (ngx_http_request_t *) obj;
 
-    p = ngx_pnalloc(r->pool, 3);
-    if (p == NULL) {
-        return NJS_ERROR;
-    }
+    njs_value_number_set(njs_vm_retval(vm), r->headers_out.status);
 
-    len = ngx_snprintf(p, 3, "%ui", r->headers_out.status) - p;
-
-    return njs_string_create(vm, value, p, len, 0);
+    return NJS_OK;
 }
 
 
@@ -957,20 +1087,13 @@
 ngx_http_js_ext_get_content_length(njs_vm_t *vm, njs_value_t *value, void *obj,
     uintptr_t data)
 {
-    size_t               len;
-    u_char              *p;
     ngx_http_request_t  *r;
 
     r = (ngx_http_request_t *) obj;
 
-    p = ngx_pnalloc(r->pool, NGX_OFF_T_LEN);
-    if (p == NULL) {
-        return NJS_ERROR;
-    }
+    njs_value_number_set(njs_vm_retval(vm), r->headers_out.content_length_n);
 
-    len = ngx_sprintf(p, "%O", r->headers_out.content_length_n) - p;
-
-    return njs_string_create(vm, value, p, len, 0);
+    return NJS_OK;
 }
 
 
@@ -1324,6 +1447,383 @@
 }
 
 
+static njs_ret_t
+ngx_http_js_ext_subrequest(njs_vm_t *vm, njs_value_t *args, nxt_uint_t nargs,
+    njs_index_t unused)
+{
+    ngx_int_t                 rc;
+    nxt_str_t                 uri_arg, args_arg, method_name, body_arg;
+    ngx_uint_t                cb_index, method, n, has_body;
+    const char               *description;
+    njs_value_t              *arg2, *options, *value;
+    njs_function_t           *callback;
+    ngx_http_request_t       *r, *sr;
+    ngx_http_request_body_t  *rb;
+
+    static const struct {
+        ngx_str_t   name;
+        ngx_uint_t  value;
+    } methods[] = {
+        { ngx_string("GET"),       NGX_HTTP_GET },
+        { ngx_string("POST"),      NGX_HTTP_POST },
+        { ngx_string("HEAD"),      NGX_HTTP_HEAD },
+        { ngx_string("OPTIONS"),   NGX_HTTP_OPTIONS },
+        { ngx_string("PROPFIND"),  NGX_HTTP_PROPFIND },
+        { ngx_string("PUT"),       NGX_HTTP_PUT },
+        { ngx_string("MKCOL"),     NGX_HTTP_MKCOL },
+        { ngx_string("DELETE"),    NGX_HTTP_DELETE },
+        { ngx_string("COPY"),      NGX_HTTP_COPY },
+        { ngx_string("MOVE"),      NGX_HTTP_MOVE },
+        { ngx_string("PROPPATCH"), NGX_HTTP_PROPPATCH },
+        { ngx_string("LOCK"),      NGX_HTTP_LOCK },
+        { ngx_string("UNLOCK"),    NGX_HTTP_UNLOCK },
+        { ngx_string("PATCH"),     NGX_HTTP_PATCH },
+        { ngx_string("TRACE"),     NGX_HTTP_TRACE },
+    };
+
+    static const nxt_str_t args_key   = nxt_string("args");
+    static const nxt_str_t method_key = nxt_string("method");
+    static const nxt_str_t body_key = nxt_string("body");
+
+    if (nargs < 2) {
+        description = "too few arguments";
+        goto exception;
+    }
+
+    r = njs_value_data(njs_argument(args, 0));
+
+    if (njs_vm_value_to_ext_string(vm, &uri_arg, njs_argument(args, 1), 0)
+        == NJS_ERROR)
+    {
+        description = "failed to convert uri arg";
+        goto exception;
+    }
+
+    options = NULL;
+
+    method = 0;
+    args_arg.length = 0;
+    body_arg.length = 0;
+    has_body = 0;
+
+    if (nargs > 2 && !njs_value_is_function(njs_argument(args, 2))) {
+        arg2 = njs_argument(args, 2);
+
+        if (njs_value_is_object(arg2)) {
+            options = arg2;
+
+        } else if (njs_value_is_string(arg2)) {
+            if (njs_vm_value_to_ext_string(vm, &args_arg, arg2, 0)
+                == NJS_ERROR)
+            {
+                description = "failed to convert args";
+                goto exception;
+            }
+
+        } else {
+            description = "failed to convert args";
+            goto exception;
+        }
+
+        cb_index = 3;
+
+    } else {
+        cb_index = 2;
+    }
+
+    if (options != NULL) {
+        value = njs_vm_object_prop(vm, options, &args_key);
+        if (value != NULL) {
+            if (njs_vm_value_to_ext_string(vm, &args_arg, value, 0)
+                == NJS_ERROR)
+            {
+                description = "failed to convert options.args";
+                goto exception;
+            }
+        }
+
+        value = njs_vm_object_prop(vm, options, &method_key);
+        if (value != NULL) {
+            if (njs_vm_value_to_ext_string(vm, &method_name, value, 0)
+                == NJS_ERROR)
+            {
+                description = "failed to convert options.method";
+                goto exception;
+            }
+
+            n = sizeof(methods) / sizeof(methods[0]);
+
+            while (method < n) {
+                if (method_name.length == methods[method].name.len
+                    && ngx_memcmp(method_name.start, methods[method].name.data,
+                                  method_name.length)
+                       == 0)
+                {
+                    break;
+                }
+
+                method++;
+            }
+
+            if (method == n) {
+                njs_value_error_set(vm, njs_vm_retval(vm),
+                                    "unknown method \"%.*s\"",
+                                    (int) method_name.length,
+                                    method_name.start);
+
+                return NJS_ERROR;
+            }
+        }
+
+        value = njs_vm_object_prop(vm, options, &body_key);
+        if (value != NULL) {
+            if (njs_vm_value_to_ext_string(vm, &body_arg, value, 0)
+                == NJS_ERROR)
+            {
+                description = "failed to convert options.body";
+                goto exception;
+            }
+
+            has_body = 1;
+        }
+    }
+
+    callback = NULL;
+
+    if (cb_index < nargs) {
+        if (!njs_value_is_function(njs_argument(args, cb_index))) {
+            description = "callback is not a function";
+            goto exception;
+
+        } else {
+            callback = njs_value_function(njs_argument(args, cb_index));
+        }
+    }
+
+    rc  = ngx_http_js_subrequest(r, &uri_arg, &args_arg, callback, &sr);
+    if (rc != NGX_OK) {
+        return NJS_ERROR;
+    }
+
+    sr->method = methods[method].value;
+    sr->method_name = methods[method].name;
+    sr->header_only = (sr->method == NGX_HTTP_HEAD) || (callback == NULL);
+
+    if (has_body) {
+        rb = ngx_pcalloc(r->pool, sizeof(ngx_http_request_body_t));
+        if (rb == NULL) {
+            return NJS_ERROR;
+        }
+
+        rb->bufs = ngx_alloc_chain_link(r->pool);
+        if (rb->bufs == NULL) {
+            return NJS_ERROR;
+        }
+
+        rb->bufs->next = NULL;
+
+        rb->bufs->buf = ngx_calloc_buf(r->pool);
+        if (rb->bufs->buf == NULL) {
+            return NJS_ERROR;
+        }
+
+        rb->bufs->buf->memory = 1;
+        rb->bufs->buf->last_buf = 1;
+
+        rb->bufs->buf->pos = body_arg.start;
+        rb->bufs->buf->last = body_arg.start + body_arg.length;
+
+        sr->request_body = rb;
+        sr->headers_in.content_length_n = body_arg.length;
+        sr->headers_in.chunked = 0;
+    }
+
+    return NJS_OK;
+
+exception:
+
+    njs_value_error_set(vm, njs_vm_retval(vm), description, NULL);
+
+    return NJS_ERROR;
+}
+
+
+static ngx_int_t
+ngx_http_js_subrequest(ngx_http_request_t *r, nxt_str_t *uri_arg,
+    nxt_str_t *args_arg, njs_function_t *callback, ngx_http_request_t **sr)
+{
+    ngx_int_t                    flags;
+    ngx_str_t                    uri, args;
+    const char                  *description;
+    njs_vm_event_t               vm_event;
+    ngx_http_js_ctx_t           *ctx;
+    ngx_http_post_subrequest_t  *ps;
+
+    ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
+
+    flags = NGX_HTTP_SUBREQUEST_BACKGROUND;
+
+    if (callback != NULL) {
+        ps = ngx_palloc(r->pool, sizeof(ngx_http_post_subrequest_t));
+        if (ps == NULL) {
+            description = "internal error";
+            goto exception;
+        }
+
+        vm_event = njs_vm_add_event(ctx->vm, callback, NULL, NULL);
+        if (vm_event == NULL) {
+            description = "internal error";
+            goto exception;
+        }
+
+        ps->handler = ngx_http_js_subrequest_done;
+        ps->data = vm_event;
+
+        flags |= NGX_HTTP_SUBREQUEST_IN_MEMORY;
+
+    } else {
+        ps = NULL;
+        vm_event = NULL;
+    }
+
+    uri.len = uri_arg->length;
+    uri.data = uri_arg->start;
+
+    args.len = args_arg->length;
+    args.data = args_arg->start;
+
+    if (ngx_http_subrequest(r, &uri, args.len ? &args : NULL, sr, ps, flags)
+        != NGX_OK)
+    {
+        if (vm_event != NULL) {
+            njs_vm_del_event(ctx->vm, vm_event);
+        }
+
+        description = "subrequest creation failed";
+        goto exception;
+    }
+
+    return NJS_OK;
+
+exception:
+
+    njs_value_error_set(ctx->vm, njs_vm_retval(ctx->vm), description, NULL);
+
+    return NJS_ERROR;
+}
+
+
+static ngx_int_t
+ngx_http_js_subrequest_done(ngx_http_request_t *r, void *data, ngx_int_t rc)
+{
+    njs_vm_event_t  vm_event = data;
+
+    nxt_int_t                ret;
+    ngx_http_js_ctx_t       *ctx;
+    njs_opaque_value_t       reply;
+    ngx_http_js_loc_conf_t  *jlcf;
+
+    if (rc != NGX_OK || r->connection->error || r->buffered) {
+        return rc;
+    }
+
+    ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
+
+    if (ctx && ctx->done) {
+        return NGX_OK;
+    }
+
+    if (ctx == NULL) {
+        ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_js_ctx_t));
+        if (ctx == NULL) {
+            return NGX_ERROR;
+        }
+
+        ngx_http_set_ctx(r, ctx, ngx_http_js_module);
+    }
+
+    ctx->done = 1;
+
+    jlcf = ngx_http_get_module_loc_conf(r->parent, ngx_http_js_module);
+
+    ctx = ngx_http_get_module_ctx(r->parent, ngx_http_js_module);
+
+    ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                   "js subrequest done s: %ui parent ctx: %p",
+                   r->headers_out.status, ctx);
+
+    if (ctx == NULL) {
+        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
+                      "js subrequest: failed to get the parent context");
+
+        return NGX_ERROR;
+    }
+
+    ret = njs_vm_external_create(ctx->vm, &reply, jlcf->rep_proto, r);
+    if (ret != NXT_OK) {
+        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
+                      "js subrequest reply creation failed");
+
+        return NGX_ERROR;
+    }
+
+    ngx_http_js_handle_event(r->parent, vm_event, &reply, 1);
+
+    return NGX_OK;
+}
+
+
+static njs_ret_t
+ngx_http_js_ext_get_parent(njs_vm_t *vm, njs_value_t *value, void *obj,
+    uintptr_t data)
+{
+    ngx_http_js_ctx_t   *ctx;
+    ngx_http_request_t  *r;
+
+    r = (ngx_http_request_t *) obj;
+
+    ctx = ngx_http_get_module_ctx(r->parent, ngx_http_js_module);
+
+    if (ctx == NULL) {
+        njs_value_error_set(vm, njs_vm_retval(vm),
+                            "failed to get the parent context", NULL);
+        return NJS_ERROR;
+    }
+
+    njs_vm_retval_set(ctx->vm, &ctx->args[0]);
+
+    return NJS_OK;
+}
+
+
+static njs_ret_t
+ngx_http_js_ext_get_body(njs_vm_t *vm, njs_value_t *value, void *obj,
+	uintptr_t data)
+{
+    size_t               len;
+    u_char              *p;
+    ngx_buf_t           *b;
+    ngx_http_request_t  *r;
+
+    r = (ngx_http_request_t *) obj;
+
+    b = r->out ? r->out->buf : NULL;
+
+    len = b ? b->last - b->pos : 0;
+
+    p = njs_string_alloc(vm, value, len, 0);
+    if (p == NULL) {
+        return NJS_ERROR;
+    }
+
+    if (len) {
+        ngx_memcpy(p, b->pos, len);
+    }
+
+    return NJS_OK;
+}
+
+
 static njs_host_event_t
 ngx_http_js_set_timer(njs_external_ptr_t external, uint64_t delay,
     njs_vm_event_t vm_event)
@@ -1382,14 +1882,15 @@
 
     c = r->connection;
 
-    ngx_http_js_handle_event(r, js_event->vm_event);
+    ngx_http_js_handle_event(r, js_event->vm_event, NULL, 0);
 
     ngx_http_run_posted_requests(c);
 }
 
 
 static void
-ngx_http_js_handle_event(ngx_http_request_t *r, njs_vm_event_t vm_event)
+ngx_http_js_handle_event(ngx_http_request_t *r, njs_vm_event_t vm_event,
+    njs_opaque_value_t *args, nxt_uint_t nargs)
 {
     njs_ret_t           rc;
     nxt_str_t           exception;
@@ -1397,7 +1898,7 @@
 
     ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
 
-    njs_vm_post_event(ctx->vm, vm_event);
+    njs_vm_post_event(ctx->vm, vm_event, args, nargs);
 
     rc = njs_vm_run(ctx->vm);
 
@@ -1525,6 +2026,13 @@
         return NGX_CONF_ERROR;
     }
 
+    jlcf->rep_proto = njs_vm_external_prototype(jlcf->vm,
+                                                &ngx_http_js_externals[2]);
+    if (jlcf->rep_proto == NULL) {
+        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "failed to add reply proto");
+        return NGX_CONF_ERROR;
+    }
+
     rc = njs_vm_compile(jlcf->vm, &start, end);
 
     if (rc != NJS_OK) {
@@ -1621,6 +2129,7 @@
      *     conf->vm = NULL;
      *     conf->req_proto = NULL;
      *     conf->res_proto = NULL;
+     *     conf->rep_proto = NULL;
      */
 
     return conf;
@@ -1637,6 +2146,7 @@
         conf->vm = prev->vm;
         conf->req_proto = prev->req_proto;
         conf->res_proto = prev->res_proto;
+        conf->rep_proto = prev->rep_proto;
     }
 
     return NGX_CONF_OK;
diff --git a/njs/njs_error.c b/njs/njs_error.c
index fd0082f..e46dcd6 100644
--- a/njs/njs_error.c
+++ b/njs/njs_error.c
@@ -486,8 +486,8 @@
 };
 
 
-static void
-njs_set_memory_error(njs_vm_t *vm)
+void
+njs_set_memory_error(njs_vm_t *vm, njs_value_t *value)
 {
     njs_object_t            *object;
     njs_object_prototype_t  *prototypes;
@@ -507,17 +507,17 @@
      */
     object->extensible = 0;
 
-    vm->retval.data.type = NJS_OBJECT_INTERNAL_ERROR;
-    vm->retval.data.truth = 1;
-    vm->retval.data.u.number = NAN;
-    vm->retval.data.u.object = object;
+    value->data.type = NJS_OBJECT_INTERNAL_ERROR;
+    value->data.truth = 1;
+    value->data.u.number = NAN;
+    value->data.u.object = object;
 }
 
 
 void
 njs_exception_memory_error(njs_vm_t *vm)
 {
-    njs_set_memory_error(vm);
+    njs_set_memory_error(vm, &vm->retval);
 }
 
 
@@ -525,7 +525,7 @@
 njs_memory_error_constructor(njs_vm_t *vm, njs_value_t *args,
     nxt_uint_t nargs, njs_index_t unused)
 {
-    njs_set_memory_error(vm);
+    njs_set_memory_error(vm, &vm->retval);
 
     return NXT_OK;
 }
diff --git a/njs/njs_error.h b/njs/njs_error.h
index 4a204fc..97561be 100644
--- a/njs/njs_error.h
+++ b/njs/njs_error.h
@@ -29,6 +29,7 @@
     const char* fmt, ...);
 
 void njs_exception_memory_error(njs_vm_t *vm);
+void njs_set_memory_error(njs_vm_t *vm, njs_value_t *value);
 
 njs_object_t *njs_error_alloc(njs_vm_t *vm, njs_value_type_t type,
     const njs_value_t *name, const njs_value_t *message);
diff --git a/njs/njs_vm.c b/njs/njs_vm.c
index 6701144..8070180 100644
--- a/njs/njs_vm.c
+++ b/njs/njs_vm.c
@@ -3655,6 +3655,54 @@
 }
 
 
+void
+njs_value_error_set(njs_vm_t *vm, njs_value_t *value, const char *fmt, ...)
+{
+    size_t        size;
+    va_list       args;
+    nxt_int_t     ret;
+    njs_value_t   string;
+    njs_object_t  *error;
+    char          buf[256];
+
+    if (fmt != NULL) {
+        va_start(args, fmt);
+        size = vsnprintf(buf, sizeof(buf), fmt, args);
+        va_end(args);
+
+    } else {
+        size = 0;
+    }
+
+    ret = njs_string_new(vm, &string, (u_char *) buf, size, size);
+    if (nxt_slow_path(ret != NXT_OK)) {
+        goto memory_error;
+    }
+
+    error = njs_error_alloc(vm, NJS_OBJECT_ERROR, NULL, &string);
+    if (nxt_slow_path(error == NULL)) {
+        goto memory_error;
+    }
+
+    value->data.u.object = error;
+    value->type = NJS_OBJECT_ERROR;
+    value->data.truth = 1;
+
+    return;
+
+memory_error:
+
+    njs_set_memory_error(vm, value);
+}
+
+
+nxt_noinline double
+njs_value_number(njs_value_t *value)
+{
+    return value->data.u.number;
+}
+
+
 nxt_noinline void *
 njs_value_data(njs_value_t *value)
 {
@@ -3662,10 +3710,52 @@
 }
 
 
+nxt_noinline njs_function_t *
+njs_value_function(njs_value_t *value)
+{
+    return value->data.u.function;
+}
+
+
 nxt_noinline nxt_int_t
 njs_value_is_void(njs_value_t *value)
 {
-    return value->type == NJS_VOID;
+    return njs_is_void(value);
+}
+
+
+nxt_noinline nxt_int_t
+njs_value_is_true(njs_value_t *value)
+{
+    return njs_is_true(value);
+}
+
+
+nxt_noinline nxt_int_t
+njs_value_is_number(njs_value_t *value)
+{
+    return njs_is_number(value);
+}
+
+
+nxt_noinline nxt_int_t
+njs_value_is_string(njs_value_t *value)
+{
+    return njs_is_string(value);
+}
+
+
+nxt_noinline nxt_int_t
+njs_value_is_object(njs_value_t *value)
+{
+    return njs_is_object(value);
+}
+
+
+nxt_noinline nxt_int_t
+njs_value_is_function(njs_value_t *value)
+{
+    return njs_is_function(value);
 }
 
 
diff --git a/njs/njscript.c b/njs/njscript.c
index 9d0980f..bc68254 100644
--- a/njs/njscript.c
+++ b/njs/njscript.c
@@ -14,6 +14,7 @@
 #include <nxt_lvlhsh.h>
 #include <nxt_random.h>
 #include <nxt_malloc.h>
+#include <nxt_djb_hash.h>
 #include <nxt_mem_cache_pool.h>
 #include <njscript.h>
 #include <njs_vm.h>
@@ -503,6 +504,43 @@
 }
 
 
+njs_vm_event_t
+njs_vm_add_event(njs_vm_t *vm, njs_function_t *function,
+    njs_host_event_t host_ev, njs_event_destructor destructor)
+{
+    njs_event_t  *event;
+
+    event = nxt_mem_cache_alloc(vm->mem_cache_pool, sizeof(njs_event_t));
+    if (nxt_slow_path(event == NULL)) {
+        return NULL;
+    }
+
+    event->host_event = host_ev;
+    event->destructor = destructor;
+    event->function = function;
+    event->posted = 0;
+    event->nargs = 0;
+    event->args = NULL;
+
+    if (njs_add_event(vm, event) != NJS_OK) {
+        return NULL;
+    }
+
+    return event;
+}
+
+
+void
+njs_vm_del_event(njs_vm_t *vm, njs_vm_event_t vm_event)
+{
+    njs_event_t  *event;
+
+    event = (njs_event_t *) vm_event;
+
+    njs_del_event(vm, event, NJS_EVENT_RELEASE | NJS_EVENT_DELETE);
+}
+
+
 nxt_int_t
 njs_vm_pending(njs_vm_t *vm)
 {
@@ -511,12 +549,24 @@
 
 
 nxt_int_t
-njs_vm_post_event(njs_vm_t *vm, njs_vm_event_t vm_event)
+njs_vm_post_event(njs_vm_t *vm, njs_vm_event_t vm_event,
+    njs_opaque_value_t *args, nxt_uint_t nargs)
 {
     njs_event_t  *event;
 
     event = (njs_event_t *) vm_event;
 
+    if (nargs != 0 && !event->posted) {
+        event->nargs = nargs;
+        event->args = nxt_mem_cache_alloc(vm->mem_cache_pool,
+                                          sizeof(njs_opaque_value_t) * nargs);
+        if (nxt_slow_path(event->args == NULL)) {
+            return NJS_ERROR;
+        }
+
+        memcpy(event->args, args, sizeof(njs_opaque_value_t) * nargs);
+    }
+
     if (!event->posted) {
         event->posted = 1;
         nxt_queue_insert_tail(&vm->posted_events, &event->link);
@@ -639,3 +689,29 @@
 
     return njs_vm_value_to_ext_string(vm, retval, &vm->retval, 1);
 }
+
+
+njs_value_t *
+njs_vm_object_prop(njs_vm_t *vm, njs_value_t *value, const nxt_str_t *key)
+{
+    nxt_int_t           ret;
+    njs_object_prop_t   *prop;
+    nxt_lvlhsh_query_t  lhq;
+
+    if (nxt_slow_path(!njs_is_object(value))) {
+        return NULL;
+    }
+
+    lhq.key = *key;
+    lhq.key_hash = nxt_djb_hash(lhq.key.start, lhq.key.length);
+    lhq.proto = &njs_object_hash_proto;
+
+    ret = nxt_lvlhsh_find(&value->data.u.object->hash, &lhq);
+    if (nxt_slow_path(ret != NXT_OK)) {
+        return NULL;
+    }
+
+    prop = lhq.value;
+
+    return &prop->value;
+}
diff --git a/njs/njscript.h b/njs/njscript.h
index ad4165a..c4deec1 100644
--- a/njs/njscript.h
+++ b/njs/njscript.h
@@ -139,8 +139,13 @@
 NXT_EXPORT nxt_int_t njs_vm_call(njs_vm_t *vm, njs_function_t *function,
     njs_opaque_value_t *args, nxt_uint_t nargs);
 
+NXT_EXPORT njs_vm_event_t njs_vm_add_event(njs_vm_t *vm,
+    njs_function_t *function, njs_host_event_t host_ev,
+    njs_event_destructor destructor);
+NXT_EXPORT void njs_vm_del_event(njs_vm_t *vm, njs_vm_event_t vm_event);
 NXT_EXPORT nxt_int_t njs_vm_pending(njs_vm_t *vm);
-NXT_EXPORT nxt_int_t njs_vm_post_event(njs_vm_t *vm, njs_vm_event_t vm_event);
+NXT_EXPORT nxt_int_t njs_vm_post_event(njs_vm_t *vm, njs_vm_event_t vm_event,
+    njs_opaque_value_t *args, nxt_uint_t nargs);
 
 NXT_EXPORT nxt_int_t njs_vm_run(njs_vm_t *vm);
 
@@ -174,10 +179,22 @@
 NXT_EXPORT void njs_value_void_set(njs_value_t *value);
 NXT_EXPORT void njs_value_boolean_set(njs_value_t *value, int yn);
 NXT_EXPORT void njs_value_number_set(njs_value_t *value, double num);
+NXT_EXPORT void njs_value_error_set(njs_vm_t *vm, njs_value_t *value,
+    const char *fmt, ...);
+
+NXT_EXPORT double njs_value_number(njs_value_t *value);
 NXT_EXPORT void *njs_value_data(njs_value_t *value);
+NXT_EXPORT njs_function_t *njs_value_function(njs_value_t *value);
 
 NXT_EXPORT nxt_int_t njs_value_is_void(njs_value_t *value);
+NXT_EXPORT nxt_int_t njs_value_is_true(njs_value_t *value);
+NXT_EXPORT nxt_int_t njs_value_is_number(njs_value_t *value);
+NXT_EXPORT nxt_int_t njs_value_is_string(njs_value_t *value);
+NXT_EXPORT nxt_int_t njs_value_is_object(njs_value_t *value);
+NXT_EXPORT nxt_int_t njs_value_is_function(njs_value_t *value);
 
+NXT_EXPORT njs_value_t *njs_vm_object_prop(njs_vm_t *vm, njs_value_t *value,
+    const nxt_str_t *key);
 
 extern const nxt_mem_proto_t  njs_vm_mem_cache_pool_proto;
 
diff --git a/njs/test/njs_unit_test.c b/njs/test/njs_unit_test.c
index f451ef9..fb555b8 100644
--- a/njs/test/njs_unit_test.c
+++ b/njs/test/njs_unit_test.c
@@ -3867,6 +3867,9 @@
                  "          valueOf: function() { return 1 } };  a"),
       nxt_string("1") },
 
+    { nxt_string("var o = {b:$r.props.b}; o.b"),
+      nxt_string("42") },
+
     /**/
 
     { nxt_string("'абвгдеёжзийклмнопрстуфхцчшщъыьэюя'.charCodeAt(5)"),
@@ -9108,6 +9111,16 @@
 
 
 static njs_ret_t
+njs_unit_test_r_get_b_external(njs_vm_t *vm, njs_value_t *value, void *obj,
+    uintptr_t data)
+{
+    njs_value_number_set(value, data);
+
+    return NJS_OK;
+}
+
+
+static njs_ret_t
 njs_unit_test_host_external(njs_vm_t *vm, njs_value_t *value, void *obj,
     uintptr_t data)
 {
@@ -9259,6 +9272,18 @@
       NULL,
       NULL,
       0 },
+
+    { nxt_string("b"),
+      NJS_EXTERN_PROPERTY,
+      NULL,
+      0,
+      njs_unit_test_r_get_b_external,
+      NULL,
+      NULL,
+      NULL,
+      NULL,
+      NULL,
+      42 },
 };