[njs] Modules: added js_shared_dict_zone support in QuickJS engine.

noreply at nginx.com noreply at nginx.com
Wed Oct 30 23:56:02 UTC 2024


details:   https://github.com/nginx/njs/commit/352c2e594e57d2bce11f7e2a773dcab417182ef1
branches:  master
commit:    352c2e594e57d2bce11f7e2a773dcab417182ef1
user:      Dmitry Volyntsev <xeioex at nginx.com>
date:      Thu, 24 Oct 2024 19:00:41 -0700
description:
Modules: added js_shared_dict_zone support in QuickJS engine.


---
 nginx/ngx_http_js_module.c      |    1 +
 nginx/ngx_js.h                  |    4 +
 nginx/ngx_js_shared_dict.c      | 1312 +++++++++++++++++++++++++++++++++++++++
 nginx/t/js_shared_dict.t        |   39 +-
 nginx/t/stream_js_shared_dict.t |    2 -
 5 files changed, 1352 insertions(+), 6 deletions(-)

diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c
index 06429241..3e43ac7d 100644
--- a/nginx/ngx_http_js_module.c
+++ b/nginx/ngx_http_js_module.c
@@ -1136,6 +1136,7 @@ static JSClassDef ngx_http_qjs_headers_out_class = {
 
 qjs_module_t *njs_http_qjs_addon_modules[] = {
     &ngx_qjs_ngx_module,
+    &ngx_qjs_ngx_shared_dict_module,
     /*
      * Shared addons should be in the same order and the same positions
      * in all nginx modules.
diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h
index 8319dc85..3a51aef0 100644
--- a/nginx/ngx_js.h
+++ b/nginx/ngx_js.h
@@ -60,6 +60,9 @@
 #define NGX_QJS_CLASS_ID_STREAM_PERIODIC (NGX_QJS_CLASS_ID_OFFSET + 8)
 #define NGX_QJS_CLASS_ID_STREAM_FLAGS (NGX_QJS_CLASS_ID_OFFSET + 9)
 #define NGX_QJS_CLASS_ID_STREAM_VARS (NGX_QJS_CLASS_ID_OFFSET + 10)
+#define NGX_QJS_CLASS_ID_SHARED (NGX_QJS_CLASS_ID_OFFSET + 11)
+#define NGX_QJS_CLASS_ID_SHARED_DICT (NGX_QJS_CLASS_ID_OFFSET + 12)
+#define NGX_QJS_CLASS_ID_SHARED_DICT_ERROR (NGX_QJS_CLASS_ID_OFFSET + 13)
 
 
 typedef struct ngx_js_loc_conf_s ngx_js_loc_conf_t;
@@ -377,6 +380,7 @@ ngx_int_t ngx_qjs_string(ngx_engine_t *e, JSValueConst val, ngx_str_t *str);
 
 extern qjs_module_t  qjs_zlib_module;
 extern qjs_module_t  ngx_qjs_ngx_module;
+extern qjs_module_t  ngx_qjs_ngx_shared_dict_module;
 
 #endif
 
diff --git a/nginx/ngx_js_shared_dict.c b/nginx/ngx_js_shared_dict.c
index 19f6f4bd..e317211e 100644
--- a/nginx/ngx_js_shared_dict.c
+++ b/nginx/ngx_js_shared_dict.c
@@ -113,6 +113,68 @@ static njs_int_t ngx_js_shared_dict_init(njs_vm_t *vm);
 static void ngx_js_dict_node_free(ngx_js_dict_t *dict,
     ngx_js_dict_node_t *node);
 
+#if (NJS_HAVE_QUICKJS)
+static int ngx_qjs_shared_own_property(JSContext *cx,
+    JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop);
+static int ngx_qjs_shared_own_property_names(JSContext *ctx,
+    JSPropertyEnum **ptab, uint32_t *plen, JSValueConst obj);
+
+static JSValue ngx_qjs_ext_ngx_shared(JSContext *cx, JSValueConst this_val);
+static JSValue ngx_qjs_ext_shared_dict_capacity(JSContext *cx,
+    JSValueConst this_val);
+static JSValue ngx_qjs_ext_shared_dict_clear(JSContext *cx,
+    JSValueConst this_val, int argc, JSValueConst *argv);
+static JSValue ngx_qjs_ext_shared_dict_delete(JSContext *cx,
+    JSValueConst this_val, int argc, JSValueConst *argv);
+static JSValue ngx_qjs_ext_shared_dict_free_space(JSContext *cx,
+    JSValueConst this_val, int argc, JSValueConst *argv);
+static JSValue ngx_qjs_ext_shared_dict_get(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv);
+static JSValue ngx_qjs_ext_shared_dict_has(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv);
+static JSValue ngx_qjs_ext_shared_dict_incr(JSContext *cx,
+    JSValueConst this_val, int argc, JSValueConst *argv);
+static JSValue ngx_qjs_ext_shared_dict_items(JSContext *cx,
+    JSValueConst this_val, int argc, JSValueConst *argv);
+static JSValue ngx_qjs_ext_shared_dict_keys(JSContext *cx,
+    JSValueConst this_val, int argc, JSValueConst *argv);
+static JSValue ngx_qjs_ext_shared_dict_name(JSContext *cx,
+    JSValueConst this_val);
+static JSValue ngx_qjs_ext_shared_dict_pop(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv);
+static JSValue ngx_qjs_ext_shared_dict_set(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv, int flags);
+static JSValue ngx_qjs_ext_shared_dict_size(JSContext *cx,
+    JSValueConst this_val, int argc, JSValueConst *argv);
+static JSValue ngx_qjs_ext_shared_dict_tag(JSContext *cx,
+    JSValueConst this_val);
+static JSValue ngx_qjs_ext_shared_dict_type(JSContext *cx,
+    JSValueConst this_val);
+
+static JSValue ngx_qjs_dict_copy_value_locked(JSContext *cx,
+    ngx_js_dict_t *dict, ngx_js_dict_node_t *node);
+static ngx_js_dict_node_t *ngx_qjs_dict_lookup(ngx_js_dict_t *dict,
+    ngx_str_t *key);
+static ngx_int_t ngx_qjs_dict_add(JSContext *cx, ngx_js_dict_t *dict,
+    ngx_str_t *key, JSValue value, ngx_msec_t timeout, ngx_msec_t now);
+static JSValue ngx_qjs_dict_delete(JSContext *cx, ngx_js_dict_t *dict,
+    ngx_str_t *key, int retval);
+static JSValue ngx_qjs_dict_get(JSContext *cx, ngx_js_dict_t *dict,
+    ngx_str_t *key);
+static JSValue ngx_qjs_dict_incr(JSContext *cx, ngx_js_dict_t *dict,
+    ngx_str_t *key, double delta, double init, ngx_msec_t timeout);
+static JSValue ngx_qjs_dict_set(JSContext *cx, ngx_js_dict_t *dict,
+    ngx_str_t *key, JSValue value, ngx_msec_t timeout, unsigned flags);
+static ngx_int_t ngx_qjs_dict_update(JSContext *cx, ngx_js_dict_t *dict,
+    ngx_js_dict_node_t *node, JSValue value, ngx_msec_t timeout,
+    ngx_msec_t now);
+
+static JSValue ngx_qjs_throw_shared_memory_error(JSContext *cx);
+
+static JSModuleDef *ngx_qjs_ngx_shared_dict_init(JSContext *cx,
+    const char *name);
+#endif
+
 
 static njs_external_t  ngx_js_ext_shared_dict[] = {
 
@@ -356,6 +418,62 @@ njs_module_t  ngx_js_shared_dict_module = {
 };
 
 
+#if (NJS_HAVE_QUICKJS)
+
+static const JSCFunctionListEntry ngx_qjs_ext_ngx[] = {
+    JS_CGETSET_DEF("shared", ngx_qjs_ext_ngx_shared, NULL),
+};
+
+static const JSCFunctionListEntry ngx_qjs_ext_shared_dict[] = {
+    JS_CGETSET_DEF("[Symbol.toStringTag]", ngx_qjs_ext_shared_dict_tag, NULL),
+    JS_CFUNC_MAGIC_DEF("add", 3, ngx_qjs_ext_shared_dict_set,
+                       NGX_JS_DICT_FLAG_MUST_NOT_EXIST),
+    JS_CGETSET_DEF("capacity", ngx_qjs_ext_shared_dict_capacity, NULL),
+    JS_CFUNC_DEF("clear", 0, ngx_qjs_ext_shared_dict_clear),
+    JS_CFUNC_DEF("delete", 1, ngx_qjs_ext_shared_dict_delete),
+    JS_CFUNC_DEF("freeSpace", 0, ngx_qjs_ext_shared_dict_free_space),
+    JS_CFUNC_DEF("get", 1, ngx_qjs_ext_shared_dict_get),
+    JS_CFUNC_DEF("has", 1, ngx_qjs_ext_shared_dict_has),
+    JS_CFUNC_DEF("incr", 3, ngx_qjs_ext_shared_dict_incr),
+    JS_CFUNC_DEF("items", 0, ngx_qjs_ext_shared_dict_items),
+    JS_CFUNC_DEF("keys", 0, ngx_qjs_ext_shared_dict_keys),
+    JS_CGETSET_DEF("name", ngx_qjs_ext_shared_dict_name, NULL),
+    JS_CFUNC_DEF("pop", 1, ngx_qjs_ext_shared_dict_pop),
+    JS_CFUNC_MAGIC_DEF("replace", 3, ngx_qjs_ext_shared_dict_set,
+                       NGX_JS_DICT_FLAG_MUST_EXIST),
+    JS_CFUNC_MAGIC_DEF("set", 3, ngx_qjs_ext_shared_dict_set, 0),
+    JS_CFUNC_DEF("size", 0, ngx_qjs_ext_shared_dict_size),
+    JS_CGETSET_DEF("type", ngx_qjs_ext_shared_dict_type, NULL),
+};
+
+static const JSCFunctionListEntry ngx_qjs_ext_shared_dict_error[] = {
+    JS_PROP_STRING_DEF("name", "SharedMemoryError",
+                       JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE),
+    JS_PROP_STRING_DEF("message", "", JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE),
+};
+
+static JSClassDef ngx_qjs_shared_dict_class = {
+    "SharedDict",
+    .finalizer = NULL,
+};
+
+static JSClassDef ngx_qjs_shared_class = {
+    "Shared",
+    .finalizer = NULL,
+    .exotic = & (JSClassExoticMethods) {
+        .get_own_property = ngx_qjs_shared_own_property,
+        .get_own_property_names = ngx_qjs_shared_own_property_names,
+    },
+};
+
+qjs_module_t  ngx_qjs_ngx_shared_dict_module = {
+    .name = "shared_dict",
+    .init = ngx_qjs_ngx_shared_dict_init,
+};
+
+#endif
+
+
 njs_int_t
 njs_js_ext_global_shared_prop(njs_vm_t *vm, njs_object_prop_t *prop,
     njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
@@ -1786,3 +1904,1197 @@ ngx_js_shared_dict_init(njs_vm_t *vm)
 
     return NJS_OK;
 }
+
+
+#if (NJS_HAVE_QUICKJS)
+
+static int
+ngx_qjs_shared_own_property(JSContext *cx, JSPropertyDescriptor *pdesc,
+    JSValueConst obj, JSAtom prop)
+{
+    int                  ret;
+    ngx_str_t            name;
+    ngx_js_dict_t       *dict;
+    ngx_shm_zone_t      *shm_zone;
+    ngx_js_main_conf_t  *conf;
+
+    name.data = (u_char *) JS_AtomToCString(cx, prop);
+    if (name.data == NULL) {
+        return -1;
+    }
+
+    name.len = ngx_strlen(name.data);
+
+    ret = 0;
+    conf = ngx_qjs_main_conf(cx);
+
+    for (dict = conf->dicts; dict != NULL; dict = dict->next) {
+        shm_zone = dict->shm_zone;
+
+        if (shm_zone->shm.name.len == name.len
+            && ngx_strncmp(shm_zone->shm.name.data, name.data, name.len)
+               == 0)
+        {
+            if (pdesc != NULL) {
+                pdesc->flags = JS_PROP_ENUMERABLE;
+                pdesc->getter = JS_UNDEFINED;
+                pdesc->setter = JS_UNDEFINED;
+                pdesc->value = JS_NewObjectClass(cx,
+                                                 NGX_QJS_CLASS_ID_SHARED_DICT);
+                if (JS_IsException(pdesc->value)) {
+                    ret = -1;
+                    break;
+                }
+
+                JS_SetOpaque(pdesc->value, shm_zone);
+            }
+
+            ret = 1;
+            break;
+        }
+    }
+
+    JS_FreeCString(cx, (char *) name.data);
+
+    return ret;
+}
+
+
+static int
+ngx_qjs_shared_own_property_names(JSContext *cx, JSPropertyEnum **ptab,
+    uint32_t *plen, JSValueConst obj)
+{
+    int                 ret;
+    JSAtom              key;
+    JSValue             keys;
+    ngx_js_dict_t       *dict;
+    ngx_shm_zone_t      *shm_zone;
+    ngx_js_main_conf_t  *conf;
+
+    keys = JS_NewObject(cx);
+    if (JS_IsException(keys)) {
+        return -1;
+    }
+
+    conf = ngx_qjs_main_conf(cx);
+
+    for (dict = conf->dicts; dict != NULL; dict = dict->next) {
+        shm_zone = dict->shm_zone;
+
+        key = JS_NewAtomLen(cx, (const char *) shm_zone->shm.name.data,
+                            shm_zone->shm.name.len);
+        if (key == JS_ATOM_NULL) {
+            return -1;
+        }
+
+        if (JS_DefinePropertyValue(cx, keys, key, JS_UNDEFINED,
+                                   JS_PROP_ENUMERABLE) < 0)
+        {
+            JS_FreeAtom(cx, key);
+            return -1;
+        }
+
+        JS_FreeAtom(cx, key);
+    }
+
+    ret = JS_GetOwnPropertyNames(cx, ptab, plen, keys, JS_GPN_STRING_MASK);
+
+    JS_FreeValue(cx, keys);
+
+    return ret;
+}
+
+
+static JSValue
+ngx_qjs_ext_ngx_shared(JSContext *cx, JSValueConst this_val)
+{
+    return JS_NewObjectProtoClass(cx, JS_NULL, NGX_QJS_CLASS_ID_SHARED);
+}
+
+
+static JSValue
+ngx_qjs_ext_shared_dict_capacity(JSContext *cx, JSValueConst this_val)
+{
+    ngx_shm_zone_t  *shm_zone;
+
+    shm_zone = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_SHARED_DICT);
+    if (shm_zone == NULL) {
+        return JS_UNDEFINED;
+    }
+
+    return JS_NewInt32(cx, shm_zone->shm.size);
+}
+
+
+static JSValue
+ngx_qjs_ext_shared_dict_clear(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    ngx_rbtree_t       *rbtree;
+    ngx_js_dict_t      *dict;
+    ngx_shm_zone_t     *shm_zone;
+    ngx_rbtree_node_t  *rn, *next;
+
+    shm_zone = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_SHARED_DICT);
+    if (shm_zone == NULL) {
+        return JS_ThrowTypeError(cx, "\"this\" is not a shared dict");
+    }
+
+    dict = shm_zone->data;
+
+    ngx_rwlock_wlock(&dict->sh->rwlock);
+
+    if (dict->timeout) {
+        ngx_js_dict_evict(dict, 0x7fffffff /* INT_MAX */);
+
+    } else {
+        rbtree = &dict->sh->rbtree;
+
+        if (rbtree->root == rbtree->sentinel) {
+            goto done;
+        }
+
+        for (rn = ngx_rbtree_min(rbtree->root, rbtree->sentinel);
+             rn != NULL;
+             rn = next)
+        {
+            next = ngx_rbtree_next(rbtree, rn);
+
+            ngx_rbtree_delete(rbtree, rn);
+
+            ngx_js_dict_node_free(dict, (ngx_js_dict_node_t *) rn);
+        }
+    }
+
+done:
+
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+
+    return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_qjs_ext_shared_dict_delete(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    ngx_str_t        key;
+    ngx_js_ctx_t    *ctx;
+    ngx_shm_zone_t  *shm_zone;
+
+    shm_zone = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_SHARED_DICT);
+    if (shm_zone == NULL) {
+        return JS_ThrowTypeError(cx, "\"this\" is not a shared dict");
+    }
+
+    ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx));
+
+    if (ngx_qjs_string(ctx->engine, argv[0], &key) != NGX_OK) {
+        return JS_EXCEPTION;
+    }
+
+    return ngx_qjs_dict_delete(cx, shm_zone->data, &key, 0);
+}
+
+
+static JSValue
+ngx_qjs_ext_shared_dict_free_space(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    size_t           bytes;
+    ngx_js_dict_t   *dict;
+    ngx_shm_zone_t  *shm_zone;
+
+    shm_zone = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_SHARED_DICT);
+    if (shm_zone == NULL) {
+        return JS_ThrowTypeError(cx, "\"this\" is not a shared dict");
+    }
+
+    dict = shm_zone->data;
+
+    ngx_rwlock_rlock(&dict->sh->rwlock);
+    bytes = dict->shpool->pfree * ngx_pagesize;
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+
+    return JS_NewInt32(cx, bytes);
+}
+
+
+static JSValue
+ngx_qjs_ext_shared_dict_get(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    ngx_str_t        key;
+    ngx_js_ctx_t    *ctx;
+    ngx_shm_zone_t  *shm_zone;
+
+    shm_zone = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_SHARED_DICT);
+    if (shm_zone == NULL) {
+        return JS_ThrowTypeError(cx, "\"this\" is not a shared dict");
+    }
+
+    ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx));
+
+    if (ngx_qjs_string(ctx->engine, argv[0], &key) != NGX_OK) {
+        return JS_EXCEPTION;
+    }
+
+    return ngx_qjs_dict_get(cx, shm_zone->data, &key);
+}
+
+
+static JSValue
+ngx_qjs_ext_shared_dict_has(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    ngx_str_t            key;
+    ngx_msec_t           now;
+    ngx_time_t          *tp;
+    ngx_js_ctx_t        *ctx;
+    ngx_js_dict_t       *dict;
+    ngx_shm_zone_t      *shm_zone;
+    ngx_js_dict_node_t  *node;
+
+    shm_zone = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_SHARED_DICT);
+    if (shm_zone == NULL) {
+        return JS_ThrowTypeError(cx, "\"this\" is not a shared dict");
+    }
+
+    ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx));
+
+    if (ngx_qjs_string(ctx->engine, argv[0], &key) != NGX_OK) {
+        return JS_EXCEPTION;
+    }
+
+    dict = shm_zone->data;
+
+    ngx_rwlock_rlock(&dict->sh->rwlock);
+
+    node = ngx_qjs_dict_lookup(dict, &key);
+
+    if (node != NULL && dict->timeout) {
+        tp = ngx_timeofday();
+        now = tp->sec * 1000 + tp->msec;
+
+        if (now >= node->expire.key) {
+            node = NULL;
+        }
+    }
+
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+
+    return JS_NewBool(cx, node != NULL);
+}
+
+
+static JSValue
+ngx_qjs_ext_shared_dict_incr(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    double           delta, init;
+    uint32_t         timeout;
+    ngx_str_t        key;
+    ngx_js_ctx_t    *ctx;
+    ngx_js_dict_t   *dict;
+    ngx_shm_zone_t  *shm_zone;
+
+    shm_zone = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_SHARED_DICT);
+    if (shm_zone == NULL) {
+        return JS_ThrowTypeError(cx, "\"this\" is not a shared dict");
+    }
+
+    dict = shm_zone->data;
+
+    if (dict->type != NGX_JS_DICT_TYPE_NUMBER) {
+        return JS_ThrowTypeError(cx, "shared dict is not a number dict");
+    }
+
+    ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx));
+
+    if (ngx_qjs_string(ctx->engine, argv[0], &key) != NGX_OK) {
+        return JS_EXCEPTION;
+    }
+
+    if (JS_ToFloat64(cx, &delta, argv[1]) < 0) {
+        return JS_EXCEPTION;
+    }
+
+    if (JS_ToFloat64(cx, &init, argv[2]) < 0) {
+        return JS_EXCEPTION;
+    }
+
+    if (argc > 3) {
+        if (JS_ToUint32(cx, &timeout, argv[3]) < 0) {
+            return JS_EXCEPTION;
+        }
+
+        if (!dict->timeout) {
+            return JS_ThrowTypeError(cx,
+                                  "shared dict must be declared with timeout");
+        }
+
+        if (timeout < 1) {
+            return JS_ThrowRangeError(cx,
+                                 "timeout must be greater than or equal to 1");
+        }
+
+    } else {
+        timeout = dict->timeout;
+    }
+
+    return ngx_qjs_dict_incr(cx, dict, &key, delta, init, timeout);
+}
+
+
+static JSValue
+ngx_qjs_ext_shared_dict_items(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    JSValue              arr, kv, v;
+    uint32_t             max_count, i;
+    ngx_msec_t           now;
+    ngx_time_t          *tp;
+    ngx_rbtree_t        *rbtree;
+    ngx_js_dict_t       *dict;
+    ngx_shm_zone_t      *shm_zone;
+    ngx_rbtree_node_t   *rn;
+    ngx_js_dict_node_t  *node;
+
+    shm_zone = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_SHARED_DICT);
+    if (shm_zone == NULL) {
+        return JS_ThrowTypeError(cx, "\"this\" is not a shared dict");
+    }
+
+    dict = shm_zone->data;
+
+    max_count = 1024;
+
+    if (argc > 0) {
+        if (JS_ToUint32(cx, &max_count, argv[0]) < 0) {
+            return JS_EXCEPTION;
+        }
+    }
+
+    rbtree = &dict->sh->rbtree;
+
+    ngx_rwlock_rlock(&dict->sh->rwlock);
+
+    if (dict->timeout) {
+        tp = ngx_timeofday();
+        now = tp->sec * 1000 + tp->msec;
+        ngx_js_dict_expire(dict, now);
+    }
+
+    if (rbtree->root == rbtree->sentinel) {
+        ngx_rwlock_unlock(&dict->sh->rwlock);
+        return JS_NewArray(cx);
+    }
+
+    arr = JS_NewArray(cx);
+    if (JS_IsException(arr)) {
+        ngx_rwlock_unlock(&dict->sh->rwlock);
+        return JS_EXCEPTION;
+    }
+
+    i = 0;
+
+    for (rn = ngx_rbtree_min(rbtree->root, rbtree->sentinel);
+         rn != NULL;
+         rn = ngx_rbtree_next(rbtree, rn))
+    {
+        if (max_count-- == 0) {
+            break;
+        }
+
+        node = (ngx_js_dict_node_t *) rn;
+
+        kv = JS_NewArray(cx);
+        if (JS_IsException(kv)) {
+            ngx_rwlock_unlock(&dict->sh->rwlock);
+            JS_FreeValue(cx, arr);
+            return JS_EXCEPTION;
+        }
+
+        v = JS_NewStringLen(cx, (const char *) node->sn.str.data,
+                            node->sn.str.len);
+        if (JS_IsException(v)) {
+            ngx_rwlock_unlock(&dict->sh->rwlock);
+            JS_FreeValue(cx, kv);
+            JS_FreeValue(cx, arr);
+            return JS_EXCEPTION;
+        }
+
+        if (JS_DefinePropertyValueUint32(cx, kv, 0, v, JS_PROP_C_W_E) < 0) {
+            ngx_rwlock_unlock(&dict->sh->rwlock);
+            JS_FreeValue(cx, v);
+            JS_FreeValue(cx, kv);
+            JS_FreeValue(cx, arr);
+            return JS_EXCEPTION;
+        }
+
+        v = ngx_qjs_dict_copy_value_locked(cx, dict, node);
+
+        if (JS_DefinePropertyValueUint32(cx, kv, 1, v, JS_PROP_C_W_E) < 0) {
+            ngx_rwlock_unlock(&dict->sh->rwlock);
+            JS_FreeValue(cx, v);
+            JS_FreeValue(cx, kv);
+            JS_FreeValue(cx, arr);
+            return JS_EXCEPTION;
+        }
+
+        if (JS_DefinePropertyValueUint32(cx, arr, i++, kv, JS_PROP_C_W_E) < 0) {
+            ngx_rwlock_unlock(&dict->sh->rwlock);
+            JS_FreeValue(cx, kv);
+            JS_FreeValue(cx, arr);
+            return JS_EXCEPTION;
+        }
+    }
+
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+
+    return arr;
+}
+
+
+static JSValue
+ngx_qjs_ext_shared_dict_keys(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv)
+{
+    JSValue              arr, key;
+    uint32_t             max_count, i;
+    ngx_msec_t           now;
+    ngx_time_t          *tp;
+    ngx_rbtree_t        *rbtree;
+    ngx_js_dict_t       *dict;
+    ngx_shm_zone_t      *shm_zone;
+    ngx_rbtree_node_t   *rn;
+    ngx_js_dict_node_t  *node;
+
+    shm_zone = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_SHARED_DICT);
+    if (shm_zone == NULL) {
+        return JS_ThrowTypeError(cx, "\"this\" is not a shared dict");
+    }
+
+    dict = shm_zone->data;
+
+    max_count = 1024;
+
+    if (argc > 0) {
+        if (JS_ToUint32(cx, &max_count, argv[0]) < 0) {
+            return JS_EXCEPTION;
+        }
+    }
+
+    rbtree = &dict->sh->rbtree;
+
+    ngx_rwlock_rlock(&dict->sh->rwlock);
+
+    if (dict->timeout) {
+        tp = ngx_timeofday();
+        now = tp->sec * 1000 + tp->msec;
+        ngx_js_dict_expire(dict, now);
+    }
+
+    if (rbtree->root == rbtree->sentinel) {
+        ngx_rwlock_unlock(&dict->sh->rwlock);
+        return JS_NewArray(cx);
+    }
+
+    arr = JS_NewArray(cx);
+    if (JS_IsException(arr)) {
+        ngx_rwlock_unlock(&dict->sh->rwlock);
+        return JS_EXCEPTION;
+    }
+
+    i = 0;
+
+    for (rn = ngx_rbtree_min(rbtree->root, rbtree->sentinel);
+         rn != NULL;
+         rn = ngx_rbtree_next(rbtree, rn))
+    {
+        if (max_count-- == 0) {
+            break;
+        }
+
+        node = (ngx_js_dict_node_t *) rn;
+
+        key = JS_NewStringLen(cx, (const char *) node->sn.str.data,
+                              node->sn.str.len);
+        if (JS_IsException(key)) {
+            ngx_rwlock_unlock(&dict->sh->rwlock);
+            JS_FreeValue(cx, arr);
+            return JS_EXCEPTION;
+        }
+
+        if (JS_DefinePropertyValueUint32(cx, arr, i++, key,
+                                         JS_PROP_C_W_E) < 0)
+        {
+            ngx_rwlock_unlock(&dict->sh->rwlock);
+            JS_FreeValue(cx, key);
+            JS_FreeValue(cx, arr);
+            return JS_EXCEPTION;
+        }
+    }
+
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+
+    return arr;
+}
+
+
+static JSValue
+ngx_qjs_ext_shared_dict_name(JSContext *cx, JSValueConst this_val)
+{
+    ngx_shm_zone_t  *shm_zone;
+
+    shm_zone = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_SHARED_DICT);
+    if (shm_zone == NULL) {
+        return JS_UNDEFINED;
+    }
+
+    return JS_NewStringLen(cx, (const char *) shm_zone->shm.name.data,
+                           shm_zone->shm.name.len);
+}
+
+
+static JSValue
+ngx_qjs_ext_shared_dict_pop(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    ngx_str_t        key;
+    ngx_js_ctx_t    *ctx;
+    ngx_shm_zone_t  *shm_zone;
+
+    shm_zone = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_SHARED_DICT);
+    if (shm_zone == NULL) {
+        return JS_ThrowTypeError(cx, "\"this\" is not a shared dict");
+    }
+
+    ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx));
+
+    if (ngx_qjs_string(ctx->engine, argv[0], &key) != NGX_OK) {
+        return JS_EXCEPTION;
+    }
+
+    return ngx_qjs_dict_delete(cx, shm_zone->data, &key, 1);
+}
+
+
+static JSValue
+ngx_qjs_ext_shared_dict_set(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv, int flags)
+{
+    JSValue          ret;
+    uint32_t         timeout;
+    ngx_str_t        key;
+    ngx_js_ctx_t    *ctx;
+    ngx_js_dict_t   *dict;
+    ngx_shm_zone_t  *shm_zone;
+
+    shm_zone = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_SHARED_DICT);
+    if (shm_zone == NULL) {
+        return JS_ThrowTypeError(cx, "\"this\" is not a shared dict");
+    }
+
+    ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx));
+
+    if (ngx_qjs_string(ctx->engine, argv[0], &key) != NGX_OK) {
+        return JS_EXCEPTION;
+    }
+
+    dict = shm_zone->data;
+
+    if (dict->type == NGX_JS_DICT_TYPE_STRING) {
+        if (!JS_IsString(argv[1])) {
+            return JS_ThrowTypeError(cx, "string value is expected");
+        }
+
+    } else {
+        if (!JS_IsNumber(argv[1])) {
+            return JS_ThrowTypeError(cx, "number value is expected");
+        }
+    }
+
+    if (!JS_IsUndefined(argv[2])) {
+        if (!JS_IsNumber(argv[2])) {
+            return JS_ThrowTypeError(cx, "timeout is not a number");
+        }
+
+        if (!dict->timeout) {
+            return JS_ThrowTypeError(cx,
+                                "shared dict must be declared with timeout");
+        }
+
+        if (JS_ToUint32(cx, &timeout, argv[2]) < 0) {
+            return JS_EXCEPTION;
+        }
+
+        if (timeout < 1) {
+            return JS_ThrowTypeError(cx,
+                                "timeout must be greater than or equal to 1");
+        }
+
+    } else {
+        timeout = dict->timeout;
+    }
+
+    ret = ngx_qjs_dict_set(cx, shm_zone->data, &key, argv[1], timeout, flags);
+    if (JS_IsException(ret)) {
+        return JS_EXCEPTION;
+    }
+
+    if (flags) {
+        /* add() or replace(). */
+        return ret;
+    }
+
+    return JS_DupValue(cx, this_val);
+}
+
+
+static JSValue
+ngx_qjs_ext_shared_dict_size(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    njs_int_t           items;
+    ngx_msec_t          now;
+    ngx_time_t         *tp;
+    ngx_rbtree_t       *rbtree;
+    ngx_js_dict_t      *dict;
+    ngx_shm_zone_t     *shm_zone;
+    ngx_rbtree_node_t  *rn;
+
+    shm_zone = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_SHARED_DICT);
+    if (shm_zone == NULL) {
+        return JS_ThrowTypeError(cx, "\"this\" is not a shared dict");
+    }
+
+    dict = shm_zone->data;
+
+    ngx_rwlock_rlock(&dict->sh->rwlock);
+
+    if (dict->timeout) {
+        tp = ngx_timeofday();
+        now = tp->sec * 1000 + tp->msec;
+        ngx_js_dict_expire(dict, now);
+    }
+
+    rbtree = &dict->sh->rbtree;
+
+    if (rbtree->root == rbtree->sentinel) {
+        ngx_rwlock_unlock(&dict->sh->rwlock);
+        return JS_NewInt32(cx, 0);
+    }
+
+    items = 0;
+
+    for (rn = ngx_rbtree_min(rbtree->root, rbtree->sentinel);
+         rn != NULL;
+         rn = ngx_rbtree_next(rbtree, rn))
+    {
+        items++;
+    }
+
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+
+    return JS_NewInt32(cx, items);
+}
+
+
+static JSValue
+ngx_qjs_ext_shared_dict_tag(JSContext *cx, JSValueConst this_val)
+{
+    return JS_NewString(cx, "SharedDict");
+}
+
+
+static JSValue
+ngx_qjs_ext_shared_dict_type(JSContext *cx, JSValueConst this_val)
+{
+    ngx_str_t        type;
+    ngx_js_dict_t   *dict;
+    ngx_shm_zone_t  *shm_zone;
+
+    shm_zone = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_SHARED_DICT);
+    if (shm_zone == NULL) {
+        return JS_UNDEFINED;
+    }
+
+    dict = shm_zone->data;
+
+    switch (dict->type) {
+    case NGX_JS_DICT_TYPE_STRING:
+        ngx_str_set(&type, "string");
+        break;
+
+    default:
+        ngx_str_set(&type, "number");
+        break;
+    }
+
+    return JS_NewStringLen(cx, (const char *) type.data, type.len);
+}
+
+
+static JSValue
+ngx_qjs_dict_copy_value_locked(JSContext *cx, ngx_js_dict_t *dict,
+    ngx_js_dict_node_t *node)
+{
+    if (dict->type == NGX_JS_DICT_TYPE_STRING) {
+        return JS_NewStringLen(cx, (const char *) node->u.value.data,
+                               node->u.value.len);
+    }
+
+    /* NGX_JS_DICT_TYPE_NUMBER */
+
+    return JS_NewFloat64(cx, node->u.number);
+}
+
+
+static ngx_js_dict_node_t *
+ngx_qjs_dict_lookup(ngx_js_dict_t *dict, ngx_str_t *key)
+{
+    uint32_t       hash;
+    ngx_rbtree_t  *rbtree;
+
+    rbtree = &dict->sh->rbtree;
+
+    hash = ngx_crc32_long(key->data, key->len);
+
+    return (ngx_js_dict_node_t *) ngx_str_rbtree_lookup(rbtree, key, hash);
+}
+
+
+static ngx_int_t
+ngx_qjs_dict_add(JSContext *cx, ngx_js_dict_t *dict, ngx_str_t *key,
+    JSValue value, ngx_msec_t timeout, ngx_msec_t now)
+{
+    size_t               n;
+    uint32_t             hash;
+    ngx_str_t            string;
+    ngx_js_dict_node_t  *node;
+
+    if (dict->timeout) {
+        ngx_js_dict_expire(dict, now);
+    }
+
+    n = sizeof(ngx_js_dict_node_t) + key->len;
+    hash = ngx_crc32_long(key->data, key->len);
+
+    node = ngx_js_dict_alloc(dict, n);
+    if (node == NULL) {
+        return NGX_ERROR;
+    }
+
+    node->sn.str.data = (u_char *) node + sizeof(ngx_js_dict_node_t);
+
+    if (dict->type == NGX_JS_DICT_TYPE_STRING) {
+        string.data = (u_char *) JS_ToCStringLen(cx, &string.len, value);
+        if (string.data == NULL) {
+            ngx_slab_free_locked(dict->shpool, node);
+            return NGX_ERROR;
+        }
+
+        node->u.value.data = ngx_js_dict_alloc(dict, string.len);
+        if (node->u.value.data == NULL) {
+            ngx_slab_free_locked(dict->shpool, node);
+            JS_FreeCString(cx, (char *) string.data);
+            return NGX_ERROR;
+        }
+
+        ngx_memcpy(node->u.value.data, string.data, string.len);
+        node->u.value.len = string.len;
+
+        JS_FreeCString(cx, (char *) string.data);
+
+    } else {
+        if (JS_ToFloat64(cx, &node->u.number, value) < 0) {
+            ngx_slab_free_locked(dict->shpool, node);
+            return NGX_ERROR;
+        }
+    }
+
+    node->sn.node.key = hash;
+
+    ngx_memcpy(node->sn.str.data, key->data, key->len);
+    node->sn.str.len = key->len;
+
+    ngx_rbtree_insert(&dict->sh->rbtree, &node->sn.node);
+
+    if (dict->timeout) {
+        node->expire.key = now + timeout;
+        ngx_rbtree_insert(&dict->sh->rbtree_expire, &node->expire);
+    }
+
+    return NGX_OK;
+}
+
+
+static JSValue
+ngx_qjs_dict_delete(JSContext *cx, ngx_js_dict_t *dict, ngx_str_t *key,
+    int retval)
+{
+    JSValue              ret;
+    ngx_msec_t           now;
+    ngx_time_t          *tp;
+    ngx_js_dict_node_t  *node;
+
+    ngx_rwlock_wlock(&dict->sh->rwlock);
+
+    node = ngx_qjs_dict_lookup(dict, key);
+
+    if (node == NULL) {
+        ngx_rwlock_unlock(&dict->sh->rwlock);
+        return JS_UNDEFINED;
+    }
+
+    if (dict->timeout) {
+        ngx_rbtree_delete(&dict->sh->rbtree_expire, &node->expire);
+    }
+
+    ngx_rbtree_delete(&dict->sh->rbtree, (ngx_rbtree_node_t *) node);
+
+    if (retval) {
+        tp = ngx_timeofday();
+        now = tp->sec * 1000 + tp->msec;
+
+        if (!dict->timeout || now < node->expire.key) {
+            ret = ngx_qjs_dict_copy_value_locked(cx, dict, node);
+
+        } else {
+            ret = JS_UNDEFINED;
+        }
+
+    } else {
+        ret = JS_TRUE;
+    }
+
+    ngx_js_dict_node_free(dict, node);
+
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+
+    return ret;
+}
+
+
+static JSValue
+ngx_qjs_dict_get(JSContext *cx, ngx_js_dict_t *dict, ngx_str_t *key)
+{
+    JSValue              ret;
+    ngx_msec_t           now;
+    ngx_time_t          *tp;
+    ngx_js_dict_node_t  *node;
+
+    ngx_rwlock_rlock(&dict->sh->rwlock);
+
+    node = ngx_qjs_dict_lookup(dict, key);
+
+    if (node == NULL) {
+        goto not_found;
+    }
+
+    if (dict->timeout) {
+        tp = ngx_timeofday();
+        now = tp->sec * 1000 + tp->msec;
+
+        if (now >= node->expire.key) {
+            goto not_found;
+        }
+    }
+
+    ret = ngx_qjs_dict_copy_value_locked(cx, dict, node);
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+
+    return ret;
+
+not_found:
+
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+
+    return JS_UNDEFINED;
+}
+
+
+static JSValue
+ngx_qjs_dict_incr(JSContext *cx, ngx_js_dict_t *dict, ngx_str_t *key,
+    double delta, double init, ngx_msec_t timeout)
+{
+    JSValue              value;
+    ngx_msec_t           now;
+    ngx_time_t          *tp;
+    ngx_js_dict_node_t  *node;
+
+    tp = ngx_timeofday();
+    now = tp->sec * 1000 + tp->msec;
+
+    ngx_rwlock_wlock(&dict->sh->rwlock);
+
+    node = ngx_qjs_dict_lookup(dict, key);
+
+    if (node == NULL) {
+        value = JS_NewFloat64(cx, init + delta);
+        if (ngx_qjs_dict_add(cx, dict, key, value, timeout, now) != NGX_OK) {
+            ngx_rwlock_unlock(&dict->sh->rwlock);
+            JS_FreeValue(cx, value);
+            return ngx_qjs_throw_shared_memory_error(cx);
+        }
+
+    } else {
+        node->u.number += delta;
+        value = JS_NewFloat64(cx, node->u.number);
+
+        if (dict->timeout) {
+            ngx_rbtree_delete(&dict->sh->rbtree_expire, &node->expire);
+            node->expire.key = now + timeout;
+            ngx_rbtree_insert(&dict->sh->rbtree_expire, &node->expire);
+        }
+    }
+
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+
+    return value;
+}
+
+
+static JSValue
+ngx_qjs_dict_set(JSContext *cx, ngx_js_dict_t *dict, ngx_str_t *key,
+    JSValue value, ngx_msec_t timeout, unsigned flags)
+{
+    ngx_msec_t           now;
+    ngx_time_t          *tp;
+    ngx_js_dict_node_t  *node;
+
+    tp = ngx_timeofday();
+    now = tp->sec * 1000 + tp->msec;
+
+    ngx_rwlock_wlock(&dict->sh->rwlock);
+
+    node = ngx_qjs_dict_lookup(dict, key);
+
+    if (node == NULL) {
+        if (flags & NGX_JS_DICT_FLAG_MUST_EXIST) {
+            ngx_rwlock_unlock(&dict->sh->rwlock);
+            return JS_FALSE;
+        }
+
+        if (ngx_qjs_dict_add(cx, dict, key, value, timeout, now) != NGX_OK) {
+            goto memory_error;
+        }
+
+        ngx_rwlock_unlock(&dict->sh->rwlock);
+
+        return JS_TRUE;
+    }
+
+    if (flags & NGX_JS_DICT_FLAG_MUST_NOT_EXIST) {
+        if (!dict->timeout || now < node->expire.key) {
+            ngx_rwlock_unlock(&dict->sh->rwlock);
+            return JS_FALSE;
+        }
+    }
+
+    if (ngx_qjs_dict_update(cx, dict, node, value, timeout, now) != NGX_OK) {
+        goto memory_error;
+    }
+
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+
+    return JS_TRUE;
+
+memory_error:
+
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+
+    return ngx_qjs_throw_shared_memory_error(cx);
+}
+
+
+static ngx_int_t
+ngx_qjs_dict_update(JSContext *cx, ngx_js_dict_t *dict,
+    ngx_js_dict_node_t *node, JSValue value, ngx_msec_t timeout, ngx_msec_t now)
+{
+    u_char     *p;
+    ngx_str_t   string;
+
+    if (dict->type == NGX_JS_DICT_TYPE_STRING) {
+        string.data = (u_char *) JS_ToCStringLen(cx, &string.len, value);
+        if (string.data == NULL) {
+            return NGX_ERROR;
+        }
+
+        p = ngx_js_dict_alloc(dict, string.len);
+        if (p == NULL) {
+            JS_FreeCString(cx, (char *) string.data);
+            return NGX_ERROR;
+        }
+
+        ngx_slab_free_locked(dict->shpool, node->u.value.data);
+        ngx_memcpy(p, string.data, string.len);
+
+        node->u.value.data = p;
+        node->u.value.len = string.len;
+
+        JS_FreeCString(cx, (char *) string.data);
+
+    } else {
+        if (JS_ToFloat64(cx, &node->u.number, value) < 0) {
+            return NGX_ERROR;
+        }
+    }
+
+    if (dict->timeout) {
+        ngx_rbtree_delete(&dict->sh->rbtree_expire, &node->expire);
+        node->expire.key = now + timeout;
+        ngx_rbtree_insert(&dict->sh->rbtree_expire, &node->expire);
+    }
+
+    return NGX_OK;
+}
+
+
+static JSValue
+ngx_qjs_shared_dict_error_constructor(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    JSValue  proto, error_ctor, result, global_obj;
+
+    global_obj = JS_GetGlobalObject(cx);
+
+    error_ctor = JS_GetPropertyStr(cx, global_obj, "Error");
+    if (JS_IsException(error_ctor)) {
+        JS_FreeValue(cx, global_obj);
+        return error_ctor;
+    }
+
+    result = JS_CallConstructor(cx, error_ctor, argc, argv);
+    JS_FreeValue(cx, error_ctor);
+    JS_FreeValue(cx, global_obj);
+
+    if (!JS_IsException(result)) {
+        proto = JS_GetClassProto(cx, NGX_QJS_CLASS_ID_SHARED_DICT_ERROR);
+        if (JS_SetPrototype(cx, result, proto) < 0) {
+            JS_FreeValue(cx, result);
+            JS_FreeValue(cx, proto);
+            return JS_EXCEPTION;
+        }
+
+        JS_FreeValue(cx, proto);
+    }
+
+    return result;
+}
+
+
+static JSValue
+ngx_qjs_throw_shared_memory_error(JSContext *cx)
+{
+    JSValue  ctor, global_obj, err;
+
+    global_obj = JS_GetGlobalObject(cx);
+
+    ctor = JS_GetPropertyStr(cx, global_obj, "SharedMemoryError");
+    JS_FreeValue(cx, global_obj);
+
+    if (JS_IsException(ctor)) {
+        return ctor;
+    }
+
+    err = JS_CallConstructor(cx, ctor, 0, NULL);
+
+    JS_FreeValue(cx, ctor);
+
+    return JS_Throw(cx, err);
+}
+
+
+static JSModuleDef *
+ngx_qjs_ngx_shared_dict_init(JSContext *cx, const char *name)
+{
+    JSValue  global_obj, ngx_obj, proto, error_ctor, error_proto, shared_ctor;
+
+    if (JS_NewClass(JS_GetRuntime(cx), NGX_QJS_CLASS_ID_SHARED,
+                    &ngx_qjs_shared_class) < 0)
+    {
+        return NULL;
+    }
+
+    if (JS_NewClass(JS_GetRuntime(cx), NGX_QJS_CLASS_ID_SHARED_DICT,
+                    &ngx_qjs_shared_dict_class) < 0)
+    {
+        return NULL;
+    }
+
+    proto = JS_NewObject(cx);
+    if (JS_IsException(proto)) {
+        return NULL;
+    }
+
+    JS_SetPropertyFunctionList(cx, proto, ngx_qjs_ext_shared_dict,
+                               njs_nitems(ngx_qjs_ext_shared_dict));
+
+    JS_SetClassProto(cx, NGX_QJS_CLASS_ID_SHARED_DICT, proto);
+
+    global_obj = JS_GetGlobalObject(cx);
+
+    error_ctor = JS_GetPropertyStr(cx, global_obj, "Error");
+    if (JS_IsException(error_ctor)) {
+        JS_FreeValue(cx, global_obj);
+        return NULL;
+    }
+
+    error_proto = JS_GetPropertyStr(cx, error_ctor, "prototype");
+    if (JS_IsException(error_proto)) {
+        JS_FreeValue(cx, error_ctor);
+        JS_FreeValue(cx, global_obj);
+        return NULL;
+    }
+
+    proto = JS_NewObjectProto(cx, error_proto);
+
+    JS_FreeValue(cx, error_ctor);
+    JS_FreeValue(cx, error_proto);
+
+    if (JS_IsException(proto)) {
+        JS_FreeValue(cx, global_obj);
+        return NULL;
+    }
+
+    JS_SetPropertyFunctionList(cx, proto, ngx_qjs_ext_shared_dict_error,
+                               njs_nitems(ngx_qjs_ext_shared_dict_error));
+
+    JS_SetClassProto(cx, NGX_QJS_CLASS_ID_SHARED_DICT_ERROR, proto);
+
+    shared_ctor = JS_NewCFunction2(cx, ngx_qjs_shared_dict_error_constructor,
+                                   "SharedDictError", 1, JS_CFUNC_constructor,
+                                   0);
+    if (JS_IsException(shared_ctor)) {
+        JS_FreeValue(cx, global_obj);
+        return NULL;
+    }
+
+    JS_SetConstructor(cx, shared_ctor, proto);
+
+    if (JS_SetPropertyStr(cx, global_obj, "SharedMemoryError", shared_ctor)
+        < 0)
+    {
+        JS_FreeValue(cx, shared_ctor);
+        JS_FreeValue(cx, global_obj);
+        return NULL;
+    }
+
+    ngx_obj = JS_GetPropertyStr(cx, global_obj, "ngx");
+    if (JS_IsException(ngx_obj)) {
+        JS_FreeValue(cx, global_obj);
+        return NULL;
+    }
+
+    JS_SetPropertyFunctionList(cx, ngx_obj, ngx_qjs_ext_ngx,
+                               njs_nitems(ngx_qjs_ext_ngx));
+
+    JS_FreeValue(cx, ngx_obj);
+    JS_FreeValue(cx, global_obj);
+
+    return JS_NewCModule(cx, name, NULL);
+}
+
+#endif
diff --git a/nginx/t/js_shared_dict.t b/nginx/t/js_shared_dict.t
index ae3321d7..8be2831f 100644
--- a/nginx/t/js_shared_dict.t
+++ b/nginx/t/js_shared_dict.t
@@ -42,6 +42,7 @@ http {
     js_shared_dict_zone zone=bar:64k type=string;
     js_shared_dict_zone zone=waka:32k timeout=1000s type=number;
     js_shared_dict_zone zone=no_timeout:32k;
+    js_shared_dict_zone zone=overflow:32k;
 
     server {
         listen       127.0.0.1:8080;
@@ -103,6 +104,10 @@ http {
             js_content test.name;
         }
 
+        location /overflow {
+            js_content test.overflow;
+        }
+
         location /pop {
             js_content test.pop;
         }
@@ -264,6 +269,27 @@ $t->write_file('test.js', <<'EOF');
         r.return(200, dict.replace(r.args.key, value));
     }
 
+    function overflow(r) {
+        var dict = ngx.shared.overflow;
+
+        var v = 'x'.repeat(1024);
+        try {
+            for (var i = 0; i < 1024; i++) {
+                dict.set('key' + i, v);
+            }
+        } catch (e) {
+            if (e instanceof SharedMemoryError) {
+                r.return(200, 'SharedMemoryError');
+                return;
+            }
+
+            r.return(200, e.toString());
+            return;
+        }
+
+        r.return(200, 'no exception');
+    }
+
     function pop(r) {
         var dict = ngx.shared[r.args.dict];
         var val = dict.pop(r.args.key);
@@ -311,14 +337,12 @@ $t->write_file('test.js', <<'EOF');
 
     export default { add, capacity, chain, clear, del, free_space, get, has,
                      incr, items, keys, name, njs: test_njs, pop, replace, set,
-                     set_clear, size, zones, engine };
+                     set_clear, size, zones, engine, overflow };
 EOF
 
 $t->try_run('no js_shared_dict_zone');
 
-plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
-
-$t->plan(51);
+$t->plan(52);
 
 ###############################################################################
 
@@ -415,6 +439,13 @@ like(http_get('/size?dict=foo'), qr/size: 0/, 'no of items in foo after clear');
 like(http_get('/set_clear'), qr/size: 0/,
 	'no of items in no_timeout after clear');
 
+TODO: {
+local $TODO = 'not yet' unless has_version('0.8.8');
+
+like(http_get('/overflow'), qr/SharedMemoryError/, 'overflow exception');
+
+}
+
 ###############################################################################
 
 sub has_version {
diff --git a/nginx/t/stream_js_shared_dict.t b/nginx/t/stream_js_shared_dict.t
index 0bdfaeb7..915cc40b 100644
--- a/nginx/t/stream_js_shared_dict.t
+++ b/nginx/t/stream_js_shared_dict.t
@@ -127,8 +127,6 @@ EOF
 
 $t->try_run('no js_shared_dict_zone');
 
-plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m;
-
 $t->plan(9);
 
 $t->run_daemon(\&stream_daemon, port(8090));


More information about the nginx-devel mailing list