Merge branch 'https' into misty
Some checks failed
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-macos (push) Failing after 3s
Build and Deploy / build-linux (push) Failing after 1m30s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
Some checks failed
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-macos (push) Failing after 3s
Build and Deploy / build-linux (push) Failing after 1m30s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped
This commit is contained in:
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@@ -77,7 +77,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Test Prosperon (Windows)
|
- name: Test Prosperon (Windows)
|
||||||
shell: msys2 {0}
|
shell: msys2 {0}
|
||||||
env: { TRACY_NO_INVARIANT_CHECK: 1 }
|
env:
|
||||||
|
TRACY_NO_INVARIANT_CHECK: 1
|
||||||
run: |
|
run: |
|
||||||
meson test --print-errorlogs -C build
|
meson test --print-errorlogs -C build
|
||||||
|
|
||||||
|
|||||||
72
meson.build
72
meson.build
@@ -148,7 +148,7 @@ src += [
|
|||||||
'anim.c', 'config.c', 'datastream.c','font.c','HandmadeMath.c','jsffi.c','model.c',
|
'anim.c', 'config.c', 'datastream.c','font.c','HandmadeMath.c','jsffi.c','model.c',
|
||||||
'render.c','simplex.c','spline.c', 'transform.c','prosperon.c', 'wildmatch.c',
|
'render.c','simplex.c','spline.c', 'transform.c','prosperon.c', 'wildmatch.c',
|
||||||
'sprite.c', 'rtree.c', 'qjs_nota.c', 'qjs_soloud.c',
|
'sprite.c', 'rtree.c', 'qjs_nota.c', 'qjs_soloud.c',
|
||||||
'qjs_qr.c', 'qjs_wota.c', 'monocypher.c', 'qjs_blob.c', 'qjs_crypto.c', 'qjs_time.c'
|
'qjs_qr.c', 'qjs_wota.c', 'monocypher.c', 'qjs_blob.c', 'qjs_crypto.c', 'qjs_time.c', 'qjs_http.c'
|
||||||
]
|
]
|
||||||
# quirc src
|
# quirc src
|
||||||
src += [
|
src += [
|
||||||
@@ -156,6 +156,76 @@ src += [
|
|||||||
'thirdparty/quirc/identify.c', 'thirdparty/quirc/version_db.c'
|
'thirdparty/quirc/identify.c', 'thirdparty/quirc/version_db.c'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
curl_opts = [
|
||||||
|
'http=enabled',
|
||||||
|
'ssl=enabled',
|
||||||
|
'openssl=enabled',
|
||||||
|
'schannel=disabled',
|
||||||
|
'secure-transport=disabled',
|
||||||
|
'dict=disabled',
|
||||||
|
'file=disabled',
|
||||||
|
'ftp=disabled',
|
||||||
|
'gopher=disabled',
|
||||||
|
'imap=disabled',
|
||||||
|
'ldap=disabled',
|
||||||
|
'ldaps=disabled',
|
||||||
|
'mqtt=disabled',
|
||||||
|
'pop3=disabled',
|
||||||
|
'rtmp=disabled',
|
||||||
|
'rtsp=disabled',
|
||||||
|
'smb=disabled',
|
||||||
|
'smtp=disabled',
|
||||||
|
'telnet=disabled',
|
||||||
|
'tftp=disabled',
|
||||||
|
'alt-svc=disabled',
|
||||||
|
'asynchdns=disabled',
|
||||||
|
'aws=disabled',
|
||||||
|
'basic-auth=disabled',
|
||||||
|
'bearer-auth=disabled',
|
||||||
|
'bindlocal=disabled',
|
||||||
|
'brotli=disabled',
|
||||||
|
'cookies=disabled',
|
||||||
|
'digest-auth=disabled',
|
||||||
|
'doh=disabled',
|
||||||
|
'form-api=disabled',
|
||||||
|
'getoptions=disabled',
|
||||||
|
'gsasl=disabled',
|
||||||
|
'gss-api=disabled',
|
||||||
|
'headers-api=disabled',
|
||||||
|
'hsts=disabled',
|
||||||
|
'http2=disabled',
|
||||||
|
'idn=disabled',
|
||||||
|
'kerberos-auth=disabled',
|
||||||
|
'libcurl-option=disabled',
|
||||||
|
'libz=disabled',
|
||||||
|
'mime=disabled',
|
||||||
|
'negotiate-auth=disabled',
|
||||||
|
'netrc=disabled',
|
||||||
|
'ntlm=disabled',
|
||||||
|
'parsedate=disabled',
|
||||||
|
'progress-meter=disabled',
|
||||||
|
'proxy=disabled',
|
||||||
|
'psl=disabled',
|
||||||
|
'sha512_256=disabled',
|
||||||
|
'shuffle-dns=disabled',
|
||||||
|
'socketpair=disabled',
|
||||||
|
'tls-srp=disabled',
|
||||||
|
'unixsockets=disabled',
|
||||||
|
'verbose-strings=disabled',
|
||||||
|
'zstd=disabled',
|
||||||
|
'debug=disabled',
|
||||||
|
'curldebug=false',
|
||||||
|
'libuv=disabled',
|
||||||
|
'tests=disabled',
|
||||||
|
'unittests=disabled',
|
||||||
|
'default_library=static'
|
||||||
|
]
|
||||||
|
|
||||||
|
curl_proj = subproject('curl', default_options: curl_opts)
|
||||||
|
deps += dependency('libcurl')
|
||||||
|
deps += dependency('zlib', static: true)
|
||||||
|
deps += dependency('openssl', static:true)
|
||||||
|
|
||||||
imsrc = [
|
imsrc = [
|
||||||
'GraphEditor.cpp','ImCurveEdit.cpp','ImGradient.cpp','imgui_draw.cpp',
|
'GraphEditor.cpp','ImCurveEdit.cpp','ImGradient.cpp','imgui_draw.cpp',
|
||||||
'imgui_tables.cpp','imgui_widgets.cpp','imgui.cpp','ImGuizmo.cpp','imnodes.cpp',
|
'imgui_tables.cpp','imgui_widgets.cpp','imgui.cpp','ImGuizmo.cpp','imnodes.cpp',
|
||||||
|
|||||||
26
scripts/modules/http.js
Normal file
26
scripts/modules/http.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
var http = this
|
||||||
|
|
||||||
|
http.fetch[prosperon.DOC] = `Initiate an asynchronous HTTP GET request.
|
||||||
|
|
||||||
|
This function enqueues an HTTP GET request for the specified URL. It supports both buffered responses (full response collected in memory) and streaming data (processed as it arrives). The request is executed asynchronously, and its completion or streaming data is handled via callbacks provided in the options. Use 'poll' to process the results.
|
||||||
|
|
||||||
|
:param url: A string representing the URL to fetch.
|
||||||
|
:param options: Either a callback function or an object with optional properties:
|
||||||
|
- 'callback': A function invoked upon request completion, receiving an object with 'data' (string or null) and 'error' (string or null) properties.
|
||||||
|
- 'on_data': A function invoked for each chunk of streaming data, receiving a string chunk as its argument. If supplied, 'callback.data' will be null.
|
||||||
|
:return: undefined
|
||||||
|
:throws:
|
||||||
|
- An error if the URL is not a string or is invalid.
|
||||||
|
- An error if the options argument is neither a function nor an object.
|
||||||
|
- An error if memory allocation or CURL initialization fails.
|
||||||
|
`
|
||||||
|
|
||||||
|
http.poll[prosperon.DOC] = `Process pending HTTP requests and invoke callbacks.
|
||||||
|
|
||||||
|
This function checks for I/O activity on all enqueued HTTP requests and processes any that have completed or received data. For completed requests, it invokes the 'callback' function (if provided) with the result. For streaming requests, it invokes the 'data' function (if provided) as data arrives. This function must be called repeatedly to drive the asynchronous request system.
|
||||||
|
|
||||||
|
:return: undefined
|
||||||
|
:throws: An error if CURL multi-handle processing fails.
|
||||||
|
`
|
||||||
|
|
||||||
|
return http
|
||||||
@@ -7598,9 +7598,9 @@ MISTUSE(sdl_audio)
|
|||||||
#include "qjs_crypto.h"
|
#include "qjs_crypto.h"
|
||||||
#include "qjs_time.h"
|
#include "qjs_time.h"
|
||||||
#include "qjs_blob.h"
|
#include "qjs_blob.h"
|
||||||
|
#include "qjs_http.h"
|
||||||
|
|
||||||
JSValue js_imgui_use(JSContext *js);
|
JSValue js_imgui_use(JSContext *js);
|
||||||
|
|
||||||
#define MISTLINE(NAME) (ModuleEntry){#NAME, js_##NAME##_use}
|
#define MISTLINE(NAME) (ModuleEntry){#NAME, js_##NAME##_use}
|
||||||
|
|
||||||
void ffi_load(JSContext *js)
|
void ffi_load(JSContext *js)
|
||||||
@@ -7634,7 +7634,7 @@ void ffi_load(JSContext *js)
|
|||||||
arrput(rt->module_registry, MISTLINE(wota));
|
arrput(rt->module_registry, MISTLINE(wota));
|
||||||
arrput(rt->module_registry, MISTLINE(crypto));
|
arrput(rt->module_registry, MISTLINE(crypto));
|
||||||
arrput(rt->module_registry, MISTLINE(blob));
|
arrput(rt->module_registry, MISTLINE(blob));
|
||||||
|
arrput(rt->module_registry, MISTLINE(http));
|
||||||
arrput(rt->module_registry, MISTLINE(sdl_audio));
|
arrput(rt->module_registry, MISTLINE(sdl_audio));
|
||||||
|
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
|
|||||||
265
source/qjs_http.c
Normal file
265
source/qjs_http.c
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
#include "quickjs.h"
|
||||||
|
#include <curl/curl.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// HTTP request structure
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
typedef struct {
|
||||||
|
char url[512]; // URL for the request
|
||||||
|
JSContext *ctx; // JS context for callbacks
|
||||||
|
JSValue callback; // Completion callback (optional)
|
||||||
|
JSValue on_data; // Streaming data callback (optional)
|
||||||
|
char *response; // Buffer for non-streaming mode
|
||||||
|
size_t size; // Size of response buffer
|
||||||
|
CURL *curl; // CURL easy handle
|
||||||
|
int done; // Request completion flag
|
||||||
|
int curl_result; // CURL result code
|
||||||
|
} HttpRequest;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Global data
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
static CURLM *g_curl_multi = NULL; // CURL multi-handle for async I/O
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Libcurl write callback for streaming or buffering
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp)
|
||||||
|
{
|
||||||
|
size_t realsize = size * nmemb;
|
||||||
|
HttpRequest *req = (HttpRequest *)userp;
|
||||||
|
|
||||||
|
// Streaming mode: call onData if provided
|
||||||
|
if (JS_IsFunction(req->ctx, req->on_data)) {
|
||||||
|
JSValue chunk = JS_NewStringLen(req->ctx, contents, realsize);
|
||||||
|
JSValue ret = JS_Call(req->ctx, req->on_data, JS_UNDEFINED, 1, &chunk);
|
||||||
|
JS_FreeValue(req->ctx, chunk);
|
||||||
|
JS_FreeValue(req->ctx, ret); // Ignore return value
|
||||||
|
return realsize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-streaming mode: buffer the response
|
||||||
|
char *ptr = realloc(req->response, req->size + realsize + 1);
|
||||||
|
if (!ptr) {
|
||||||
|
return 0; // Out of memory, tell CURL to abort
|
||||||
|
}
|
||||||
|
req->response = ptr;
|
||||||
|
memcpy(&(req->response[req->size]), contents, realsize);
|
||||||
|
req->size += realsize;
|
||||||
|
req->response[req->size] = '\0'; // Null-terminate
|
||||||
|
return realsize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// JS function: http.fetch(url, options)
|
||||||
|
// - Enqueues an async HTTP request with optional streaming
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
static JSValue js_http_fetch(JSContext *ctx, JSValueConst this_val,
|
||||||
|
int argc, JSValueConst *argv)
|
||||||
|
{
|
||||||
|
if (argc < 2 || !JS_IsString(argv[0])) {
|
||||||
|
return JS_ThrowTypeError(ctx, "fetch expects a URL string and an options object or callback");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get URL
|
||||||
|
const char *url = JS_ToCString(ctx, argv[0]);
|
||||||
|
if (!url) {
|
||||||
|
return JS_ThrowTypeError(ctx, "Invalid URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate request object
|
||||||
|
HttpRequest *req = calloc(1, sizeof(*req));
|
||||||
|
if (!req) {
|
||||||
|
JS_FreeCString(ctx, url);
|
||||||
|
return JS_ThrowInternalError(ctx, "Failed to allocate memory");
|
||||||
|
}
|
||||||
|
strncpy(req->url, url, sizeof(req->url) - 1);
|
||||||
|
req->ctx = ctx;
|
||||||
|
req->callback = JS_NULL;
|
||||||
|
req->on_data = JS_NULL;
|
||||||
|
|
||||||
|
// Parse second argument: callback or options object
|
||||||
|
if (JS_IsFunction(ctx, argv[1])) {
|
||||||
|
req->callback = JS_DupValue(ctx, argv[1]);
|
||||||
|
} else if (JS_IsObject(argv[1])) {
|
||||||
|
JSValue callback = JS_GetPropertyStr(ctx, argv[1], "callback");
|
||||||
|
JSValue on_data = JS_GetPropertyStr(ctx, argv[1], "on_data");
|
||||||
|
if (JS_IsFunction(ctx, callback)) {
|
||||||
|
req->callback = JS_DupValue(ctx, callback);
|
||||||
|
}
|
||||||
|
if (JS_IsFunction(ctx, on_data)) {
|
||||||
|
req->on_data = JS_DupValue(ctx, on_data);
|
||||||
|
}
|
||||||
|
JS_FreeValue(ctx, callback);
|
||||||
|
JS_FreeValue(ctx, on_data);
|
||||||
|
} else {
|
||||||
|
JS_FreeCString(ctx, url);
|
||||||
|
free(req);
|
||||||
|
return JS_ThrowTypeError(ctx, "Second argument must be a callback or options object");
|
||||||
|
}
|
||||||
|
|
||||||
|
JS_FreeCString(ctx, url);
|
||||||
|
|
||||||
|
// Initialize CURL easy handle
|
||||||
|
req->curl = curl_easy_init();
|
||||||
|
if (!req->curl) {
|
||||||
|
JS_FreeValue(ctx, req->callback);
|
||||||
|
JS_FreeValue(ctx, req->on_data);
|
||||||
|
free(req);
|
||||||
|
return JS_ThrowInternalError(ctx, "Failed to create CURL handle");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set CURL options
|
||||||
|
curl_easy_setopt(req->curl, CURLOPT_URL, req->url);
|
||||||
|
curl_easy_setopt(req->curl, CURLOPT_WRITEFUNCTION, write_callback);
|
||||||
|
curl_easy_setopt(req->curl, CURLOPT_WRITEDATA, req);
|
||||||
|
curl_easy_setopt(req->curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||||
|
curl_easy_setopt(req->curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||||
|
curl_easy_setopt(req->curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||||
|
curl_easy_setopt(req->curl, CURLOPT_USERAGENT, "prosperon");
|
||||||
|
curl_easy_setopt(req->curl, CURLOPT_PRIVATE, req);
|
||||||
|
|
||||||
|
// Add to multi-handle
|
||||||
|
if (!g_curl_multi) {
|
||||||
|
curl_easy_cleanup(req->curl);
|
||||||
|
JS_FreeValue(ctx, req->callback);
|
||||||
|
JS_FreeValue(ctx, req->on_data);
|
||||||
|
free(req);
|
||||||
|
return JS_ThrowInternalError(ctx, "CURL multi-handle not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
CURLMcode mc = curl_multi_add_handle(g_curl_multi, req->curl);
|
||||||
|
if (mc != CURLM_OK) {
|
||||||
|
curl_easy_cleanup(req->curl);
|
||||||
|
JS_FreeValue(ctx, req->callback);
|
||||||
|
JS_FreeValue(ctx, req->on_data);
|
||||||
|
free(req);
|
||||||
|
return JS_ThrowInternalError(ctx, "curl_multi_add_handle failed: %s", curl_multi_strerror(mc));
|
||||||
|
}
|
||||||
|
|
||||||
|
return JS_UNDEFINED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// JS function: http.poll()
|
||||||
|
// - Checks for I/O and completed requests, invoking callbacks as needed
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
static JSValue js_http_poll(JSContext *ctx, JSValueConst this_val,
|
||||||
|
int argc, JSValueConst *argv)
|
||||||
|
{
|
||||||
|
if (!g_curl_multi) {
|
||||||
|
return JS_UNDEFINED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform pending transfers
|
||||||
|
int still_running = 0;
|
||||||
|
CURLMcode mc = curl_multi_perform(g_curl_multi, &still_running);
|
||||||
|
if (mc != CURLM_OK) {
|
||||||
|
return JS_ThrowInternalError(ctx, "curl_multi_perform failed: %s", curl_multi_strerror(mc));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for completed requests
|
||||||
|
CURLMsg *msg;
|
||||||
|
int msgq = 0;
|
||||||
|
while ((msg = curl_multi_info_read(g_curl_multi, &msgq))) {
|
||||||
|
if (msg->msg == CURLMSG_DONE) {
|
||||||
|
CURL *easy = msg->easy_handle;
|
||||||
|
HttpRequest *req = NULL;
|
||||||
|
curl_easy_getinfo(easy, CURLINFO_PRIVATE, (char **)&req);
|
||||||
|
|
||||||
|
// Remove from multi-handle
|
||||||
|
curl_multi_remove_handle(g_curl_multi, easy);
|
||||||
|
|
||||||
|
// Mark as done
|
||||||
|
req->curl_result = msg->data.result;
|
||||||
|
req->done = 1;
|
||||||
|
|
||||||
|
// Call completion callback if provided
|
||||||
|
if (JS_IsFunction(req->ctx, req->callback)) {
|
||||||
|
JSValue arg = JS_NewObject(req->ctx);
|
||||||
|
if (req->curl_result == CURLE_OK) {
|
||||||
|
JSValue data = (req->response && req->size > 0) ?
|
||||||
|
JS_NewStringLen(req->ctx, req->response, req->size) : JS_NULL;
|
||||||
|
JS_DefinePropertyValueStr(req->ctx, arg, "data", data, JS_PROP_C_W_E);
|
||||||
|
JS_DefinePropertyValueStr(req->ctx, arg, "error", JS_NULL, JS_PROP_C_W_E);
|
||||||
|
} else {
|
||||||
|
JS_DefinePropertyValueStr(req->ctx, arg, "data", JS_NULL, JS_PROP_C_W_E);
|
||||||
|
const char *err_str = curl_easy_strerror(req->curl_result);
|
||||||
|
JS_DefinePropertyValueStr(req->ctx, arg, "error",
|
||||||
|
JS_NewString(req->ctx, err_str ? err_str : "Unknown error"),
|
||||||
|
JS_PROP_C_W_E);
|
||||||
|
}
|
||||||
|
JSValue ret = JS_Call(req->ctx, req->callback, JS_UNDEFINED, 1, &arg);
|
||||||
|
JS_FreeValue(req->ctx, arg);
|
||||||
|
JS_FreeValue(req->ctx, ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
JS_FreeValue(req->ctx, req->callback);
|
||||||
|
JS_FreeValue(req->ctx, req->on_data);
|
||||||
|
curl_easy_cleanup(req->curl);
|
||||||
|
free(req->response);
|
||||||
|
free(req);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JS_UNDEFINED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Module initialization
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
static const JSCFunctionListEntry js_http_funcs[] = {
|
||||||
|
JS_CFUNC_DEF("fetch", 2, js_http_fetch),
|
||||||
|
JS_CFUNC_DEF("poll", 0, js_http_poll),
|
||||||
|
};
|
||||||
|
|
||||||
|
JSValue js_http_use(JSContext *ctx)
|
||||||
|
{
|
||||||
|
// Initialize CURL globally (once per process)
|
||||||
|
static int s_curl_init_done = 0;
|
||||||
|
if (!s_curl_init_done) {
|
||||||
|
s_curl_init_done = 1;
|
||||||
|
if (curl_global_init(CURL_GLOBAL_ALL) != 0) {
|
||||||
|
return JS_ThrowInternalError(ctx, "Failed to initialize CURL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize multi-handle (once per module)
|
||||||
|
if (!g_curl_multi) {
|
||||||
|
g_curl_multi = curl_multi_init();
|
||||||
|
if (!g_curl_multi) {
|
||||||
|
return JS_ThrowInternalError(ctx, "Failed to initialize CURL multi-handle");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export fetch and poll functions
|
||||||
|
JSValue export_obj = JS_NewObject(ctx);
|
||||||
|
JS_SetPropertyFunctionList(ctx, export_obj, js_http_funcs,
|
||||||
|
sizeof(js_http_funcs) / sizeof(JSCFunctionListEntry));
|
||||||
|
return export_obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int js_http_init(JSContext *ctx, JSModuleDef *m)
|
||||||
|
{
|
||||||
|
JS_SetModuleExport(ctx, m, "default", js_http_use(ctx));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef JS_SHARED_LIBRARY
|
||||||
|
#define JS_INIT_MODULE js_init_module
|
||||||
|
#else
|
||||||
|
#define JS_INIT_MODULE js_init_module_http
|
||||||
|
#endif
|
||||||
|
|
||||||
|
JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name)
|
||||||
|
{
|
||||||
|
JSModuleDef *m = JS_NewCModule(ctx, module_name, js_http_init);
|
||||||
|
if (!m) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
JS_AddModuleExport(ctx, m, "default");
|
||||||
|
return m;
|
||||||
|
}
|
||||||
8
source/qjs_http.h
Normal file
8
source/qjs_http.h
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#ifndef QJS_HTTP_H
|
||||||
|
#define QJS_HTTP_H
|
||||||
|
|
||||||
|
#include "quickjs.h"
|
||||||
|
|
||||||
|
JSValue js_http_use(JSContext*);
|
||||||
|
|
||||||
|
#endif
|
||||||
13
subprojects/curl.wrap
Normal file
13
subprojects/curl.wrap
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[wrap-file]
|
||||||
|
directory = curl-8.10.1
|
||||||
|
source_url = https://github.com/curl/curl/releases/download/curl-8_10_1/curl-8.10.1.tar.xz
|
||||||
|
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/curl_8.10.1-1/curl-8.10.1.tar.xz
|
||||||
|
source_filename = curl-8.10.1.tar.xz
|
||||||
|
source_hash = 73a4b0e99596a09fa5924a4fb7e4b995a85fda0d18a2c02ab9cf134bebce04ee
|
||||||
|
patch_filename = curl_8.10.1-1_patch.zip
|
||||||
|
patch_url = https://wrapdb.mesonbuild.com/v2/curl_8.10.1-1/get_patch
|
||||||
|
patch_hash = 707c28f35fc9b0e8d68c0c2800712007612f922a31da9637ce706a2159f3ddd8
|
||||||
|
wrapdb_version = 8.10.1-1
|
||||||
|
|
||||||
|
[provide]
|
||||||
|
dependency_names = libcurl
|
||||||
15
subprojects/openssl.wrap
Normal file
15
subprojects/openssl.wrap
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[wrap-file]
|
||||||
|
directory = openssl-3.0.8
|
||||||
|
source_url = https://www.openssl.org/source/openssl-3.0.8.tar.gz
|
||||||
|
source_filename = openssl-3.0.8.tar.gz
|
||||||
|
source_hash = 6c13d2bf38fdf31eac3ce2a347073673f5d63263398f1f69d0df4a41253e4b3e
|
||||||
|
patch_filename = openssl_3.0.8-3_patch.zip
|
||||||
|
patch_url = https://wrapdb.mesonbuild.com/v2/openssl_3.0.8-3/get_patch
|
||||||
|
patch_hash = 300da189e106942347d61a4a4295aa2edbcf06184f8d13b4cee0bed9fb936963
|
||||||
|
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/openssl_3.0.8-3/openssl-3.0.8.tar.gz
|
||||||
|
wrapdb_version = 3.0.8-3
|
||||||
|
|
||||||
|
[provide]
|
||||||
|
libcrypto = libcrypto_dep
|
||||||
|
libssl = libssl_dep
|
||||||
|
openssl = openssl_dep
|
||||||
13
subprojects/zlib.wrap
Normal file
13
subprojects/zlib.wrap
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[wrap-file]
|
||||||
|
directory = zlib-1.3.1
|
||||||
|
source_url = http://zlib.net/fossils/zlib-1.3.1.tar.gz
|
||||||
|
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/zlib_1.3.1-1/zlib-1.3.1.tar.gz
|
||||||
|
source_filename = zlib-1.3.1.tar.gz
|
||||||
|
source_hash = 9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23
|
||||||
|
patch_filename = zlib_1.3.1-1_patch.zip
|
||||||
|
patch_url = https://wrapdb.mesonbuild.com/v2/zlib_1.3.1-1/get_patch
|
||||||
|
patch_hash = e79b98eb24a75392009cec6f99ca5cdca9881ff20bfa174e8b8926d5c7a47095
|
||||||
|
wrapdb_version = 1.3.1-1
|
||||||
|
|
||||||
|
[provide]
|
||||||
|
zlib = zlib_dep
|
||||||
246
tests/http.js
Normal file
246
tests/http.js
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
// http_test.js
|
||||||
|
var http = use('http');
|
||||||
|
var os = use('os');
|
||||||
|
|
||||||
|
var getter = http.fetch('tortoise.png')
|
||||||
|
var file = getter.data() // invokes the data stream to wait for it
|
||||||
|
|
||||||
|
var got = false
|
||||||
|
var count = 0
|
||||||
|
http.fetch("https://dictionary.ink/find?word=theological", {
|
||||||
|
on_data: e => {
|
||||||
|
console.log(e.length)
|
||||||
|
count++
|
||||||
|
},
|
||||||
|
callback: e => {
|
||||||
|
for (var i in e) console.log(i)
|
||||||
|
console.log(e.data)
|
||||||
|
got = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
while (!got) {
|
||||||
|
http.poll()
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`got hit ${count} times`)
|
||||||
|
|
||||||
|
os.exit()
|
||||||
|
|
||||||
|
// Deep comparison function (unchanged from previous version)
|
||||||
|
function deepCompare(expected, actual, path = '') {
|
||||||
|
if (expected === actual) return { passed: true, messages: [] };
|
||||||
|
|
||||||
|
if (typeof expected === 'string' && typeof actual === 'string') {
|
||||||
|
if (expected === actual) {
|
||||||
|
return { passed: true, messages: [] };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
passed: false,
|
||||||
|
messages: [`String mismatch at ${path}: expected "${expected}", got "${actual}"`]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof expected === 'object' && expected !== null &&
|
||||||
|
typeof actual === 'object' && actual !== null) {
|
||||||
|
const expKeys = Object.keys(expected).sort();
|
||||||
|
const actKeys = Object.keys(actual).sort();
|
||||||
|
if (JSON.stringify(expKeys) !== JSON.stringify(actKeys)) {
|
||||||
|
return {
|
||||||
|
passed: false,
|
||||||
|
messages: [`Object keys mismatch at ${path}: expected ${expKeys}, got ${actKeys}`]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let messages = [];
|
||||||
|
for (let key of expKeys) {
|
||||||
|
const result = deepCompare(expected[key], actual[key], `${path}.${key}`);
|
||||||
|
if (!result.passed) messages.push(...result.messages);
|
||||||
|
}
|
||||||
|
return { passed: messages.length === 0, messages };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
passed: false,
|
||||||
|
messages: [`Value mismatch at ${path}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test cases (slightly modified to include state tracking)
|
||||||
|
var testCases = [
|
||||||
|
{
|
||||||
|
name: "Basic GET request",
|
||||||
|
url: "https://api.github.com",
|
||||||
|
expected: { contains: "GitHub" },
|
||||||
|
validate: function(result) { return result.toLowerCase().includes("github"); },
|
||||||
|
completed: false,
|
||||||
|
result: null,
|
||||||
|
error: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "JSON response",
|
||||||
|
url: "https://api.github.com/users/octocat",
|
||||||
|
expected: { login: "octocat" },
|
||||||
|
validate: function(result) {
|
||||||
|
let parsed = JSON.parse(result);
|
||||||
|
return deepCompare({ login: "octocat" }, { login: parsed.login });
|
||||||
|
},
|
||||||
|
completed: false,
|
||||||
|
result: null,
|
||||||
|
error: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Follow redirect",
|
||||||
|
url: "http://github.com",
|
||||||
|
expected: { contains: "gihtub" },
|
||||||
|
validate: function(result) { return result.toLowerCase().includes("github"); },
|
||||||
|
completed: false,
|
||||||
|
result: null,
|
||||||
|
error: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid URL",
|
||||||
|
url: "http://nonexistent.domain.xyz",
|
||||||
|
expectError: true,
|
||||||
|
validate: function(result) { return true; },
|
||||||
|
completed: false,
|
||||||
|
result: null,
|
||||||
|
error: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Malformed URL",
|
||||||
|
url: "not-a-url",
|
||||||
|
expectError: true,
|
||||||
|
validate: function(result) { return true; },
|
||||||
|
completed: false,
|
||||||
|
result: null,
|
||||||
|
error: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Large response",
|
||||||
|
url: "https://www.gutenberg.org/files/1342/1342-0.txt",
|
||||||
|
expected: { contains: "Pride and Prejudice" },
|
||||||
|
validate: function(result) { return result.includes("Pride and Prejudice"); },
|
||||||
|
completed: false,
|
||||||
|
result: null,
|
||||||
|
error: null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Test execution state
|
||||||
|
var results = [];
|
||||||
|
var testCount = 0;
|
||||||
|
var activeRequests = 0;
|
||||||
|
var timeout = 5000; // 5 seconds timeout per test
|
||||||
|
|
||||||
|
// Start tests
|
||||||
|
function startTests() {
|
||||||
|
testCount = testCases.length;
|
||||||
|
activeRequests = testCount;
|
||||||
|
|
||||||
|
for (let i = 0; i < testCases.length; i++) {
|
||||||
|
let test = testCases[i];
|
||||||
|
let testName = `Test ${i + 1}: ${test.name}`;
|
||||||
|
|
||||||
|
http.fetch(test.url, function(result) {
|
||||||
|
test.completed = true;
|
||||||
|
activeRequests--;
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
test.error = result.error;
|
||||||
|
} else {
|
||||||
|
test.result = result.data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start polling loop
|
||||||
|
pollTests();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll and check test completion
|
||||||
|
function pollTests() {
|
||||||
|
let startTime = os.now();
|
||||||
|
while (true) {
|
||||||
|
http.poll();
|
||||||
|
|
||||||
|
let allCompleted = activeRequests === 0;
|
||||||
|
let timedOut = (os.now() - startTime) >= timeout;
|
||||||
|
if (allCompleted || timedOut) {
|
||||||
|
processResults();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sleep a bit to avoid pegging the CPU (requires a C function or std.sleep)
|
||||||
|
os.sleep(0.01);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process and report results
|
||||||
|
function processResults() {
|
||||||
|
for (let i = 0; i < testCases.length; i++) {
|
||||||
|
let test = testCases[i];
|
||||||
|
let testName = `Test ${i + 1}: ${test.name}`;
|
||||||
|
let passed = true;
|
||||||
|
let messages = [];
|
||||||
|
|
||||||
|
if (!test.completed) {
|
||||||
|
passed = false;
|
||||||
|
messages.push("Test timed out");
|
||||||
|
} else if (test.error) {
|
||||||
|
if (test.expectError) {
|
||||||
|
// Expected error occurred
|
||||||
|
} else {
|
||||||
|
passed = false;
|
||||||
|
messages.push(`Request failed: ${test.error}`);
|
||||||
|
}
|
||||||
|
} else if (test.expectError) {
|
||||||
|
passed = false;
|
||||||
|
messages.push("Expected request to fail but it succeeded");
|
||||||
|
} else {
|
||||||
|
const validation = test.validate(test.result);
|
||||||
|
if (typeof validation === 'boolean') {
|
||||||
|
if (!validation) {
|
||||||
|
passed = false;
|
||||||
|
messages.push(`Validation failed for ${test.url}`);
|
||||||
|
messages.push(`Expected to contain: ${JSON.stringify(test.expected)}`);
|
||||||
|
messages.push(`Got: ${test.result.substring(0, 100)}...`);
|
||||||
|
}
|
||||||
|
} else if (!validation.passed) {
|
||||||
|
passed = false;
|
||||||
|
messages.push(...validation.messages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({ testName, passed, messages });
|
||||||
|
|
||||||
|
if (!passed) {
|
||||||
|
console.log(`\nDetailed Failure Report for ${testName}:`);
|
||||||
|
console.log(`URL: ${test.url}`);
|
||||||
|
console.log(messages.join("\n"));
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
console.log("\nTest Summary:");
|
||||||
|
results.forEach(result => {
|
||||||
|
console.log(`${result.testName} - ${result.passed ? "Passed" : "Failed"}`);
|
||||||
|
if (!result.passed) {
|
||||||
|
console.log(result.messages.join("\n"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let passedCount = results.filter(r => r.passed).length;
|
||||||
|
console.log(`\nResult: ${passedCount}/${testCount} tests passed`);
|
||||||
|
|
||||||
|
if (passedCount < testCount) {
|
||||||
|
console.log("Overall: FAILED");
|
||||||
|
os.exit(1);
|
||||||
|
} else {
|
||||||
|
console.log("Overall: PASSED");
|
||||||
|
os.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the tests
|
||||||
|
startTests();
|
||||||
Reference in New Issue
Block a user