From fe291de024125a40b0a137c06b2bb724a023aac1 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 20 Aug 2026 06:30:15 +0800 Subject: [PATCH 1/6] feat(http): add QuickJS script handler --- CMakeLists.txt | 61 +- Makefile | 26 +- Makefile.in | 33 + Makefile.vars | 5 + config.ini | 2 + configure | 2 + docs/PLAN.md | 1 + docs/cn/HttpJsHandler.md | 226 ++++ docs/cn/HttpLuaHandler.md | 6 +- docs/cn/README.md | 4 + examples/http_server_test.cpp | 33 +- examples/scripts/hello.js | 26 + hconfig.h.in | 1 + http/server/HttpJsHandler.cpp | 1812 +++++++++++++++++++++++++++++ http/server/HttpJsHandler.h | 47 + http/server/HttpScriptHandler.cpp | 28 +- http/server/HttpService.cpp | 26 +- http/server/HttpService.h | 2 +- redis/AsyncRedisClient.cpp | 8 +- scripts/unittest.sh | 12 + unittest/CMakeLists.txt | 26 + unittest/http_js_handler_test.cpp | 137 +++ unittest/http_js_mqtt_test.cpp | 71 ++ unittest/http_js_redis_test.cpp | 113 ++ unittest/http_js_ws_test.cpp | 83 ++ 25 files changed, 2759 insertions(+), 32 deletions(-) create mode 100644 docs/cn/HttpJsHandler.md create mode 100644 examples/scripts/hello.js create mode 100644 http/server/HttpJsHandler.cpp create mode 100644 http/server/HttpJsHandler.h create mode 100644 unittest/http_js_handler_test.cpp create mode 100644 unittest/http_js_mqtt_test.cpp create mode 100644 unittest/http_js_redis_test.cpp create mode 100644 unittest/http_js_ws_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e1ab1cfd..35eb2947a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,7 @@ option(WITH_GNUTLS "with gnutls library" OFF) option(WITH_MBEDTLS "with mbedtls library" OFF) option(WITH_LUA "with lua library" OFF) +option(WITH_JS "with quickjs library" OFF) option(WITH_KCP "compile event/kcp" OFF) @@ -213,6 +214,49 @@ if(WITH_LUA) endif() endif() +if(WITH_JS) + add_definitions(-DWITH_JS) + find_path(QUICKJS_INCLUDE_DIR + NAMES quickjs.h + HINTS + ${QUICKJS_ROOT}/include/quickjs + ${QUICKJS_ROOT}/include + /opt/homebrew/opt/quickjs/include/quickjs + /opt/homebrew/opt/quickjs/include + /usr/local/opt/quickjs/include/quickjs + /usr/local/opt/quickjs/include + /usr/local/include/quickjs + /usr/local/include + /usr/include/quickjs + /usr/include) + find_library(QUICKJS_LIBRARY + NAMES quickjs libquickjs + HINTS + ${QUICKJS_ROOT}/lib/quickjs + ${QUICKJS_ROOT}/lib + /opt/homebrew/opt/quickjs/lib/quickjs + /opt/homebrew/opt/quickjs/lib + /usr/local/opt/quickjs/lib/quickjs + /usr/local/opt/quickjs/lib + /usr/local/lib/quickjs + /usr/local/lib + /usr/lib) + if(NOT QUICKJS_INCLUDE_DIR OR NOT QUICKJS_LIBRARY) + message(FATAL_ERROR "WITH_JS requires QuickJS. Set QUICKJS_ROOT or QUICKJS_INCLUDE_DIR and QUICKJS_LIBRARY.") + endif() + include_directories(${QUICKJS_INCLUDE_DIR}) + set(LIBS ${LIBS} ${QUICKJS_LIBRARY}) + if(WITH_EVPP AND WITH_HTTP AND WITH_HTTP_CLIENT) + add_definitions(-DHVJS_WITH_HTTP) + endif() + if(WITH_EVPP AND WITH_REDIS) + add_definitions(-DHVJS_WITH_REDIS) + endif() + if(WITH_EVPP AND WITH_MQTT) + add_definitions(-DHVJS_WITH_MQTT) + endif() +endif() + if(WIN32 OR MINGW) add_definitions(-DWIN32_LEAN_AND_MEAN -D_CRT_SECURE_NO_WARNINGS -D_WIN32_WINNT=0x0600) set(LIBS ${LIBS} secur32 crypt32 winmm iphlpapi ws2_32) @@ -286,8 +330,14 @@ if(WITH_EVPP) endif() if(WITH_HTTP_SERVER) set(LIBHV_HEADERS ${LIBHV_HEADERS} ${HTTP_SERVER_HEADERS}) + if(WITH_LUA OR WITH_JS) + set(LIBHV_HEADERS ${LIBHV_HEADERS} http/server/HttpScriptHandler.h) + endif() if(WITH_LUA) - set(LIBHV_HEADERS ${LIBHV_HEADERS} http/server/HttpScriptHandler.h http/server/HttpLuaHandler.h) + set(LIBHV_HEADERS ${LIBHV_HEADERS} http/server/HttpLuaHandler.h) + endif() + if(WITH_JS) + set(LIBHV_HEADERS ${LIBHV_HEADERS} http/server/HttpJsHandler.h) endif() set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} http/server) endif() @@ -308,6 +358,15 @@ if(WITH_MQTT) endif() list_source_directories(LIBHV_SRCS ${LIBHV_SRCDIRS}) +if(NOT WITH_LUA) + list(FILTER LIBHV_SRCS EXCLUDE REGEX "(^|/)HttpLuaHandler\\.cpp$") +endif() +if(NOT WITH_JS) + list(FILTER LIBHV_SRCS EXCLUDE REGEX "(^|/)HttpJsHandler\\.cpp$") +endif() +if(NOT WITH_LUA AND NOT WITH_JS) + list(FILTER LIBHV_SRCS EXCLUDE REGEX "(^|/)HttpScriptHandler\\.cpp$") +endif() if(WIN32) set(CMAKE_RC_FLAGS_DEBUG -D_DEBUG) configure_file(${PROJECT_SOURCE_DIR}/${PROJECT_NAME}.rc.in ${CMAKE_BINARY_DIR}/${PROJECT_NAME}.rc) diff --git a/Makefile b/Makefile index 4f573da45..60875c196 100644 --- a/Makefile +++ b/Makefile @@ -49,8 +49,14 @@ endif ifeq ($(WITH_HTTP_SERVER), yes) LIBHV_HEADERS += $(HTTP_SERVER_HEADERS) LIBHV_SRCDIRS += http/server +ifneq ($(filter yes,$(WITH_LUA) $(WITH_JS)),) +LIBHV_HEADERS += http/server/HttpScriptHandler.h +endif ifeq ($(WITH_LUA), yes) -LIBHV_HEADERS += http/server/HttpScriptHandler.h http/server/HttpLuaHandler.h +LIBHV_HEADERS += http/server/HttpLuaHandler.h +endif +ifeq ($(WITH_JS), yes) +LIBHV_HEADERS += http/server/HttpJsHandler.h endif endif @@ -424,6 +430,24 @@ ifeq ($(WITH_REDIS), yes) endif endif endif +ifeq ($(WITH_JS), yes) +ifeq ($(WITH_EVPP), yes) +ifeq ($(WITH_HTTP), yes) +ifeq ($(WITH_HTTP_SERVER), yes) +ifeq ($(WITH_HTTP_CLIENT), yes) + $(CXX) -g -Wall -O0 -std=c++11 -DWITH_JS -DHVJS_WITH_HTTP $(JS_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ihttp -Ihttp/server -Ihttp/client -o bin/http_js_handler_test unittest/http_js_handler_test.cpp -Llib -lhv -pthread $(JS_LIBS) +ifeq ($(WITH_REDIS), yes) + $(CXX) -g -Wall -O0 -std=c++11 -DWITH_JS -DHVJS_WITH_HTTP -DHVJS_WITH_REDIS $(JS_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ihttp -Ihttp/server -Ihttp/client -Iredis -o bin/http_js_redis_test unittest/http_js_redis_test.cpp unittest/redis_test_server.cpp -Llib -lhv -pthread $(JS_LIBS) +endif + $(CXX) -g -Wall -O0 -std=c++11 -DWITH_JS -DHVJS_WITH_HTTP $(JS_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ihttp -Ihttp/server -Ihttp/client -o bin/http_js_ws_test unittest/http_js_ws_test.cpp -Llib -lhv -pthread $(JS_LIBS) +ifeq ($(WITH_MQTT), yes) + $(CXX) -g -Wall -O0 -std=c++11 -DWITH_JS -DHVJS_WITH_HTTP -DHVJS_WITH_MQTT $(JS_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ihttp -Ihttp/server -Ihttp/client -Imqtt -o bin/http_js_mqtt_test unittest/http_js_mqtt_test.cpp -Llib -lhv -pthread $(JS_LIBS) +endif +endif +endif +endif +endif +endif run-unittest: unittest bash scripts/unittest.sh diff --git a/Makefile.in b/Makefile.in index 25ea81d54..bcb5fce5a 100644 --- a/Makefile.in +++ b/Makefile.in @@ -129,6 +129,17 @@ ifeq ($(ALL_SRCS), ) ALL_SRCS = $(wildcard *.c *.cc *.cpp) endif override SRCS += $(filter-out %_test.c %_test.cc %_test.cpp, $(ALL_SRCS)) +ifeq ($(filter clean,$(MAKECMDGOALS)),) +ifneq ($(WITH_LUA), yes) +override SRCS := $(filter-out %/HttpLuaHandler.cpp HttpLuaHandler.cpp, $(SRCS)) +endif +ifneq ($(WITH_JS), yes) +override SRCS := $(filter-out %/HttpJsHandler.cpp HttpJsHandler.cpp, $(SRCS)) +endif +ifeq ($(filter yes,$(WITH_LUA) $(WITH_JS)),) +override SRCS := $(filter-out %/HttpScriptHandler.cpp HttpScriptHandler.cpp, $(SRCS)) +endif +endif # OBJS += $(patsubst %.c, %.o, $(SRCS)) # OBJS += $(patsubst %.cc, %.o, $(SRCS)) # OBJS += $(patsubst %.cpp, %.o, $(SRCS)) @@ -185,6 +196,28 @@ endif endif endif +ifeq ($(WITH_JS), yes) + CPPFLAGS += -DWITH_JS $(JS_CFLAGS) + LDFLAGS += $(JS_LIBS) +ifeq ($(WITH_EVPP), yes) +ifeq ($(WITH_HTTP), yes) +ifeq ($(WITH_HTTP_CLIENT), yes) + CPPFLAGS += -DHVJS_WITH_HTTP +endif +endif +endif +ifeq ($(WITH_EVPP), yes) +ifeq ($(WITH_REDIS), yes) + CPPFLAGS += -DHVJS_WITH_REDIS +endif +endif +ifeq ($(WITH_EVPP), yes) +ifeq ($(WITH_MQTT), yes) + CPPFLAGS += -DHVJS_WITH_MQTT +endif +endif +endif + CPPFLAGS += $(addprefix -D, $(DEFINES)) CPPFLAGS += $(addprefix -I, $(INCDIRS)) CPPFLAGS += $(addprefix -I, $(SRCDIRS)) diff --git a/Makefile.vars b/Makefile.vars index f2d3a13c0..89f3fda04 100644 --- a/Makefile.vars +++ b/Makefile.vars @@ -13,6 +13,11 @@ LUA_PREFIX ?= $(shell for dir in /opt/homebrew/opt/lua /usr/local/opt/lua /usr; LUA_INCLUDE_DIR ?= $(shell if [ -n "$(LUA_PREFIX)" ]; then for dir in "$(LUA_PREFIX)/include/lua" "$(LUA_PREFIX)/include/lua5.5" "$(LUA_PREFIX)/include/lua5.4" "$(LUA_PREFIX)/include/lua5.3" "$(LUA_PREFIX)/include"; do if [ -f "$$dir/lua.h" ]; then echo $$dir; break; fi; done; fi) LUA_CFLAGS ?= $(shell if [ -n "$(LUA_PKG_CONFIG)" ]; then $(PKG_CONFIG) --cflags $(LUA_PKG_CONFIG); elif [ -n "$(LUA_INCLUDE_DIR)" ]; then echo -I$(LUA_INCLUDE_DIR); fi) LUA_LIBS ?= $(shell if [ -n "$(LUA_PKG_CONFIG)" ]; then $(PKG_CONFIG) --libs $(LUA_PKG_CONFIG); elif [ -n "$(LUA_PREFIX)" ]; then echo -L$(LUA_PREFIX)/lib -llua; else echo -llua; fi) +QUICKJS_ROOT ?= $(shell for dir in /opt/homebrew/opt/quickjs /usr/local/opt/quickjs /usr; do if [ -f "$$dir/include/quickjs/quickjs.h" ] || [ -f "$$dir/include/quickjs.h" ]; then echo $$dir; break; fi; done) +QUICKJS_INCLUDE_DIR ?= $(shell if [ -n "$(QUICKJS_ROOT)" ]; then for dir in "$(QUICKJS_ROOT)/include/quickjs" "$(QUICKJS_ROOT)/include"; do if [ -f "$$dir/quickjs.h" ]; then echo $$dir; break; fi; done; fi) +QUICKJS_LIB_DIR ?= $(shell if [ -n "$(QUICKJS_ROOT)" ]; then for dir in "$(QUICKJS_ROOT)/lib/quickjs" "$(QUICKJS_ROOT)/lib"; do if [ -f "$$dir/libquickjs.a" ] || [ -f "$$dir/libquickjs.dylib" ] || [ -f "$$dir/libquickjs.so" ]; then echo $$dir; break; fi; done; fi) +JS_CFLAGS ?= $(shell if [ -n "$(QUICKJS_INCLUDE_DIR)" ]; then echo -I$(QUICKJS_INCLUDE_DIR); fi) +JS_LIBS ?= $(shell if [ -n "$(QUICKJS_LIB_DIR)" ]; then echo -L$(QUICKJS_LIB_DIR) -lquickjs; else echo -lquickjs; fi) BASE_HEADERS = base/hplatform.h\ \ diff --git a/config.ini b/config.ini index 6f5a13d93..e4a87fe9e 100644 --- a/config.ini +++ b/config.ini @@ -40,6 +40,8 @@ WITH_GNUTLS=no WITH_MBEDTLS=no # for http lua handler WITH_LUA=no +# for http js handler (QuickJS) +WITH_JS=no # rudp WITH_KCP=no diff --git a/configure b/configure index ea7b7d407..aa7a66d4c 100755 --- a/configure +++ b/configure @@ -33,6 +33,7 @@ modules: --with-redis compile redis module? (DEFAULT: $WITH_REDIS) --with-rpc compile hrpc (libhrpc, needs protobuf)? (DEFAULT: $WITH_RPC) --with-lua compile lua module? (DEFAULT: $WITH_LUA) + --with-js compile js module? (DEFAULT: $WITH_JS) features: --enable-uds enable Unix Domain Socket? (DEFAULT: $ENABLE_UDS) @@ -305,6 +306,7 @@ option=ENABLE_UDS && check_option option=USE_MULTIMAP && check_option option=WITH_KCP && check_option option=WITH_IO_URING && check_option +option=WITH_JS && check_option # end confile cat << END >> $confile diff --git a/docs/PLAN.md b/docs/PLAN.md index 2f6450a7d..ce5743ed8 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -12,6 +12,7 @@ - redis client - async DNS - lua binding +- http js script handler - hrpc = libhv + protobuf ## Plan diff --git a/docs/cn/HttpJsHandler.md b/docs/cn/HttpJsHandler.md new file mode 100644 index 000000000..b7a7c8a8c --- /dev/null +++ b/docs/cn/HttpJsHandler.md @@ -0,0 +1,226 @@ +# Http JS Handler + +`HttpScriptHandler` 支持把 `.js` 脚本作为 HTTP 请求处理器执行。JS handler 基于 QuickJS,适合把少量业务逻辑从 C++ 编译周期里解耦出来,并且可以在脚本中使用 `async` / `await` 调用 libhv 的异步能力。 + +该功能是可选模块,默认不编译。 + +## 编译 + +需要 QuickJS 开发库。 + +Makefile: + +```bash +make libhv WITH_JS=yes WITH_HTTP=yes +make http_server_test WITH_JS=yes WITH_HTTP=yes +make unittest WITH_JS=yes WITH_HTTP=yes WITH_REDIS=yes WITH_MQTT=yes +``` + +如果 QuickJS 安装在自定义路径,可以显式指定: + +```bash +make libhv WITH_JS=yes \ + QUICKJS_ROOT=/opt/homebrew/opt/quickjs +``` + +或: + +```bash +make libhv WITH_JS=yes \ + JS_CFLAGS="-I/usr/local/include/quickjs" \ + JS_LIBS="-L/usr/local/lib/quickjs -lquickjs" +``` + +CMake: + +```bash +cmake -S . -B build -DWITH_JS=ON -DWITH_HTTP=ON -DWITH_HTTP_CLIENT=ON -DBUILD_UNITTEST=ON +cmake --build build +``` + +如果 CMake 没有自动找到 QuickJS: + +```bash +cmake -S . -B build -DWITH_JS=ON -DQUICKJS_ROOT=/opt/homebrew/opt/quickjs +``` + +## 基本用法 + +C++: + +```cpp +#include "HttpServer.h" +#include "HttpScriptHandler.h" + +using namespace hv; + +int main() { + HttpService router; + router.GET("/hello", HttpScriptHandler("scripts/hello.js")); + + HttpServer server; + server.port = 8080; + server.service = &router; + server.run(); + return 0; +} +``` + +JS: + +```js +async function get(ctx) { + const hv = require("hv"); + await hv.sleep(100); + return { + ok: true, + id: ctx.query("id", ""), + path: ctx.path() + }; +} +``` + +如果需要明确指定 JS 引擎,也可以直接使用 `HttpJsHandler("scripts/hello.js")`。推荐用户代码优先使用 `HttpScriptHandler`,这样同一个路由入口可以按脚本后缀分发到不同脚本引擎。 + +## 目录映射 + +`HttpService::Script(path, script_dir)` 可以把 URL 前缀映射到脚本目录,内部同样使用 `HttpScriptHandler`: + +```cpp +router.Script("/script/", "scripts"); +``` + +访问 `/script/user?id=42` 时会调用 `scripts/user.js`。访问 `/script/` 时会调用 `scripts/index.js`。如果同时启用了 Lua 和 JS,未带后缀的脚本路径会优先匹配 `.lua`,再匹配 `.js`。 + +目录映射默认支持 `GET`、`POST`、`PUT`、`DELETE`、`PATCH`。路径中包含 `..` 路径段时返回 `403`。 + +## ctx API + +```js +ctx.method() // GET/POST/... +ctx.path() // URL path +ctx.param(name, defaultValue) +ctx.query(name, defaultValue) +ctx.header(name, defaultValue) +ctx.body() + +ctx.status(code) +ctx.setHeader(name, value) +ctx.set_header(name, value) +ctx.text(str) +ctx.json(value) +``` + +handler 可以直接调用 `ctx.text` / `ctx.json`,也可以返回字符串、数字状态码或 JS 对象: + +```js +function post(ctx) { + ctx.status(201); + ctx.setHeader("X-From", "js"); + return ctx.text("created"); +} +``` + +## 内置模块 + +JS handler 提供受控的内置模块,不兼容 Node.js,也不支持 npm 包加载。也就是说,首版不支持 `require("axios")`;请使用 libhv 提供的内置模块。 + +```js +const hv = require("hv"); +hv.version() // libhv 版本串 +hv.log("hello") // INFO 日志 +await hv.sleep(1000) +``` + +### hv/http + +需同时启用 `WITH_HTTP` 和 `WITH_HTTP_CLIENT`。 + +```js +const http = require("hv/http"); + +const resp = await http.get("http://127.0.0.1:8080/ping"); +// resp: { status, body, headers } + +await http.post("http://127.0.0.1:8080/echo", "body", { + "Content-Type": "text/plain" +}); + +await http.request("GET", "http://127.0.0.1:8080/ping"); +``` + +### hv/ws + +需同时启用 `WITH_HTTP` 和 `WITH_HTTP_CLIENT`。 + +```js +const wsmod = require("hv/ws"); + +const ws = await wsmod.connect("ws://127.0.0.1:8888/"); +ws.send("hello"); +const msg = await ws.recv(); +ws.close(); +``` + +`recv()` 在收到消息前保持 pending;连接关闭时会 reject。 + +### hv/redis + +需启用 `WITH_REDIS`。 + +```js +const redis = require("hv/redis"); + +const r = redis.new({ host: "127.0.0.1", port: 6379, timeout: 3000 }); +await r.set("k", "v"); +const v = await r.get("k"); +const n = await r.incr("c"); +const pong = await r.command(["PING"]); +``` + +Redis 回复映射:string -> string,integer -> number,nil -> null,array -> array,error reply -> rejected Promise。 + +### hv/mqtt + +需启用 `WITH_MQTT`。 + +```js +const mqtt = require("hv/mqtt"); + +const client = await mqtt.connect({ + host: "127.0.0.1", + port: 1883, + id: "client-1", + username: "", + password: "", + keepalive: 60, + clean_session: true, + ssl: false, + timeout: 3000, + reconnect: { + min_delay: 1000, + max_delay: 10000, + delay_policy: 2, + max_retry: 0 + } +}); + +client.subscribe("topic", 1); +client.publish("topic", "payload", 1, false); +const msg = await client.recv(); // { topic, payload, qos } +client.disconnect(); +``` + +## 异步模型 + +每次 HTTP 请求会创建独立 QuickJS runtime/context。脚本可以返回普通值,也可以返回 Promise;`HttpJsHandler` 会等待 Promise fulfilled/rejected 后再发送 HTTP 响应。`await hv.sleep()`、`await http.get()`、`await ws.recv()`、`await redis.command()`、`await mqtt.connect()` 都在当前 IO 线程的 event loop 上推进,不会阻塞 loop。 + +`HttpJsHandler` 会缓存脚本文本,并在 `reload_on_change=true` 时根据文件 `mtime` 自动重新读取;每个请求仍使用独立 QuickJS runtime/context,因此脚本里的全局变量不会跨请求共享。 + +## 示例 + +```bash +make http_server_test WITH_JS=yes WITH_HTTP=yes +bin/http_server_test 8080 +curl "http://127.0.0.1:8080/script/hello?id=42" +``` diff --git a/docs/cn/HttpLuaHandler.md b/docs/cn/HttpLuaHandler.md index d77a32518..4c75ec419 100644 --- a/docs/cn/HttpLuaHandler.md +++ b/docs/cn/HttpLuaHandler.md @@ -1,6 +1,6 @@ # Http Lua Handler -`HttpScriptHandler` 允许 `HttpService` 调用脚本里的 `handle(ctx)` 方法处理 HTTP 请求。当前支持 `.lua` 脚本,适合把少量业务逻辑从 C++ 编译周期里解耦出来:修改脚本后无需重新编译服务,下一次请求会自动加载新脚本。 +`HttpScriptHandler` 允许 `HttpService` 调用脚本里的 `handle(ctx)` 方法处理 HTTP 请求。启用 `WITH_LUA` 时支持 `.lua` 脚本,启用 `WITH_JS` 时也支持 `.js` 脚本。它适合把少量业务逻辑从 C++ 编译周期里解耦出来:修改脚本后无需重新编译服务,下一次请求会自动加载新脚本。 该功能是可选模块,默认不编译。 @@ -76,7 +76,7 @@ end router.Script("/script/", "scripts"); ``` -访问 `/script/user?id=42` 时会调用 `scripts/user.lua`。访问 `/script/` 时会调用 `scripts/index.lua`。当前目录映射只自动补 `.lua` 后缀。 +访问 `/script/user?id=42` 时会调用 `scripts/user.lua`。访问 `/script/` 时会调用 `scripts/index.lua`。如果同时启用了 Lua 和 JS,未带后缀的脚本路径会优先匹配 `.lua`,再匹配 `.js`。 目录映射默认支持 `GET`、`POST`、`PUT`、`DELETE`、`PATCH`。路径中包含 `..` 路径段时返回 `403`。 @@ -168,7 +168,7 @@ end ## 热更新 -`HttpScriptHandler` 当前会把 `.lua` 文件转给 `HttpLuaHandler`。`HttpLuaHandler` 会记录脚本文件的 `mtime`。每次请求前,如果文件被修改,会重新加载脚本。 +`HttpScriptHandler` 当前会把 `.lua` 文件转给 `HttpLuaHandler`,把 `.js` 文件转给 `HttpJsHandler`。`HttpLuaHandler` 会记录脚本文件的 `mtime`。每次请求前,如果文件被修改,会重新加载脚本。 重新加载失败时: diff --git a/docs/cn/README.md b/docs/cn/README.md index e6d625d41..10e3de9ca 100644 --- a/docs/cn/README.md +++ b/docs/cn/README.md @@ -9,6 +9,10 @@ - [Lua Binding: hv.* Lua 绑定](lua.md) +## js接口 + +- [Http JS Handler: HTTP JS脚本处理器](HttpJsHandler.md) + ## c++接口 - [class EventLoop: 事件循环类](EventLoop.md) diff --git a/examples/http_server_test.cpp b/examples/http_server_test.cpp index b6854008d..402afe77e 100644 --- a/examples/http_server_test.cpp +++ b/examples/http_server_test.cpp @@ -5,10 +5,10 @@ */ #include "HttpServer.h" -#include "hthread.h" // import hv_gettid -#include "hasync.h" // import hv::async +#include "hthread.h" // import hv_gettid +#include "hasync.h" // import hv::async -#ifdef WITH_LUA +#if defined(WITH_LUA) || defined(WITH_JS) #include "HttpScriptHandler.h" #endif @@ -55,9 +55,7 @@ int main(int argc, char** argv) { /* API handlers */ // curl -v http://ip:port/ping - router.GET("/ping", [](HttpRequest* req, HttpResponse* resp) { - return resp->String("pong"); - }); + router.GET("/ping", [](HttpRequest* req, HttpResponse* resp) { return resp->String("pong"); }); // curl -v http://ip:port/data router.GET("/data", [](HttpRequest* req, HttpResponse* resp) { @@ -66,9 +64,7 @@ int main(int argc, char** argv) { }); // curl -v http://ip:port/paths - router.GET("/paths", [&router](HttpRequest* req, HttpResponse* resp) { - return resp->Json(router.Paths()); - }); + router.GET("/paths", [&router](HttpRequest* req, HttpResponse* resp) { return resp->Json(router.Paths()); }); // curl -v http://ip:port/get?env=1 router.GET("/get", [](const HttpContextPtr& ctx) { @@ -81,9 +77,7 @@ int main(int argc, char** argv) { }); // curl -v http://ip:port/echo -d "hello,world!" - router.POST("/echo", [](const HttpContextPtr& ctx) { - return ctx->send(ctx->body(), ctx->type()); - }); + router.POST("/echo", [](const HttpContextPtr& ctx) { return ctx->send(ctx->body(), ctx->type()); }); // curl -v http://ip:port/user/123 router.GET("/user/{id}", [](const HttpContextPtr& ctx) { @@ -95,8 +89,14 @@ int main(int argc, char** argv) { #ifdef WITH_LUA // curl -v "http://ip:port/lua/hello?id=42" router.GET("/lua/hello", HttpScriptHandler("examples/scripts/hello.lua")); +#endif +#ifdef WITH_JS + // curl -v "http://ip:port/js/hello?id=42" + router.GET("/js/hello", HttpScriptHandler("examples/scripts/hello.js")); +#endif +#if defined(WITH_LUA) || defined(WITH_JS) // curl -v "http://ip:port/script/hello?id=42" - // curl -v "http://ip:port/script/async?host=example.com" (coroutine sync-style async) + // curl -v "http://ip:port/script/async?host=example.com" (sync-style async) router.Script("/script/", "examples/scripts"); #endif @@ -111,9 +111,7 @@ int main(int argc, char** argv) { // curl -v http://ip:port/close // Test HTTP_STATUS_CLOSE: closes connection without sending any response - router.GET("/close", [](HttpRequest* req, HttpResponse* resp) { - return HTTP_STATUS_CLOSE; - }); + router.GET("/close", [](HttpRequest* req, HttpResponse* resp) { return HTTP_STATUS_CLOSE; }); // middleware router.AllowCORS(); @@ -146,7 +144,8 @@ int main(int argc, char** argv) { server.start(); // press Enter to stop - while (getchar() != '\n'); + while (getchar() != '\n') + ; hv::async::cleanup(); return 0; } diff --git a/examples/scripts/hello.js b/examples/scripts/hello.js new file mode 100644 index 000000000..fd6799282 --- /dev/null +++ b/examples/scripts/hello.js @@ -0,0 +1,26 @@ +function get(ctx) { + const hv = require("hv"); + hv.log("js get", ctx.path()); + return { + ok: true, + method: "GET", + path: ctx.path(), + id: ctx.query("id", "") + }; +} + +function post(ctx) { + const hv = require("hv"); + hv.log("js post", ctx.path()); + return ctx.text("POST " + ctx.body()); +} + +function handle(ctx) { + const hv = require("hv"); + hv.log("js fallback", ctx.method(), ctx.path()); + return { + ok: true, + method: ctx.method(), + path: ctx.path() + }; +} diff --git a/hconfig.h.in b/hconfig.h.in index 2bef47660..37af2e788 100644 --- a/hconfig.h.in +++ b/hconfig.h.in @@ -101,5 +101,6 @@ #cmakedefine WITH_IO_URING 1 #cmakedefine WITH_LUA 1 +#cmakedefine WITH_JS 1 #endif // HV_CONFIG_H_ diff --git a/http/server/HttpJsHandler.cpp b/http/server/HttpJsHandler.cpp new file mode 100644 index 000000000..7a16dc1e0 --- /dev/null +++ b/http/server/HttpJsHandler.cpp @@ -0,0 +1,1812 @@ +#ifdef WITH_JS + +#include "HttpJsHandler.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include "EventLoop.h" +#include "hfile.h" +#include "hlog.h" +#include "hpath.h" +#include "hstring.h" +#include "htime.h" +#include "hversion.h" +#ifdef HVJS_WITH_HTTP +#include "AsyncHttpClient.h" +#include "WebSocketClient.h" +#endif +#ifdef HVJS_WITH_REDIS +#include "AsyncRedisClient.h" +#endif +#ifdef HVJS_WITH_MQTT +#include "mqtt_client.h" +#endif + +namespace hv { + +namespace { + +struct JsHttpTask; + +static const int JS_HTTP_METHOD_REQUEST = -1; + +struct JsHttpTask { + JSRuntime* rt; + JSContext* js; + hloop_t* loop; + EventLoopPtr loop_ptr; + HttpContextPtr ctx; + JSValue promise; + bool async; + bool finished; + bool in_call; + bool closing; + int refcount; + std::string error; + + JsHttpTask() : rt(NULL), js(NULL), loop(NULL), promise(JS_UNDEFINED), async(false), finished(false), in_call(false), closing(false), refcount(1) {} +}; + +struct JsPromiseOp { + JsHttpTask* task; + JSValue resolve; + JSValue reject; + bool completed; + bool defer_delete; + + JsPromiseOp() : task(NULL), resolve(JS_UNDEFINED), reject(JS_UNDEFINED), completed(false), defer_delete(false) {} + + virtual ~JsPromiseOp() {} +}; + +struct JsSleep : public JsPromiseOp { + htimer_t* timer; + TimerID timer_id; + + JsSleep() : timer(NULL), timer_id(INVALID_TIMER_ID) {} +}; + +struct JsImmediatePromise : public JsPromiseOp {}; + +static std::mutex& js_class_id_mutex() { + static std::mutex mutex; + return mutex; +} + +static void js_new_class_id(JSClassID* class_id) { + std::lock_guard lock(js_class_id_mutex()); + JS_NewClassID(class_id); +} + +static void task_ref(JsHttpTask* task) { + ++task->refcount; +} + +static void task_unref(JsHttpTask* task) { + if (--task->refcount != 0) return; + task->closing = true; + if (!JS_IsUndefined(task->promise)) { + JS_FreeValue(task->js, task->promise); + task->promise = JS_UNDEFINED; + } + if (task->js) { + if (task->rt) { + JS_RunGC(task->rt); + } + JS_FreeContext(task->js); + task->js = NULL; + } + if (task->rt) { + JS_RunGC(task->rt); + JS_FreeRuntime(task->rt); + task->rt = NULL; + } + delete task; +} + +static void drain_jobs(JsHttpTask* task); +static std::string js_to_string(JSContext* ctx, JSValueConst value); +static std::string js_exception_string(JSContext* ctx); +static void js_promise_complete(JsPromiseOp* op, JSValue value, bool ok); + +static void drain_event_cb(hevent_t* ev) { + JsHttpTask* task = (JsHttpTask*)hevent_userdata(ev); + drain_jobs(task); + task_unref(task); +} + +static void schedule_drain(JsHttpTask* task) { + if (task == NULL || task->closing) return; + task_ref(task); + if (task->loop_ptr) { + task->loop_ptr->queueInLoop([task]() { + drain_jobs(task); + task_unref(task); + }); + } + else if (task->loop) { + hevent_t ev; + memset(&ev, 0, sizeof(ev)); + ev.cb = drain_event_cb; + ev.userdata = task; + hloop_post_event(task->loop, &ev); + } + else { + task_unref(task); + } +} + +template static JSValue js_new_promise(JSContext* js, JsHttpTask* task, T** out) { + JSValue funcs[2]; + JSValue promise = JS_NewPromiseCapability(js, funcs); + if (JS_IsException(promise)) return promise; + T* op = new T(); + op->task = task; + op->resolve = funcs[0]; + op->reject = funcs[1]; + task_ref(task); + *out = op; + return promise; +} + +static void js_promise_complete(JsPromiseOp* op, JSValue value, bool ok) { + JsHttpTask* task = op->task; + if (op->completed) { + JS_FreeValue(task->js, value); + return; + } + op->completed = true; + if (!task->closing) { + JSValue func = ok ? op->resolve : op->reject; + JSValue ret = JS_Call(task->js, func, JS_UNDEFINED, 1, &value); + if (JS_IsException(ret) && task->error.empty()) { + task->error = js_exception_string(task->js); + } + JS_FreeValue(task->js, ret); + JS_FreeValue(task->js, value); + JS_FreeValue(task->js, op->resolve); + JS_FreeValue(task->js, op->reject); + op->resolve = JS_UNDEFINED; + op->reject = JS_UNDEFINED; + if (task->in_call) { + op->defer_delete = true; + schedule_drain(task); + return; + } + schedule_drain(task); + } + else { + JS_FreeValue(task->js, value); + JS_FreeValue(task->js, op->resolve); + JS_FreeValue(task->js, op->reject); + op->resolve = JS_UNDEFINED; + op->reject = JS_UNDEFINED; + } + delete op; + task_unref(task); +} + +static void js_promise_resolve(JsPromiseOp* op, JSValue value) { + js_promise_complete(op, value, true); +} + +static void js_promise_reject(JsPromiseOp* op, const char* message) { + js_promise_complete(op, JS_NewString(op->task->js, message ? message : "error"), false); +} + +static JSValue js_rejected_promise(JSContext* js, const char* message) { + JSValue funcs[2]; + JSValue promise = JS_NewPromiseCapability(js, funcs); + if (JS_IsException(promise)) return promise; + JSValue reason = JS_NewString(js, message ? message : "error"); + JSValue ret = JS_Call(js, funcs[1], JS_UNDEFINED, 1, &reason); + JS_FreeValue(js, ret); + JS_FreeValue(js, reason); + JS_FreeValue(js, funcs[0]); + JS_FreeValue(js, funcs[1]); + return promise; +} + +static JSValue js_async_resolved_promise(JSContext* js, JsHttpTask* task, JSValue value) { + if (task == NULL) { + JS_FreeValue(js, value); + return JS_ThrowInternalError(js, "invalid HttpJsHandler task"); + } + JsImmediatePromise* op = NULL; + JSValue promise = js_new_promise(js, task, &op); + if (JS_IsException(promise)) { + JS_FreeValue(js, value); + return promise; + } + js_promise_resolve(op, value); + return promise; +} + +static void js_finish_deferred_op(JsPromiseOp* op) { + if (op == NULL || !op->completed || !op->defer_delete) return; + JsHttpTask* task = op->task; + delete op; + task_unref(task); +} + +static std::string js_to_string(JSContext* ctx, JSValueConst value) { + size_t len = 0; + const char* str = JS_ToCStringLen(ctx, &len, value); + if (str == NULL) return std::string(); + std::string out(str, len); + JS_FreeCString(ctx, str); + return out; +} + +static bool js_get_property(JSContext* js, JSValueConst obj, const char* name, JSValue* out) { + *out = JS_UNDEFINED; + if (!JS_IsObject(obj)) return false; + *out = JS_GetPropertyStr(js, obj, name); + return !JS_IsUndefined(*out) && !JS_IsException(*out); +} + +static std::string js_get_string_property(JSContext* js, JSValueConst obj, const char* name, const char* defvalue = "") { + JSValue value; + if (!js_get_property(js, obj, name, &value) || JS_IsNull(value)) { + if (!JS_IsUndefined(value) && !JS_IsException(value)) JS_FreeValue(js, value); + return defvalue; + } + std::string out = js_to_string(js, value); + JS_FreeValue(js, value); + return out; +} + +static int js_get_int_property(JSContext* js, JSValueConst obj, const char* name, int defvalue = 0) { + JSValue value; + if (!js_get_property(js, obj, name, &value) || JS_IsNull(value)) { + if (!JS_IsUndefined(value) && !JS_IsException(value)) JS_FreeValue(js, value); + return defvalue; + } + int32_t out = defvalue; + JS_ToInt32(js, &out, value); + JS_FreeValue(js, value); + return out; +} + +static bool js_get_bool_property(JSContext* js, JSValueConst obj, const char* name, bool defvalue = false) { + JSValue value; + if (!js_get_property(js, obj, name, &value) || JS_IsNull(value)) { + if (!JS_IsUndefined(value) && !JS_IsException(value)) JS_FreeValue(js, value); + return defvalue; + } + bool out = JS_ToBool(js, value) != 0; + JS_FreeValue(js, value); + return out; +} + +static std::string js_exception_string(JSContext* ctx) { + JSValue exception = JS_GetException(ctx); + std::string msg = js_to_string(ctx, exception); + JS_FreeValue(ctx, exception); + return msg.empty() ? "javascript exception" : msg; +} + +static JsHttpTask* js_get_task(JSContext* js) { + return (JsHttpTask*)JS_GetContextOpaque(js); +} + +static JSValue js_ctx_method(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + (void)argc; + (void)argv; + JsHttpTask* task = js_get_task(js); + if (task == NULL || !task->ctx || !task->ctx->request) { + return JS_ThrowTypeError(js, "invalid HttpContext"); + } + return JS_NewString(js, http_method_str(task->ctx->request->method)); +} + +static JSValue js_ctx_path(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + (void)argc; + (void)argv; + JsHttpTask* task = js_get_task(js); + if (task == NULL || !task->ctx) { + return JS_ThrowTypeError(js, "invalid HttpContext"); + } + std::string path = task->ctx->path(); + return JS_NewStringLen(js, path.data(), path.size()); +} + +static JSValue js_ctx_query(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + JsHttpTask* task = js_get_task(js); + if (task == NULL || !task->ctx) { + return JS_ThrowTypeError(js, "invalid HttpContext"); + } + std::string key = argc > 0 ? js_to_string(js, argv[0]) : std::string(); + std::string defvalue = argc > 1 ? js_to_string(js, argv[1]) : std::string(); + std::string value = task->ctx->param(key.c_str(), defvalue); + return JS_NewStringLen(js, value.data(), value.size()); +} + +static JSValue js_ctx_header(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + JsHttpTask* task = js_get_task(js); + if (task == NULL || !task->ctx) { + return JS_ThrowTypeError(js, "invalid HttpContext"); + } + std::string key = argc > 0 ? js_to_string(js, argv[0]) : std::string(); + std::string defvalue = argc > 1 ? js_to_string(js, argv[1]) : std::string(); + std::string value = task->ctx->header(key.c_str(), defvalue); + return JS_NewStringLen(js, value.data(), value.size()); +} + +static JSValue js_ctx_body(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + (void)argc; + (void)argv; + JsHttpTask* task = js_get_task(js); + if (task == NULL || !task->ctx) { + return JS_ThrowTypeError(js, "invalid HttpContext"); + } + std::string& body = task->ctx->body(); + return JS_NewStringLen(js, body.data(), body.size()); +} + +static JSValue js_ctx_status(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + JsHttpTask* task = js_get_task(js); + if (task == NULL || !task->ctx || argc < 1) { + return JS_ThrowTypeError(js, "invalid HttpContext"); + } + int32_t status = 0; + if (JS_ToInt32(js, &status, argv[0]) != 0) return JS_EXCEPTION; + task->ctx->response->status_code = (http_status)status; + return JS_NewInt32(js, status); +} + +static JSValue js_ctx_set_header(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + JsHttpTask* task = js_get_task(js); + if (task == NULL || !task->ctx || argc < 2) { + return JS_ThrowTypeError(js, "invalid HttpContext"); + } + std::string key = js_to_string(js, argv[0]); + std::string value = js_to_string(js, argv[1]); + task->ctx->setHeader(key.c_str(), value); + return JS_UNDEFINED; +} + +static JSValue js_ctx_text(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + JsHttpTask* task = js_get_task(js); + if (task == NULL || !task->ctx || argc < 1) { + return JS_ThrowTypeError(js, "invalid HttpContext"); + } + std::string text = js_to_string(js, argv[0]); + task->ctx->response->String(text); + return JS_NewInt32(js, task->ctx->response->status_code); +} + +static JSValue js_ctx_json(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + JsHttpTask* task = js_get_task(js); + if (task == NULL || !task->ctx || argc < 1) { + return JS_ThrowTypeError(js, "invalid HttpContext"); + } + JSValue json = JS_JSONStringify(js, argv[0], JS_UNDEFINED, JS_UNDEFINED); + if (JS_IsException(json)) return json; + std::string body = js_to_string(js, json); + JS_FreeValue(js, json); + task->ctx->response->SetContentType(APPLICATION_JSON); + task->ctx->response->body = body; + return JS_NewInt32(js, task->ctx->response->status_code); +} + +static JSValue js_new_ctx(JSContext* js, const HttpContextPtr& ctx) { + (void)ctx; + JSValue obj = JS_NewObject(js); + JS_SetPropertyStr(js, obj, "method", JS_NewCFunction(js, js_ctx_method, "method", 0)); + JS_SetPropertyStr(js, obj, "path", JS_NewCFunction(js, js_ctx_path, "path", 0)); + JS_SetPropertyStr(js, obj, "param", JS_NewCFunction(js, js_ctx_query, "param", 1)); + JS_SetPropertyStr(js, obj, "query", JS_NewCFunction(js, js_ctx_query, "query", 1)); + JS_SetPropertyStr(js, obj, "header", JS_NewCFunction(js, js_ctx_header, "header", 1)); + JS_SetPropertyStr(js, obj, "body", JS_NewCFunction(js, js_ctx_body, "body", 0)); + JS_SetPropertyStr(js, obj, "status", JS_NewCFunction(js, js_ctx_status, "status", 1)); + JS_SetPropertyStr(js, obj, "setHeader", JS_NewCFunction(js, js_ctx_set_header, "setHeader", 2)); + JS_SetPropertyStr(js, obj, "set_header", JS_NewCFunction(js, js_ctx_set_header, "set_header", 2)); + JS_SetPropertyStr(js, obj, "text", JS_NewCFunction(js, js_ctx_text, "text", 1)); + JS_SetPropertyStr(js, obj, "json", JS_NewCFunction(js, js_ctx_json, "json", 1)); + return obj; +} + +static void task_finish(JsHttpTask* task, JSValue result); + +static void drain_jobs(JsHttpTask* task) { + JSContext* job_ctx = NULL; + while (JS_IsJobPending(task->rt)) { + int rc = JS_ExecutePendingJob(task->rt, &job_ctx); + if (rc < 0) { + task->error = js_exception_string(job_ctx ? job_ctx : task->js); + break; + } + } + if (!task->finished && !JS_IsUndefined(task->promise)) { + JSPromiseStateEnum state = JS_PromiseState(task->js, task->promise); + if (state != JS_PROMISE_PENDING) { + JSValue value = JS_PromiseResult(task->js, task->promise); + task_finish(task, value); + return; + } + } + if (!task->error.empty()) { + task_finish(task, JS_UNDEFINED); + } +} + +static void sleep_timer_cb(htimer_t* timer) { + JsSleep* sleep = (JsSleep*)hevent_userdata(timer); + js_promise_resolve(sleep, JS_UNDEFINED); +} + +static JSValue js_hv_sleep(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + JsHttpTask* task = (JsHttpTask*)JS_GetContextOpaque(js); + if (task == NULL || argc < 1) return JS_EXCEPTION; + int32_t ms = 0; + if (JS_ToInt32(js, &ms, argv[0]) != 0) return JS_EXCEPTION; + JSValue funcs[2]; + JSValue promise = JS_NewPromiseCapability(js, funcs); + if (JS_IsException(promise)) return promise; + + JsSleep* sleep = new JsSleep(); + sleep->task = task; + sleep->resolve = funcs[0]; + JS_FreeValue(js, funcs[1]); + task_ref(task); + if (task->loop_ptr) { + sleep->timer_id = task->loop_ptr->setTimeout(ms, [sleep](TimerID) { js_promise_resolve(sleep, JS_UNDEFINED); }); + } + else { + sleep->timer = htimer_add(task->loop, sleep_timer_cb, (uint32_t)ms, 1); + if (sleep->timer) hevent_set_userdata(sleep->timer, sleep); + } + if (sleep->timer == NULL && sleep->timer_id == INVALID_TIMER_ID) { + task_unref(task); + JS_FreeValue(js, sleep->resolve); + delete sleep; + JS_FreeValue(js, promise); + return JS_ThrowInternalError(js, "hv.sleep: failed to create timer"); + } + return promise; +} + +static JSValue js_hv_version(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + (void)argc; + (void)argv; + return JS_NewString(js, HV_VERSION_STRING); +} + +static JSValue js_hv_log(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + std::string line; + for (int i = 0; i < argc; ++i) { + if (i != 0) line += "\\t"; + line += js_to_string(js, argv[i]); + } + hlogi("%s", line.c_str()); + return JS_UNDEFINED; +} +#ifdef HVJS_WITH_HTTP +struct JsHttpRequest : public JsPromiseOp { + std::shared_ptr client; +}; + +static JSValue js_push_headers(JSContext* js, const http_headers& headers) { + JSValue obj = JS_NewObject(js); + for (auto& kv : headers) { + JS_SetPropertyStr(js, obj, kv.first.c_str(), JS_NewStringLen(js, kv.second.data(), kv.second.size())); + } + return obj; +} + +static JSValue js_push_http_response(JSContext* js, const HttpResponsePtr& resp) { + JSValue obj = JS_NewObject(js); + JS_SetPropertyStr(js, obj, "status", JS_NewInt32(js, resp ? resp->status_code : 0)); + if (resp) { + JS_SetPropertyStr(js, obj, "body", JS_NewStringLen(js, resp->body.data(), resp->body.size())); + JS_SetPropertyStr(js, obj, "headers", js_push_headers(js, resp->headers)); + } + else { + JS_SetPropertyStr(js, obj, "body", JS_NewString(js, "")); + JS_SetPropertyStr(js, obj, "headers", JS_NewObject(js)); + } + return obj; +} + +static int js_fill_http_request(JSContext* js, JSValueConst* argv, int argc, http_method method, int url_index, HttpRequestPtr* out) { + if (argc <= url_index) { + JS_ThrowTypeError(js, "missing url"); + return -1; + } + std::string url = js_to_string(js, argv[url_index]); + auto req = std::make_shared(); + req->method = method; + req->url = url; + if (argc > url_index + 1 && !JS_IsUndefined(argv[url_index + 1]) && !JS_IsNull(argv[url_index + 1])) { + std::string body = js_to_string(js, argv[url_index + 1]); + req->body = body; + } + if (argc > url_index + 2 && JS_IsObject(argv[url_index + 2])) { + JSPropertyEnum* tab = NULL; + uint32_t len = 0; + if (JS_GetOwnPropertyNames(js, &tab, &len, argv[url_index + 2], JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) == 0) { + for (uint32_t i = 0; i < len; ++i) { + JSValue key = JS_AtomToString(js, tab[i].atom); + JSValue value = JS_GetProperty(js, argv[url_index + 2], tab[i].atom); + std::string k = js_to_string(js, key); + std::string v = js_to_string(js, value); + if (!k.empty()) req->headers[k] = v; + JS_FreeValue(js, value); + JS_FreeValue(js, key); + } + JS_FreePropertyEnum(js, tab, len); + } + } + *out = req; + return 0; +} + +static JSValue js_http_request(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv, int magic) { + (void)this_val; + JsHttpTask* task = js_get_task(js); + if (task == NULL || !task->loop_ptr) { + return js_rejected_promise(js, "hv.http: no shared event loop on this thread"); + } + http_method method = (http_method)magic; + int url_index = 0; + if (magic == JS_HTTP_METHOD_REQUEST) { + if (argc < 2) return js_rejected_promise(js, "hv.http: request needs method and url"); + std::string m = js_to_string(js, argv[0]); + toupper(m); + method = http_method_enum(m.c_str()); + url_index = 1; + } + if (method == HTTP_CUSTOM_METHOD) { + return js_rejected_promise(js, "hv.http: unsupported method"); + } + + HttpRequestPtr req; + if (js_fill_http_request(js, argv, argc, method, url_index, &req) != 0) { + return JS_EXCEPTION; + } + + JsHttpRequest* op = NULL; + JSValue promise = js_new_promise(js, task, &op); + if (JS_IsException(promise)) return promise; + op->client = std::make_shared(task->loop_ptr); + std::shared_ptr client = op->client; + task->in_call = true; + int ret = client->send(req, [op, client](const HttpResponsePtr& resp) { + if (op->task->loop_ptr) { + op->task->loop_ptr->queueInLoop([client]() {}); + } + JSContext* js = op->task->js; + if (resp) { + js_promise_resolve(op, js_push_http_response(js, resp)); + } + else { + js_promise_reject(op, "hv.http: request failed"); + } + }); + if (ret != 0) { + js_promise_reject(op, "hv.http: request failed"); + } + task->in_call = false; + js_finish_deferred_op(op); + return promise; +} + +static JSValue js_require_http(JSContext* js) { + JSValue http = JS_NewObject(js); + JS_SetPropertyStr(js, http, "request", JS_NewCFunctionMagic(js, js_http_request, "request", 2, JS_CFUNC_generic_magic, JS_HTTP_METHOD_REQUEST)); + JS_SetPropertyStr(js, http, "get", JS_NewCFunctionMagic(js, js_http_request, "get", 1, JS_CFUNC_generic_magic, HTTP_GET)); + JS_SetPropertyStr(js, http, "post", JS_NewCFunctionMagic(js, js_http_request, "post", 2, JS_CFUNC_generic_magic, HTTP_POST)); + JS_SetPropertyStr(js, http, "put", JS_NewCFunctionMagic(js, js_http_request, "put", 2, JS_CFUNC_generic_magic, HTTP_PUT)); + JS_SetPropertyStr(js, http, "delete", JS_NewCFunctionMagic(js, js_http_request, "delete", 1, JS_CFUNC_generic_magic, HTTP_DELETE)); + return http; +} +#endif +#ifdef HVJS_WITH_REDIS +static JSClassID s_redis_class_id; +static std::once_flag s_redis_class_once; + +struct JsRedisState { + std::shared_ptr client; + bool destroyed; + + JsRedisState() : destroyed(false) {} + + ~JsRedisState() { + destroyed = true; + if (client) { + client->stop(true); + client.reset(); + } + } +}; + +struct JsRedisClient { + std::shared_ptr state; +}; + +struct JsRedisCommand : public JsPromiseOp { + std::shared_ptr redis; +}; + +static void js_redis_finalizer(JSRuntime* rt, JSValue val) { + (void)rt; + JsRedisClient* box = (JsRedisClient*)JS_GetOpaque(val, s_redis_class_id); + if (box) { + delete box; + } +} + +static JsRedisClient* js_redis_client(JSContext* js, JSValueConst this_val) { + JsRedisClient* box = (JsRedisClient*)JS_GetOpaque2(js, this_val, s_redis_class_id); + return box; +} + +static void js_redis_register_class(JSContext* js) { + std::call_once(s_redis_class_once, []() { js_new_class_id(&s_redis_class_id); }); + JSRuntime* rt = JS_GetRuntime(js); + if (!JS_IsRegisteredClass(rt, s_redis_class_id)) { + JSClassDef def; + memset(&def, 0, sizeof(def)); + def.class_name = "hv.redis.client"; + def.finalizer = js_redis_finalizer; + JS_NewClass(rt, s_redis_class_id, &def); + } +} + +static JSValue js_push_redis_reply(JSContext* js, const RedisReply& reply) { + switch (reply.type) { + case REDIS_REPLY_STRING: return JS_NewStringLen(js, reply.str.data(), reply.str.size()); + case REDIS_REPLY_INTEGER: return JS_NewInt64(js, reply.integer); + case REDIS_REPLY_ARRAY: { + if (reply.null_array) return JS_NULL; + JSValue arr = JS_NewArray(js); + for (uint32_t i = 0; i < reply.elements.size(); ++i) { + JSValue item = reply.elements[i].isNil() ? JS_NULL : js_push_redis_reply(js, reply.elements[i]); + JS_SetPropertyUint32(js, arr, i, item); + } + return arr; + } + case REDIS_REPLY_NIL: + default: return JS_NULL; + } +} + +static void js_redis_resolve_result(JsRedisCommand* op, const RedisResult& result) { + JSContext* js = op->task->js; + if (!op->redis || op->redis->destroyed) { + js_promise_reject(op, "hv.redis: client closed"); + return; + } + if (result.code != 0) { + char err[64]; + snprintf(err, sizeof(err), "hv.redis: request failed (%d)", result.code); + js_promise_reject(op, err); + return; + } + if (result.reply.isError()) { + js_promise_reject(op, result.reply.error().c_str()); + return; + } + js_promise_resolve(op, js_push_redis_reply(js, result.reply)); +} + +static bool js_build_redis_command(JSContext* js, JSValueConst* argv, int argc, int first, RedisCommand* cmd) { + if (argc <= first) return false; + if (JS_IsArray(js, argv[first]) && argc == first + 1) { + JSValue lenv = JS_GetPropertyStr(js, argv[first], "length"); + uint32_t len = 0; + JS_ToUint32(js, &len, lenv); + JS_FreeValue(js, lenv); + for (uint32_t i = 0; i < len; ++i) { + JSValue item = JS_GetPropertyUint32(js, argv[first], i); + cmd->push_back(js_to_string(js, item)); + JS_FreeValue(js, item); + } + } + else { + for (int i = first; i < argc; ++i) { + cmd->push_back(js_to_string(js, argv[i])); + } + } + return !cmd->empty(); +} + +static const char* js_redis_verb_name(int magic) { + switch (magic) { + case 1: return "GET"; + case 2: return "SET"; + case 3: return "DEL"; + case 4: return "INCR"; + case 5: return "DECR"; + case 6: return "EXPIRE"; + case 7: return "EXISTS"; + default: return NULL; + } +} + +static JSValue js_redis_command(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv, int magic) { + JsRedisClient* box = js_redis_client(js, this_val); + JsRedisState* state = box ? box->state.get() : NULL; + if (state == NULL || !state->client || state->destroyed) { + return js_rejected_promise(js, "hv.redis: client closed"); + } + RedisCommand cmd; + if (magic != 0) { + const char* verb = js_redis_verb_name(magic); + if (verb == NULL) { + return js_rejected_promise(js, "hv.redis: unknown command"); + } + cmd.push_back(verb); + for (int i = 0; i < argc; ++i) { + cmd.push_back(js_to_string(js, argv[i])); + } + } + else if (!js_build_redis_command(js, argv, argc, 0, &cmd)) { + return js_rejected_promise(js, "hv.redis: empty or invalid command"); + } + + JsHttpTask* task = js_get_task(js); + JsRedisCommand* op = NULL; + JSValue promise = js_new_promise(js, task, &op); + if (JS_IsException(promise)) return promise; + op->redis = box->state; + task->in_call = true; + int ret = state->client->command(cmd, [op](const RedisResult& result) { js_redis_resolve_result(op, result); }); + if (ret != 0) { + js_promise_reject(op, "hv.redis: request failed"); + } + task->in_call = false; + js_finish_deferred_op(op); + return promise; +} + +static JSValue js_redis_new(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + JsHttpTask* task = js_get_task(js); + if (task == NULL || !task->loop_ptr) { + return JS_ThrowTypeError(js, "hv.redis: no shared event loop on this thread"); + } + js_redis_register_class(js); + + std::string host = "127.0.0.1"; + int port = 6379; + std::string auth; + int db = 0; + int timeout = 0; + if (argc > 0 && JS_IsObject(argv[0])) { + host = js_get_string_property(js, argv[0], "host", "127.0.0.1"); + port = js_get_int_property(js, argv[0], "port", 6379); + auth = js_get_string_property(js, argv[0], "auth", ""); + db = js_get_int_property(js, argv[0], "db", 0); + timeout = js_get_int_property(js, argv[0], "timeout", 0); + } + + JSValue obj = JS_NewObjectClass(js, s_redis_class_id); + if (JS_IsException(obj)) return obj; + JsRedisClient* box = new JsRedisClient(); + box->state = std::make_shared(); + box->state->client = std::make_shared(task->loop_ptr); + box->state->client->setHost(host); + box->state->client->setPort(port); + if (!auth.empty()) box->state->client->setAuth(auth); + if (db > 0) box->state->client->setDb(db); + if (timeout > 0) box->state->client->setTimeout(timeout); + box->state->client->start(false); + JS_SetOpaque(obj, box); + + JS_SetPropertyStr(js, obj, "command", JS_NewCFunctionMagic(js, js_redis_command, "command", 1, JS_CFUNC_generic_magic, 0)); + static const char* verbs[] = {"GET", "SET", "DEL", "INCR", "DECR", "EXPIRE", "EXISTS", NULL}; + for (int i = 0; verbs[i]; ++i) { + std::string name = verbs[i]; + for (char& c : name) c = (char)::tolower((unsigned char)c); + JS_SetPropertyStr(js, obj, name.c_str(), JS_NewCFunctionMagic(js, js_redis_command, name.c_str(), 1, JS_CFUNC_generic_magic, i + 1)); + } + return obj; +} + +static JSValue js_require_redis(JSContext* js) { + JSValue redis = JS_NewObject(js); + JS_SetPropertyStr(js, redis, "new", JS_NewCFunction(js, js_redis_new, "new", 1)); + return redis; +} +#endif +#ifdef HVJS_WITH_HTTP +static JSClassID s_ws_class_id; +static std::once_flag s_ws_class_once; + +struct JsWsState { + std::shared_ptr client; + std::deque inbox; + JsPromiseOp* connect_op; + JsPromiseOp* recv_op; + bool js_alive; + bool connected; + bool closed; + + JsWsState() : connect_op(NULL), recv_op(NULL), js_alive(false), connected(false), closed(false) {} + + void detach() { + closed = true; + connected = false; + if (client) { + client->onopen = NULL; + client->onmessage = NULL; + client->onclose = NULL; + client->close(); + client.reset(); + } + } + + ~JsWsState() { detach(); } +}; + +struct JsWsClient { + std::shared_ptr state; +}; + +struct JsWsConnect : public JsPromiseOp { + std::shared_ptr state; +}; + +struct JsWsRecv : public JsPromiseOp { + std::shared_ptr state; +}; + +static JsWsClient* js_ws_client(JSContext* js, JSValueConst this_val) { + return (JsWsClient*)JS_GetOpaque2(js, this_val, s_ws_class_id); +} + +static void js_ws_detach_after_callback(const EventLoopPtr& loop, const std::shared_ptr& state) { + if (!state) return; + if (loop) { + loop->queueInLoop([state]() { state->detach(); }); + } + else { + state->detach(); + } +} + +static void js_ws_finalizer(JSRuntime* rt, JSValue val) { + (void)rt; + JsWsClient* box = (JsWsClient*)JS_GetOpaque(val, s_ws_class_id); + if (box && box->state) { + box->state->js_alive = false; + if (box->state->connect_op == NULL && box->state->recv_op == NULL) { + box->state->detach(); + } + } + delete box; +} + +static void js_ws_register_class(JSContext* js) { + std::call_once(s_ws_class_once, []() { js_new_class_id(&s_ws_class_id); }); + JSRuntime* rt = JS_GetRuntime(js); + if (!JS_IsRegisteredClass(rt, s_ws_class_id)) { + JSClassDef def; + memset(&def, 0, sizeof(def)); + def.class_name = "hv.ws.client"; + def.finalizer = js_ws_finalizer; + JS_NewClass(rt, s_ws_class_id, &def); + } +} + +static void js_ws_try_deliver(const std::shared_ptr& state) { + if (!state || state->recv_op == NULL) return; + JsWsRecv* op = static_cast(state->recv_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + if (!state->inbox.empty()) { + std::string msg = std::move(state->inbox.front()); + state->inbox.pop_front(); + state->recv_op = NULL; + js_promise_resolve(op, JS_NewStringLen(op->task->js, msg.data(), msg.size())); + } + else if (state->closed) { + state->recv_op = NULL; + js_promise_reject(op, "closed"); + } + if (!hold->js_alive && hold->connect_op == NULL && hold->recv_op == NULL) { + js_ws_detach_after_callback(loop, hold); + } +} + +static JSValue js_ws_send(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + JsWsClient* box = js_ws_client(js, this_val); + JsWsState* state = box ? box->state.get() : NULL; + if (state == NULL || !state->client || !state->connected) { + return JS_ThrowTypeError(js, "hv.ws: closed"); + } + std::string msg = argc > 0 ? js_to_string(js, argv[0]) : std::string(); + enum ws_opcode opcode = WS_OPCODE_TEXT; + if (argc > 1 && js_to_string(js, argv[1]) == "binary") { + opcode = WS_OPCODE_BINARY; + } + int ret = state->client->send(msg.data(), (int)msg.size(), opcode); + if (ret < 0) { + return JS_ThrowInternalError(js, "hv.ws: send failed"); + } + return JS_NewInt32(js, ret); +} + +static JSValue js_ws_recv(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)argc; + (void)argv; + JsWsClient* box = js_ws_client(js, this_val); + JsWsState* state = box ? box->state.get() : NULL; + if (state == NULL || !state->client) { + return js_rejected_promise(js, "closed"); + } + if (!state->inbox.empty()) { + std::string msg = std::move(state->inbox.front()); + state->inbox.pop_front(); + return js_async_resolved_promise(js, js_get_task(js), JS_NewStringLen(js, msg.data(), msg.size())); + } + if (state->closed || !state->connected) { + return js_rejected_promise(js, "closed"); + } + if (state->recv_op != NULL) { + return js_rejected_promise(js, "hv.ws: recv already pending"); + } + JsHttpTask* task = js_get_task(js); + JsWsRecv* op = NULL; + JSValue promise = js_new_promise(js, task, &op); + if (JS_IsException(promise)) return promise; + op->state = box->state; + state->recv_op = op; + return promise; +} + +static JSValue js_ws_close(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)argc; + (void)argv; + JsWsClient* box = js_ws_client(js, this_val); + if (box && box->state) { + std::shared_ptr state = box->state; + if (state->connect_op) { + JsPromiseOp* op = state->connect_op; + state->connect_op = NULL; + js_promise_reject(op, "closed"); + } + if (state->recv_op) { + JsPromiseOp* op = state->recv_op; + state->recv_op = NULL; + js_promise_reject(op, "closed"); + } + state->detach(); + } + return JS_UNDEFINED; +} + +static JSValue js_ws_new_client_object(JSContext* js, const std::shared_ptr& state) { + JSValue obj = JS_NewObjectClass(js, s_ws_class_id); + if (JS_IsException(obj)) return obj; + JsWsClient* box = new JsWsClient(); + box->state = state; + state->js_alive = true; + JS_SetOpaque(obj, box); + JS_SetPropertyStr(js, obj, "send", JS_NewCFunction(js, js_ws_send, "send", 1)); + JS_SetPropertyStr(js, obj, "recv", JS_NewCFunction(js, js_ws_recv, "recv", 0)); + JS_SetPropertyStr(js, obj, "close", JS_NewCFunction(js, js_ws_close, "close", 0)); + return obj; +} + +static JSValue js_ws_connect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + JsHttpTask* task = js_get_task(js); + if (task == NULL || !task->loop_ptr) { + return js_rejected_promise(js, "hv.ws: no shared event loop on this thread"); + } + if (argc < 1) { + return js_rejected_promise(js, "hv.ws: connect needs url"); + } + std::string url = js_to_string(js, argv[0]); + js_ws_register_class(js); + std::shared_ptr state = std::make_shared(); + state->client = std::make_shared(task->loop_ptr); + + JsWsConnect* op = NULL; + JSValue promise = js_new_promise(js, task, &op); + if (JS_IsException(promise)) return promise; + state->connect_op = op; + op->state = state; + state->client->onopen = [state]() { + state->connected = true; + state->closed = false; + if (state->connect_op) { + JsWsConnect* op = static_cast(state->connect_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + state->connect_op = NULL; + JSValue obj = js_ws_new_client_object(op->task->js, hold); + if (JS_IsException(obj)) { + js_promise_reject(op, "hv.ws: create client failed"); + js_ws_detach_after_callback(loop, hold); + } + else { + js_promise_resolve(op, obj); + } + } + }; + state->client->onmessage = [state](const std::string& msg) { + state->inbox.push_back(msg); + js_ws_try_deliver(state); + }; + state->client->onclose = [state]() { + state->connected = false; + state->closed = true; + if (state->connect_op) { + JsWsConnect* op = static_cast(state->connect_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + state->connect_op = NULL; + js_promise_reject(op, "closed"); + js_ws_detach_after_callback(loop, hold); + } + js_ws_try_deliver(state); + }; + task->in_call = true; + int ret = state->client->open(url.c_str()); + if (ret != 0) { + state->connect_op = NULL; + js_promise_reject(op, "hv.ws: open failed"); + state->detach(); + } + task->in_call = false; + js_finish_deferred_op(op); + return promise; +} + +static JSValue js_require_ws(JSContext* js) { + JSValue ws = JS_NewObject(js); + JS_SetPropertyStr(js, ws, "connect", JS_NewCFunction(js, js_ws_connect, "connect", 1)); + return ws; +} +#endif +#ifdef HVJS_WITH_MQTT +static JSClassID s_mqtt_class_id; +static std::once_flag s_mqtt_class_once; + +struct JsMqttMessage { + std::string topic; + std::string payload; + int qos; +}; + +struct JsMqttState { + mqtt_client_t* client; + std::deque inbox; + JsPromiseOp* connect_op; + JsPromiseOp* recv_op; + bool js_alive; + bool closed; + bool reconnect; + + JsMqttState() : client(NULL), connect_op(NULL), recv_op(NULL), js_alive(false), closed(false), reconnect(false) {} + + void detach() { + closed = true; + if (client) { + mqtt_client_set_callback(client, NULL); + mqtt_client_set_userdata(client, NULL); + mqtt_client_free(client); + client = NULL; + } + } + + ~JsMqttState() { detach(); } +}; + +struct JsMqttClient { + std::shared_ptr state; +}; + +struct JsMqttConnect : public JsPromiseOp { + std::shared_ptr state; +}; + +struct JsMqttRecv : public JsPromiseOp { + std::shared_ptr state; +}; + +struct JsMqttDetachEvent { + std::shared_ptr state; +}; + +static JsMqttClient* js_mqtt_client(JSContext* js, JSValueConst this_val) { + return (JsMqttClient*)JS_GetOpaque2(js, this_val, s_mqtt_class_id); +} + +static JSValue js_mqtt_recv(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); +static JSValue js_mqtt_publish(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); +static JSValue js_mqtt_subscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); +static JSValue js_mqtt_unsubscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); +static JSValue js_mqtt_disconnect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); +static void js_mqtt_detach_after_callback(const EventLoopPtr& loop, hloop_t* raw_loop, const std::shared_ptr& state); + +static void js_mqtt_finalizer(JSRuntime* rt, JSValue val) { + (void)rt; + JsMqttClient* box = (JsMqttClient*)JS_GetOpaque(val, s_mqtt_class_id); + if (box && box->state) { + box->state->js_alive = false; + if (box->state->connect_op == NULL && box->state->recv_op == NULL) { + box->state->detach(); + } + } + delete box; +} + +static void js_mqtt_register_class(JSContext* js) { + std::call_once(s_mqtt_class_once, []() { js_new_class_id(&s_mqtt_class_id); }); + JSRuntime* rt = JS_GetRuntime(js); + if (!JS_IsRegisteredClass(rt, s_mqtt_class_id)) { + JSClassDef def; + memset(&def, 0, sizeof(def)); + def.class_name = "hv.mqtt.client"; + def.finalizer = js_mqtt_finalizer; + JS_NewClass(rt, s_mqtt_class_id, &def); + } +} + +static JSValue js_mqtt_new_client_object(JSContext* js, const std::shared_ptr& state) { + JSValue obj = JS_NewObjectClass(js, s_mqtt_class_id); + if (JS_IsException(obj)) return obj; + JsMqttClient* box = new JsMqttClient(); + box->state = state; + state->js_alive = true; + JS_SetOpaque(obj, box); + JS_SetPropertyStr(js, obj, "recv", JS_NewCFunction(js, js_mqtt_recv, "recv", 0)); + JS_SetPropertyStr(js, obj, "publish", JS_NewCFunction(js, js_mqtt_publish, "publish", 2)); + JS_SetPropertyStr(js, obj, "subscribe", JS_NewCFunction(js, js_mqtt_subscribe, "subscribe", 1)); + JS_SetPropertyStr(js, obj, "unsubscribe", JS_NewCFunction(js, js_mqtt_unsubscribe, "unsubscribe", 1)); + JS_SetPropertyStr(js, obj, "disconnect", JS_NewCFunction(js, js_mqtt_disconnect, "disconnect", 0)); + return obj; +} + +static JSValue js_push_mqtt_message(JSContext* js, const JsMqttMessage& msg) { + JSValue obj = JS_NewObject(js); + JS_SetPropertyStr(js, obj, "topic", JS_NewStringLen(js, msg.topic.data(), msg.topic.size())); + JS_SetPropertyStr(js, obj, "payload", JS_NewStringLen(js, msg.payload.data(), msg.payload.size())); + JS_SetPropertyStr(js, obj, "qos", JS_NewInt32(js, msg.qos)); + return obj; +} + +static const char* js_mqtt_closed_reason(const JsMqttState* state) { + return state && state->reconnect ? "reconnecting" : "closed"; +} + +static void js_mqtt_try_deliver(JsMqttState* state) { + if (!state || state->recv_op == NULL) return; + JsMqttRecv* op = static_cast(state->recv_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + hloop_t* raw_loop = op->task ? op->task->loop : NULL; + if (!state->inbox.empty()) { + JsMqttMessage msg = std::move(state->inbox.front()); + state->inbox.pop_front(); + state->recv_op = NULL; + js_promise_resolve(op, js_push_mqtt_message(op->task->js, msg)); + } + else if (state->closed) { + state->recv_op = NULL; + js_promise_reject(op, js_mqtt_closed_reason(state)); + } + if (!hold->js_alive && hold->connect_op == NULL && hold->recv_op == NULL) { + js_mqtt_detach_after_callback(loop, raw_loop, hold); + } +} + +static void js_mqtt_detach_event_cb(hevent_t* ev) { + JsMqttDetachEvent* detach = (JsMqttDetachEvent*)hevent_userdata(ev); + if (detach) { + detach->state->detach(); + delete detach; + } +} + +static void js_mqtt_detach_after_callback(const EventLoopPtr& loop, hloop_t* raw_loop, const std::shared_ptr& state) { + if (!state) return; + state->reconnect = false; + if (state->client) { + mqtt_client_set_reconnect(state->client, NULL); + } + if (loop) { + loop->queueInLoop([state]() { state->detach(); }); + } + else if (raw_loop) { + JsMqttDetachEvent* detach = new JsMqttDetachEvent(); + detach->state = state; + hevent_t ev; + memset(&ev, 0, sizeof(ev)); + ev.cb = js_mqtt_detach_event_cb; + ev.userdata = detach; + hloop_post_event(raw_loop, &ev); + } + else { + state->detach(); + } +} + +static void js_mqtt_on_event(mqtt_client_t* client, int type) { + JsMqttState* state = (JsMqttState*)mqtt_client_get_userdata(client); + if (state == NULL) return; + switch (type) { + case MQTT_TYPE_CONNACK: + state->closed = false; + if (state->connect_op) { + JsMqttConnect* op = static_cast(state->connect_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + hloop_t* raw_loop = op->task ? op->task->loop : NULL; + state->connect_op = NULL; + JSValue obj = js_mqtt_new_client_object(op->task->js, hold); + if (JS_IsException(obj)) { + js_promise_reject(op, "hv.mqtt: create client failed"); + js_mqtt_detach_after_callback(loop, raw_loop, hold); + } + else { + js_promise_resolve(op, obj); + } + } + break; + case MQTT_TYPE_PUBLISH: { + JsMqttMessage msg; + if (client->message.topic && client->message.topic_len > 0) { + msg.topic.assign(client->message.topic, client->message.topic_len); + } + if (client->message.payload && client->message.payload_len > 0) { + msg.payload.assign(client->message.payload, client->message.payload_len); + } + msg.qos = client->message.qos; + state->inbox.push_back(std::move(msg)); + js_mqtt_try_deliver(state); + break; + } + case MQTT_TYPE_DISCONNECT: + state->closed = true; + if (state->connect_op) { + JsMqttConnect* op = static_cast(state->connect_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + hloop_t* raw_loop = op->task ? op->task->loop : NULL; + state->connect_op = NULL; + js_promise_reject(op, "connect failed"); + js_mqtt_detach_after_callback(loop, raw_loop, hold); + } + if (state->recv_op) { + JsMqttRecv* op = static_cast(state->recv_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + hloop_t* raw_loop = op->task ? op->task->loop : NULL; + state->recv_op = NULL; + js_promise_reject(op, js_mqtt_closed_reason(state)); + if (!hold->js_alive && hold->connect_op == NULL && hold->recv_op == NULL) { + js_mqtt_detach_after_callback(loop, raw_loop, hold); + } + } + break; + default: break; + } +} + +static bool js_parse_reconnect(JSContext* js, JSValueConst obj, reconn_setting_t* out) { + JSValue reconnect; + if (!js_get_property(js, obj, "reconnect", &reconnect) || !JS_IsObject(reconnect)) { + if (!JS_IsUndefined(reconnect) && !JS_IsException(reconnect)) JS_FreeValue(js, reconnect); + return false; + } + reconn_setting_init(out); + out->min_delay = (uint32_t)js_get_int_property(js, reconnect, "min_delay", (int)out->min_delay); + out->max_delay = (uint32_t)js_get_int_property(js, reconnect, "max_delay", (int)out->max_delay); + out->delay_policy = (uint32_t)js_get_int_property(js, reconnect, "delay_policy", (int)out->delay_policy); + out->max_retry_cnt = (uint32_t)js_get_int_property(js, reconnect, "max_retry", (int)out->max_retry_cnt); + if (out->max_retry_cnt == 0) out->max_retry_cnt = INFINITE; + if (out->min_delay == 0) out->min_delay = 1; + if (out->max_delay < out->min_delay) out->max_delay = out->min_delay; + if (out->delay_policy > 1 && out->delay_policy > UINT32_MAX / out->min_delay) { + out->delay_policy = DEFAULT_RECONNECT_DELAY_POLICY; + } + JS_FreeValue(js, reconnect); + return true; +} + +static JSValue js_mqtt_connect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + JsHttpTask* task = js_get_task(js); + if (task == NULL || task->loop == NULL) { + return js_rejected_promise(js, "hv.mqtt: no event loop on this thread"); + } + if (argc < 1 || !JS_IsObject(argv[0])) { + return js_rejected_promise(js, "hv.mqtt: connect needs options"); + } + + std::string host = js_get_string_property(js, argv[0], "host", "127.0.0.1"); + int port = js_get_int_property(js, argv[0], "port", DEFAULT_MQTT_PORT); + int ssl = js_get_bool_property(js, argv[0], "ssl", false) ? 1 : 0; + std::string id = js_get_string_property(js, argv[0], "id", ""); + std::string username = js_get_string_property(js, argv[0], "username", ""); + std::string password = js_get_string_property(js, argv[0], "password", ""); + int keepalive = js_get_int_property(js, argv[0], "keepalive", 0); + int timeout = js_get_int_property(js, argv[0], "connect_timeout", 0); + if (timeout <= 0) timeout = js_get_int_property(js, argv[0], "timeout", 0); + bool clean_session = js_get_bool_property(js, argv[0], "clean_session", true); + + js_mqtt_register_class(js); + std::shared_ptr state = std::make_shared(); + state->client = mqtt_client_new(task->loop); + if (state->client == NULL) { + return js_rejected_promise(js, "hv.mqtt: create client failed"); + } + mqtt_client_set_userdata(state->client, state.get()); + mqtt_client_set_callback(state->client, js_mqtt_on_event); + if (!id.empty()) mqtt_client_set_id(state->client, id.c_str()); + if (!username.empty() || !password.empty()) { + mqtt_client_set_auth(state->client, username.c_str(), password.c_str()); + } + if (keepalive > 0) state->client->keepalive = (unsigned short)keepalive; + state->client->clean_session = clean_session ? 1 : 0; + if (timeout > 0) mqtt_client_set_connect_timeout(state->client, timeout); + reconn_setting_t reconn; + if (js_parse_reconnect(js, argv[0], &reconn)) { + mqtt_client_set_reconnect(state->client, &reconn); + state->reconnect = true; + } + + JsMqttConnect* op = NULL; + JSValue promise = js_new_promise(js, task, &op); + if (JS_IsException(promise)) { + state->detach(); + return promise; + } + state->connect_op = op; + op->state = state; + task->in_call = true; + int ret = mqtt_client_connect(state->client, host.c_str(), port, ssl); + if (ret != 0) { + state->connect_op = NULL; + js_promise_reject(op, "hv.mqtt: connect failed"); + state->detach(); + } + task->in_call = false; + js_finish_deferred_op(op); + return promise; +} + +static JSValue js_mqtt_recv(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)argc; + (void)argv; + JsMqttClient* box = js_mqtt_client(js, this_val); + std::shared_ptr state = box ? box->state : std::shared_ptr(); + if (!state || state->client == NULL) { + return js_rejected_promise(js, "closed"); + } + if (!state->inbox.empty()) { + JsMqttMessage msg = std::move(state->inbox.front()); + state->inbox.pop_front(); + return js_async_resolved_promise(js, js_get_task(js), js_push_mqtt_message(js, msg)); + } + if (state->closed) { + return js_rejected_promise(js, js_mqtt_closed_reason(state.get())); + } + if (state->recv_op != NULL) { + return js_rejected_promise(js, "hv.mqtt: recv already pending"); + } + JsHttpTask* task = js_get_task(js); + JsMqttRecv* op = NULL; + JSValue promise = js_new_promise(js, task, &op); + if (JS_IsException(promise)) return promise; + op->state = state; + state->recv_op = op; + return promise; +} + +static JSValue js_mqtt_publish(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + JsMqttClient* box = js_mqtt_client(js, this_val); + JsMqttState* state = box ? box->state.get() : NULL; + if (state == NULL || state->client == NULL || state->closed) { + return JS_ThrowTypeError(js, "hv.mqtt: closed"); + } + if (argc < 2) { + return JS_ThrowTypeError(js, "hv.mqtt: publish needs topic and payload"); + } + std::string topic = js_to_string(js, argv[0]); + std::string payload = js_to_string(js, argv[1]); + int32_t qos = 0; + if (argc > 2 && JS_ToInt32(js, &qos, argv[2]) != 0) return JS_EXCEPTION; + int retain = argc > 3 ? JS_ToBool(js, argv[3]) : 0; + mqtt_message_t msg; + memset(&msg, 0, sizeof(msg)); + msg.topic = topic.c_str(); + msg.topic_len = (unsigned int)topic.size(); + msg.payload = payload.c_str(); + msg.payload_len = (unsigned int)payload.size(); + msg.qos = (unsigned char)qos; + msg.retain = (unsigned char)retain; + int mid = mqtt_client_publish(state->client, &msg); + if (mid < 0) { + return JS_ThrowInternalError(js, "hv.mqtt: publish failed"); + } + return JS_NewInt32(js, mid); +} + +static JSValue js_mqtt_subscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + JsMqttClient* box = js_mqtt_client(js, this_val); + JsMqttState* state = box ? box->state.get() : NULL; + if (state == NULL || state->client == NULL || state->closed) { + return JS_ThrowTypeError(js, "hv.mqtt: closed"); + } + if (argc < 1) { + return JS_ThrowTypeError(js, "hv.mqtt: subscribe needs topic"); + } + std::string topic = js_to_string(js, argv[0]); + int32_t qos = 0; + if (argc > 1 && JS_ToInt32(js, &qos, argv[1]) != 0) return JS_EXCEPTION; + int mid = mqtt_client_subscribe(state->client, topic.c_str(), qos); + if (mid < 0) { + return JS_ThrowInternalError(js, "hv.mqtt: subscribe failed"); + } + return JS_NewInt32(js, mid); +} + +static JSValue js_mqtt_unsubscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + JsMqttClient* box = js_mqtt_client(js, this_val); + JsMqttState* state = box ? box->state.get() : NULL; + if (state == NULL || state->client == NULL || state->closed) { + return JS_ThrowTypeError(js, "hv.mqtt: closed"); + } + if (argc < 1) { + return JS_ThrowTypeError(js, "hv.mqtt: unsubscribe needs topic"); + } + std::string topic = js_to_string(js, argv[0]); + int mid = mqtt_client_unsubscribe(state->client, topic.c_str()); + if (mid < 0) { + return JS_ThrowInternalError(js, "hv.mqtt: unsubscribe failed"); + } + return JS_NewInt32(js, mid); +} + +static JSValue js_mqtt_disconnect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)argc; + (void)argv; + JsMqttClient* box = js_mqtt_client(js, this_val); + if (box && box->state) { + std::shared_ptr state = box->state; + state->reconnect = false; + if (state->connect_op) { + JsPromiseOp* op = state->connect_op; + state->connect_op = NULL; + js_promise_reject(op, "closed"); + } + if (state->recv_op) { + JsPromiseOp* op = state->recv_op; + state->recv_op = NULL; + js_promise_reject(op, "closed"); + } + state->detach(); + } + return JS_UNDEFINED; +} + +static JSValue js_require_mqtt(JSContext* js) { + JSValue mqtt = JS_NewObject(js); + JS_SetPropertyStr(js, mqtt, "connect", JS_NewCFunction(js, js_mqtt_connect, "connect", 1)); + return mqtt; +} +#endif + +static JSValue js_require(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + if (argc < 1) { + return JS_ThrowTypeError(js, "require needs a module name"); + } + std::string name = js_to_string(js, argv[0]); + if (name == "hv") { + JSValue hv = JS_NewObject(js); + JS_SetPropertyStr(js, hv, "version", JS_NewCFunction(js, js_hv_version, "version", 0)); + JS_SetPropertyStr(js, hv, "log", JS_NewCFunction(js, js_hv_log, "log", 1)); + JS_SetPropertyStr(js, hv, "sleep", JS_NewCFunction(js, js_hv_sleep, "sleep", 1)); + return hv; + } +#ifdef HVJS_WITH_HTTP + if (name == "hv/http") { + return js_require_http(js); + } +#endif +#ifdef HVJS_WITH_REDIS + if (name == "hv/redis") { + return js_require_redis(js); + } +#endif +#ifdef HVJS_WITH_HTTP + if (name == "hv/ws") { + return js_require_ws(js); + } +#endif +#ifdef HVJS_WITH_MQTT + if (name == "hv/mqtt") { + return js_require_mqtt(js); + } +#endif + return JS_ThrowReferenceError(js, "module '%s' is not available", name.c_str()); +} + +static bool load_file(const std::string& filepath, std::string* out, std::string* err) { + HFile file; + if (file.open(filepath.c_str(), "rb") != 0) { + if (err) *err = strerror(errno); + return false; + } + size_t size = hv_filesize(filepath.c_str()); + out->resize(size); + if (size > 0) { + int nread = file.read(&(*out)[0], (int)size); + if (nread < 0 || (size_t)nread != size) { + if (err) *err = "read script failed"; + return false; + } + } + return true; +} + +static time_t file_mtime(const std::string& filepath) { + struct stat st; + if (stat(filepath.c_str(), &st) != 0) { + return 0; + } + return st.st_mtime; +} + +static bool push_handler_fn(JSContext* js, JSValueConst global, http_method method, JSValue* fn) { + std::string name = http_method_str(method); + tolower(name); + *fn = JS_GetPropertyStr(js, global, name.c_str()); + if (JS_IsFunction(js, *fn)) return true; + JS_FreeValue(js, *fn); + *fn = JS_GetPropertyStr(js, global, "handle"); + if (JS_IsFunction(js, *fn)) return true; + JS_FreeValue(js, *fn); + *fn = JS_UNDEFINED; + return false; +} + +static bool apply_result(JSContext* js, JSValueConst value, const HttpContextPtr& ctx, std::string* err) { + if (JS_IsUndefined(value) || JS_IsNull(value)) { + return true; + } + if (JS_IsNumber(value)) { + int32_t status = 0; + if (JS_ToInt32(js, &status, value) == 0 && ctx->response->status_code == HTTP_STATUS_OK) { + ctx->response->status_code = (http_status)status; + } + return true; + } + if (JS_IsString(value)) { + std::string body = js_to_string(js, value); + ctx->response->String(body); + return true; + } + JSValue json = JS_JSONStringify(js, value, JS_UNDEFINED, JS_UNDEFINED); + if (!JS_IsException(json)) { + std::string body = js_to_string(js, json); + ctx->response->SetContentType(APPLICATION_JSON); + ctx->response->body = body; + JS_FreeValue(js, json); + return true; + } + if (err) *err = js_exception_string(js); + return false; +} + +static void task_finish(JsHttpTask* task, JSValue result) { + if (task->finished) return; + task->finished = true; + if (!task->error.empty()) { + hloge("[js] http handler error: %s", task->error.c_str()); + task->ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + task->ctx->response->String(task->error); + } + else if (!JS_IsUndefined(task->promise) && JS_PromiseState(task->js, task->promise) == JS_PROMISE_REJECTED) { + std::string err = js_to_string(task->js, result); + hloge("[js] http handler rejected: %s", err.c_str()); + task->ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + task->ctx->response->String(err); + } + else { + std::string err; + if (!apply_result(task->js, result, task->ctx, &err)) { + hloge("[js] http handler error: %s", err.c_str()); + task->ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + task->ctx->response->String(err); + } + } + JS_FreeValue(task->js, result); + if (task->async) { + task->ctx->send(); + } + task_unref(task); +} + +} // namespace + +struct HttpJsHandler::State { + std::mutex mutex; + std::string code; + time_t mtime; + bool loaded; + + State() : mtime(0), loaded(false) {} +}; + +HttpJsHandler::HttpJsHandler(const char* filepath, const HttpJsHandlerOptions& options) + : filepath_(filepath ? filepath : ""), options_(options), state_(std::make_shared()) {} + +bool HttpJsHandler::loadScript(std::string* code, std::string* err) { + std::lock_guard lock(state_->mutex); + if (state_->loaded && !options_.reload_on_change) { + if (code) *code = state_->code; + return true; + } + + time_t mtime = file_mtime(filepath_); + if (mtime == 0) { + if (err) *err = strerror(errno); + return false; + } + if (state_->loaded && state_->mtime == mtime) { + if (code) *code = state_->code; + return true; + } + + std::string latest; + if (!load_file(filepath_, &latest, err)) { + return false; + } + state_->code = latest; + state_->mtime = mtime; + state_->loaded = true; + if (code) *code = state_->code; + return true; +} + +int HttpJsHandler::operator()(const HttpContextPtr& ctx) { + if (!ctx || !ctx->request || !ctx->response) { + if (ctx && ctx->response) { + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + ctx->response->String("js handler: invalid http context"); + } + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + + std::string code, err; + if (!loadScript(&code, &err)) { + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + ctx->response->String(err); + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + + JsHttpTask* task = new JsHttpTask(); + task->ctx = ctx; + task->rt = JS_NewRuntime(); + task->js = task->rt ? JS_NewContext(task->rt) : NULL; + if (task->rt == NULL || task->js == NULL) { + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + ctx->response->String("js handler: failed to create quickjs runtime"); + task_unref(task); + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + JS_SetContextOpaque(task->js, task); + if (ctx->writer && ctx->writer->io()) { + task->loop = hevent_loop(ctx->writer->io()); + } + task->loop_ptr = currentThreadEventLoopPtr; + if (task->loop == NULL && task->loop_ptr) { + task->loop = task->loop_ptr->loop(); + } + if (task->loop == NULL) { + EventLoop* loop = currentThreadEventLoop; + if (loop) { + task->loop = loop->loop(); + } + } + if (task->loop == NULL) { + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + ctx->response->String("js handler: no event loop on this thread"); + task_unref(task); + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + + JSValue global = JS_GetGlobalObject(task->js); + JS_SetPropertyStr(task->js, global, "require", JS_NewCFunction(task->js, js_require, "require", 1)); + + JSValue eval = JS_Eval(task->js, code.c_str(), code.size(), filepath_.c_str(), JS_EVAL_TYPE_GLOBAL); + if (JS_IsException(eval)) { + std::string msg = js_exception_string(task->js); + JS_FreeValue(task->js, global); + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + ctx->response->String(msg); + task_unref(task); + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + JS_FreeValue(task->js, eval); + + JSValue fn; + if (!push_handler_fn(task->js, global, ctx->request->method, &fn)) { + JS_FreeValue(task->js, global); + ctx->response->status_code = HTTP_STATUS_NOT_IMPLEMENTED; + ctx->response->String("no js handler function"); + task_unref(task); + return HTTP_STATUS_NOT_IMPLEMENTED; + } + + JSValue js_ctx = js_new_ctx(task->js, ctx); + JSValue ret = JS_Call(task->js, fn, JS_UNDEFINED, 1, &js_ctx); + JS_FreeValue(task->js, js_ctx); + JS_FreeValue(task->js, fn); + if (JS_IsException(ret)) { + std::string msg = js_exception_string(task->js); + JS_FreeValue(task->js, global); + JS_FreeValue(task->js, ret); + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + ctx->response->String(msg); + task_unref(task); + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + + JSValue promise_ctor = JS_GetPropertyStr(task->js, global, "Promise"); + JSValue promise_resolve = JS_GetPropertyStr(task->js, promise_ctor, "resolve"); + JS_FreeValue(task->js, global); + JSValue promise_arg = ret; + task->promise = JS_Call(task->js, promise_resolve, promise_ctor, 1, &promise_arg); + JS_FreeValue(task->js, promise_resolve); + JS_FreeValue(task->js, promise_ctor); + JS_FreeValue(task->js, ret); + if (JS_IsException(task->promise)) { + std::string msg = js_exception_string(task->js); + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + ctx->response->String(msg); + task_unref(task); + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + + task_ref(task); + drain_jobs(task); + bool finished = task->finished; + int status = ctx->response->status_code; + if (!finished) { + task->async = true; + } + task_unref(task); + if (finished) { + return status; + } + return HTTP_STATUS_NEXT; +} + +} // namespace hv +#endif // WITH_JS diff --git a/http/server/HttpJsHandler.h b/http/server/HttpJsHandler.h new file mode 100644 index 000000000..8c9a5f7b3 --- /dev/null +++ b/http/server/HttpJsHandler.h @@ -0,0 +1,47 @@ +#ifndef HV_HTTP_JS_HANDLER_H_ +#define HV_HTTP_JS_HANDLER_H_ + +#include +#include + +#include "hexport.h" +#include "HttpService.h" + +namespace hv { + +struct HV_EXPORT HttpJsHandlerOptions { + bool reload_on_change; + + HttpJsHandlerOptions() { reload_on_change = true; } +}; + +// HttpJsHandler runs a QuickJS script to handle an HTTP request. +// +// The first implementation uses one QuickJS runtime/context per request. This +// keeps request lifetime, Promise continuations and loop-thread affinity simple; +// scripts can use async functions and await hv.sleep() without blocking the +// server IO loop. The public route surface mirrors HttpLuaHandler: a per-method +// function (get/post/...) takes precedence over handle(ctx). +class HV_EXPORT HttpJsHandler { +public: + HttpJsHandler(const char* filepath, const HttpJsHandlerOptions& options = HttpJsHandlerOptions()); + + int operator()(const HttpContextPtr& ctx); + + const std::string& filepath() const { return filepath_; } + +private: + struct State; + + bool loadScript(std::string* code, std::string* err); + + std::string filepath_; + HttpJsHandlerOptions options_; + std::shared_ptr state_; +}; + +typedef std::shared_ptr HttpJsHandlerPtr; + +} // namespace hv + +#endif // HV_HTTP_JS_HANDLER_H_ diff --git a/http/server/HttpScriptHandler.cpp b/http/server/HttpScriptHandler.cpp index 83ee2bae7..c66e958eb 100644 --- a/http/server/HttpScriptHandler.cpp +++ b/http/server/HttpScriptHandler.cpp @@ -1,17 +1,28 @@ #include "HttpScriptHandler.h" -#ifdef WITH_LUA +#if defined(WITH_LUA) || defined(WITH_JS) #include "hbase.h" #include "hstring.h" +#ifdef WITH_JS +#include "HttpJsHandler.h" +#endif +#ifdef WITH_LUA #include "HttpLuaHandler.h" +#endif #include namespace hv { struct HttpScriptHandler::State { +#ifdef WITH_LUA std::once_flag lua_once; HttpLuaHandlerPtr lua_handler; +#endif +#ifdef WITH_JS + std::once_flag js_once; + HttpJsHandlerPtr js_handler; +#endif }; namespace { @@ -44,6 +55,7 @@ HttpScriptHandler& HttpScriptHandler::operator=(const HttpScriptHandler& rhs) { } int HttpScriptHandler::operator()(const HttpContextPtr& ctx) { +#ifdef WITH_LUA if (filepath_has_suffix(filepath_, "lua")) { std::call_once(state_->lua_once, [this]() { HttpLuaHandlerOptions lua_options; @@ -52,6 +64,18 @@ int HttpScriptHandler::operator()(const HttpContextPtr& ctx) { }); return (*state_->lua_handler)(ctx); } +#endif + +#ifdef WITH_JS + if (filepath_has_suffix(filepath_, "js")) { + std::call_once(state_->js_once, [this]() { + HttpJsHandlerOptions js_options; + js_options.reload_on_change = options_.reload_on_change; + state_->js_handler = std::make_shared(filepath_.c_str(), js_options); + }); + return (*state_->js_handler)(ctx); + } +#endif if (ctx && ctx->response) { ctx->response->status_code = HTTP_STATUS_NOT_IMPLEMENTED; @@ -62,4 +86,4 @@ int HttpScriptHandler::operator()(const HttpContextPtr& ctx) { } // namespace hv -#endif // WITH_LUA +#endif // WITH_LUA || WITH_JS diff --git a/http/server/HttpService.cpp b/http/server/HttpService.cpp index da7b946c4..c28710e70 100644 --- a/http/server/HttpService.cpp +++ b/http/server/HttpService.cpp @@ -1,7 +1,7 @@ #include "HttpService.h" #include "HttpMiddleware.h" #include "HttpRouter.h" -#ifdef WITH_LUA +#if defined(WITH_LUA) || defined(WITH_JS) #include "HttpScriptHandler.h" #include "hpath.h" #include "hstring.h" @@ -115,7 +115,7 @@ std::string HttpService::GetStaticFilepath(const char* path) { return filepath; } -#ifdef WITH_LUA +#if defined(WITH_LUA) || defined(WITH_JS) void HttpService::Script(const char* path, const char* script_dir) { std::string route_path(path ? path : ""); if (route_path.empty()) return; @@ -147,7 +147,27 @@ void HttpService::Script(const char* path, const char* script_dir) { } std::string script = HPath::join(root, name); if (HPath::suffixname(script).empty()) { - script += ".lua"; + std::string base_script = script; + script.clear(); +#if defined(WITH_LUA) && defined(WITH_JS) + std::string lua_script = base_script + ".lua"; + if (HPath::exists(lua_script.c_str())) { + script = lua_script; + } + if (script.empty()) { + std::string js_script = base_script + ".js"; + if (HPath::exists(js_script.c_str())) { + script = js_script; + } + } + if (script.empty()) { + script = lua_script; + } +#elif defined(WITH_LUA) + script = base_script + ".lua"; +#else + script = base_script + ".js"; +#endif } HttpScriptHandlerPtr script_handler; diff --git a/http/server/HttpService.h b/http/server/HttpService.h index 37073d637..c6a35f0b4 100644 --- a/http/server/HttpService.h +++ b/http/server/HttpService.h @@ -201,7 +201,7 @@ struct HV_EXPORT HttpService { // @retval / => /var/www/html/index.html std::string GetStaticFilepath(const char* path); -#ifdef WITH_LUA +#if defined(WITH_LUA) || defined(WITH_JS) void Script(const char* path, const char* script_dir); #endif diff --git a/redis/AsyncRedisClient.cpp b/redis/AsyncRedisClient.cpp index f6e9f8e3e..99d38e92b 100644 --- a/redis/AsyncRedisClient.cpp +++ b/redis/AsyncRedisClient.cpp @@ -352,10 +352,10 @@ struct AsyncRedisClient::Impl { void failPending(int code) { while (!pending.empty()) { - const std::shared_ptr& request = pending.front(); + std::shared_ptr request = pending.front(); + pending.pop_front(); cancelTimeout(request); invokeRequestCallback(request, code); - pending.pop_front(); } } @@ -411,14 +411,14 @@ struct AsyncRedisClient::Impl { handleClientError(ERR_RESPONSE); return; } - const std::shared_ptr& request = pending.front(); + std::shared_ptr request = pending.front(); request->replies.push_back(reply); if (request->replies.size() < request->expected_replies) { return; } + pending.pop_front(); cancelTimeout(request); invokeRequestCallback(request, 0); - pending.pop_front(); } void handleClientError(int code) { diff --git a/scripts/unittest.sh b/scripts/unittest.sh index d765500ab..ebffce7cd 100755 --- a/scripts/unittest.sh +++ b/scripts/unittest.sh @@ -47,6 +47,18 @@ fi if [ -x bin/http_lua_handler_test ]; then bin/http_lua_handler_test fi +if [ -x bin/http_js_handler_test ]; then + bin/http_js_handler_test || exit $? +fi +if [ -x bin/http_js_redis_test ]; then + bin/http_js_redis_test || exit $? +fi +if [ -x bin/http_js_ws_test ]; then + bin/http_js_ws_test || exit $? +fi +if [ -x bin/http_js_mqtt_test ]; then + bin/http_js_mqtt_test || exit $? +fi if [ -x bin/lua_http_test ]; then bin/lua_http_test fi diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index 2b62d081d..47b5b4bdd 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -133,6 +133,31 @@ set(HTTP_LUA_UNITTEST_TARGETS ${HTTP_LUA_UNITTEST_TARGETS} lua_redis_test) endif() endif() +if(WITH_JS AND WITH_EVPP AND WITH_HTTP AND WITH_HTTP_SERVER AND WITH_HTTP_CLIENT) +add_executable(http_js_handler_test http_js_handler_test.cpp) +target_include_directories(http_js_handler_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../http ../http/server ../http/client) +target_link_libraries(http_js_handler_test ${HV_LIBRARIES}) +set(HTTP_JS_UNITTEST_TARGETS http_js_handler_test) +if(WITH_REDIS) +add_executable(http_js_redis_test http_js_redis_test.cpp redis_test_server.cpp) +target_include_directories(http_js_redis_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../http ../http/server ../http/client ../redis) +target_link_libraries(http_js_redis_test ${HV_LIBRARIES}) +set(HTTP_JS_UNITTEST_TARGETS ${HTTP_JS_UNITTEST_TARGETS} http_js_redis_test) +endif() +if(WITH_HTTP_CLIENT) +add_executable(http_js_ws_test http_js_ws_test.cpp) +target_include_directories(http_js_ws_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../http ../http/server ../http/client) +target_link_libraries(http_js_ws_test ${HV_LIBRARIES}) +set(HTTP_JS_UNITTEST_TARGETS ${HTTP_JS_UNITTEST_TARGETS} http_js_ws_test) +endif() +if(WITH_MQTT) +add_executable(http_js_mqtt_test http_js_mqtt_test.cpp) +target_include_directories(http_js_mqtt_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../http ../http/server ../http/client ../mqtt) +target_link_libraries(http_js_mqtt_test ${HV_LIBRARIES}) +set(HTTP_JS_UNITTEST_TARGETS ${HTTP_JS_UNITTEST_TARGETS} http_js_mqtt_test) +endif() +endif() + # ------event: async dns------ add_executable(hdns_test hdns_test.c) target_include_directories(hdns_test PRIVATE .. ../base ../ssl ../event) @@ -199,6 +224,7 @@ add_custom_target(unittest DEPENDS sendmail http_router_test ${HTTP_LUA_UNITTEST_TARGETS} + ${HTTP_JS_UNITTEST_TARGETS} hdns_test hdns_benchmark ${REDIS_UNITTEST_TARGETS} diff --git a/unittest/http_js_handler_test.cpp b/unittest/http_js_handler_test.cpp new file mode 100644 index 000000000..e5b28dc29 --- /dev/null +++ b/unittest/http_js_handler_test.cpp @@ -0,0 +1,137 @@ +/* + * http_js_handler_test - integration test for HttpJsHandler. + * + * Starts a real HttpServer whose JS route awaits hv.sleep(ms). Concurrent + * requests on a single IO thread must complete faster than serial execution, + * proving the QuickJS promise continuation is driven by the event loop without + * blocking it. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "hbase.h" +#include "hfile.h" +#include "hpath.h" +#include "htime.h" +#include "HttpJsHandler.h" +#include "HttpServer.h" +#include "HttpService.h" +#include "HttpScriptHandler.h" +#include "requests.h" + +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + fprintf(stderr, "CHECK failed: %s at %s:%d\n", #expr, __FILE__, __LINE__); \ + abort(); \ + } \ + } while (0) + +static std::string write_script(const char* name, const char* content) { + hv_mkdir_p("tmp/http_js_handler_test"); + std::string path = HPath::join("tmp/http_js_handler_test", name); + HFile file; + int ret = file.open(path.c_str(), "wb"); + CHECK(ret == 0); + file.write(content, strlen(content)); + file.close(); + return path; +} + +int main() { + std::string script = write_script("sleep.js", "const hv = require('hv');\n" + "const http = require('hv/http');\n" + "async function get(ctx) {\n" + " await hv.sleep(300);\n" + " const resp = await http.get('http://' + ctx.header('Host') + '/ping');\n" + " return { ok: true, id: ctx.query('id'), upstream: resp.body };\n" + "}\n"); + std::string direct_script = write_script("direct.js", "function get(ctx) {\n" + " ctx.setHeader('X-From', 'js');\n" + " return ctx.text('direct:' + ctx.query('id', ''));\n" + "}\n"); + std::string circular_script = write_script("circular.js", "function get(ctx) {\n" + " const data = { ok: true };\n" + " data.self = data;\n" + " return data;\n" + "}\n"); + + HttpService service; + service.GET("/ping", [](HttpRequest* req, HttpResponse* resp) { + (void)req; + resp->body = "pong"; + return 200; + }); + service.GET("/sleep", hv::HttpScriptHandler(script.c_str())); + service.GET("/direct", hv::HttpJsHandler(direct_script.c_str())); + service.GET("/circular", hv::HttpJsHandler(circular_script.c_str())); + + hv::HttpServer server(&service); + server.setThreadNum(1); + server.setPort(0); + CHECK(server.start() == 0); + CHECK(server.port > 0); + hv_msleep(200); + + const int N = 5; + std::vector threads; + std::atomic ok_count(0); + + uint64_t start = gettimeofday_ms(); + const int server_port = server.port; + for (int i = 0; i < N; ++i) { + threads.emplace_back([i, server_port, &ok_count]() { + char url[128]; + snprintf(url, sizeof(url), "http://127.0.0.1:%d/sleep?id=%d", server_port, i); + auto resp = requests::get(url); + if (resp == NULL) { + fprintf(stderr, "request %d failed: null response\n", i); + return; + } + if (resp->status_code != 200) { + fprintf(stderr, "request %d failed: status=%d body=%s\n", i, resp->status_code, resp->body.c_str()); + return; + } + char needle[32]; + snprintf(needle, sizeof(needle), "\"id\":\"%d\"", i); + if (resp->body.find("\"ok\":true") != std::string::npos && resp->body.find(needle) != std::string::npos && + resp->body.find("\"upstream\":\"pong\"") != std::string::npos) { + ok_count++; + } + else { + fprintf(stderr, "request %d failed: body=%s\n", i, resp->body.c_str()); + } + }); + } + for (auto& t : threads) t.join(); + uint64_t elapsed = gettimeofday_ms() - start; + + char direct_url[128]; + snprintf(direct_url, sizeof(direct_url), "http://127.0.0.1:%d/direct?id=7", server_port); + auto direct_resp = requests::get(direct_url); + char circular_url[128]; + snprintf(circular_url, sizeof(circular_url), "http://127.0.0.1:%d/circular", server_port); + auto circular_resp = requests::get(circular_url); + + server.stop(); + hv_msleep(100); + + printf("ok_count=%d/%d elapsed=%llums (each handler awaits 300ms)\n", ok_count.load(), N, (unsigned long long)elapsed); + CHECK(ok_count.load() == N); + CHECK(elapsed < 1200); + CHECK(direct_resp != NULL); + CHECK(direct_resp->status_code == 200); + CHECK(direct_resp->body == "direct:7"); + CHECK(direct_resp->GetHeader("X-From") == "js"); + CHECK(circular_resp != NULL); + CHECK(circular_resp->status_code == 500); + CHECK(circular_resp->body.find("circular") != std::string::npos); + printf("ALL http_js_handler_test PASSED\n"); + return 0; +} diff --git a/unittest/http_js_mqtt_test.cpp b/unittest/http_js_mqtt_test.cpp new file mode 100644 index 000000000..c480ce90f --- /dev/null +++ b/unittest/http_js_mqtt_test.cpp @@ -0,0 +1,71 @@ +/* + * http_js_mqtt_test - HttpJsHandler + hv/mqtt Promise binding smoke test. + * + * No live MQTT broker is required: this verifies the module registers and its + * connect() failure path rejects without crashing or hanging. + */ + +#include +#include +#include +#include + +#include "hbase.h" +#include "hfile.h" +#include "hpath.h" +#include "HttpServer.h" +#include "HttpService.h" +#include "HttpScriptHandler.h" +#include "requests.h" + +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + fprintf(stderr, "CHECK failed: %s at %s:%d\n", #expr, __FILE__, __LINE__); \ + abort(); \ + } \ + } while (0) + +static std::string write_script(const char* name, const char* content) { + hv_mkdir_p("tmp/http_js_mqtt_test"); + std::string path = HPath::join("tmp/http_js_mqtt_test", name); + HFile file; + int ret = file.open(path.c_str(), "wb"); + CHECK(ret == 0); + file.write(content, strlen(content)); + file.close(); + return path; +} + +int main() { + std::string script = write_script("mqtt.js", "const mqtt = require('hv/mqtt');\n" + "async function get(ctx) {\n" + " let err = '';\n" + " try { await mqtt.connect({ host: '127.0.0.1', port: 1, timeout: 500 }); }\n" + " catch (e) { err = String(e); }\n" + " return { ok: true, err };\n" + "}\n"); + + HttpService service; + service.GET("/mqtt", hv::HttpScriptHandler(script.c_str())); + + hv::HttpServer server(&service); + server.setThreadNum(1); + server.setPort(0); + CHECK(server.start() == 0); + CHECK(server.port > 0); + hv_msleep(200); + + char url[128]; + snprintf(url, sizeof(url), "http://127.0.0.1:%d/mqtt", server.port); + auto resp = requests::get(url); + server.stop(); + hv_msleep(100); + + CHECK(resp != NULL); + CHECK(resp->status_code == 200); + CHECK(resp->body.find("\"ok\":true") != std::string::npos); + CHECK(resp->body.find("\"err\":\"") != std::string::npos); + printf("ALL http_js_mqtt_test PASSED\n"); + return 0; +} diff --git a/unittest/http_js_redis_test.cpp b/unittest/http_js_redis_test.cpp new file mode 100644 index 000000000..967de8ead --- /dev/null +++ b/unittest/http_js_redis_test.cpp @@ -0,0 +1,113 @@ +/* + * http_js_redis_test - HttpJsHandler + hv/redis Promise binding. + * + * Uses the in-process FakeRedisServer so the test does not depend on a local + * Redis daemon. The JS handler awaits Redis command promises and returns JSON. + */ + +#include +#include +#include +#include + +#include "hbase.h" +#include "hfile.h" +#include "hpath.h" +#include "HttpServer.h" +#include "HttpService.h" +#include "HttpScriptHandler.h" +#include "requests.h" +#include "redis_test_server.h" + +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + fprintf(stderr, "CHECK failed: %s at %s:%d\n", #expr, __FILE__, __LINE__); \ + abort(); \ + } \ + } while (0) + +static std::string write_script(const char* name, const char* content) { + hv_mkdir_p("tmp/http_js_redis_test"); + std::string path = HPath::join("tmp/http_js_redis_test", name); + HFile file; + int ret = file.open(path.c_str(), "wb"); + CHECK(ret == 0); + file.write(content, strlen(content)); + file.close(); + return path; +} + +int main() { + FakeRedisServer redis_server; + redis_server.setCommandHandler([](const hv::RedisCommand& cmd) { + hv::RedisReply reply; + if (cmd[0] == "PING") { + reply.type = hv::REDIS_REPLY_STRING; + reply.str = "PONG"; + } + else if (cmd[0] == "SET") { + reply.type = hv::REDIS_REPLY_STRING; + reply.str = "OK"; + } + else if (cmd[0] == "GET") { + reply.type = hv::REDIS_REPLY_STRING; + reply.str = "v"; + reply.bulk = true; + } + else if (cmd[0] == "INCR") { + reply.type = hv::REDIS_REPLY_INTEGER; + reply.integer = 1; + } + else { + reply.type = hv::REDIS_REPLY_ERROR; + reply.str = "ERR unsupported"; + } + return reply; + }); + redis_server.start(); + CHECK(redis_server.port() > 0); + + char script_buf[2048]; + snprintf(script_buf, sizeof(script_buf), + "const redis = require('hv/redis');\n" + "async function get(ctx) {\n" + " const r = redis.new({ host: '127.0.0.1', port: %d, timeout: 3000 });\n" + " const ok = await r.set('k', 'v');\n" + " const v = await r.get('k');\n" + " const n = await r.incr('c');\n" + " const pong = await r.command(['PING']);\n" + " let err = '';\n" + " try { await r.command('BADCMD'); } catch (e) { err = String(e); }\n" + " return { ok, v, n, pong, err };\n" + "}\n", + redis_server.port()); + std::string script = write_script("redis.js", script_buf); + + HttpService service; + service.GET("/redis", hv::HttpScriptHandler(script.c_str())); + + hv::HttpServer server(&service); + server.setThreadNum(1); + server.setPort(0); + CHECK(server.start() == 0); + CHECK(server.port > 0); + hv_msleep(200); + + char url[128]; + snprintf(url, sizeof(url), "http://127.0.0.1:%d/redis", server.port); + auto resp = requests::get(url); + server.stop(); + redis_server.stop(); + hv_msleep(100); + + CHECK(resp != NULL); + CHECK(resp->status_code == 200); + CHECK(resp->body.find("\"ok\":\"OK\"") != std::string::npos); + CHECK(resp->body.find("\"v\":\"v\"") != std::string::npos); + CHECK(resp->body.find("\"n\":1") != std::string::npos); + CHECK(resp->body.find("\"pong\":\"PONG\"") != std::string::npos); + CHECK(resp->body.find("\"err\":\"ERR unsupported\"") != std::string::npos); + printf("ALL http_js_redis_test PASSED\n"); + return 0; +} diff --git a/unittest/http_js_ws_test.cpp b/unittest/http_js_ws_test.cpp new file mode 100644 index 000000000..18eb66df9 --- /dev/null +++ b/unittest/http_js_ws_test.cpp @@ -0,0 +1,83 @@ +/* + * http_js_ws_test - HttpJsHandler + hv/ws Promise binding. + */ + +#include +#include +#include +#include + +#include "hbase.h" +#include "hfile.h" +#include "hpath.h" +#include "HttpServer.h" +#include "HttpService.h" +#include "HttpScriptHandler.h" +#include "WebSocketServer.h" +#include "requests.h" + +#define CHECK(expr) \ + do { \ + if (!(expr)) { \ + fprintf(stderr, "CHECK failed: %s at %s:%d\n", #expr, __FILE__, __LINE__); \ + abort(); \ + } \ + } while (0) + +static std::string write_script(const char* name, const char* content) { + hv_mkdir_p("tmp/http_js_ws_test"); + std::string path = HPath::join("tmp/http_js_ws_test", name); + HFile file; + int ret = file.open(path.c_str(), "wb"); + CHECK(ret == 0); + file.write(content, strlen(content)); + file.close(); + return path; +} + +int main() { + WebSocketService ws_service; + ws_service.onmessage = [](const WebSocketChannelPtr& channel, const std::string& msg) { channel->send(msg); }; + hv::WebSocketServer ws_server(&ws_service); + ws_server.setPort(0); + ws_server.setThreadNum(1); + CHECK(ws_server.start() == 0); + CHECK(ws_server.port > 0); + + char script_buf[1024]; + snprintf(script_buf, sizeof(script_buf), + "const wsmod = require('hv/ws');\n" + "async function get(ctx) {\n" + " const ws = await wsmod.connect('ws://127.0.0.1:%d/');\n" + " ws.send('hello-js');\n" + " const msg = await ws.recv();\n" + " ws.close();\n" + " return { ok: true, msg };\n" + "}\n", + ws_server.port); + std::string script = write_script("ws.js", script_buf); + + HttpService service; + service.GET("/ws", hv::HttpScriptHandler(script.c_str())); + + hv::HttpServer server(&service); + server.setThreadNum(1); + server.setPort(0); + CHECK(server.start() == 0); + CHECK(server.port > 0); + hv_msleep(200); + + char url[128]; + snprintf(url, sizeof(url), "http://127.0.0.1:%d/ws", server.port); + auto resp = requests::get(url); + server.stop(); + ws_server.stop(); + hv_msleep(100); + + CHECK(resp != NULL); + CHECK(resp->status_code == 200); + CHECK(resp->body.find("\"ok\":true") != std::string::npos); + CHECK(resp->body.find("\"msg\":\"hello-js\"") != std::string::npos); + printf("ALL http_js_ws_test PASSED\n"); + return 0; +} From 266deee888fd35eb60c529e10e5999d3b55ebf73 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 20 Aug 2026 12:58:46 +0800 Subject: [PATCH 2/6] refactor(js): split quickjs bindings --- .github/workflows/CI.yml | 4 +- CMakeLists.txt | 6 +- Makefile | 16 +- docs/cn/HttpJsHandler.md | 8 + examples/CMakeLists.txt | 10 + examples/hvjs.cpp | 167 ++++ examples/js/http_client.js | 13 + examples/js/mqtt_client.js | 29 + examples/js/redis_client.js | 27 + examples/js/sleep.js | 19 + examples/js/ws_client.js | 19 + http/server/HttpJsHandler.cpp | 1457 +-------------------------------- js/hvjs.cpp | 373 +++++++++ js/hvjs.h | 93 +++ js/hvjs_http.cpp | 402 +++++++++ js/hvjs_mqtt.cpp | 456 +++++++++++ js/hvjs_redis.cpp | 236 ++++++ 17 files changed, 1907 insertions(+), 1428 deletions(-) create mode 100644 examples/hvjs.cpp create mode 100644 examples/js/http_client.js create mode 100644 examples/js/mqtt_client.js create mode 100644 examples/js/redis_client.js create mode 100644 examples/js/sleep.js create mode 100644 examples/js/ws_client.js create mode 100644 js/hvjs.cpp create mode 100644 js/hvjs.h create mode 100644 js/hvjs_http.cpp create mode 100644 js/hvjs_mqtt.cpp create mode 100644 js/hvjs_redis.cpp diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 242cb24c9..10e612d2c 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -19,8 +19,8 @@ jobs: - name: build run: | sudo apt update - sudo apt install libssl-dev libnghttp2-dev liblua5.4-dev libprotobuf-dev libprotoc-dev protobuf-compiler - ./configure --with-openssl --with-nghttp2 --with-kcp --with-mqtt --with-redis --with-lua --with-rpc + sudo apt install libssl-dev libnghttp2-dev liblua5.4-dev libprotobuf-dev libprotoc-dev protobuf-compiler quickjs libquickjs + ./configure --with-openssl --with-nghttp2 --with-kcp --with-mqtt --with-redis --with-lua --with-js --with-rpc make libhv evpp # hrpc = separate libhrpc (needs protobuf); apt installs protobuf under /usr make libhrpc hrpc PROTOBUF_PREFIX=/usr diff --git a/CMakeLists.txt b/CMakeLists.txt index 35eb2947a..5ed5ee6ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -240,6 +240,7 @@ if(WITH_JS) /usr/local/opt/quickjs/lib /usr/local/lib/quickjs /usr/local/lib + /usr/lib/quickjs /usr/lib) if(NOT QUICKJS_INCLUDE_DIR OR NOT QUICKJS_LIBRARY) message(FATAL_ERROR "WITH_JS requires QuickJS. Set QUICKJS_ROOT or QUICKJS_INCLUDE_DIR and QUICKJS_LIBRARY.") @@ -287,7 +288,7 @@ if(APPLE) endif() # see Makefile -set(ALL_SRCDIRS . base ssl event event/kcp util cpputil evpp redis protocol http http/client http/server mqtt) +set(ALL_SRCDIRS . base ssl event event/kcp util cpputil evpp redis protocol http http/client http/server mqtt js) set(CORE_SRCDIRS . base ssl event) if(WIN32 OR MINGW) if(WITH_WEPOLL) @@ -318,6 +319,9 @@ endif() if(WITH_EVPP) set(LIBHV_HEADERS ${LIBHV_HEADERS} ${CPPUTIL_HEADERS} ${EVPP_HEADERS}) set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} cpputil evpp) + if(WITH_JS) + set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} js) + endif() if(WITH_REDIS) set(LIBHV_HEADERS ${LIBHV_HEADERS} ${REDIS_HEADERS}) set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} redis) diff --git a/Makefile b/Makefile index 60875c196..fa69443fc 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ include config.mk include Makefile.vars MAKEF=$(MAKE) -f Makefile.in -ALL_SRCDIRS=. base ssl event event/kcp util cpputil evpp redis protocol http http/client http/server mqtt +ALL_SRCDIRS=. base ssl event event/kcp util cpputil evpp redis protocol http http/client http/server mqtt js CORE_SRCDIRS=. base ssl event ifeq ($(WITH_KCP), yes) CORE_SRCDIRS += event/kcp @@ -29,6 +29,12 @@ LIBHV_SRCDIRS += cpputil endif endif +ifeq ($(WITH_JS), yes) +ifeq ($(WITH_EVPP), yes) +LIBHV_SRCDIRS += js +endif +endif + ifeq ($(WITH_EVPP), yes) LIBHV_HEADERS += $(CPPUTIL_HEADERS) $(EVPP_HEADERS) LIBHV_SRCDIRS += cpputil evpp @@ -121,6 +127,11 @@ ifeq ($(WITH_EVPP), yes) EXAMPLES += hvlua endif endif +ifeq ($(WITH_JS), yes) +ifeq ($(WITH_EVPP), yes) +EXAMPLES += hvjs +endif +endif examples: $(EXAMPLES) @echo "make examples done." @@ -234,6 +245,9 @@ host: prepare hvlua: prepare libhv $(CXX) -g -Wall -O0 -std=c++11 -DWITH_LUA $(LUA_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ilua -o bin/hvlua examples/hvlua.cpp -Llib -lhv -pthread $(LUA_LIBS) +hvjs: prepare libhv + $(CXX) -g -Wall -O0 -std=c++11 -DWITH_JS $(JS_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ijs -o bin/hvjs examples/hvjs.cpp -Llib -lhv -pthread $(JS_LIBS) + multi-acceptor-processes: prepare $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS)" SRCS="examples/multi-thread/multi-acceptor-processes.c" diff --git a/docs/cn/HttpJsHandler.md b/docs/cn/HttpJsHandler.md index b7a7c8a8c..1b1cbdfab 100644 --- a/docs/cn/HttpJsHandler.md +++ b/docs/cn/HttpJsHandler.md @@ -224,3 +224,11 @@ make http_server_test WITH_JS=yes WITH_HTTP=yes bin/http_server_test 8080 curl "http://127.0.0.1:8080/script/hello?id=42" ``` + +也可以直接使用 `hvjs` 运行独立脚本示例: + +```bash +make hvjs WITH_JS=yes WITH_HTTP=yes WITH_REDIS=yes WITH_MQTT=yes +bin/hvjs examples/js/sleep.js +bin/hvjs examples/js/http_client.js http://127.0.0.1:8080/ping +``` diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 89eb21fbb..45c02c2a9 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -133,6 +133,16 @@ if(WITH_EVPP) list(APPEND EXAMPLES hvlua) endif() + + if(WITH_JS) + include_directories(../js) + + # hvjs: standalone QuickJS runtime on top of libhv's event loop + add_executable(hvjs hvjs.cpp) + target_link_libraries(hvjs ${HV_LIBRARIES}) + + list(APPEND EXAMPLES hvjs) + endif() if(WITH_HTTP) include_directories(../http) diff --git a/examples/hvjs.cpp b/examples/hvjs.cpp new file mode 100644 index 000000000..ce83eb392 --- /dev/null +++ b/examples/hvjs.cpp @@ -0,0 +1,167 @@ +// hvjs: standalone QuickJS runtime on top of libhv's event loop. +// +// Usage: hvjs script.js [args...] +// +// The runtime publishes a shared EventLoop as this thread's loop so async JS +// bindings can reuse libhv clients on the same event loop. Scripts may use +// async/await with the built-in modules exposed through require("hv"), +// require("hv/http"), require("hv/ws"), require("hv/redis") and +// require("hv/mqtt") when the corresponding libhv modules are enabled. + +#include +#include + +#include +#include + +#include + +#include "EventLoop.h" +#include "hfile.h" +#include "hlog.h" +#include "hvjs.h" + +namespace { + +struct HvJsCliTask : public hv::js::HvJsTask { + int exit_code; + + HvJsCliTask() : exit_code(0) {} +}; + +static void usage(const char* prog) { + fprintf(stderr, "Usage: %s script.js [args...]\n", prog); +} + +static bool load_file(const char* filepath, std::string* out) { + HFile file; + if (file.open(filepath, "rb") != 0) { + return false; + } + size_t size = hv_filesize(filepath); + out->resize(size); + if (size == 0) return true; + int nread = file.read(&(*out)[0], (int)size); + return nread >= 0 && (size_t)nread == size; +} + +static JSValue js_print(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + for (int i = 0; i < argc; ++i) { + if (i != 0) fputc(' ', stdout); + std::string s = hv::js::hvjs_to_string(js, argv[i]); + fputs(s.c_str(), stdout); + } + fputc('\n', stdout); + return JS_UNDEFINED; +} + +static void set_args(JSContext* js, int argc, char** argv) { + JSValue arr = JS_NewArray(js); + for (int i = 1; i < argc; ++i) { + JS_SetPropertyUint32(js, arr, i - 1, JS_NewString(js, argv[i])); + } + JSValue global = JS_GetGlobalObject(js); + JS_SetPropertyStr(js, global, "arg", arr); + JS_FreeValue(js, global); +} + +static void finish(hv::js::HvJsTask* base, JSValue result) { + HvJsCliTask* task = static_cast(base); + if (task->finished) return; + task->finished = true; + if (!task->error.empty()) { + fprintf(stderr, "hvjs: %s\n", task->error.c_str()); + task->exit_code = 1; + } + else if (!JS_IsUndefined(task->promise) && JS_PromiseState(task->js, task->promise) == JS_PROMISE_REJECTED) { + std::string err = hv::js::hvjs_to_string(task->js, result); + fprintf(stderr, "hvjs: %s\n", err.c_str()); + task->exit_code = 1; + } + JS_FreeValue(task->js, result); + if (task->loop_ptr) { + task->loop_ptr->stop(); + } + else if (task->loop) { + hloop_stop(task->loop); + } + hv::js::hvjs_task_unref(task); +} + +} // namespace + +int main(int argc, char** argv) { + if (argc < 2) { + usage(argv[0]); + return 1; + } + const char* script = argv[1]; + std::string code; + if (!load_file(script, &code)) { + fprintf(stderr, "hvjs: failed to read %s\n", script); + return 1; + } + + setvbuf(stdout, NULL, _IOLBF, 0); + hlog_set_handler(stdout_logger); + + hv::EventLoopPtr loop = std::make_shared(); + hv::ThreadLocalStorage::set(hv::ThreadLocalStorage::EVENT_LOOP, loop.get()); + + HvJsCliTask* task = new HvJsCliTask(); + task->loop_ptr = loop; + task->loop = loop->loop(); + task->finish = finish; + task->rt = JS_NewRuntime(); + task->js = task->rt ? JS_NewContext(task->rt) : NULL; + if (task->rt == NULL || task->js == NULL) { + fprintf(stderr, "hvjs: failed to create quickjs runtime\n"); + hv::js::hvjs_task_unref(task); + return 1; + } + JS_SetContextOpaque(task->js, task); + set_args(task->js, argc, argv); + + JSValue global = JS_GetGlobalObject(task->js); + JS_SetPropertyStr(task->js, global, "require", JS_NewCFunction(task->js, hv::js::hvjs_require, "require", 1)); + JS_SetPropertyStr(task->js, global, "print", JS_NewCFunction(task->js, js_print, "print", 1)); + + std::string wrapped = "(async function(){\n"; + wrapped += code; + wrapped += "\n})()"; + JSValue eval = JS_Eval(task->js, wrapped.c_str(), wrapped.size(), script, JS_EVAL_TYPE_GLOBAL); + if (JS_IsException(eval)) { + std::string err = hv::js::hvjs_exception_string(task->js); + JS_FreeValue(task->js, global); + fprintf(stderr, "hvjs: %s\n", err.c_str()); + hv::js::hvjs_task_unref(task); + return 1; + } + + JSValue promise_ctor = JS_GetPropertyStr(task->js, global, "Promise"); + JSValue promise_resolve = JS_GetPropertyStr(task->js, promise_ctor, "resolve"); + JS_FreeValue(task->js, global); + JSValue promise_arg = eval; + task->promise = JS_Call(task->js, promise_resolve, promise_ctor, 1, &promise_arg); + JS_FreeValue(task->js, promise_resolve); + JS_FreeValue(task->js, promise_ctor); + JS_FreeValue(task->js, eval); + if (JS_IsException(task->promise)) { + std::string err = hv::js::hvjs_exception_string(task->js); + fprintf(stderr, "hvjs: %s\n", err.c_str()); + hv::js::hvjs_task_unref(task); + return 1; + } + + hv::js::hvjs_task_ref(task); + hv::js::hvjs_drain_jobs(task); + int exit_code = task->exit_code; + if (!task->finished) { + loop->run(); + exit_code = task->exit_code; + } + hv::js::hvjs_task_unref(task); + hv::ThreadLocalStorage::set(hv::ThreadLocalStorage::EVENT_LOOP, NULL); + return exit_code; +} diff --git a/examples/js/http_client.js b/examples/js/http_client.js new file mode 100644 index 000000000..458fc922b --- /dev/null +++ b/examples/js/http_client.js @@ -0,0 +1,13 @@ +// hv/http Promise client demo. +// Usage: hvjs examples/js/http_client.js [url] + +const hv = require("hv"); +const http = require("hv/http"); + +const url = arg[1] || "http://127.0.0.1:18090/ping"; + +const resp = await http.get(url); +hv.log("GET", url, "->", resp.status, "body:", resp.body); + +const second = await http.get(url); +hv.log("2nd GET ->", second.status); diff --git a/examples/js/mqtt_client.js b/examples/js/mqtt_client.js new file mode 100644 index 000000000..f609597a0 --- /dev/null +++ b/examples/js/mqtt_client.js @@ -0,0 +1,29 @@ +// hv/mqtt Promise client demo. +// Usage: hvjs examples/js/mqtt_client.js [host] [port] [topic] + +const hv = require("hv"); +const mqtt = require("hv/mqtt"); + +const host = arg[1] || "127.0.0.1"; +const port = Number(arg[2] || 1883); +const topic = arg[3] || "hv/js/test"; + +const client = await mqtt.connect({ + host, + port, + id: "hvjs-demo", + keepalive: 60, + reconnect: { min_delay: 1000, max_delay: 10000, delay_policy: 2 }, +}); + +hv.log("connected to mqtt", host, port); + +client.subscribe(topic, 1); +client.publish(topic, "hello from js", 1); + +for (let i = 1; i <= 3; ++i) { + const msg = await client.recv(); + hv.log("recv ->", msg.topic, msg.payload, "qos", msg.qos); +} + +client.disconnect(); diff --git a/examples/js/redis_client.js b/examples/js/redis_client.js new file mode 100644 index 000000000..42e452315 --- /dev/null +++ b/examples/js/redis_client.js @@ -0,0 +1,27 @@ +// hv/redis Promise client demo. +// Usage: hvjs examples/js/redis_client.js [host] [port] + +const hv = require("hv"); +const redis = require("hv/redis"); + +const host = arg[1] || "127.0.0.1"; +const port = Number(arg[2] || 6379); + +const r = redis.new({ host, port, timeout: 3000 }); + +const ok = await r.set("hv:js:key", "hello"); +hv.log("SET ->", ok); + +const v = await r.get("hv:js:key"); +hv.log("GET ->", v); + +const n = await r.incr("hv:js:counter"); +hv.log("INCR ->", n); + +try { + const res = await r.command(["HSET", "hv:js:hash", "field", "val"]); + hv.log("HSET ->", res); +} +catch (e) { + hv.log("HSET err:", String(e)); +} diff --git a/examples/js/sleep.js b/examples/js/sleep.js new file mode 100644 index 000000000..dfdb466fc --- /dev/null +++ b/examples/js/sleep.js @@ -0,0 +1,19 @@ +// hvjs event-loop sleep example. +// Run: bin/hvjs examples/js/sleep.js + +const hv = require("hv"); + +async function worker(name, ms) { + for (let i = 1; i <= 3; ++i) { + hv.log(name, "step", i); + await hv.sleep(ms); + } + hv.log(name, "done"); +} + +await Promise.all([ + worker("A", 300), + worker("B", 500), +]); + +print("sleep example done"); diff --git a/examples/js/ws_client.js b/examples/js/ws_client.js new file mode 100644 index 000000000..0c8376ce3 --- /dev/null +++ b/examples/js/ws_client.js @@ -0,0 +1,19 @@ +// hv/ws Promise WebSocket client demo. +// Usage: hvjs examples/js/ws_client.js [url] + +const hv = require("hv"); +const wsmod = require("hv/ws"); + +const url = arg[1] || "ws://127.0.0.1:8888/"; +const ws = await wsmod.connect(url); + +hv.log("connected to", url); +ws.send("hello from js"); + +for (let i = 1; i <= 3; ++i) { + const msg = await ws.recv(); + hv.log("recv ->", msg); + ws.send("echo " + i); +} + +ws.close(); diff --git a/http/server/HttpJsHandler.cpp b/http/server/HttpJsHandler.cpp index 7a16dc1e0..7187cf0bd 100644 --- a/http/server/HttpJsHandler.cpp +++ b/http/server/HttpJsHandler.cpp @@ -3,305 +3,33 @@ #include "HttpJsHandler.h" #include -#include -#include -#include #include #include -#include #include #include #include -#include -#include - -#include #include "EventLoop.h" #include "hfile.h" #include "hlog.h" #include "hpath.h" #include "hstring.h" -#include "htime.h" -#include "hversion.h" -#ifdef HVJS_WITH_HTTP -#include "AsyncHttpClient.h" -#include "WebSocketClient.h" -#endif -#ifdef HVJS_WITH_REDIS -#include "AsyncRedisClient.h" -#endif -#ifdef HVJS_WITH_MQTT -#include "mqtt_client.h" -#endif +#include "hvjs.h" namespace hv { namespace { -struct JsHttpTask; - -static const int JS_HTTP_METHOD_REQUEST = -1; - -struct JsHttpTask { - JSRuntime* rt; - JSContext* js; - hloop_t* loop; - EventLoopPtr loop_ptr; +struct JsHttpTask : public hv::js::HvJsTask { HttpContextPtr ctx; - JSValue promise; bool async; - bool finished; - bool in_call; - bool closing; - int refcount; - std::string error; - - JsHttpTask() : rt(NULL), js(NULL), loop(NULL), promise(JS_UNDEFINED), async(false), finished(false), in_call(false), closing(false), refcount(1) {} -}; - -struct JsPromiseOp { - JsHttpTask* task; - JSValue resolve; - JSValue reject; - bool completed; - bool defer_delete; - JsPromiseOp() : task(NULL), resolve(JS_UNDEFINED), reject(JS_UNDEFINED), completed(false), defer_delete(false) {} - - virtual ~JsPromiseOp() {} -}; - -struct JsSleep : public JsPromiseOp { - htimer_t* timer; - TimerID timer_id; - - JsSleep() : timer(NULL), timer_id(INVALID_TIMER_ID) {} + JsHttpTask() : async(false) {} }; -struct JsImmediatePromise : public JsPromiseOp {}; - -static std::mutex& js_class_id_mutex() { - static std::mutex mutex; - return mutex; -} - -static void js_new_class_id(JSClassID* class_id) { - std::lock_guard lock(js_class_id_mutex()); - JS_NewClassID(class_id); -} - -static void task_ref(JsHttpTask* task) { - ++task->refcount; -} - -static void task_unref(JsHttpTask* task) { - if (--task->refcount != 0) return; - task->closing = true; - if (!JS_IsUndefined(task->promise)) { - JS_FreeValue(task->js, task->promise); - task->promise = JS_UNDEFINED; - } - if (task->js) { - if (task->rt) { - JS_RunGC(task->rt); - } - JS_FreeContext(task->js); - task->js = NULL; - } - if (task->rt) { - JS_RunGC(task->rt); - JS_FreeRuntime(task->rt); - task->rt = NULL; - } - delete task; -} - -static void drain_jobs(JsHttpTask* task); -static std::string js_to_string(JSContext* ctx, JSValueConst value); -static std::string js_exception_string(JSContext* ctx); -static void js_promise_complete(JsPromiseOp* op, JSValue value, bool ok); - -static void drain_event_cb(hevent_t* ev) { - JsHttpTask* task = (JsHttpTask*)hevent_userdata(ev); - drain_jobs(task); - task_unref(task); -} - -static void schedule_drain(JsHttpTask* task) { - if (task == NULL || task->closing) return; - task_ref(task); - if (task->loop_ptr) { - task->loop_ptr->queueInLoop([task]() { - drain_jobs(task); - task_unref(task); - }); - } - else if (task->loop) { - hevent_t ev; - memset(&ev, 0, sizeof(ev)); - ev.cb = drain_event_cb; - ev.userdata = task; - hloop_post_event(task->loop, &ev); - } - else { - task_unref(task); - } -} - -template static JSValue js_new_promise(JSContext* js, JsHttpTask* task, T** out) { - JSValue funcs[2]; - JSValue promise = JS_NewPromiseCapability(js, funcs); - if (JS_IsException(promise)) return promise; - T* op = new T(); - op->task = task; - op->resolve = funcs[0]; - op->reject = funcs[1]; - task_ref(task); - *out = op; - return promise; -} - -static void js_promise_complete(JsPromiseOp* op, JSValue value, bool ok) { - JsHttpTask* task = op->task; - if (op->completed) { - JS_FreeValue(task->js, value); - return; - } - op->completed = true; - if (!task->closing) { - JSValue func = ok ? op->resolve : op->reject; - JSValue ret = JS_Call(task->js, func, JS_UNDEFINED, 1, &value); - if (JS_IsException(ret) && task->error.empty()) { - task->error = js_exception_string(task->js); - } - JS_FreeValue(task->js, ret); - JS_FreeValue(task->js, value); - JS_FreeValue(task->js, op->resolve); - JS_FreeValue(task->js, op->reject); - op->resolve = JS_UNDEFINED; - op->reject = JS_UNDEFINED; - if (task->in_call) { - op->defer_delete = true; - schedule_drain(task); - return; - } - schedule_drain(task); - } - else { - JS_FreeValue(task->js, value); - JS_FreeValue(task->js, op->resolve); - JS_FreeValue(task->js, op->reject); - op->resolve = JS_UNDEFINED; - op->reject = JS_UNDEFINED; - } - delete op; - task_unref(task); -} - -static void js_promise_resolve(JsPromiseOp* op, JSValue value) { - js_promise_complete(op, value, true); -} - -static void js_promise_reject(JsPromiseOp* op, const char* message) { - js_promise_complete(op, JS_NewString(op->task->js, message ? message : "error"), false); -} - -static JSValue js_rejected_promise(JSContext* js, const char* message) { - JSValue funcs[2]; - JSValue promise = JS_NewPromiseCapability(js, funcs); - if (JS_IsException(promise)) return promise; - JSValue reason = JS_NewString(js, message ? message : "error"); - JSValue ret = JS_Call(js, funcs[1], JS_UNDEFINED, 1, &reason); - JS_FreeValue(js, ret); - JS_FreeValue(js, reason); - JS_FreeValue(js, funcs[0]); - JS_FreeValue(js, funcs[1]); - return promise; -} - -static JSValue js_async_resolved_promise(JSContext* js, JsHttpTask* task, JSValue value) { - if (task == NULL) { - JS_FreeValue(js, value); - return JS_ThrowInternalError(js, "invalid HttpJsHandler task"); - } - JsImmediatePromise* op = NULL; - JSValue promise = js_new_promise(js, task, &op); - if (JS_IsException(promise)) { - JS_FreeValue(js, value); - return promise; - } - js_promise_resolve(op, value); - return promise; -} - -static void js_finish_deferred_op(JsPromiseOp* op) { - if (op == NULL || !op->completed || !op->defer_delete) return; - JsHttpTask* task = op->task; - delete op; - task_unref(task); -} - -static std::string js_to_string(JSContext* ctx, JSValueConst value) { - size_t len = 0; - const char* str = JS_ToCStringLen(ctx, &len, value); - if (str == NULL) return std::string(); - std::string out(str, len); - JS_FreeCString(ctx, str); - return out; -} - -static bool js_get_property(JSContext* js, JSValueConst obj, const char* name, JSValue* out) { - *out = JS_UNDEFINED; - if (!JS_IsObject(obj)) return false; - *out = JS_GetPropertyStr(js, obj, name); - return !JS_IsUndefined(*out) && !JS_IsException(*out); -} - -static std::string js_get_string_property(JSContext* js, JSValueConst obj, const char* name, const char* defvalue = "") { - JSValue value; - if (!js_get_property(js, obj, name, &value) || JS_IsNull(value)) { - if (!JS_IsUndefined(value) && !JS_IsException(value)) JS_FreeValue(js, value); - return defvalue; - } - std::string out = js_to_string(js, value); - JS_FreeValue(js, value); - return out; -} - -static int js_get_int_property(JSContext* js, JSValueConst obj, const char* name, int defvalue = 0) { - JSValue value; - if (!js_get_property(js, obj, name, &value) || JS_IsNull(value)) { - if (!JS_IsUndefined(value) && !JS_IsException(value)) JS_FreeValue(js, value); - return defvalue; - } - int32_t out = defvalue; - JS_ToInt32(js, &out, value); - JS_FreeValue(js, value); - return out; -} - -static bool js_get_bool_property(JSContext* js, JSValueConst obj, const char* name, bool defvalue = false) { - JSValue value; - if (!js_get_property(js, obj, name, &value) || JS_IsNull(value)) { - if (!JS_IsUndefined(value) && !JS_IsException(value)) JS_FreeValue(js, value); - return defvalue; - } - bool out = JS_ToBool(js, value) != 0; - JS_FreeValue(js, value); - return out; -} - -static std::string js_exception_string(JSContext* ctx) { - JSValue exception = JS_GetException(ctx); - std::string msg = js_to_string(ctx, exception); - JS_FreeValue(ctx, exception); - return msg.empty() ? "javascript exception" : msg; -} - static JsHttpTask* js_get_task(JSContext* js) { - return (JsHttpTask*)JS_GetContextOpaque(js); + return static_cast(hv::js::hvjs_get_task(js)); } static JSValue js_ctx_method(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { @@ -333,8 +61,8 @@ static JSValue js_ctx_query(JSContext* js, JSValueConst this_val, int argc, JSVa if (task == NULL || !task->ctx) { return JS_ThrowTypeError(js, "invalid HttpContext"); } - std::string key = argc > 0 ? js_to_string(js, argv[0]) : std::string(); - std::string defvalue = argc > 1 ? js_to_string(js, argv[1]) : std::string(); + std::string key = argc > 0 ? hv::js::hvjs_to_string(js, argv[0]) : std::string(); + std::string defvalue = argc > 1 ? hv::js::hvjs_to_string(js, argv[1]) : std::string(); std::string value = task->ctx->param(key.c_str(), defvalue); return JS_NewStringLen(js, value.data(), value.size()); } @@ -345,8 +73,8 @@ static JSValue js_ctx_header(JSContext* js, JSValueConst this_val, int argc, JSV if (task == NULL || !task->ctx) { return JS_ThrowTypeError(js, "invalid HttpContext"); } - std::string key = argc > 0 ? js_to_string(js, argv[0]) : std::string(); - std::string defvalue = argc > 1 ? js_to_string(js, argv[1]) : std::string(); + std::string key = argc > 0 ? hv::js::hvjs_to_string(js, argv[0]) : std::string(); + std::string defvalue = argc > 1 ? hv::js::hvjs_to_string(js, argv[1]) : std::string(); std::string value = task->ctx->header(key.c_str(), defvalue); return JS_NewStringLen(js, value.data(), value.size()); } @@ -381,8 +109,8 @@ static JSValue js_ctx_set_header(JSContext* js, JSValueConst this_val, int argc, if (task == NULL || !task->ctx || argc < 2) { return JS_ThrowTypeError(js, "invalid HttpContext"); } - std::string key = js_to_string(js, argv[0]); - std::string value = js_to_string(js, argv[1]); + std::string key = hv::js::hvjs_to_string(js, argv[0]); + std::string value = hv::js::hvjs_to_string(js, argv[1]); task->ctx->setHeader(key.c_str(), value); return JS_UNDEFINED; } @@ -393,7 +121,7 @@ static JSValue js_ctx_text(JSContext* js, JSValueConst this_val, int argc, JSVal if (task == NULL || !task->ctx || argc < 1) { return JS_ThrowTypeError(js, "invalid HttpContext"); } - std::string text = js_to_string(js, argv[0]); + std::string text = hv::js::hvjs_to_string(js, argv[0]); task->ctx->response->String(text); return JS_NewInt32(js, task->ctx->response->status_code); } @@ -406,7 +134,7 @@ static JSValue js_ctx_json(JSContext* js, JSValueConst this_val, int argc, JSVal } JSValue json = JS_JSONStringify(js, argv[0], JS_UNDEFINED, JS_UNDEFINED); if (JS_IsException(json)) return json; - std::string body = js_to_string(js, json); + std::string body = hv::js::hvjs_to_string(js, json); JS_FreeValue(js, json); task->ctx->response->SetContentType(APPLICATION_JSON); task->ctx->response->body = body; @@ -432,1128 +160,8 @@ static JSValue js_new_ctx(JSContext* js, const HttpContextPtr& ctx) { static void task_finish(JsHttpTask* task, JSValue result); -static void drain_jobs(JsHttpTask* task) { - JSContext* job_ctx = NULL; - while (JS_IsJobPending(task->rt)) { - int rc = JS_ExecutePendingJob(task->rt, &job_ctx); - if (rc < 0) { - task->error = js_exception_string(job_ctx ? job_ctx : task->js); - break; - } - } - if (!task->finished && !JS_IsUndefined(task->promise)) { - JSPromiseStateEnum state = JS_PromiseState(task->js, task->promise); - if (state != JS_PROMISE_PENDING) { - JSValue value = JS_PromiseResult(task->js, task->promise); - task_finish(task, value); - return; - } - } - if (!task->error.empty()) { - task_finish(task, JS_UNDEFINED); - } -} - -static void sleep_timer_cb(htimer_t* timer) { - JsSleep* sleep = (JsSleep*)hevent_userdata(timer); - js_promise_resolve(sleep, JS_UNDEFINED); -} - -static JSValue js_hv_sleep(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - (void)this_val; - JsHttpTask* task = (JsHttpTask*)JS_GetContextOpaque(js); - if (task == NULL || argc < 1) return JS_EXCEPTION; - int32_t ms = 0; - if (JS_ToInt32(js, &ms, argv[0]) != 0) return JS_EXCEPTION; - JSValue funcs[2]; - JSValue promise = JS_NewPromiseCapability(js, funcs); - if (JS_IsException(promise)) return promise; - - JsSleep* sleep = new JsSleep(); - sleep->task = task; - sleep->resolve = funcs[0]; - JS_FreeValue(js, funcs[1]); - task_ref(task); - if (task->loop_ptr) { - sleep->timer_id = task->loop_ptr->setTimeout(ms, [sleep](TimerID) { js_promise_resolve(sleep, JS_UNDEFINED); }); - } - else { - sleep->timer = htimer_add(task->loop, sleep_timer_cb, (uint32_t)ms, 1); - if (sleep->timer) hevent_set_userdata(sleep->timer, sleep); - } - if (sleep->timer == NULL && sleep->timer_id == INVALID_TIMER_ID) { - task_unref(task); - JS_FreeValue(js, sleep->resolve); - delete sleep; - JS_FreeValue(js, promise); - return JS_ThrowInternalError(js, "hv.sleep: failed to create timer"); - } - return promise; -} - -static JSValue js_hv_version(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - (void)this_val; - (void)argc; - (void)argv; - return JS_NewString(js, HV_VERSION_STRING); -} - -static JSValue js_hv_log(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - (void)this_val; - std::string line; - for (int i = 0; i < argc; ++i) { - if (i != 0) line += "\\t"; - line += js_to_string(js, argv[i]); - } - hlogi("%s", line.c_str()); - return JS_UNDEFINED; -} -#ifdef HVJS_WITH_HTTP -struct JsHttpRequest : public JsPromiseOp { - std::shared_ptr client; -}; - -static JSValue js_push_headers(JSContext* js, const http_headers& headers) { - JSValue obj = JS_NewObject(js); - for (auto& kv : headers) { - JS_SetPropertyStr(js, obj, kv.first.c_str(), JS_NewStringLen(js, kv.second.data(), kv.second.size())); - } - return obj; -} - -static JSValue js_push_http_response(JSContext* js, const HttpResponsePtr& resp) { - JSValue obj = JS_NewObject(js); - JS_SetPropertyStr(js, obj, "status", JS_NewInt32(js, resp ? resp->status_code : 0)); - if (resp) { - JS_SetPropertyStr(js, obj, "body", JS_NewStringLen(js, resp->body.data(), resp->body.size())); - JS_SetPropertyStr(js, obj, "headers", js_push_headers(js, resp->headers)); - } - else { - JS_SetPropertyStr(js, obj, "body", JS_NewString(js, "")); - JS_SetPropertyStr(js, obj, "headers", JS_NewObject(js)); - } - return obj; -} - -static int js_fill_http_request(JSContext* js, JSValueConst* argv, int argc, http_method method, int url_index, HttpRequestPtr* out) { - if (argc <= url_index) { - JS_ThrowTypeError(js, "missing url"); - return -1; - } - std::string url = js_to_string(js, argv[url_index]); - auto req = std::make_shared(); - req->method = method; - req->url = url; - if (argc > url_index + 1 && !JS_IsUndefined(argv[url_index + 1]) && !JS_IsNull(argv[url_index + 1])) { - std::string body = js_to_string(js, argv[url_index + 1]); - req->body = body; - } - if (argc > url_index + 2 && JS_IsObject(argv[url_index + 2])) { - JSPropertyEnum* tab = NULL; - uint32_t len = 0; - if (JS_GetOwnPropertyNames(js, &tab, &len, argv[url_index + 2], JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) == 0) { - for (uint32_t i = 0; i < len; ++i) { - JSValue key = JS_AtomToString(js, tab[i].atom); - JSValue value = JS_GetProperty(js, argv[url_index + 2], tab[i].atom); - std::string k = js_to_string(js, key); - std::string v = js_to_string(js, value); - if (!k.empty()) req->headers[k] = v; - JS_FreeValue(js, value); - JS_FreeValue(js, key); - } - JS_FreePropertyEnum(js, tab, len); - } - } - *out = req; - return 0; -} - -static JSValue js_http_request(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv, int magic) { - (void)this_val; - JsHttpTask* task = js_get_task(js); - if (task == NULL || !task->loop_ptr) { - return js_rejected_promise(js, "hv.http: no shared event loop on this thread"); - } - http_method method = (http_method)magic; - int url_index = 0; - if (magic == JS_HTTP_METHOD_REQUEST) { - if (argc < 2) return js_rejected_promise(js, "hv.http: request needs method and url"); - std::string m = js_to_string(js, argv[0]); - toupper(m); - method = http_method_enum(m.c_str()); - url_index = 1; - } - if (method == HTTP_CUSTOM_METHOD) { - return js_rejected_promise(js, "hv.http: unsupported method"); - } - - HttpRequestPtr req; - if (js_fill_http_request(js, argv, argc, method, url_index, &req) != 0) { - return JS_EXCEPTION; - } - - JsHttpRequest* op = NULL; - JSValue promise = js_new_promise(js, task, &op); - if (JS_IsException(promise)) return promise; - op->client = std::make_shared(task->loop_ptr); - std::shared_ptr client = op->client; - task->in_call = true; - int ret = client->send(req, [op, client](const HttpResponsePtr& resp) { - if (op->task->loop_ptr) { - op->task->loop_ptr->queueInLoop([client]() {}); - } - JSContext* js = op->task->js; - if (resp) { - js_promise_resolve(op, js_push_http_response(js, resp)); - } - else { - js_promise_reject(op, "hv.http: request failed"); - } - }); - if (ret != 0) { - js_promise_reject(op, "hv.http: request failed"); - } - task->in_call = false; - js_finish_deferred_op(op); - return promise; -} - -static JSValue js_require_http(JSContext* js) { - JSValue http = JS_NewObject(js); - JS_SetPropertyStr(js, http, "request", JS_NewCFunctionMagic(js, js_http_request, "request", 2, JS_CFUNC_generic_magic, JS_HTTP_METHOD_REQUEST)); - JS_SetPropertyStr(js, http, "get", JS_NewCFunctionMagic(js, js_http_request, "get", 1, JS_CFUNC_generic_magic, HTTP_GET)); - JS_SetPropertyStr(js, http, "post", JS_NewCFunctionMagic(js, js_http_request, "post", 2, JS_CFUNC_generic_magic, HTTP_POST)); - JS_SetPropertyStr(js, http, "put", JS_NewCFunctionMagic(js, js_http_request, "put", 2, JS_CFUNC_generic_magic, HTTP_PUT)); - JS_SetPropertyStr(js, http, "delete", JS_NewCFunctionMagic(js, js_http_request, "delete", 1, JS_CFUNC_generic_magic, HTTP_DELETE)); - return http; -} -#endif -#ifdef HVJS_WITH_REDIS -static JSClassID s_redis_class_id; -static std::once_flag s_redis_class_once; - -struct JsRedisState { - std::shared_ptr client; - bool destroyed; - - JsRedisState() : destroyed(false) {} - - ~JsRedisState() { - destroyed = true; - if (client) { - client->stop(true); - client.reset(); - } - } -}; - -struct JsRedisClient { - std::shared_ptr state; -}; - -struct JsRedisCommand : public JsPromiseOp { - std::shared_ptr redis; -}; - -static void js_redis_finalizer(JSRuntime* rt, JSValue val) { - (void)rt; - JsRedisClient* box = (JsRedisClient*)JS_GetOpaque(val, s_redis_class_id); - if (box) { - delete box; - } -} - -static JsRedisClient* js_redis_client(JSContext* js, JSValueConst this_val) { - JsRedisClient* box = (JsRedisClient*)JS_GetOpaque2(js, this_val, s_redis_class_id); - return box; -} - -static void js_redis_register_class(JSContext* js) { - std::call_once(s_redis_class_once, []() { js_new_class_id(&s_redis_class_id); }); - JSRuntime* rt = JS_GetRuntime(js); - if (!JS_IsRegisteredClass(rt, s_redis_class_id)) { - JSClassDef def; - memset(&def, 0, sizeof(def)); - def.class_name = "hv.redis.client"; - def.finalizer = js_redis_finalizer; - JS_NewClass(rt, s_redis_class_id, &def); - } -} - -static JSValue js_push_redis_reply(JSContext* js, const RedisReply& reply) { - switch (reply.type) { - case REDIS_REPLY_STRING: return JS_NewStringLen(js, reply.str.data(), reply.str.size()); - case REDIS_REPLY_INTEGER: return JS_NewInt64(js, reply.integer); - case REDIS_REPLY_ARRAY: { - if (reply.null_array) return JS_NULL; - JSValue arr = JS_NewArray(js); - for (uint32_t i = 0; i < reply.elements.size(); ++i) { - JSValue item = reply.elements[i].isNil() ? JS_NULL : js_push_redis_reply(js, reply.elements[i]); - JS_SetPropertyUint32(js, arr, i, item); - } - return arr; - } - case REDIS_REPLY_NIL: - default: return JS_NULL; - } -} - -static void js_redis_resolve_result(JsRedisCommand* op, const RedisResult& result) { - JSContext* js = op->task->js; - if (!op->redis || op->redis->destroyed) { - js_promise_reject(op, "hv.redis: client closed"); - return; - } - if (result.code != 0) { - char err[64]; - snprintf(err, sizeof(err), "hv.redis: request failed (%d)", result.code); - js_promise_reject(op, err); - return; - } - if (result.reply.isError()) { - js_promise_reject(op, result.reply.error().c_str()); - return; - } - js_promise_resolve(op, js_push_redis_reply(js, result.reply)); -} - -static bool js_build_redis_command(JSContext* js, JSValueConst* argv, int argc, int first, RedisCommand* cmd) { - if (argc <= first) return false; - if (JS_IsArray(js, argv[first]) && argc == first + 1) { - JSValue lenv = JS_GetPropertyStr(js, argv[first], "length"); - uint32_t len = 0; - JS_ToUint32(js, &len, lenv); - JS_FreeValue(js, lenv); - for (uint32_t i = 0; i < len; ++i) { - JSValue item = JS_GetPropertyUint32(js, argv[first], i); - cmd->push_back(js_to_string(js, item)); - JS_FreeValue(js, item); - } - } - else { - for (int i = first; i < argc; ++i) { - cmd->push_back(js_to_string(js, argv[i])); - } - } - return !cmd->empty(); -} - -static const char* js_redis_verb_name(int magic) { - switch (magic) { - case 1: return "GET"; - case 2: return "SET"; - case 3: return "DEL"; - case 4: return "INCR"; - case 5: return "DECR"; - case 6: return "EXPIRE"; - case 7: return "EXISTS"; - default: return NULL; - } -} - -static JSValue js_redis_command(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv, int magic) { - JsRedisClient* box = js_redis_client(js, this_val); - JsRedisState* state = box ? box->state.get() : NULL; - if (state == NULL || !state->client || state->destroyed) { - return js_rejected_promise(js, "hv.redis: client closed"); - } - RedisCommand cmd; - if (magic != 0) { - const char* verb = js_redis_verb_name(magic); - if (verb == NULL) { - return js_rejected_promise(js, "hv.redis: unknown command"); - } - cmd.push_back(verb); - for (int i = 0; i < argc; ++i) { - cmd.push_back(js_to_string(js, argv[i])); - } - } - else if (!js_build_redis_command(js, argv, argc, 0, &cmd)) { - return js_rejected_promise(js, "hv.redis: empty or invalid command"); - } - - JsHttpTask* task = js_get_task(js); - JsRedisCommand* op = NULL; - JSValue promise = js_new_promise(js, task, &op); - if (JS_IsException(promise)) return promise; - op->redis = box->state; - task->in_call = true; - int ret = state->client->command(cmd, [op](const RedisResult& result) { js_redis_resolve_result(op, result); }); - if (ret != 0) { - js_promise_reject(op, "hv.redis: request failed"); - } - task->in_call = false; - js_finish_deferred_op(op); - return promise; -} - -static JSValue js_redis_new(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - (void)this_val; - JsHttpTask* task = js_get_task(js); - if (task == NULL || !task->loop_ptr) { - return JS_ThrowTypeError(js, "hv.redis: no shared event loop on this thread"); - } - js_redis_register_class(js); - - std::string host = "127.0.0.1"; - int port = 6379; - std::string auth; - int db = 0; - int timeout = 0; - if (argc > 0 && JS_IsObject(argv[0])) { - host = js_get_string_property(js, argv[0], "host", "127.0.0.1"); - port = js_get_int_property(js, argv[0], "port", 6379); - auth = js_get_string_property(js, argv[0], "auth", ""); - db = js_get_int_property(js, argv[0], "db", 0); - timeout = js_get_int_property(js, argv[0], "timeout", 0); - } - - JSValue obj = JS_NewObjectClass(js, s_redis_class_id); - if (JS_IsException(obj)) return obj; - JsRedisClient* box = new JsRedisClient(); - box->state = std::make_shared(); - box->state->client = std::make_shared(task->loop_ptr); - box->state->client->setHost(host); - box->state->client->setPort(port); - if (!auth.empty()) box->state->client->setAuth(auth); - if (db > 0) box->state->client->setDb(db); - if (timeout > 0) box->state->client->setTimeout(timeout); - box->state->client->start(false); - JS_SetOpaque(obj, box); - - JS_SetPropertyStr(js, obj, "command", JS_NewCFunctionMagic(js, js_redis_command, "command", 1, JS_CFUNC_generic_magic, 0)); - static const char* verbs[] = {"GET", "SET", "DEL", "INCR", "DECR", "EXPIRE", "EXISTS", NULL}; - for (int i = 0; verbs[i]; ++i) { - std::string name = verbs[i]; - for (char& c : name) c = (char)::tolower((unsigned char)c); - JS_SetPropertyStr(js, obj, name.c_str(), JS_NewCFunctionMagic(js, js_redis_command, name.c_str(), 1, JS_CFUNC_generic_magic, i + 1)); - } - return obj; -} - -static JSValue js_require_redis(JSContext* js) { - JSValue redis = JS_NewObject(js); - JS_SetPropertyStr(js, redis, "new", JS_NewCFunction(js, js_redis_new, "new", 1)); - return redis; -} -#endif -#ifdef HVJS_WITH_HTTP -static JSClassID s_ws_class_id; -static std::once_flag s_ws_class_once; - -struct JsWsState { - std::shared_ptr client; - std::deque inbox; - JsPromiseOp* connect_op; - JsPromiseOp* recv_op; - bool js_alive; - bool connected; - bool closed; - - JsWsState() : connect_op(NULL), recv_op(NULL), js_alive(false), connected(false), closed(false) {} - - void detach() { - closed = true; - connected = false; - if (client) { - client->onopen = NULL; - client->onmessage = NULL; - client->onclose = NULL; - client->close(); - client.reset(); - } - } - - ~JsWsState() { detach(); } -}; - -struct JsWsClient { - std::shared_ptr state; -}; - -struct JsWsConnect : public JsPromiseOp { - std::shared_ptr state; -}; - -struct JsWsRecv : public JsPromiseOp { - std::shared_ptr state; -}; - -static JsWsClient* js_ws_client(JSContext* js, JSValueConst this_val) { - return (JsWsClient*)JS_GetOpaque2(js, this_val, s_ws_class_id); -} - -static void js_ws_detach_after_callback(const EventLoopPtr& loop, const std::shared_ptr& state) { - if (!state) return; - if (loop) { - loop->queueInLoop([state]() { state->detach(); }); - } - else { - state->detach(); - } -} - -static void js_ws_finalizer(JSRuntime* rt, JSValue val) { - (void)rt; - JsWsClient* box = (JsWsClient*)JS_GetOpaque(val, s_ws_class_id); - if (box && box->state) { - box->state->js_alive = false; - if (box->state->connect_op == NULL && box->state->recv_op == NULL) { - box->state->detach(); - } - } - delete box; -} - -static void js_ws_register_class(JSContext* js) { - std::call_once(s_ws_class_once, []() { js_new_class_id(&s_ws_class_id); }); - JSRuntime* rt = JS_GetRuntime(js); - if (!JS_IsRegisteredClass(rt, s_ws_class_id)) { - JSClassDef def; - memset(&def, 0, sizeof(def)); - def.class_name = "hv.ws.client"; - def.finalizer = js_ws_finalizer; - JS_NewClass(rt, s_ws_class_id, &def); - } -} - -static void js_ws_try_deliver(const std::shared_ptr& state) { - if (!state || state->recv_op == NULL) return; - JsWsRecv* op = static_cast(state->recv_op); - std::shared_ptr hold = op->state; - EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); - if (!state->inbox.empty()) { - std::string msg = std::move(state->inbox.front()); - state->inbox.pop_front(); - state->recv_op = NULL; - js_promise_resolve(op, JS_NewStringLen(op->task->js, msg.data(), msg.size())); - } - else if (state->closed) { - state->recv_op = NULL; - js_promise_reject(op, "closed"); - } - if (!hold->js_alive && hold->connect_op == NULL && hold->recv_op == NULL) { - js_ws_detach_after_callback(loop, hold); - } -} - -static JSValue js_ws_send(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - JsWsClient* box = js_ws_client(js, this_val); - JsWsState* state = box ? box->state.get() : NULL; - if (state == NULL || !state->client || !state->connected) { - return JS_ThrowTypeError(js, "hv.ws: closed"); - } - std::string msg = argc > 0 ? js_to_string(js, argv[0]) : std::string(); - enum ws_opcode opcode = WS_OPCODE_TEXT; - if (argc > 1 && js_to_string(js, argv[1]) == "binary") { - opcode = WS_OPCODE_BINARY; - } - int ret = state->client->send(msg.data(), (int)msg.size(), opcode); - if (ret < 0) { - return JS_ThrowInternalError(js, "hv.ws: send failed"); - } - return JS_NewInt32(js, ret); -} - -static JSValue js_ws_recv(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - (void)argc; - (void)argv; - JsWsClient* box = js_ws_client(js, this_val); - JsWsState* state = box ? box->state.get() : NULL; - if (state == NULL || !state->client) { - return js_rejected_promise(js, "closed"); - } - if (!state->inbox.empty()) { - std::string msg = std::move(state->inbox.front()); - state->inbox.pop_front(); - return js_async_resolved_promise(js, js_get_task(js), JS_NewStringLen(js, msg.data(), msg.size())); - } - if (state->closed || !state->connected) { - return js_rejected_promise(js, "closed"); - } - if (state->recv_op != NULL) { - return js_rejected_promise(js, "hv.ws: recv already pending"); - } - JsHttpTask* task = js_get_task(js); - JsWsRecv* op = NULL; - JSValue promise = js_new_promise(js, task, &op); - if (JS_IsException(promise)) return promise; - op->state = box->state; - state->recv_op = op; - return promise; -} - -static JSValue js_ws_close(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - (void)argc; - (void)argv; - JsWsClient* box = js_ws_client(js, this_val); - if (box && box->state) { - std::shared_ptr state = box->state; - if (state->connect_op) { - JsPromiseOp* op = state->connect_op; - state->connect_op = NULL; - js_promise_reject(op, "closed"); - } - if (state->recv_op) { - JsPromiseOp* op = state->recv_op; - state->recv_op = NULL; - js_promise_reject(op, "closed"); - } - state->detach(); - } - return JS_UNDEFINED; -} - -static JSValue js_ws_new_client_object(JSContext* js, const std::shared_ptr& state) { - JSValue obj = JS_NewObjectClass(js, s_ws_class_id); - if (JS_IsException(obj)) return obj; - JsWsClient* box = new JsWsClient(); - box->state = state; - state->js_alive = true; - JS_SetOpaque(obj, box); - JS_SetPropertyStr(js, obj, "send", JS_NewCFunction(js, js_ws_send, "send", 1)); - JS_SetPropertyStr(js, obj, "recv", JS_NewCFunction(js, js_ws_recv, "recv", 0)); - JS_SetPropertyStr(js, obj, "close", JS_NewCFunction(js, js_ws_close, "close", 0)); - return obj; -} - -static JSValue js_ws_connect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - (void)this_val; - JsHttpTask* task = js_get_task(js); - if (task == NULL || !task->loop_ptr) { - return js_rejected_promise(js, "hv.ws: no shared event loop on this thread"); - } - if (argc < 1) { - return js_rejected_promise(js, "hv.ws: connect needs url"); - } - std::string url = js_to_string(js, argv[0]); - js_ws_register_class(js); - std::shared_ptr state = std::make_shared(); - state->client = std::make_shared(task->loop_ptr); - - JsWsConnect* op = NULL; - JSValue promise = js_new_promise(js, task, &op); - if (JS_IsException(promise)) return promise; - state->connect_op = op; - op->state = state; - state->client->onopen = [state]() { - state->connected = true; - state->closed = false; - if (state->connect_op) { - JsWsConnect* op = static_cast(state->connect_op); - std::shared_ptr hold = op->state; - EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); - state->connect_op = NULL; - JSValue obj = js_ws_new_client_object(op->task->js, hold); - if (JS_IsException(obj)) { - js_promise_reject(op, "hv.ws: create client failed"); - js_ws_detach_after_callback(loop, hold); - } - else { - js_promise_resolve(op, obj); - } - } - }; - state->client->onmessage = [state](const std::string& msg) { - state->inbox.push_back(msg); - js_ws_try_deliver(state); - }; - state->client->onclose = [state]() { - state->connected = false; - state->closed = true; - if (state->connect_op) { - JsWsConnect* op = static_cast(state->connect_op); - std::shared_ptr hold = op->state; - EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); - state->connect_op = NULL; - js_promise_reject(op, "closed"); - js_ws_detach_after_callback(loop, hold); - } - js_ws_try_deliver(state); - }; - task->in_call = true; - int ret = state->client->open(url.c_str()); - if (ret != 0) { - state->connect_op = NULL; - js_promise_reject(op, "hv.ws: open failed"); - state->detach(); - } - task->in_call = false; - js_finish_deferred_op(op); - return promise; -} - -static JSValue js_require_ws(JSContext* js) { - JSValue ws = JS_NewObject(js); - JS_SetPropertyStr(js, ws, "connect", JS_NewCFunction(js, js_ws_connect, "connect", 1)); - return ws; -} -#endif -#ifdef HVJS_WITH_MQTT -static JSClassID s_mqtt_class_id; -static std::once_flag s_mqtt_class_once; - -struct JsMqttMessage { - std::string topic; - std::string payload; - int qos; -}; - -struct JsMqttState { - mqtt_client_t* client; - std::deque inbox; - JsPromiseOp* connect_op; - JsPromiseOp* recv_op; - bool js_alive; - bool closed; - bool reconnect; - - JsMqttState() : client(NULL), connect_op(NULL), recv_op(NULL), js_alive(false), closed(false), reconnect(false) {} - - void detach() { - closed = true; - if (client) { - mqtt_client_set_callback(client, NULL); - mqtt_client_set_userdata(client, NULL); - mqtt_client_free(client); - client = NULL; - } - } - - ~JsMqttState() { detach(); } -}; - -struct JsMqttClient { - std::shared_ptr state; -}; - -struct JsMqttConnect : public JsPromiseOp { - std::shared_ptr state; -}; - -struct JsMqttRecv : public JsPromiseOp { - std::shared_ptr state; -}; - -struct JsMqttDetachEvent { - std::shared_ptr state; -}; - -static JsMqttClient* js_mqtt_client(JSContext* js, JSValueConst this_val) { - return (JsMqttClient*)JS_GetOpaque2(js, this_val, s_mqtt_class_id); -} - -static JSValue js_mqtt_recv(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); -static JSValue js_mqtt_publish(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); -static JSValue js_mqtt_subscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); -static JSValue js_mqtt_unsubscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); -static JSValue js_mqtt_disconnect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); -static void js_mqtt_detach_after_callback(const EventLoopPtr& loop, hloop_t* raw_loop, const std::shared_ptr& state); - -static void js_mqtt_finalizer(JSRuntime* rt, JSValue val) { - (void)rt; - JsMqttClient* box = (JsMqttClient*)JS_GetOpaque(val, s_mqtt_class_id); - if (box && box->state) { - box->state->js_alive = false; - if (box->state->connect_op == NULL && box->state->recv_op == NULL) { - box->state->detach(); - } - } - delete box; -} - -static void js_mqtt_register_class(JSContext* js) { - std::call_once(s_mqtt_class_once, []() { js_new_class_id(&s_mqtt_class_id); }); - JSRuntime* rt = JS_GetRuntime(js); - if (!JS_IsRegisteredClass(rt, s_mqtt_class_id)) { - JSClassDef def; - memset(&def, 0, sizeof(def)); - def.class_name = "hv.mqtt.client"; - def.finalizer = js_mqtt_finalizer; - JS_NewClass(rt, s_mqtt_class_id, &def); - } -} - -static JSValue js_mqtt_new_client_object(JSContext* js, const std::shared_ptr& state) { - JSValue obj = JS_NewObjectClass(js, s_mqtt_class_id); - if (JS_IsException(obj)) return obj; - JsMqttClient* box = new JsMqttClient(); - box->state = state; - state->js_alive = true; - JS_SetOpaque(obj, box); - JS_SetPropertyStr(js, obj, "recv", JS_NewCFunction(js, js_mqtt_recv, "recv", 0)); - JS_SetPropertyStr(js, obj, "publish", JS_NewCFunction(js, js_mqtt_publish, "publish", 2)); - JS_SetPropertyStr(js, obj, "subscribe", JS_NewCFunction(js, js_mqtt_subscribe, "subscribe", 1)); - JS_SetPropertyStr(js, obj, "unsubscribe", JS_NewCFunction(js, js_mqtt_unsubscribe, "unsubscribe", 1)); - JS_SetPropertyStr(js, obj, "disconnect", JS_NewCFunction(js, js_mqtt_disconnect, "disconnect", 0)); - return obj; -} - -static JSValue js_push_mqtt_message(JSContext* js, const JsMqttMessage& msg) { - JSValue obj = JS_NewObject(js); - JS_SetPropertyStr(js, obj, "topic", JS_NewStringLen(js, msg.topic.data(), msg.topic.size())); - JS_SetPropertyStr(js, obj, "payload", JS_NewStringLen(js, msg.payload.data(), msg.payload.size())); - JS_SetPropertyStr(js, obj, "qos", JS_NewInt32(js, msg.qos)); - return obj; -} - -static const char* js_mqtt_closed_reason(const JsMqttState* state) { - return state && state->reconnect ? "reconnecting" : "closed"; -} - -static void js_mqtt_try_deliver(JsMqttState* state) { - if (!state || state->recv_op == NULL) return; - JsMqttRecv* op = static_cast(state->recv_op); - std::shared_ptr hold = op->state; - EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); - hloop_t* raw_loop = op->task ? op->task->loop : NULL; - if (!state->inbox.empty()) { - JsMqttMessage msg = std::move(state->inbox.front()); - state->inbox.pop_front(); - state->recv_op = NULL; - js_promise_resolve(op, js_push_mqtt_message(op->task->js, msg)); - } - else if (state->closed) { - state->recv_op = NULL; - js_promise_reject(op, js_mqtt_closed_reason(state)); - } - if (!hold->js_alive && hold->connect_op == NULL && hold->recv_op == NULL) { - js_mqtt_detach_after_callback(loop, raw_loop, hold); - } -} - -static void js_mqtt_detach_event_cb(hevent_t* ev) { - JsMqttDetachEvent* detach = (JsMqttDetachEvent*)hevent_userdata(ev); - if (detach) { - detach->state->detach(); - delete detach; - } -} - -static void js_mqtt_detach_after_callback(const EventLoopPtr& loop, hloop_t* raw_loop, const std::shared_ptr& state) { - if (!state) return; - state->reconnect = false; - if (state->client) { - mqtt_client_set_reconnect(state->client, NULL); - } - if (loop) { - loop->queueInLoop([state]() { state->detach(); }); - } - else if (raw_loop) { - JsMqttDetachEvent* detach = new JsMqttDetachEvent(); - detach->state = state; - hevent_t ev; - memset(&ev, 0, sizeof(ev)); - ev.cb = js_mqtt_detach_event_cb; - ev.userdata = detach; - hloop_post_event(raw_loop, &ev); - } - else { - state->detach(); - } -} - -static void js_mqtt_on_event(mqtt_client_t* client, int type) { - JsMqttState* state = (JsMqttState*)mqtt_client_get_userdata(client); - if (state == NULL) return; - switch (type) { - case MQTT_TYPE_CONNACK: - state->closed = false; - if (state->connect_op) { - JsMqttConnect* op = static_cast(state->connect_op); - std::shared_ptr hold = op->state; - EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); - hloop_t* raw_loop = op->task ? op->task->loop : NULL; - state->connect_op = NULL; - JSValue obj = js_mqtt_new_client_object(op->task->js, hold); - if (JS_IsException(obj)) { - js_promise_reject(op, "hv.mqtt: create client failed"); - js_mqtt_detach_after_callback(loop, raw_loop, hold); - } - else { - js_promise_resolve(op, obj); - } - } - break; - case MQTT_TYPE_PUBLISH: { - JsMqttMessage msg; - if (client->message.topic && client->message.topic_len > 0) { - msg.topic.assign(client->message.topic, client->message.topic_len); - } - if (client->message.payload && client->message.payload_len > 0) { - msg.payload.assign(client->message.payload, client->message.payload_len); - } - msg.qos = client->message.qos; - state->inbox.push_back(std::move(msg)); - js_mqtt_try_deliver(state); - break; - } - case MQTT_TYPE_DISCONNECT: - state->closed = true; - if (state->connect_op) { - JsMqttConnect* op = static_cast(state->connect_op); - std::shared_ptr hold = op->state; - EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); - hloop_t* raw_loop = op->task ? op->task->loop : NULL; - state->connect_op = NULL; - js_promise_reject(op, "connect failed"); - js_mqtt_detach_after_callback(loop, raw_loop, hold); - } - if (state->recv_op) { - JsMqttRecv* op = static_cast(state->recv_op); - std::shared_ptr hold = op->state; - EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); - hloop_t* raw_loop = op->task ? op->task->loop : NULL; - state->recv_op = NULL; - js_promise_reject(op, js_mqtt_closed_reason(state)); - if (!hold->js_alive && hold->connect_op == NULL && hold->recv_op == NULL) { - js_mqtt_detach_after_callback(loop, raw_loop, hold); - } - } - break; - default: break; - } -} - -static bool js_parse_reconnect(JSContext* js, JSValueConst obj, reconn_setting_t* out) { - JSValue reconnect; - if (!js_get_property(js, obj, "reconnect", &reconnect) || !JS_IsObject(reconnect)) { - if (!JS_IsUndefined(reconnect) && !JS_IsException(reconnect)) JS_FreeValue(js, reconnect); - return false; - } - reconn_setting_init(out); - out->min_delay = (uint32_t)js_get_int_property(js, reconnect, "min_delay", (int)out->min_delay); - out->max_delay = (uint32_t)js_get_int_property(js, reconnect, "max_delay", (int)out->max_delay); - out->delay_policy = (uint32_t)js_get_int_property(js, reconnect, "delay_policy", (int)out->delay_policy); - out->max_retry_cnt = (uint32_t)js_get_int_property(js, reconnect, "max_retry", (int)out->max_retry_cnt); - if (out->max_retry_cnt == 0) out->max_retry_cnt = INFINITE; - if (out->min_delay == 0) out->min_delay = 1; - if (out->max_delay < out->min_delay) out->max_delay = out->min_delay; - if (out->delay_policy > 1 && out->delay_policy > UINT32_MAX / out->min_delay) { - out->delay_policy = DEFAULT_RECONNECT_DELAY_POLICY; - } - JS_FreeValue(js, reconnect); - return true; -} - -static JSValue js_mqtt_connect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - (void)this_val; - JsHttpTask* task = js_get_task(js); - if (task == NULL || task->loop == NULL) { - return js_rejected_promise(js, "hv.mqtt: no event loop on this thread"); - } - if (argc < 1 || !JS_IsObject(argv[0])) { - return js_rejected_promise(js, "hv.mqtt: connect needs options"); - } - - std::string host = js_get_string_property(js, argv[0], "host", "127.0.0.1"); - int port = js_get_int_property(js, argv[0], "port", DEFAULT_MQTT_PORT); - int ssl = js_get_bool_property(js, argv[0], "ssl", false) ? 1 : 0; - std::string id = js_get_string_property(js, argv[0], "id", ""); - std::string username = js_get_string_property(js, argv[0], "username", ""); - std::string password = js_get_string_property(js, argv[0], "password", ""); - int keepalive = js_get_int_property(js, argv[0], "keepalive", 0); - int timeout = js_get_int_property(js, argv[0], "connect_timeout", 0); - if (timeout <= 0) timeout = js_get_int_property(js, argv[0], "timeout", 0); - bool clean_session = js_get_bool_property(js, argv[0], "clean_session", true); - - js_mqtt_register_class(js); - std::shared_ptr state = std::make_shared(); - state->client = mqtt_client_new(task->loop); - if (state->client == NULL) { - return js_rejected_promise(js, "hv.mqtt: create client failed"); - } - mqtt_client_set_userdata(state->client, state.get()); - mqtt_client_set_callback(state->client, js_mqtt_on_event); - if (!id.empty()) mqtt_client_set_id(state->client, id.c_str()); - if (!username.empty() || !password.empty()) { - mqtt_client_set_auth(state->client, username.c_str(), password.c_str()); - } - if (keepalive > 0) state->client->keepalive = (unsigned short)keepalive; - state->client->clean_session = clean_session ? 1 : 0; - if (timeout > 0) mqtt_client_set_connect_timeout(state->client, timeout); - reconn_setting_t reconn; - if (js_parse_reconnect(js, argv[0], &reconn)) { - mqtt_client_set_reconnect(state->client, &reconn); - state->reconnect = true; - } - - JsMqttConnect* op = NULL; - JSValue promise = js_new_promise(js, task, &op); - if (JS_IsException(promise)) { - state->detach(); - return promise; - } - state->connect_op = op; - op->state = state; - task->in_call = true; - int ret = mqtt_client_connect(state->client, host.c_str(), port, ssl); - if (ret != 0) { - state->connect_op = NULL; - js_promise_reject(op, "hv.mqtt: connect failed"); - state->detach(); - } - task->in_call = false; - js_finish_deferred_op(op); - return promise; -} - -static JSValue js_mqtt_recv(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - (void)argc; - (void)argv; - JsMqttClient* box = js_mqtt_client(js, this_val); - std::shared_ptr state = box ? box->state : std::shared_ptr(); - if (!state || state->client == NULL) { - return js_rejected_promise(js, "closed"); - } - if (!state->inbox.empty()) { - JsMqttMessage msg = std::move(state->inbox.front()); - state->inbox.pop_front(); - return js_async_resolved_promise(js, js_get_task(js), js_push_mqtt_message(js, msg)); - } - if (state->closed) { - return js_rejected_promise(js, js_mqtt_closed_reason(state.get())); - } - if (state->recv_op != NULL) { - return js_rejected_promise(js, "hv.mqtt: recv already pending"); - } - JsHttpTask* task = js_get_task(js); - JsMqttRecv* op = NULL; - JSValue promise = js_new_promise(js, task, &op); - if (JS_IsException(promise)) return promise; - op->state = state; - state->recv_op = op; - return promise; -} - -static JSValue js_mqtt_publish(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - JsMqttClient* box = js_mqtt_client(js, this_val); - JsMqttState* state = box ? box->state.get() : NULL; - if (state == NULL || state->client == NULL || state->closed) { - return JS_ThrowTypeError(js, "hv.mqtt: closed"); - } - if (argc < 2) { - return JS_ThrowTypeError(js, "hv.mqtt: publish needs topic and payload"); - } - std::string topic = js_to_string(js, argv[0]); - std::string payload = js_to_string(js, argv[1]); - int32_t qos = 0; - if (argc > 2 && JS_ToInt32(js, &qos, argv[2]) != 0) return JS_EXCEPTION; - int retain = argc > 3 ? JS_ToBool(js, argv[3]) : 0; - mqtt_message_t msg; - memset(&msg, 0, sizeof(msg)); - msg.topic = topic.c_str(); - msg.topic_len = (unsigned int)topic.size(); - msg.payload = payload.c_str(); - msg.payload_len = (unsigned int)payload.size(); - msg.qos = (unsigned char)qos; - msg.retain = (unsigned char)retain; - int mid = mqtt_client_publish(state->client, &msg); - if (mid < 0) { - return JS_ThrowInternalError(js, "hv.mqtt: publish failed"); - } - return JS_NewInt32(js, mid); -} - -static JSValue js_mqtt_subscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - JsMqttClient* box = js_mqtt_client(js, this_val); - JsMqttState* state = box ? box->state.get() : NULL; - if (state == NULL || state->client == NULL || state->closed) { - return JS_ThrowTypeError(js, "hv.mqtt: closed"); - } - if (argc < 1) { - return JS_ThrowTypeError(js, "hv.mqtt: subscribe needs topic"); - } - std::string topic = js_to_string(js, argv[0]); - int32_t qos = 0; - if (argc > 1 && JS_ToInt32(js, &qos, argv[1]) != 0) return JS_EXCEPTION; - int mid = mqtt_client_subscribe(state->client, topic.c_str(), qos); - if (mid < 0) { - return JS_ThrowInternalError(js, "hv.mqtt: subscribe failed"); - } - return JS_NewInt32(js, mid); -} - -static JSValue js_mqtt_unsubscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - JsMqttClient* box = js_mqtt_client(js, this_val); - JsMqttState* state = box ? box->state.get() : NULL; - if (state == NULL || state->client == NULL || state->closed) { - return JS_ThrowTypeError(js, "hv.mqtt: closed"); - } - if (argc < 1) { - return JS_ThrowTypeError(js, "hv.mqtt: unsubscribe needs topic"); - } - std::string topic = js_to_string(js, argv[0]); - int mid = mqtt_client_unsubscribe(state->client, topic.c_str()); - if (mid < 0) { - return JS_ThrowInternalError(js, "hv.mqtt: unsubscribe failed"); - } - return JS_NewInt32(js, mid); -} - -static JSValue js_mqtt_disconnect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - (void)argc; - (void)argv; - JsMqttClient* box = js_mqtt_client(js, this_val); - if (box && box->state) { - std::shared_ptr state = box->state; - state->reconnect = false; - if (state->connect_op) { - JsPromiseOp* op = state->connect_op; - state->connect_op = NULL; - js_promise_reject(op, "closed"); - } - if (state->recv_op) { - JsPromiseOp* op = state->recv_op; - state->recv_op = NULL; - js_promise_reject(op, "closed"); - } - state->detach(); - } - return JS_UNDEFINED; -} - -static JSValue js_require_mqtt(JSContext* js) { - JSValue mqtt = JS_NewObject(js); - JS_SetPropertyStr(js, mqtt, "connect", JS_NewCFunction(js, js_mqtt_connect, "connect", 1)); - return mqtt; -} -#endif - -static JSValue js_require(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { - (void)this_val; - if (argc < 1) { - return JS_ThrowTypeError(js, "require needs a module name"); - } - std::string name = js_to_string(js, argv[0]); - if (name == "hv") { - JSValue hv = JS_NewObject(js); - JS_SetPropertyStr(js, hv, "version", JS_NewCFunction(js, js_hv_version, "version", 0)); - JS_SetPropertyStr(js, hv, "log", JS_NewCFunction(js, js_hv_log, "log", 1)); - JS_SetPropertyStr(js, hv, "sleep", JS_NewCFunction(js, js_hv_sleep, "sleep", 1)); - return hv; - } -#ifdef HVJS_WITH_HTTP - if (name == "hv/http") { - return js_require_http(js); - } -#endif -#ifdef HVJS_WITH_REDIS - if (name == "hv/redis") { - return js_require_redis(js); - } -#endif -#ifdef HVJS_WITH_HTTP - if (name == "hv/ws") { - return js_require_ws(js); - } -#endif -#ifdef HVJS_WITH_MQTT - if (name == "hv/mqtt") { - return js_require_mqtt(js); - } -#endif - return JS_ThrowReferenceError(js, "module '%s' is not available", name.c_str()); +static void http_js_task_finish(hv::js::HvJsTask* task, JSValue result) { + task_finish(static_cast(task), result); } static bool load_file(const std::string& filepath, std::string* out, std::string* err) { @@ -1607,19 +215,19 @@ static bool apply_result(JSContext* js, JSValueConst value, const HttpContextPtr return true; } if (JS_IsString(value)) { - std::string body = js_to_string(js, value); + std::string body = hv::js::hvjs_to_string(js, value); ctx->response->String(body); return true; } JSValue json = JS_JSONStringify(js, value, JS_UNDEFINED, JS_UNDEFINED); if (!JS_IsException(json)) { - std::string body = js_to_string(js, json); + std::string body = hv::js::hvjs_to_string(js, json); ctx->response->SetContentType(APPLICATION_JSON); ctx->response->body = body; JS_FreeValue(js, json); return true; } - if (err) *err = js_exception_string(js); + if (err) *err = hv::js::hvjs_exception_string(js); return false; } @@ -1632,7 +240,7 @@ static void task_finish(JsHttpTask* task, JSValue result) { task->ctx->response->String(task->error); } else if (!JS_IsUndefined(task->promise) && JS_PromiseState(task->js, task->promise) == JS_PROMISE_REJECTED) { - std::string err = js_to_string(task->js, result); + std::string err = hv::js::hvjs_to_string(task->js, result); hloge("[js] http handler rejected: %s", err.c_str()); task->ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; task->ctx->response->String(err); @@ -1649,7 +257,7 @@ static void task_finish(JsHttpTask* task, JSValue result) { if (task->async) { task->ctx->send(); } - task_unref(task); + hv::js::hvjs_task_unref(task); } } // namespace @@ -1712,12 +320,13 @@ int HttpJsHandler::operator()(const HttpContextPtr& ctx) { JsHttpTask* task = new JsHttpTask(); task->ctx = ctx; + task->finish = http_js_task_finish; task->rt = JS_NewRuntime(); task->js = task->rt ? JS_NewContext(task->rt) : NULL; if (task->rt == NULL || task->js == NULL) { ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; ctx->response->String("js handler: failed to create quickjs runtime"); - task_unref(task); + hv::js::hvjs_task_unref(task); return HTTP_STATUS_INTERNAL_SERVER_ERROR; } JS_SetContextOpaque(task->js, task); @@ -1737,20 +346,20 @@ int HttpJsHandler::operator()(const HttpContextPtr& ctx) { if (task->loop == NULL) { ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; ctx->response->String("js handler: no event loop on this thread"); - task_unref(task); + hv::js::hvjs_task_unref(task); return HTTP_STATUS_INTERNAL_SERVER_ERROR; } JSValue global = JS_GetGlobalObject(task->js); - JS_SetPropertyStr(task->js, global, "require", JS_NewCFunction(task->js, js_require, "require", 1)); + JS_SetPropertyStr(task->js, global, "require", JS_NewCFunction(task->js, hv::js::hvjs_require, "require", 1)); JSValue eval = JS_Eval(task->js, code.c_str(), code.size(), filepath_.c_str(), JS_EVAL_TYPE_GLOBAL); if (JS_IsException(eval)) { - std::string msg = js_exception_string(task->js); + std::string msg = hv::js::hvjs_exception_string(task->js); JS_FreeValue(task->js, global); ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; ctx->response->String(msg); - task_unref(task); + hv::js::hvjs_task_unref(task); return HTTP_STATUS_INTERNAL_SERVER_ERROR; } JS_FreeValue(task->js, eval); @@ -1760,7 +369,7 @@ int HttpJsHandler::operator()(const HttpContextPtr& ctx) { JS_FreeValue(task->js, global); ctx->response->status_code = HTTP_STATUS_NOT_IMPLEMENTED; ctx->response->String("no js handler function"); - task_unref(task); + hv::js::hvjs_task_unref(task); return HTTP_STATUS_NOT_IMPLEMENTED; } @@ -1769,12 +378,12 @@ int HttpJsHandler::operator()(const HttpContextPtr& ctx) { JS_FreeValue(task->js, js_ctx); JS_FreeValue(task->js, fn); if (JS_IsException(ret)) { - std::string msg = js_exception_string(task->js); + std::string msg = hv::js::hvjs_exception_string(task->js); JS_FreeValue(task->js, global); JS_FreeValue(task->js, ret); ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; ctx->response->String(msg); - task_unref(task); + hv::js::hvjs_task_unref(task); return HTTP_STATUS_INTERNAL_SERVER_ERROR; } @@ -1787,21 +396,21 @@ int HttpJsHandler::operator()(const HttpContextPtr& ctx) { JS_FreeValue(task->js, promise_ctor); JS_FreeValue(task->js, ret); if (JS_IsException(task->promise)) { - std::string msg = js_exception_string(task->js); + std::string msg = hv::js::hvjs_exception_string(task->js); ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; ctx->response->String(msg); - task_unref(task); + hv::js::hvjs_task_unref(task); return HTTP_STATUS_INTERNAL_SERVER_ERROR; } - task_ref(task); - drain_jobs(task); + hv::js::hvjs_task_ref(task); + hv::js::hvjs_drain_jobs(task); bool finished = task->finished; int status = ctx->response->status_code; if (!finished) { task->async = true; } - task_unref(task); + hv::js::hvjs_task_unref(task); if (finished) { return status; } diff --git a/js/hvjs.cpp b/js/hvjs.cpp new file mode 100644 index 000000000..7d5546251 --- /dev/null +++ b/js/hvjs.cpp @@ -0,0 +1,373 @@ +#ifdef WITH_JS + +#include "hvjs.h" + +#include + +#include + +#include "hlog.h" +#include "hversion.h" + +namespace hv { +namespace js { + +namespace { + +struct HvJsSleep : public HvJsPromiseOp { + htimer_t* timer; + TimerID timer_id; + + HvJsSleep() : timer(NULL), timer_id(INVALID_TIMER_ID) {} +}; + +struct HvJsImmediatePromise : public HvJsPromiseOp {}; + +std::mutex& js_class_id_mutex() { + static std::mutex mutex; + return mutex; +} + +void drain_event_cb(hevent_t* ev) { + HvJsTask* task = (HvJsTask*)hevent_userdata(ev); + hvjs_drain_jobs(task); + hvjs_task_unref(task); +} + +void promise_complete(HvJsPromiseOp* op, JSValue value, bool ok) { + HvJsTask* task = op->task; + if (op->completed) { + JS_FreeValue(task->js, value); + return; + } + op->completed = true; + if (!task->closing) { + JSValue func = ok ? op->resolve : op->reject; + JSValue ret = JS_Call(task->js, func, JS_UNDEFINED, 1, &value); + if (JS_IsException(ret) && task->error.empty()) { + task->error = hvjs_exception_string(task->js); + } + JS_FreeValue(task->js, ret); + JS_FreeValue(task->js, value); + JS_FreeValue(task->js, op->resolve); + JS_FreeValue(task->js, op->reject); + op->resolve = JS_UNDEFINED; + op->reject = JS_UNDEFINED; + if (task->in_call) { + op->defer_delete = true; + hvjs_schedule_drain(task); + return; + } + hvjs_schedule_drain(task); + } + else { + JS_FreeValue(task->js, value); + JS_FreeValue(task->js, op->resolve); + JS_FreeValue(task->js, op->reject); + op->resolve = JS_UNDEFINED; + op->reject = JS_UNDEFINED; + } + delete op; + hvjs_task_unref(task); +} + +void sleep_timer_cb(htimer_t* timer) { + HvJsSleep* sleep = (HvJsSleep*)hevent_userdata(timer); + hvjs_promise_resolve(sleep, JS_UNDEFINED); +} + +JSValue js_hv_sleep(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + HvJsTask* task = hvjs_get_task(js); + if (task == NULL || argc < 1) return JS_EXCEPTION; + int32_t ms = 0; + if (JS_ToInt32(js, &ms, argv[0]) != 0) return JS_EXCEPTION; + JSValue funcs[2]; + JSValue promise = JS_NewPromiseCapability(js, funcs); + if (JS_IsException(promise)) return promise; + + HvJsSleep* sleep = new HvJsSleep(); + sleep->task = task; + sleep->resolve = funcs[0]; + JS_FreeValue(js, funcs[1]); + hvjs_task_ref(task); + if (task->loop_ptr) { + sleep->timer_id = task->loop_ptr->setTimeout(ms, [sleep](TimerID) { hvjs_promise_resolve(sleep, JS_UNDEFINED); }); + } + else { + sleep->timer = htimer_add(task->loop, sleep_timer_cb, (uint32_t)ms, 1); + if (sleep->timer) hevent_set_userdata(sleep->timer, sleep); + } + if (sleep->timer == NULL && sleep->timer_id == INVALID_TIMER_ID) { + hvjs_task_unref(task); + JS_FreeValue(js, sleep->resolve); + delete sleep; + JS_FreeValue(js, promise); + return JS_ThrowInternalError(js, "hv.sleep: failed to create timer"); + } + return promise; +} + +JSValue js_hv_version(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + (void)argc; + (void)argv; + return JS_NewString(js, HV_VERSION_STRING); +} + +JSValue js_hv_log(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + std::string line; + for (int i = 0; i < argc; ++i) { + if (i != 0) line += "\t"; + line += hvjs_to_string(js, argv[i]); + } + hlogi("%s", line.c_str()); + return JS_UNDEFINED; +} + +JSValue require_hv(JSContext* js) { + JSValue hv = JS_NewObject(js); + JS_SetPropertyStr(js, hv, "version", JS_NewCFunction(js, js_hv_version, "version", 0)); + JS_SetPropertyStr(js, hv, "log", JS_NewCFunction(js, js_hv_log, "log", 1)); + JS_SetPropertyStr(js, hv, "sleep", JS_NewCFunction(js, js_hv_sleep, "sleep", 1)); + return hv; +} + +} // namespace + +HvJsTask::HvJsTask() : rt(NULL), js(NULL), loop(NULL), promise(JS_UNDEFINED), finished(false), in_call(false), closing(false), refcount(1), finish(NULL) {} + +HvJsTask::~HvJsTask() {} + +HvJsPromiseOp::HvJsPromiseOp() : task(NULL), resolve(JS_UNDEFINED), reject(JS_UNDEFINED), completed(false), defer_delete(false) {} + +HvJsPromiseOp::~HvJsPromiseOp() {} + +void hvjs_task_ref(HvJsTask* task) { + ++task->refcount; +} + +void hvjs_task_unref(HvJsTask* task) { + if (--task->refcount != 0) return; + task->closing = true; + if (!JS_IsUndefined(task->promise)) { + JS_FreeValue(task->js, task->promise); + task->promise = JS_UNDEFINED; + } + if (task->js) { + if (task->rt) { + JS_RunGC(task->rt); + } + JS_FreeContext(task->js); + task->js = NULL; + } + if (task->rt) { + JS_RunGC(task->rt); + JS_FreeRuntime(task->rt); + task->rt = NULL; + } + delete task; +} + +void hvjs_schedule_drain(HvJsTask* task) { + if (task == NULL || task->closing) return; + hvjs_task_ref(task); + if (task->loop_ptr) { + task->loop_ptr->queueInLoop([task]() { + hvjs_drain_jobs(task); + hvjs_task_unref(task); + }); + } + else if (task->loop) { + hevent_t ev; + memset(&ev, 0, sizeof(ev)); + ev.cb = drain_event_cb; + ev.userdata = task; + hloop_post_event(task->loop, &ev); + } + else { + hvjs_task_unref(task); + } +} + +void hvjs_drain_jobs(HvJsTask* task) { + JSContext* job_ctx = NULL; + while (JS_IsJobPending(task->rt)) { + int rc = JS_ExecutePendingJob(task->rt, &job_ctx); + if (rc < 0) { + task->error = hvjs_exception_string(job_ctx ? job_ctx : task->js); + break; + } + } + if (!task->finished && !JS_IsUndefined(task->promise)) { + JSPromiseStateEnum state = JS_PromiseState(task->js, task->promise); + if (state != JS_PROMISE_PENDING) { + JSValue value = JS_PromiseResult(task->js, task->promise); + if (task->finish) { + task->finish(task, value); + } + else { + JS_FreeValue(task->js, value); + task->finished = true; + hvjs_task_unref(task); + } + return; + } + } + if (!task->error.empty()) { + if (task->finish) { + task->finish(task, JS_UNDEFINED); + } + else { + task->finished = true; + hvjs_task_unref(task); + } + } +} + +void hvjs_promise_resolve(HvJsPromiseOp* op, JSValue value) { + promise_complete(op, value, true); +} + +void hvjs_promise_reject(HvJsPromiseOp* op, const char* message) { + promise_complete(op, JS_NewString(op->task->js, message ? message : "error"), false); +} + +JSValue hvjs_rejected_promise(JSContext* js, const char* message) { + JSValue funcs[2]; + JSValue promise = JS_NewPromiseCapability(js, funcs); + if (JS_IsException(promise)) return promise; + JSValue reason = JS_NewString(js, message ? message : "error"); + JSValue ret = JS_Call(js, funcs[1], JS_UNDEFINED, 1, &reason); + JS_FreeValue(js, ret); + JS_FreeValue(js, reason); + JS_FreeValue(js, funcs[0]); + JS_FreeValue(js, funcs[1]); + return promise; +} + +JSValue hvjs_async_resolved_promise(JSContext* js, HvJsTask* task, JSValue value) { + if (task == NULL) { + JS_FreeValue(js, value); + return JS_ThrowInternalError(js, "invalid hvjs task"); + } + HvJsImmediatePromise* op = NULL; + JSValue promise = hvjs_new_promise(js, task, &op); + if (JS_IsException(promise)) { + JS_FreeValue(js, value); + return promise; + } + hvjs_promise_resolve(op, value); + return promise; +} + +void hvjs_finish_deferred_op(HvJsPromiseOp* op) { + if (op == NULL || !op->completed || !op->defer_delete) return; + HvJsTask* task = op->task; + delete op; + hvjs_task_unref(task); +} + +std::string hvjs_to_string(JSContext* ctx, JSValueConst value) { + size_t len = 0; + const char* str = JS_ToCStringLen(ctx, &len, value); + if (str == NULL) return std::string(); + std::string out(str, len); + JS_FreeCString(ctx, str); + return out; +} + +std::string hvjs_exception_string(JSContext* ctx) { + JSValue exception = JS_GetException(ctx); + std::string msg = hvjs_to_string(ctx, exception); + JS_FreeValue(ctx, exception); + return msg.empty() ? "javascript exception" : msg; +} + +bool hvjs_get_property(JSContext* js, JSValueConst obj, const char* name, JSValue* out) { + *out = JS_UNDEFINED; + if (!JS_IsObject(obj)) return false; + *out = JS_GetPropertyStr(js, obj, name); + return !JS_IsUndefined(*out) && !JS_IsException(*out); +} + +std::string hvjs_get_string_property(JSContext* js, JSValueConst obj, const char* name, const char* defvalue) { + JSValue value; + if (!hvjs_get_property(js, obj, name, &value) || JS_IsNull(value)) { + if (!JS_IsUndefined(value) && !JS_IsException(value)) JS_FreeValue(js, value); + return defvalue; + } + std::string out = hvjs_to_string(js, value); + JS_FreeValue(js, value); + return out; +} + +int hvjs_get_int_property(JSContext* js, JSValueConst obj, const char* name, int defvalue) { + JSValue value; + if (!hvjs_get_property(js, obj, name, &value) || JS_IsNull(value)) { + if (!JS_IsUndefined(value) && !JS_IsException(value)) JS_FreeValue(js, value); + return defvalue; + } + int32_t out = defvalue; + JS_ToInt32(js, &out, value); + JS_FreeValue(js, value); + return out; +} + +bool hvjs_get_bool_property(JSContext* js, JSValueConst obj, const char* name, bool defvalue) { + JSValue value; + if (!hvjs_get_property(js, obj, name, &value) || JS_IsNull(value)) { + if (!JS_IsUndefined(value) && !JS_IsException(value)) JS_FreeValue(js, value); + return defvalue; + } + bool out = JS_ToBool(js, value) != 0; + JS_FreeValue(js, value); + return out; +} + +HvJsTask* hvjs_get_task(JSContext* js) { + return (HvJsTask*)JS_GetContextOpaque(js); +} + +void hvjs_new_class_id(JSClassID* class_id) { + std::lock_guard lock(js_class_id_mutex()); + JS_NewClassID(class_id); +} + +JSValue hvjs_require(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + if (argc < 1) { + return JS_ThrowTypeError(js, "require needs a module name"); + } + std::string name = hvjs_to_string(js, argv[0]); + if (name == "hv") { + return require_hv(js); + } +#ifdef HVJS_WITH_HTTP + if (name == "hv/http") { + return hvjs_require_http(js); + } +#endif +#ifdef HVJS_WITH_REDIS + if (name == "hv/redis") { + return hvjs_require_redis(js); + } +#endif +#ifdef HVJS_WITH_HTTP + if (name == "hv/ws") { + return hvjs_require_ws(js); + } +#endif +#ifdef HVJS_WITH_MQTT + if (name == "hv/mqtt") { + return hvjs_require_mqtt(js); + } +#endif + return JS_ThrowReferenceError(js, "module '%s' is not available", name.c_str()); +} + +} // namespace js +} // namespace hv + +#endif // WITH_JS diff --git a/js/hvjs.h b/js/hvjs.h new file mode 100644 index 000000000..baf6a7173 --- /dev/null +++ b/js/hvjs.h @@ -0,0 +1,93 @@ +#ifndef HV_JS_H_ +#define HV_JS_H_ + +#include + +#include + +#include "EventLoop.h" +#include "hexport.h" + +namespace hv { +namespace js { + +struct HV_EXPORT HvJsTask { + typedef void (*FinishCallback)(HvJsTask* task, JSValue result); + + JSRuntime* rt; + JSContext* js; + hloop_t* loop; + EventLoopPtr loop_ptr; + JSValue promise; + bool finished; + bool in_call; + bool closing; + int refcount; + std::string error; + FinishCallback finish; + + HvJsTask(); + virtual ~HvJsTask(); +}; + +struct HV_EXPORT HvJsPromiseOp { + HvJsTask* task; + JSValue resolve; + JSValue reject; + bool completed; + bool defer_delete; + + HvJsPromiseOp(); + virtual ~HvJsPromiseOp(); +}; + +HV_EXPORT void hvjs_task_ref(HvJsTask* task); +HV_EXPORT void hvjs_task_unref(HvJsTask* task); +HV_EXPORT void hvjs_schedule_drain(HvJsTask* task); +HV_EXPORT void hvjs_drain_jobs(HvJsTask* task); + +template JSValue hvjs_new_promise(JSContext* js, HvJsTask* task, T** out) { + JSValue funcs[2]; + JSValue promise = JS_NewPromiseCapability(js, funcs); + if (JS_IsException(promise)) return promise; + T* op = new T(); + op->task = task; + op->resolve = funcs[0]; + op->reject = funcs[1]; + hvjs_task_ref(task); + *out = op; + return promise; +} + +HV_EXPORT void hvjs_promise_resolve(HvJsPromiseOp* op, JSValue value); +HV_EXPORT void hvjs_promise_reject(HvJsPromiseOp* op, const char* message); +HV_EXPORT JSValue hvjs_rejected_promise(JSContext* js, const char* message); +HV_EXPORT JSValue hvjs_async_resolved_promise(JSContext* js, HvJsTask* task, JSValue value); +HV_EXPORT void hvjs_finish_deferred_op(HvJsPromiseOp* op); + +HV_EXPORT std::string hvjs_to_string(JSContext* ctx, JSValueConst value); +HV_EXPORT std::string hvjs_exception_string(JSContext* ctx); +HV_EXPORT bool hvjs_get_property(JSContext* js, JSValueConst obj, const char* name, JSValue* out); +HV_EXPORT std::string hvjs_get_string_property(JSContext* js, JSValueConst obj, const char* name, const char* defvalue = ""); +HV_EXPORT int hvjs_get_int_property(JSContext* js, JSValueConst obj, const char* name, int defvalue = 0); +HV_EXPORT bool hvjs_get_bool_property(JSContext* js, JSValueConst obj, const char* name, bool defvalue = false); +HV_EXPORT HvJsTask* hvjs_get_task(JSContext* js); +HV_EXPORT void hvjs_new_class_id(JSClassID* class_id); + +HV_EXPORT JSValue hvjs_require(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); + +#ifdef HVJS_WITH_HTTP +HV_EXPORT JSValue hvjs_require_http(JSContext* js); +HV_EXPORT JSValue hvjs_require_ws(JSContext* js); +#endif +#ifdef HVJS_WITH_REDIS +HV_EXPORT JSValue hvjs_require_redis(JSContext* js); +#endif +#ifdef HVJS_WITH_MQTT +HV_EXPORT JSValue hvjs_require_mqtt(JSContext* js); +#endif + +} // namespace js +} // namespace hv + +#endif // HV_JS_H_ diff --git a/js/hvjs_http.cpp b/js/hvjs_http.cpp new file mode 100644 index 000000000..2bfd25736 --- /dev/null +++ b/js/hvjs_http.cpp @@ -0,0 +1,402 @@ +#ifdef WITH_JS + +#include "hvjs.h" + +#ifdef HVJS_WITH_HTTP + +#include +#include + +#include +#include +#include +#include +#include + +#include "AsyncHttpClient.h" +#include "WebSocketClient.h" +#include "hstring.h" + +namespace hv { +namespace js { +namespace { + +static const int JS_HTTP_METHOD_REQUEST = -1; + +struct HvJsHttpRequest : public HvJsPromiseOp { + std::shared_ptr client; +}; + +JSValue js_push_headers(JSContext* js, const http_headers& headers) { + JSValue obj = JS_NewObject(js); + for (auto& kv : headers) { + JS_SetPropertyStr(js, obj, kv.first.c_str(), JS_NewStringLen(js, kv.second.data(), kv.second.size())); + } + return obj; +} + +JSValue js_push_http_response(JSContext* js, const HttpResponsePtr& resp) { + JSValue obj = JS_NewObject(js); + JS_SetPropertyStr(js, obj, "status", JS_NewInt32(js, resp ? resp->status_code : 0)); + if (resp) { + JS_SetPropertyStr(js, obj, "body", JS_NewStringLen(js, resp->body.data(), resp->body.size())); + JS_SetPropertyStr(js, obj, "headers", js_push_headers(js, resp->headers)); + } + else { + JS_SetPropertyStr(js, obj, "body", JS_NewString(js, "")); + JS_SetPropertyStr(js, obj, "headers", JS_NewObject(js)); + } + return obj; +} + +int js_fill_http_request(JSContext* js, JSValueConst* argv, int argc, http_method method, int url_index, HttpRequestPtr* out) { + if (argc <= url_index) { + JS_ThrowTypeError(js, "missing url"); + return -1; + } + std::string url = hvjs_to_string(js, argv[url_index]); + auto req = std::make_shared(); + req->method = method; + req->url = url; + if (argc > url_index + 1 && !JS_IsUndefined(argv[url_index + 1]) && !JS_IsNull(argv[url_index + 1])) { + std::string body = hvjs_to_string(js, argv[url_index + 1]); + req->body = body; + } + if (argc > url_index + 2 && JS_IsObject(argv[url_index + 2])) { + JSPropertyEnum* tab = NULL; + uint32_t len = 0; + if (JS_GetOwnPropertyNames(js, &tab, &len, argv[url_index + 2], JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) == 0) { + for (uint32_t i = 0; i < len; ++i) { + JSValue key = JS_AtomToString(js, tab[i].atom); + JSValue value = JS_GetProperty(js, argv[url_index + 2], tab[i].atom); + std::string k = hvjs_to_string(js, key); + std::string v = hvjs_to_string(js, value); + if (!k.empty()) req->headers[k] = v; + JS_FreeValue(js, value); + JS_FreeValue(js, key); + } + JS_FreePropertyEnum(js, tab, len); + } + } + *out = req; + return 0; +} + +JSValue js_http_request(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv, int magic) { + (void)this_val; + HvJsTask* task = hvjs_get_task(js); + if (task == NULL || !task->loop_ptr) { + return hvjs_rejected_promise(js, "hv.http: no shared event loop on this thread"); + } + http_method method = (http_method)magic; + int url_index = 0; + if (magic == JS_HTTP_METHOD_REQUEST) { + if (argc < 2) return hvjs_rejected_promise(js, "hv.http: request needs method and url"); + std::string m = hvjs_to_string(js, argv[0]); + toupper(m); + method = http_method_enum(m.c_str()); + url_index = 1; + } + if (method == HTTP_CUSTOM_METHOD) { + return hvjs_rejected_promise(js, "hv.http: unsupported method"); + } + + HttpRequestPtr req; + if (js_fill_http_request(js, argv, argc, method, url_index, &req) != 0) { + return JS_EXCEPTION; + } + + HvJsHttpRequest* op = NULL; + JSValue promise = hvjs_new_promise(js, task, &op); + if (JS_IsException(promise)) return promise; + op->client = std::make_shared(task->loop_ptr); + std::shared_ptr client = op->client; + task->in_call = true; + int ret = client->send(req, [op, client](const HttpResponsePtr& resp) { + if (op->task->loop_ptr) { + op->task->loop_ptr->queueInLoop([client]() {}); + } + JSContext* js = op->task->js; + if (resp) { + hvjs_promise_resolve(op, js_push_http_response(js, resp)); + } + else { + hvjs_promise_reject(op, "hv.http: request failed"); + } + }); + if (ret != 0) { + hvjs_promise_reject(op, "hv.http: request failed"); + } + task->in_call = false; + hvjs_finish_deferred_op(op); + return promise; +} + +static JSClassID s_ws_class_id; +static std::once_flag s_ws_class_once; + +struct HvJsWsState { + std::shared_ptr client; + std::deque inbox; + HvJsPromiseOp* connect_op; + HvJsPromiseOp* recv_op; + bool js_alive; + bool connected; + bool closed; + + HvJsWsState() : connect_op(NULL), recv_op(NULL), js_alive(false), connected(false), closed(false) {} + + void detach() { + closed = true; + connected = false; + if (client) { + client->onopen = NULL; + client->onmessage = NULL; + client->onclose = NULL; + client->close(); + client.reset(); + } + } + + ~HvJsWsState() { detach(); } +}; + +struct HvJsWsClient { + std::shared_ptr state; +}; + +struct HvJsWsConnect : public HvJsPromiseOp { + std::shared_ptr state; +}; + +struct HvJsWsRecv : public HvJsPromiseOp { + std::shared_ptr state; +}; + +HvJsWsClient* js_ws_client(JSContext* js, JSValueConst this_val) { + return (HvJsWsClient*)JS_GetOpaque2(js, this_val, s_ws_class_id); +} + +void js_ws_detach_after_callback(const EventLoopPtr& loop, const std::shared_ptr& state) { + if (!state) return; + if (loop) { + loop->queueInLoop([state]() { state->detach(); }); + } + else { + state->detach(); + } +} + +void js_ws_finalizer(JSRuntime* rt, JSValue val) { + (void)rt; + HvJsWsClient* box = (HvJsWsClient*)JS_GetOpaque(val, s_ws_class_id); + if (box && box->state) { + box->state->js_alive = false; + if (box->state->connect_op == NULL && box->state->recv_op == NULL) { + box->state->detach(); + } + } + delete box; +} + +void js_ws_register_class(JSContext* js) { + std::call_once(s_ws_class_once, []() { hvjs_new_class_id(&s_ws_class_id); }); + JSRuntime* rt = JS_GetRuntime(js); + if (!JS_IsRegisteredClass(rt, s_ws_class_id)) { + JSClassDef def; + memset(&def, 0, sizeof(def)); + def.class_name = "hv.ws.client"; + def.finalizer = js_ws_finalizer; + JS_NewClass(rt, s_ws_class_id, &def); + } +} + +void js_ws_try_deliver(const std::shared_ptr& state) { + if (!state || state->recv_op == NULL) return; + HvJsWsRecv* op = static_cast(state->recv_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + if (!state->inbox.empty()) { + std::string msg = std::move(state->inbox.front()); + state->inbox.pop_front(); + state->recv_op = NULL; + hvjs_promise_resolve(op, JS_NewStringLen(op->task->js, msg.data(), msg.size())); + } + else if (state->closed) { + state->recv_op = NULL; + hvjs_promise_reject(op, "closed"); + } + if (!hold->js_alive && hold->connect_op == NULL && hold->recv_op == NULL) { + js_ws_detach_after_callback(loop, hold); + } +} + +JSValue js_ws_send(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + HvJsWsClient* box = js_ws_client(js, this_val); + HvJsWsState* state = box ? box->state.get() : NULL; + if (state == NULL || !state->client || !state->connected) { + return JS_ThrowTypeError(js, "hv.ws: closed"); + } + std::string msg = argc > 0 ? hvjs_to_string(js, argv[0]) : std::string(); + enum ws_opcode opcode = WS_OPCODE_TEXT; + if (argc > 1 && hvjs_to_string(js, argv[1]) == "binary") { + opcode = WS_OPCODE_BINARY; + } + int ret = state->client->send(msg.data(), (int)msg.size(), opcode); + if (ret < 0) { + return JS_ThrowInternalError(js, "hv.ws: send failed"); + } + return JS_NewInt32(js, ret); +} + +JSValue js_ws_recv(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)argc; + (void)argv; + HvJsWsClient* box = js_ws_client(js, this_val); + HvJsWsState* state = box ? box->state.get() : NULL; + if (state == NULL || !state->client) { + return hvjs_rejected_promise(js, "closed"); + } + if (!state->inbox.empty()) { + std::string msg = std::move(state->inbox.front()); + state->inbox.pop_front(); + return hvjs_async_resolved_promise(js, hvjs_get_task(js), JS_NewStringLen(js, msg.data(), msg.size())); + } + if (state->closed || !state->connected) { + return hvjs_rejected_promise(js, "closed"); + } + if (state->recv_op != NULL) { + return hvjs_rejected_promise(js, "hv.ws: recv already pending"); + } + HvJsTask* task = hvjs_get_task(js); + HvJsWsRecv* op = NULL; + JSValue promise = hvjs_new_promise(js, task, &op); + if (JS_IsException(promise)) return promise; + op->state = box->state; + state->recv_op = op; + return promise; +} + +JSValue js_ws_close(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)argc; + (void)argv; + HvJsWsClient* box = js_ws_client(js, this_val); + if (box && box->state) { + std::shared_ptr state = box->state; + if (state->connect_op) { + HvJsPromiseOp* op = state->connect_op; + state->connect_op = NULL; + hvjs_promise_reject(op, "closed"); + } + if (state->recv_op) { + HvJsPromiseOp* op = state->recv_op; + state->recv_op = NULL; + hvjs_promise_reject(op, "closed"); + } + state->detach(); + } + return JS_UNDEFINED; +} + +JSValue js_ws_new_client_object(JSContext* js, const std::shared_ptr& state) { + JSValue obj = JS_NewObjectClass(js, s_ws_class_id); + if (JS_IsException(obj)) return obj; + HvJsWsClient* box = new HvJsWsClient(); + box->state = state; + state->js_alive = true; + JS_SetOpaque(obj, box); + JS_SetPropertyStr(js, obj, "send", JS_NewCFunction(js, js_ws_send, "send", 1)); + JS_SetPropertyStr(js, obj, "recv", JS_NewCFunction(js, js_ws_recv, "recv", 0)); + JS_SetPropertyStr(js, obj, "close", JS_NewCFunction(js, js_ws_close, "close", 0)); + return obj; +} + +JSValue js_ws_connect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + HvJsTask* task = hvjs_get_task(js); + if (task == NULL || !task->loop_ptr) { + return hvjs_rejected_promise(js, "hv.ws: no shared event loop on this thread"); + } + if (argc < 1) { + return hvjs_rejected_promise(js, "hv.ws: connect needs url"); + } + std::string url = hvjs_to_string(js, argv[0]); + js_ws_register_class(js); + std::shared_ptr state = std::make_shared(); + state->client = std::make_shared(task->loop_ptr); + + HvJsWsConnect* op = NULL; + JSValue promise = hvjs_new_promise(js, task, &op); + if (JS_IsException(promise)) return promise; + state->connect_op = op; + op->state = state; + state->client->onopen = [state]() { + state->connected = true; + state->closed = false; + if (state->connect_op) { + HvJsWsConnect* op = static_cast(state->connect_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + state->connect_op = NULL; + JSValue obj = js_ws_new_client_object(op->task->js, hold); + if (JS_IsException(obj)) { + hvjs_promise_reject(op, "hv.ws: create client failed"); + js_ws_detach_after_callback(loop, hold); + } + else { + hvjs_promise_resolve(op, obj); + } + } + }; + state->client->onmessage = [state](const std::string& msg) { + state->inbox.push_back(msg); + js_ws_try_deliver(state); + }; + state->client->onclose = [state]() { + state->connected = false; + state->closed = true; + if (state->connect_op) { + HvJsWsConnect* op = static_cast(state->connect_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + state->connect_op = NULL; + hvjs_promise_reject(op, "closed"); + js_ws_detach_after_callback(loop, hold); + } + js_ws_try_deliver(state); + }; + task->in_call = true; + int ret = state->client->open(url.c_str()); + if (ret != 0) { + state->connect_op = NULL; + hvjs_promise_reject(op, "hv.ws: open failed"); + state->detach(); + } + task->in_call = false; + hvjs_finish_deferred_op(op); + return promise; +} + +} // namespace + +JSValue hvjs_require_http(JSContext* js) { + JSValue http = JS_NewObject(js); + JS_SetPropertyStr(js, http, "request", JS_NewCFunctionMagic(js, js_http_request, "request", 2, JS_CFUNC_generic_magic, JS_HTTP_METHOD_REQUEST)); + JS_SetPropertyStr(js, http, "get", JS_NewCFunctionMagic(js, js_http_request, "get", 1, JS_CFUNC_generic_magic, HTTP_GET)); + JS_SetPropertyStr(js, http, "post", JS_NewCFunctionMagic(js, js_http_request, "post", 2, JS_CFUNC_generic_magic, HTTP_POST)); + JS_SetPropertyStr(js, http, "put", JS_NewCFunctionMagic(js, js_http_request, "put", 2, JS_CFUNC_generic_magic, HTTP_PUT)); + JS_SetPropertyStr(js, http, "delete", JS_NewCFunctionMagic(js, js_http_request, "delete", 1, JS_CFUNC_generic_magic, HTTP_DELETE)); + return http; +} + +JSValue hvjs_require_ws(JSContext* js) { + JSValue ws = JS_NewObject(js); + JS_SetPropertyStr(js, ws, "connect", JS_NewCFunction(js, js_ws_connect, "connect", 1)); + return ws; +} + +} // namespace js +} // namespace hv + +#endif // HVJS_WITH_HTTP +#endif // WITH_JS diff --git a/js/hvjs_mqtt.cpp b/js/hvjs_mqtt.cpp new file mode 100644 index 000000000..bf3409537 --- /dev/null +++ b/js/hvjs_mqtt.cpp @@ -0,0 +1,456 @@ +#ifdef WITH_JS + +#include "hvjs.h" + +#ifdef HVJS_WITH_MQTT + +#include +#include + +#include +#include +#include +#include + +#include "mqtt_client.h" + +namespace hv { +namespace js { +namespace { + +static JSClassID s_mqtt_class_id; +static std::once_flag s_mqtt_class_once; + +struct HvJsMqttMessage { + std::string topic; + std::string payload; + int qos; +}; + +struct HvJsMqttState { + mqtt_client_t* client; + std::deque inbox; + HvJsPromiseOp* connect_op; + HvJsPromiseOp* recv_op; + bool js_alive; + bool closed; + bool reconnect; + + HvJsMqttState() : client(NULL), connect_op(NULL), recv_op(NULL), js_alive(false), closed(false), reconnect(false) {} + + void detach() { + closed = true; + if (client) { + mqtt_client_set_callback(client, NULL); + mqtt_client_set_userdata(client, NULL); + mqtt_client_free(client); + client = NULL; + } + } + + ~HvJsMqttState() { detach(); } +}; + +struct HvJsMqttClient { + std::shared_ptr state; +}; + +struct HvJsMqttConnect : public HvJsPromiseOp { + std::shared_ptr state; +}; + +struct HvJsMqttRecv : public HvJsPromiseOp { + std::shared_ptr state; +}; + +struct HvJsMqttDetachEvent { + std::shared_ptr state; +}; + +HvJsMqttClient* js_mqtt_client(JSContext* js, JSValueConst this_val) { + return (HvJsMqttClient*)JS_GetOpaque2(js, this_val, s_mqtt_class_id); +} + +JSValue js_mqtt_recv(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); +JSValue js_mqtt_publish(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); +JSValue js_mqtt_subscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); +JSValue js_mqtt_unsubscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); +JSValue js_mqtt_disconnect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv); +void js_mqtt_detach_after_callback(const EventLoopPtr& loop, hloop_t* raw_loop, const std::shared_ptr& state); + +void js_mqtt_finalizer(JSRuntime* rt, JSValue val) { + (void)rt; + HvJsMqttClient* box = (HvJsMqttClient*)JS_GetOpaque(val, s_mqtt_class_id); + if (box && box->state) { + box->state->js_alive = false; + if (box->state->connect_op == NULL && box->state->recv_op == NULL) { + box->state->detach(); + } + } + delete box; +} + +void js_mqtt_register_class(JSContext* js) { + std::call_once(s_mqtt_class_once, []() { hvjs_new_class_id(&s_mqtt_class_id); }); + JSRuntime* rt = JS_GetRuntime(js); + if (!JS_IsRegisteredClass(rt, s_mqtt_class_id)) { + JSClassDef def; + memset(&def, 0, sizeof(def)); + def.class_name = "hv.mqtt.client"; + def.finalizer = js_mqtt_finalizer; + JS_NewClass(rt, s_mqtt_class_id, &def); + } +} + +JSValue js_mqtt_new_client_object(JSContext* js, const std::shared_ptr& state) { + JSValue obj = JS_NewObjectClass(js, s_mqtt_class_id); + if (JS_IsException(obj)) return obj; + HvJsMqttClient* box = new HvJsMqttClient(); + box->state = state; + state->js_alive = true; + JS_SetOpaque(obj, box); + JS_SetPropertyStr(js, obj, "recv", JS_NewCFunction(js, js_mqtt_recv, "recv", 0)); + JS_SetPropertyStr(js, obj, "publish", JS_NewCFunction(js, js_mqtt_publish, "publish", 2)); + JS_SetPropertyStr(js, obj, "subscribe", JS_NewCFunction(js, js_mqtt_subscribe, "subscribe", 1)); + JS_SetPropertyStr(js, obj, "unsubscribe", JS_NewCFunction(js, js_mqtt_unsubscribe, "unsubscribe", 1)); + JS_SetPropertyStr(js, obj, "disconnect", JS_NewCFunction(js, js_mqtt_disconnect, "disconnect", 0)); + return obj; +} + +JSValue js_push_mqtt_message(JSContext* js, const HvJsMqttMessage& msg) { + JSValue obj = JS_NewObject(js); + JS_SetPropertyStr(js, obj, "topic", JS_NewStringLen(js, msg.topic.data(), msg.topic.size())); + JS_SetPropertyStr(js, obj, "payload", JS_NewStringLen(js, msg.payload.data(), msg.payload.size())); + JS_SetPropertyStr(js, obj, "qos", JS_NewInt32(js, msg.qos)); + return obj; +} + +const char* js_mqtt_closed_reason(const HvJsMqttState* state) { + return state && state->reconnect ? "reconnecting" : "closed"; +} + +void js_mqtt_try_deliver(HvJsMqttState* state) { + if (!state || state->recv_op == NULL) return; + HvJsMqttRecv* op = static_cast(state->recv_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + hloop_t* raw_loop = op->task ? op->task->loop : NULL; + if (!state->inbox.empty()) { + HvJsMqttMessage msg = std::move(state->inbox.front()); + state->inbox.pop_front(); + state->recv_op = NULL; + hvjs_promise_resolve(op, js_push_mqtt_message(op->task->js, msg)); + } + else if (state->closed) { + state->recv_op = NULL; + hvjs_promise_reject(op, js_mqtt_closed_reason(state)); + } + if (!hold->js_alive && hold->connect_op == NULL && hold->recv_op == NULL) { + js_mqtt_detach_after_callback(loop, raw_loop, hold); + } +} + +void js_mqtt_detach_event_cb(hevent_t* ev) { + HvJsMqttDetachEvent* detach = (HvJsMqttDetachEvent*)hevent_userdata(ev); + if (detach) { + detach->state->detach(); + delete detach; + } +} + +void js_mqtt_detach_after_callback(const EventLoopPtr& loop, hloop_t* raw_loop, const std::shared_ptr& state) { + if (!state) return; + state->reconnect = false; + if (state->client) { + mqtt_client_set_reconnect(state->client, NULL); + } + if (loop) { + loop->queueInLoop([state]() { state->detach(); }); + } + else if (raw_loop) { + HvJsMqttDetachEvent* detach = new HvJsMqttDetachEvent(); + detach->state = state; + hevent_t ev; + memset(&ev, 0, sizeof(ev)); + ev.cb = js_mqtt_detach_event_cb; + ev.userdata = detach; + hloop_post_event(raw_loop, &ev); + } + else { + state->detach(); + } +} + +void js_mqtt_on_event(mqtt_client_t* client, int type) { + HvJsMqttState* state = (HvJsMqttState*)mqtt_client_get_userdata(client); + if (state == NULL) return; + switch (type) { + case MQTT_TYPE_CONNACK: + state->closed = false; + if (state->connect_op) { + HvJsMqttConnect* op = static_cast(state->connect_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + hloop_t* raw_loop = op->task ? op->task->loop : NULL; + state->connect_op = NULL; + JSValue obj = js_mqtt_new_client_object(op->task->js, hold); + if (JS_IsException(obj)) { + hvjs_promise_reject(op, "hv.mqtt: create client failed"); + js_mqtt_detach_after_callback(loop, raw_loop, hold); + } + else { + hvjs_promise_resolve(op, obj); + } + } + break; + case MQTT_TYPE_PUBLISH: { + HvJsMqttMessage msg; + if (client->message.topic && client->message.topic_len > 0) { + msg.topic.assign(client->message.topic, client->message.topic_len); + } + if (client->message.payload && client->message.payload_len > 0) { + msg.payload.assign(client->message.payload, client->message.payload_len); + } + msg.qos = client->message.qos; + state->inbox.push_back(std::move(msg)); + js_mqtt_try_deliver(state); + break; + } + case MQTT_TYPE_DISCONNECT: + state->closed = true; + if (state->connect_op) { + HvJsMqttConnect* op = static_cast(state->connect_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + hloop_t* raw_loop = op->task ? op->task->loop : NULL; + state->connect_op = NULL; + hvjs_promise_reject(op, "connect failed"); + js_mqtt_detach_after_callback(loop, raw_loop, hold); + } + if (state->recv_op) { + HvJsMqttRecv* op = static_cast(state->recv_op); + std::shared_ptr hold = op->state; + EventLoopPtr loop = op->task ? op->task->loop_ptr : EventLoopPtr(); + hloop_t* raw_loop = op->task ? op->task->loop : NULL; + state->recv_op = NULL; + hvjs_promise_reject(op, js_mqtt_closed_reason(state)); + if (!hold->js_alive && hold->connect_op == NULL && hold->recv_op == NULL) { + js_mqtt_detach_after_callback(loop, raw_loop, hold); + } + } + break; + default: break; + } +} + +bool js_parse_reconnect(JSContext* js, JSValueConst obj, reconn_setting_t* out) { + JSValue reconnect; + if (!hvjs_get_property(js, obj, "reconnect", &reconnect) || !JS_IsObject(reconnect)) { + if (!JS_IsUndefined(reconnect) && !JS_IsException(reconnect)) JS_FreeValue(js, reconnect); + return false; + } + reconn_setting_init(out); + out->min_delay = (uint32_t)hvjs_get_int_property(js, reconnect, "min_delay", (int)out->min_delay); + out->max_delay = (uint32_t)hvjs_get_int_property(js, reconnect, "max_delay", (int)out->max_delay); + out->delay_policy = (uint32_t)hvjs_get_int_property(js, reconnect, "delay_policy", (int)out->delay_policy); + out->max_retry_cnt = (uint32_t)hvjs_get_int_property(js, reconnect, "max_retry", (int)out->max_retry_cnt); + if (out->max_retry_cnt == 0) out->max_retry_cnt = INFINITE; + if (out->min_delay == 0) out->min_delay = 1; + if (out->max_delay < out->min_delay) out->max_delay = out->min_delay; + if (out->delay_policy > 1 && out->delay_policy > UINT32_MAX / out->min_delay) { + out->delay_policy = DEFAULT_RECONNECT_DELAY_POLICY; + } + JS_FreeValue(js, reconnect); + return true; +} + +JSValue js_mqtt_connect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + HvJsTask* task = hvjs_get_task(js); + if (task == NULL || task->loop == NULL) { + return hvjs_rejected_promise(js, "hv.mqtt: no event loop on this thread"); + } + if (argc < 1 || !JS_IsObject(argv[0])) { + return hvjs_rejected_promise(js, "hv.mqtt: connect needs options"); + } + + std::string host = hvjs_get_string_property(js, argv[0], "host", "127.0.0.1"); + int port = hvjs_get_int_property(js, argv[0], "port", DEFAULT_MQTT_PORT); + int ssl = hvjs_get_bool_property(js, argv[0], "ssl", false) ? 1 : 0; + std::string id = hvjs_get_string_property(js, argv[0], "id", ""); + std::string username = hvjs_get_string_property(js, argv[0], "username", ""); + std::string password = hvjs_get_string_property(js, argv[0], "password", ""); + int keepalive = hvjs_get_int_property(js, argv[0], "keepalive", 0); + int timeout = hvjs_get_int_property(js, argv[0], "connect_timeout", 0); + if (timeout <= 0) timeout = hvjs_get_int_property(js, argv[0], "timeout", 0); + bool clean_session = hvjs_get_bool_property(js, argv[0], "clean_session", true); + + js_mqtt_register_class(js); + std::shared_ptr state = std::make_shared(); + state->client = mqtt_client_new(task->loop); + if (state->client == NULL) { + return hvjs_rejected_promise(js, "hv.mqtt: create client failed"); + } + mqtt_client_set_userdata(state->client, state.get()); + mqtt_client_set_callback(state->client, js_mqtt_on_event); + if (!id.empty()) mqtt_client_set_id(state->client, id.c_str()); + if (!username.empty() || !password.empty()) { + mqtt_client_set_auth(state->client, username.c_str(), password.c_str()); + } + if (keepalive > 0) state->client->keepalive = (unsigned short)keepalive; + state->client->clean_session = clean_session ? 1 : 0; + if (timeout > 0) mqtt_client_set_connect_timeout(state->client, timeout); + reconn_setting_t reconn; + if (js_parse_reconnect(js, argv[0], &reconn)) { + mqtt_client_set_reconnect(state->client, &reconn); + state->reconnect = true; + } + + HvJsMqttConnect* op = NULL; + JSValue promise = hvjs_new_promise(js, task, &op); + if (JS_IsException(promise)) { + state->detach(); + return promise; + } + state->connect_op = op; + op->state = state; + task->in_call = true; + int ret = mqtt_client_connect(state->client, host.c_str(), port, ssl); + if (ret != 0) { + state->connect_op = NULL; + hvjs_promise_reject(op, "hv.mqtt: connect failed"); + state->detach(); + } + task->in_call = false; + hvjs_finish_deferred_op(op); + return promise; +} + +JSValue js_mqtt_recv(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)argc; + (void)argv; + HvJsMqttClient* box = js_mqtt_client(js, this_val); + std::shared_ptr state = box ? box->state : std::shared_ptr(); + if (!state || state->client == NULL) { + return hvjs_rejected_promise(js, "closed"); + } + if (!state->inbox.empty()) { + HvJsMqttMessage msg = std::move(state->inbox.front()); + state->inbox.pop_front(); + return hvjs_async_resolved_promise(js, hvjs_get_task(js), js_push_mqtt_message(js, msg)); + } + if (state->closed) { + return hvjs_rejected_promise(js, js_mqtt_closed_reason(state.get())); + } + if (state->recv_op != NULL) { + return hvjs_rejected_promise(js, "hv.mqtt: recv already pending"); + } + HvJsTask* task = hvjs_get_task(js); + HvJsMqttRecv* op = NULL; + JSValue promise = hvjs_new_promise(js, task, &op); + if (JS_IsException(promise)) return promise; + op->state = state; + state->recv_op = op; + return promise; +} + +JSValue js_mqtt_publish(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + HvJsMqttClient* box = js_mqtt_client(js, this_val); + HvJsMqttState* state = box ? box->state.get() : NULL; + if (state == NULL || state->client == NULL || state->closed) { + return JS_ThrowTypeError(js, "hv.mqtt: closed"); + } + if (argc < 2) { + return JS_ThrowTypeError(js, "hv.mqtt: publish needs topic and payload"); + } + std::string topic = hvjs_to_string(js, argv[0]); + std::string payload = hvjs_to_string(js, argv[1]); + int32_t qos = 0; + if (argc > 2 && JS_ToInt32(js, &qos, argv[2]) != 0) return JS_EXCEPTION; + int retain = argc > 3 ? JS_ToBool(js, argv[3]) : 0; + mqtt_message_t msg; + memset(&msg, 0, sizeof(msg)); + msg.topic = topic.c_str(); + msg.topic_len = (unsigned int)topic.size(); + msg.payload = payload.c_str(); + msg.payload_len = (unsigned int)payload.size(); + msg.qos = (unsigned char)qos; + msg.retain = (unsigned char)retain; + int mid = mqtt_client_publish(state->client, &msg); + if (mid < 0) { + return JS_ThrowInternalError(js, "hv.mqtt: publish failed"); + } + return JS_NewInt32(js, mid); +} + +JSValue js_mqtt_subscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + HvJsMqttClient* box = js_mqtt_client(js, this_val); + HvJsMqttState* state = box ? box->state.get() : NULL; + if (state == NULL || state->client == NULL || state->closed) { + return JS_ThrowTypeError(js, "hv.mqtt: closed"); + } + if (argc < 1) { + return JS_ThrowTypeError(js, "hv.mqtt: subscribe needs topic"); + } + std::string topic = hvjs_to_string(js, argv[0]); + int32_t qos = 0; + if (argc > 1 && JS_ToInt32(js, &qos, argv[1]) != 0) return JS_EXCEPTION; + int mid = mqtt_client_subscribe(state->client, topic.c_str(), qos); + if (mid < 0) { + return JS_ThrowInternalError(js, "hv.mqtt: subscribe failed"); + } + return JS_NewInt32(js, mid); +} + +JSValue js_mqtt_unsubscribe(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + HvJsMqttClient* box = js_mqtt_client(js, this_val); + HvJsMqttState* state = box ? box->state.get() : NULL; + if (state == NULL || state->client == NULL || state->closed) { + return JS_ThrowTypeError(js, "hv.mqtt: closed"); + } + if (argc < 1) { + return JS_ThrowTypeError(js, "hv.mqtt: unsubscribe needs topic"); + } + std::string topic = hvjs_to_string(js, argv[0]); + int mid = mqtt_client_unsubscribe(state->client, topic.c_str()); + if (mid < 0) { + return JS_ThrowInternalError(js, "hv.mqtt: unsubscribe failed"); + } + return JS_NewInt32(js, mid); +} + +JSValue js_mqtt_disconnect(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)argc; + (void)argv; + HvJsMqttClient* box = js_mqtt_client(js, this_val); + if (box && box->state) { + std::shared_ptr state = box->state; + state->reconnect = false; + if (state->connect_op) { + HvJsPromiseOp* op = state->connect_op; + state->connect_op = NULL; + hvjs_promise_reject(op, "closed"); + } + if (state->recv_op) { + HvJsPromiseOp* op = state->recv_op; + state->recv_op = NULL; + hvjs_promise_reject(op, "closed"); + } + state->detach(); + } + return JS_UNDEFINED; +} + +} // namespace + +JSValue hvjs_require_mqtt(JSContext* js) { + JSValue mqtt = JS_NewObject(js); + JS_SetPropertyStr(js, mqtt, "connect", JS_NewCFunction(js, js_mqtt_connect, "connect", 1)); + return mqtt; +} + +} // namespace js +} // namespace hv + +#endif // HVJS_WITH_MQTT +#endif // WITH_JS diff --git a/js/hvjs_redis.cpp b/js/hvjs_redis.cpp new file mode 100644 index 000000000..0a3dfaa5b --- /dev/null +++ b/js/hvjs_redis.cpp @@ -0,0 +1,236 @@ +#ifdef WITH_JS + +#include "hvjs.h" + +#ifdef HVJS_WITH_REDIS + +#include +#include +#include +#include + +#include +#include +#include + +#include "AsyncRedisClient.h" + +namespace hv { +namespace js { +namespace { + +static JSClassID s_redis_class_id; +static std::once_flag s_redis_class_once; + +struct HvJsRedisState { + std::shared_ptr client; + bool destroyed; + + HvJsRedisState() : destroyed(false) {} + + ~HvJsRedisState() { + destroyed = true; + if (client) { + client->stop(true); + client.reset(); + } + } +}; + +struct HvJsRedisClient { + std::shared_ptr state; +}; + +struct HvJsRedisCommand : public HvJsPromiseOp { + std::shared_ptr redis; +}; + +void js_redis_finalizer(JSRuntime* rt, JSValue val) { + (void)rt; + HvJsRedisClient* box = (HvJsRedisClient*)JS_GetOpaque(val, s_redis_class_id); + if (box) { + delete box; + } +} + +HvJsRedisClient* js_redis_client(JSContext* js, JSValueConst this_val) { + HvJsRedisClient* box = (HvJsRedisClient*)JS_GetOpaque2(js, this_val, s_redis_class_id); + return box; +} + +void js_redis_register_class(JSContext* js) { + std::call_once(s_redis_class_once, []() { hvjs_new_class_id(&s_redis_class_id); }); + JSRuntime* rt = JS_GetRuntime(js); + if (!JS_IsRegisteredClass(rt, s_redis_class_id)) { + JSClassDef def; + memset(&def, 0, sizeof(def)); + def.class_name = "hv.redis.client"; + def.finalizer = js_redis_finalizer; + JS_NewClass(rt, s_redis_class_id, &def); + } +} + +JSValue js_push_redis_reply(JSContext* js, const RedisReply& reply) { + switch (reply.type) { + case REDIS_REPLY_STRING: return JS_NewStringLen(js, reply.str.data(), reply.str.size()); + case REDIS_REPLY_INTEGER: return JS_NewInt64(js, reply.integer); + case REDIS_REPLY_ARRAY: { + if (reply.null_array) return JS_NULL; + JSValue arr = JS_NewArray(js); + for (uint32_t i = 0; i < reply.elements.size(); ++i) { + JSValue item = reply.elements[i].isNil() ? JS_NULL : js_push_redis_reply(js, reply.elements[i]); + JS_SetPropertyUint32(js, arr, i, item); + } + return arr; + } + case REDIS_REPLY_NIL: + default: return JS_NULL; + } +} + +void js_redis_resolve_result(HvJsRedisCommand* op, const RedisResult& result) { + JSContext* js = op->task->js; + if (!op->redis || op->redis->destroyed) { + hvjs_promise_reject(op, "hv.redis: client closed"); + return; + } + if (result.code != 0) { + char err[64]; + snprintf(err, sizeof(err), "hv.redis: request failed (%d)", result.code); + hvjs_promise_reject(op, err); + return; + } + if (result.reply.isError()) { + hvjs_promise_reject(op, result.reply.error().c_str()); + return; + } + hvjs_promise_resolve(op, js_push_redis_reply(js, result.reply)); +} + +bool js_build_redis_command(JSContext* js, JSValueConst* argv, int argc, int first, RedisCommand* cmd) { + if (argc <= first) return false; + if (JS_IsArray(js, argv[first]) && argc == first + 1) { + JSValue lenv = JS_GetPropertyStr(js, argv[first], "length"); + uint32_t len = 0; + JS_ToUint32(js, &len, lenv); + JS_FreeValue(js, lenv); + for (uint32_t i = 0; i < len; ++i) { + JSValue item = JS_GetPropertyUint32(js, argv[first], i); + cmd->push_back(hvjs_to_string(js, item)); + JS_FreeValue(js, item); + } + } + else { + for (int i = first; i < argc; ++i) { + cmd->push_back(hvjs_to_string(js, argv[i])); + } + } + return !cmd->empty(); +} + +const char* js_redis_verb_name(int magic) { + switch (magic) { + case 1: return "GET"; + case 2: return "SET"; + case 3: return "DEL"; + case 4: return "INCR"; + case 5: return "DECR"; + case 6: return "EXPIRE"; + case 7: return "EXISTS"; + default: return NULL; + } +} + +JSValue js_redis_command(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv, int magic) { + HvJsRedisClient* box = js_redis_client(js, this_val); + HvJsRedisState* state = box ? box->state.get() : NULL; + if (state == NULL || !state->client || state->destroyed) { + return hvjs_rejected_promise(js, "hv.redis: client closed"); + } + RedisCommand cmd; + if (magic != 0) { + const char* verb = js_redis_verb_name(magic); + if (verb == NULL) { + return hvjs_rejected_promise(js, "hv.redis: unknown command"); + } + cmd.push_back(verb); + for (int i = 0; i < argc; ++i) { + cmd.push_back(hvjs_to_string(js, argv[i])); + } + } + else if (!js_build_redis_command(js, argv, argc, 0, &cmd)) { + return hvjs_rejected_promise(js, "hv.redis: empty or invalid command"); + } + + HvJsTask* task = hvjs_get_task(js); + HvJsRedisCommand* op = NULL; + JSValue promise = hvjs_new_promise(js, task, &op); + if (JS_IsException(promise)) return promise; + op->redis = box->state; + task->in_call = true; + int ret = state->client->command(cmd, [op](const RedisResult& result) { js_redis_resolve_result(op, result); }); + if (ret != 0) { + hvjs_promise_reject(op, "hv.redis: request failed"); + } + task->in_call = false; + hvjs_finish_deferred_op(op); + return promise; +} + +JSValue js_redis_new(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { + (void)this_val; + HvJsTask* task = hvjs_get_task(js); + if (task == NULL || !task->loop_ptr) { + return JS_ThrowTypeError(js, "hv.redis: no shared event loop on this thread"); + } + js_redis_register_class(js); + + std::string host = "127.0.0.1"; + int port = 6379; + std::string auth; + int db = 0; + int timeout = 0; + if (argc > 0 && JS_IsObject(argv[0])) { + host = hvjs_get_string_property(js, argv[0], "host", "127.0.0.1"); + port = hvjs_get_int_property(js, argv[0], "port", 6379); + auth = hvjs_get_string_property(js, argv[0], "auth", ""); + db = hvjs_get_int_property(js, argv[0], "db", 0); + timeout = hvjs_get_int_property(js, argv[0], "timeout", 0); + } + + JSValue obj = JS_NewObjectClass(js, s_redis_class_id); + if (JS_IsException(obj)) return obj; + HvJsRedisClient* box = new HvJsRedisClient(); + box->state = std::make_shared(); + box->state->client = std::make_shared(task->loop_ptr); + box->state->client->setHost(host); + box->state->client->setPort(port); + if (!auth.empty()) box->state->client->setAuth(auth); + if (db > 0) box->state->client->setDb(db); + if (timeout > 0) box->state->client->setTimeout(timeout); + box->state->client->start(false); + JS_SetOpaque(obj, box); + + JS_SetPropertyStr(js, obj, "command", JS_NewCFunctionMagic(js, js_redis_command, "command", 1, JS_CFUNC_generic_magic, 0)); + static const char* verbs[] = {"GET", "SET", "DEL", "INCR", "DECR", "EXPIRE", "EXISTS", NULL}; + for (int i = 0; verbs[i]; ++i) { + std::string name = verbs[i]; + for (char& c : name) c = (char)::tolower((unsigned char)c); + JS_SetPropertyStr(js, obj, name.c_str(), JS_NewCFunctionMagic(js, js_redis_command, name.c_str(), 1, JS_CFUNC_generic_magic, i + 1)); + } + return obj; +} + +} // namespace + +JSValue hvjs_require_redis(JSContext* js) { + JSValue redis = JS_NewObject(js); + JS_SetPropertyStr(js, redis, "new", JS_NewCFunction(js, js_redis_new, "new", 1)); + return redis; +} + +} // namespace js +} // namespace hv + +#endif // HVJS_WITH_REDIS +#endif // WITH_JS From c76caf5dae4909f9f38e4d20c5891cd02cc7c516 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 20 Aug 2026 13:27:42 +0800 Subject: [PATCH 3/6] fix(js): support older quickjs promise api --- examples/hvjs.cpp | 8 ++- http/server/HttpJsHandler.cpp | 8 ++- js/hvjs.cpp | 112 +++++++++++++++++++++++++++++----- js/hvjs.h | 4 ++ 4 files changed, 116 insertions(+), 16 deletions(-) diff --git a/examples/hvjs.cpp b/examples/hvjs.cpp index ce83eb392..db5467e03 100644 --- a/examples/hvjs.cpp +++ b/examples/hvjs.cpp @@ -74,7 +74,7 @@ static void finish(hv::js::HvJsTask* base, JSValue result) { fprintf(stderr, "hvjs: %s\n", task->error.c_str()); task->exit_code = 1; } - else if (!JS_IsUndefined(task->promise) && JS_PromiseState(task->js, task->promise) == JS_PROMISE_REJECTED) { + else if (task->promise_rejected) { std::string err = hv::js::hvjs_to_string(task->js, result); fprintf(stderr, "hvjs: %s\n", err.c_str()); task->exit_code = 1; @@ -153,6 +153,12 @@ int main(int argc, char** argv) { hv::js::hvjs_task_unref(task); return 1; } + std::string err; + if (!hv::js::hvjs_watch_promise(task, &err)) { + fprintf(stderr, "hvjs: %s\n", err.c_str()); + hv::js::hvjs_task_unref(task); + return 1; + } hv::js::hvjs_task_ref(task); hv::js::hvjs_drain_jobs(task); diff --git a/http/server/HttpJsHandler.cpp b/http/server/HttpJsHandler.cpp index 7187cf0bd..d17255d06 100644 --- a/http/server/HttpJsHandler.cpp +++ b/http/server/HttpJsHandler.cpp @@ -239,7 +239,7 @@ static void task_finish(JsHttpTask* task, JSValue result) { task->ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; task->ctx->response->String(task->error); } - else if (!JS_IsUndefined(task->promise) && JS_PromiseState(task->js, task->promise) == JS_PROMISE_REJECTED) { + else if (task->promise_rejected) { std::string err = hv::js::hvjs_to_string(task->js, result); hloge("[js] http handler rejected: %s", err.c_str()); task->ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; @@ -402,6 +402,12 @@ int HttpJsHandler::operator()(const HttpContextPtr& ctx) { hv::js::hvjs_task_unref(task); return HTTP_STATUS_INTERNAL_SERVER_ERROR; } + if (!hv::js::hvjs_watch_promise(task, &err)) { + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + ctx->response->String(err); + hv::js::hvjs_task_unref(task); + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } hv::js::hvjs_task_ref(task); hv::js::hvjs_drain_jobs(task); diff --git a/js/hvjs.cpp b/js/hvjs.cpp index 7d5546251..284fed12f 100644 --- a/js/hvjs.cpp +++ b/js/hvjs.cpp @@ -23,6 +23,9 @@ struct HvJsSleep : public HvJsPromiseOp { struct HvJsImmediatePromise : public HvJsPromiseOp {}; +static JSClassID s_task_ref_class_id; +static std::once_flag s_task_ref_class_once; + std::mutex& js_class_id_mutex() { static std::mutex mutex; return mutex; @@ -76,6 +79,38 @@ void sleep_timer_cb(htimer_t* timer) { hvjs_promise_resolve(sleep, JS_UNDEFINED); } +void register_task_ref_class(JSContext* js) { + std::call_once(s_task_ref_class_once, []() { hvjs_new_class_id(&s_task_ref_class_id); }); + JSRuntime* rt = JS_GetRuntime(js); + if (!JS_IsRegisteredClass(rt, s_task_ref_class_id)) { + JSClassDef def; + memset(&def, 0, sizeof(def)); + def.class_name = "hv.js.task"; + JS_NewClass(rt, s_task_ref_class_id, &def); + } +} + +JSValue new_task_ref_value(JSContext* js, HvJsTask* task) { + register_task_ref_class(js); + JSValue obj = JS_NewObjectClass(js, s_task_ref_class_id); + if (JS_IsException(obj)) return obj; + JS_SetOpaque(obj, task); + return obj; +} + +JSValue promise_settle_cb(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValue* func_data) { + (void)this_val; + if (argc < 1) return JS_UNDEFINED; + HvJsTask* task = (HvJsTask*)JS_GetOpaque(func_data[0], s_task_ref_class_id); + if (task == NULL || task->closing || task->promise_settled) { + return JS_UNDEFINED; + } + task->promise_result = JS_DupValue(js, argv[0]); + task->promise_rejected = magic != 0; + task->promise_settled = true; + return JS_UNDEFINED; +} + JSValue js_hv_sleep(JSContext* js, JSValueConst this_val, int argc, JSValueConst* argv) { (void)this_val; HvJsTask* task = hvjs_get_task(js); @@ -136,7 +171,9 @@ JSValue require_hv(JSContext* js) { } // namespace -HvJsTask::HvJsTask() : rt(NULL), js(NULL), loop(NULL), promise(JS_UNDEFINED), finished(false), in_call(false), closing(false), refcount(1), finish(NULL) {} +HvJsTask::HvJsTask() + : rt(NULL), js(NULL), loop(NULL), promise(JS_UNDEFINED), promise_result(JS_UNDEFINED), promise_settled(false), promise_rejected(false), finished(false), + in_call(false), closing(false), refcount(1), finish(NULL) {} HvJsTask::~HvJsTask() {} @@ -151,6 +188,10 @@ void hvjs_task_ref(HvJsTask* task) { void hvjs_task_unref(HvJsTask* task) { if (--task->refcount != 0) return; task->closing = true; + if (!JS_IsUndefined(task->promise_result)) { + JS_FreeValue(task->js, task->promise_result); + task->promise_result = JS_UNDEFINED; + } if (!JS_IsUndefined(task->promise)) { JS_FreeValue(task->js, task->promise); task->promise = JS_UNDEFINED; @@ -191,6 +232,51 @@ void hvjs_schedule_drain(HvJsTask* task) { } } +bool hvjs_watch_promise(HvJsTask* task, std::string* err) { + if (task == NULL || task->js == NULL || JS_IsUndefined(task->promise)) return false; + JSContext* js = task->js; + JSValue then = JS_GetPropertyStr(js, task->promise, "then"); + if (JS_IsException(then)) { + if (err) *err = hvjs_exception_string(js); + return false; + } + if (!JS_IsFunction(js, then)) { + JS_FreeValue(js, then); + if (err) *err = "javascript result is not thenable"; + return false; + } + + JSValue task_ref = new_task_ref_value(js, task); + if (JS_IsException(task_ref)) { + JS_FreeValue(js, then); + if (err) *err = hvjs_exception_string(js); + return false; + } + JSValue on_fulfilled = JS_NewCFunctionData(js, promise_settle_cb, 1, 0, 1, &task_ref); + JSValue on_rejected = JS_NewCFunctionData(js, promise_settle_cb, 1, 1, 1, &task_ref); + if (JS_IsException(on_fulfilled) || JS_IsException(on_rejected)) { + if (err) *err = hvjs_exception_string(js); + JS_FreeValue(js, on_fulfilled); + JS_FreeValue(js, on_rejected); + JS_FreeValue(js, task_ref); + JS_FreeValue(js, then); + return false; + } + JSValue args[2] = {on_fulfilled, on_rejected}; + JSValue ret = JS_Call(js, then, task->promise, 2, args); + JS_FreeValue(js, on_fulfilled); + JS_FreeValue(js, on_rejected); + JS_FreeValue(js, task_ref); + JS_FreeValue(js, then); + if (JS_IsException(ret)) { + if (err) *err = hvjs_exception_string(js); + JS_FreeValue(js, ret); + return false; + } + JS_FreeValue(js, ret); + return true; +} + void hvjs_drain_jobs(HvJsTask* task) { JSContext* job_ctx = NULL; while (JS_IsJobPending(task->rt)) { @@ -200,20 +286,18 @@ void hvjs_drain_jobs(HvJsTask* task) { break; } } - if (!task->finished && !JS_IsUndefined(task->promise)) { - JSPromiseStateEnum state = JS_PromiseState(task->js, task->promise); - if (state != JS_PROMISE_PENDING) { - JSValue value = JS_PromiseResult(task->js, task->promise); - if (task->finish) { - task->finish(task, value); - } - else { - JS_FreeValue(task->js, value); - task->finished = true; - hvjs_task_unref(task); - } - return; + if (!task->finished && task->promise_settled) { + JSValue value = task->promise_result; + task->promise_result = JS_UNDEFINED; + if (task->finish) { + task->finish(task, value); } + else { + JS_FreeValue(task->js, value); + task->finished = true; + hvjs_task_unref(task); + } + return; } if (!task->error.empty()) { if (task->finish) { diff --git a/js/hvjs.h b/js/hvjs.h index baf6a7173..5c1f1f0dc 100644 --- a/js/hvjs.h +++ b/js/hvjs.h @@ -19,6 +19,9 @@ struct HV_EXPORT HvJsTask { hloop_t* loop; EventLoopPtr loop_ptr; JSValue promise; + JSValue promise_result; + bool promise_settled; + bool promise_rejected; bool finished; bool in_call; bool closing; @@ -44,6 +47,7 @@ struct HV_EXPORT HvJsPromiseOp { HV_EXPORT void hvjs_task_ref(HvJsTask* task); HV_EXPORT void hvjs_task_unref(HvJsTask* task); HV_EXPORT void hvjs_schedule_drain(HvJsTask* task); +HV_EXPORT bool hvjs_watch_promise(HvJsTask* task, std::string* err = NULL); HV_EXPORT void hvjs_drain_jobs(HvJsTask* task); template JSValue hvjs_new_promise(JSContext* js, HvJsTask* task, T** out) { From 42a4aadd985372305a5bf9a780e91f1db2cf58e7 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 20 Aug 2026 13:37:09 +0800 Subject: [PATCH 4/6] fix(js): support older quickjs property api --- js/hvjs_http.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/hvjs_http.cpp b/js/hvjs_http.cpp index 2bfd25736..747ecd875 100644 --- a/js/hvjs_http.cpp +++ b/js/hvjs_http.cpp @@ -75,7 +75,7 @@ int js_fill_http_request(JSContext* js, JSValueConst* argv, int argc, http_metho JS_FreeValue(js, value); JS_FreeValue(js, key); } - JS_FreePropertyEnum(js, tab, len); + js_free(js, tab); } } *out = req; From 87afc6429cb811480b463e7c5708d42e6e88230d Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 20 Aug 2026 13:52:39 +0800 Subject: [PATCH 5/6] ci: test quickjs with static build --- .github/workflows/CI.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 10e612d2c..9eb454521 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -20,10 +20,16 @@ jobs: run: | sudo apt update sudo apt install libssl-dev libnghttp2-dev liblua5.4-dev libprotobuf-dev libprotoc-dev protobuf-compiler quickjs libquickjs - ./configure --with-openssl --with-nghttp2 --with-kcp --with-mqtt --with-redis --with-lua --with-js --with-rpc + ./configure --with-openssl --with-nghttp2 --with-kcp --with-mqtt --with-redis --with-lua --with-rpc make libhv evpp # hrpc = separate libhrpc (needs protobuf); apt installs protobuf under /usr make libhrpc hrpc PROTOBUF_PREFIX=/usr + # Ubuntu packages libquickjs as a non-PIC static library, so JS is + # covered in a static libhv build instead of linking it into libhv.so. + make clean + rm -f lib/libhv.so lib/libhv.so.* + ./configure --disable-shared --with-openssl --with-nghttp2 --with-kcp --with-mqtt --with-redis --with-lua --with-js --with-rpc + make libhv evpp - name: test run: | From 7fde185c9764c30d157285eb379c4d5b544c47d3 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 20 Aug 2026 14:17:26 +0800 Subject: [PATCH 6/6] ci: isolate quickjs static coverage --- .github/workflows/CI.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 9eb454521..343909e90 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -20,16 +20,19 @@ jobs: run: | sudo apt update sudo apt install libssl-dev libnghttp2-dev liblua5.4-dev libprotobuf-dev libprotoc-dev protobuf-compiler quickjs libquickjs - ./configure --with-openssl --with-nghttp2 --with-kcp --with-mqtt --with-redis --with-lua --with-rpc - make libhv evpp - # hrpc = separate libhrpc (needs protobuf); apt installs protobuf under /usr - make libhrpc hrpc PROTOBUF_PREFIX=/usr # Ubuntu packages libquickjs as a non-PIC static library, so JS is # covered in a static libhv build instead of linking it into libhv.so. make clean - rm -f lib/libhv.so lib/libhv.so.* - ./configure --disable-shared --with-openssl --with-nghttp2 --with-kcp --with-mqtt --with-redis --with-lua --with-js --with-rpc + ./configure --disable-shared --with-http --with-mqtt --with-redis --with-js + make libhv hvjs unittest + bin/hvjs examples/js/sleep.js + make run-unittest + make clean + rm -f bin/hvjs bin/http_js_handler_test bin/http_js_redis_test bin/http_js_ws_test bin/http_js_mqtt_test + ./configure --with-openssl --with-nghttp2 --with-kcp --with-mqtt --with-redis --with-lua --with-rpc make libhv evpp + # hrpc = separate libhrpc (needs protobuf); apt installs protobuf under /usr + make libhrpc hrpc PROTOBUF_PREFIX=/usr - name: test run: |