diff --git a/client/build.sh b/client/build.sh index 4eb497a..fec001a 100755 --- a/client/build.sh +++ b/client/build.sh @@ -82,17 +82,22 @@ WISP_VERSION=$(cat $WISP_CLIENT/package.json | jq -r '.version') sed -i "s/__wisp_version__/$WISP_VERSION/" $OUT_FILE -#add extra libraries -sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/copyright.js" $OUT_FILE -sed -i "/__extra_libraries__/r $WISP_CLIENT/polyfill.js" $OUT_FILE -sed -i "/__extra_libraries__/r $WISP_CLIENT/wisp.js" $OUT_FILE -sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/messages.js" $OUT_FILE +#js files are inserted in reverse order sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/tls_socket.js" $OUT_FILE -sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/websocket.js" $OUT_FILE sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/ws_polyfill.js" $OUT_FILE +sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/websocket.js" $OUT_FILE +sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/http.js" $OUT_FILE +sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/session.js" $OUT_FILE + +sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/copyright.js" $OUT_FILE +sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/messages.js" $OUT_FILE sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/util.js" $OUT_FILE sed -i "/__extra_libraries__/r $JAVSCRIPT_DIR/logger.js" $OUT_FILE +sed -i "/__extra_libraries__/r $WISP_CLIENT/polyfill.js" $OUT_FILE +sed -i "/__extra_libraries__/r $WISP_CLIENT/wisp.js" $OUT_FILE + + #apply patches python3 tools/patch_js.py $FRAGMENTS_DIR $OUT_FILE diff --git a/client/exported_funcs.txt b/client/exported_funcs.txt index 32955b5..d867ac1 100644 --- a/client/exported_funcs.txt +++ b/client/exported_funcs.txt @@ -1,10 +1,13 @@ init_curl -start_request -cleanup_handle -create_handle -tick_request -active_requests +session_create +session_perform +session_set_options +session_add_request +session_get_active +session_remove_request +session_cleanup +create_request get_version get_cacert get_error_str diff --git a/client/javascript/ftp.js b/client/javascript/ftp.js index 40f74b4..51c0d5d 100644 --- a/client/javascript/ftp.js +++ b/client/javascript/ftp.js @@ -27,7 +27,7 @@ class FTPSession { }; let headers_callback = () => {this.headers_callback()}; - http_handle = create_handle(url, data_callback, finish_callback, headers_callback); + http_handle = create_request(url, data_callback, finish_callback, headers_callback); _ftp_set_options(http_handle, url, 1); start_request(http_handle); }); diff --git a/client/javascript/http.js b/client/javascript/http.js new file mode 100644 index 0000000..8e1bc89 --- /dev/null +++ b/client/javascript/http.js @@ -0,0 +1,141 @@ +class HTTPSession extends CurlSession { + constructor() { + super(); + this.set_connections(50, 40); + } + + request_async(url, params, body) { + return new Promise((resolve, reject) => { + let stream_controller; + let http_handle; + let response_obj; + let aborted = false; + + //handle abort signals + if (params.signal instanceof AbortSignal) { + params.signal.addEventListener("abort", () => { + if (aborted) return; + aborted = true; + _cleanup_handle(http_handle); + if (!response_obj) { + reject(new DOMException("The operation was aborted.")); + } + else { + stream_controller.error("The operation was aborted."); + } + }); + } + + let stream = new ReadableStream({ + start(controller) { + stream_controller = controller; + } + }); + + let data_callback = (new_data) => { + try { + stream_controller.enqueue(new_data); + } + catch (e) { + //the readable stream has been closed elsewhere, so cancel the request + if (e instanceof TypeError) { + _cleanup_handle(http_handle); + } + else { + throw e; + } + } + } + let headers_callback = () => { + let response_json = c_func_str(_http_get_info, [http_handle]); + response_obj = this.constructor.create_response(stream, JSON.parse(response_json)); + resolve(response_obj); + } + let finish_callback = (error) => { + if (error != 0) { + error_msg(`Request "${url}" failed with error code ${error}: ${get_error_str(error)}`); + reject(`Request failed with error code ${error}: ${get_error_str(error)}`); + } + try { + stream_controller.close(); + } //this will only fail if the stream is already errored or closed, which isn't a problem + catch {} + this.remove_request(http_handle); + } + + let body_length = body ? body.length : 0; + let params_json = JSON.stringify(params); + + http_handle = this.create_request(url, data_callback, finish_callback, headers_callback); + c_func(_http_set_options, [http_handle, params_json, body, body_length]); + this.start_request(http_handle); + }); + } + + async fetch(url, params={}) { + let body = await this.constructor.create_options(params); + return await this.request_async(url, params, body); + } + + static create_response(response_data, response_info) { + response_info.ok = response_info.status >= 200 && response_info.status < 300; + response_info.statusText = status_messages[response_info.status] || ""; + if (response_info.status === 204 || response_info.status === 205) { + response_data = null; + } + + //construct base response object + let response_obj = new Response(response_data, response_info); + for (let key in response_info) { + if (key == "headers") continue; + Object.defineProperty(response_obj, key, { + writable: false, + value: response_info[key] + }); + } + + //create headers object + Object.defineProperty(response_obj, "headers", { + writable: false, + value: new Headers() + }); + Object.defineProperty(response_obj, "raw_headers", { + writable: false, + value: response_info.headers + }); + for (let [header_name, header_value] of response_info.headers) { + response_obj.headers.append(header_name, header_value); + } + + return response_obj; + } + + static async create_options(params) { + let body = null; + let request_obj = new Request("/", params); + let array_buffer = await request_obj.arrayBuffer(); + if (array_buffer.byteLength > 0) { + body = new Uint8Array(array_buffer); + } + + let headers = params.headers || {}; + if (params.headers instanceof Headers) { + for(let [key, value] of headers) { + headers[key] = value; + } + } + params.headers = new HeadersDict(headers); + + if (params.referrer) { + params.headers["Referer"] = params.referrer; + } + if (!params.headers["User-Agent"]) { + params.headers["User-Agent"] = navigator.userAgent; + } + if (body) { + params.headers["Content-Type"] = request_obj.headers.get("Content-Type"); + } + + return body; + } +} \ No newline at end of file diff --git a/client/javascript/main.js b/client/javascript/main.js index 2cecac2..c4f6623 100644 --- a/client/javascript/main.js +++ b/client/javascript/main.js @@ -26,11 +26,10 @@ const libcurl = (function() { /* __extra_libraries__ */ var websocket_url = null; -var event_loop = null; -var active_requests = 0; var wasm_ready = false; var version_dict = null; var api = null; +var main_session = null; const libcurl_version = "__library_version__"; const wisp_version = "__wisp_version__"; @@ -43,118 +42,6 @@ function check_loaded(check_websocket) { } } -function create_handle(url, js_data_callback, js_end_callback, js_headers_callback) { - let end_callback_ptr; - let data_callback_ptr; - let headers_callback_ptr; - - function end_callback(error) { - Module.removeFunction(end_callback_ptr); - Module.removeFunction(data_callback_ptr); - Module.removeFunction(headers_callback_ptr); - - active_requests --; - js_end_callback(error); - } - - function data_callback(chunk_ptr, chunk_size) { - let data = Module.HEAPU8.subarray(chunk_ptr, chunk_ptr + chunk_size); - let chunk = new Uint8Array(data); - js_data_callback(chunk); - } - - function headers_callback() { - js_headers_callback(); - } - - end_callback_ptr = Module.addFunction(end_callback, "vi"); - headers_callback_ptr = Module.addFunction(headers_callback, "v"); - data_callback_ptr = Module.addFunction(data_callback, "vii"); - let http_handle = c_func(_create_handle, [url, data_callback_ptr, end_callback_ptr, headers_callback_ptr]); - - return http_handle; -} - -function start_request(http_handle) { - _start_request(http_handle); - _tick_request(); - active_requests ++; - - if (!event_loop) { - event_loop = setInterval(() => { - if (_active_requests() || active_requests) { - _tick_request(); - } - else { - clearInterval(event_loop); - event_loop = null; - } - }, 0); - } -} - -function create_response(response_data, response_info) { - response_info.ok = response_info.status >= 200 && response_info.status < 300; - response_info.statusText = status_messages[response_info.status] || ""; - if (response_info.status === 204 || response_info.status === 205) { - response_data = null; - } - - //construct base response object - let response_obj = new Response(response_data, response_info); - for (let key in response_info) { - if (key == "headers") continue; - Object.defineProperty(response_obj, key, { - writable: false, - value: response_info[key] - }); - } - - //create headers object - Object.defineProperty(response_obj, "headers", { - writable: false, - value: new Headers() - }); - Object.defineProperty(response_obj, "raw_headers", { - writable: false, - value: response_info.headers - }); - for (let [header_name, header_value] of response_info.headers) { - response_obj.headers.append(header_name, header_value); - } - - return response_obj; -} - -async function create_options(params) { - let body = null; - let request_obj = new Request("/", params); - let array_buffer = await request_obj.arrayBuffer(); - if (array_buffer.byteLength > 0) { - body = new Uint8Array(array_buffer); - } - - let headers = params.headers || {}; - if (params.headers instanceof Headers) { - for(let [key, value] of headers) { - headers[key] = value; - } - } - params.headers = new HeadersDict(headers); - - if (params.referrer) { - params.headers["Referer"] = params.referrer; - } - if (!params.headers["User-Agent"]) { - params.headers["User-Agent"] = navigator.userAgent; - } - if (body) { - params.headers["Content-Type"] = request_obj.headers.get("Content-Type"); - } - - return body; -} - //wrap perform_request in a promise function perform_request_async(url, params, body) { return new Promise((resolve, reject) => { @@ -218,7 +105,7 @@ function perform_request_async(url, params, body) { let body_length = body ? body.length : 0; let params_json = JSON.stringify(params); - http_handle = create_handle(url, data_callback, finish_callback, headers_callback); + http_handle = create_request(url, data_callback, finish_callback, headers_callback); c_func(_http_set_options, [http_handle, params_json, body, body_length]); start_request(http_handle); }); @@ -263,6 +150,10 @@ function main() { let load_event = new Event("libcurl_load"); document.dispatchEvent(load_event); } + + main_session = new HTTPSession(); + api.fetch = main_session.fetch.bind(main_session); + api.onload(); } @@ -274,18 +165,19 @@ function load_wasm(url) { Module.onRuntimeInitialized = main; api = { - fetch: libcurl_fetch, set_websocket: set_websocket_url, load_wasm: load_wasm, - WebSocket: FakeWebSocket, - CurlWebSocket: CurlWebSocket, - TLSSocket: TLSSocket, get_cacert: get_cacert, get_error_string: get_error_str, wisp_connections: _wisp_connections, WispConnection: WispConnection, transport: "wisp", + + WebSocket: WebSocket, + CurlWebSocket: CurlWebSocket, + TLSSocket: TLSSocket, + fetch: () => {throw "not ready"}, get copyright() {return copyright_notice}, get version() {return get_version()}, diff --git a/client/javascript/session.js b/client/javascript/session.js new file mode 100644 index 0000000..f156042 --- /dev/null +++ b/client/javascript/session.js @@ -0,0 +1,98 @@ +class CurlSession { + constructor(options={}) { + check_loaded(true); + + this.options = options; + this.session_ptr = _session_create(); + this.active_requests = 0; + this.event_loop = null; + this.requests_list = []; + } + + assert_ready() { + if (!this.session_ptr) { + throw "session has been removed"; + } + } + + set_connections(connections_limit, cache_limit) { + this.assert_ready(); + _session_set_options(this.session_ptr, connections_limit, cache_limit); + } + + create_request(url, js_data_callback, js_end_callback, js_headers_callback) { + this.assert_ready(); + let end_callback_ptr; + let data_callback_ptr; + let headers_callback_ptr; + + let end_callback = (error) => { + Module.removeFunction(end_callback_ptr); + Module.removeFunction(data_callback_ptr); + Module.removeFunction(headers_callback_ptr); + + this.active_requests--; + js_end_callback(error); + } + + let data_callback = (chunk_ptr, chunk_size) => { + let data = Module.HEAPU8.subarray(chunk_ptr, chunk_ptr + chunk_size); + let chunk = new Uint8Array(data); + js_data_callback(chunk); + } + + let headers_callback = () => { + js_headers_callback(); + } + + end_callback_ptr = Module.addFunction(end_callback, "vi"); + headers_callback_ptr = Module.addFunction(headers_callback, "v"); + data_callback_ptr = Module.addFunction(data_callback, "vii"); + let request_ptr = c_func(_create_request, [url, data_callback_ptr, end_callback_ptr, headers_callback_ptr]); + + return request_ptr; + } + + remove_request(request_ptr) { + this.assert_ready(); + _session_remove_request(this.session_ptr, request_ptr); + + let request_index = this.requests_list.indexOf(request_ptr); + if (request_index !== -1) { + this.requests_list.splice(request_index, 1); + } + } + + start_request(request_ptr) { + this.assert_ready(); + _session_add_request(this.session_ptr, request_ptr); + _session_perform(this.session_ptr); + + this.active_requests++; + this.requests_list.push(request_ptr); + + if (this.event_loop) { + return; + } + + this.event_loop = setInterval(() => { + let libcurl_active = _session_get_active(this.session_ptr); + if (libcurl_active || this.active_requests) { + _session_perform(this.session_ptr); + } + else { + clearInterval(this.event_loop); + this.event_loop = null; + } + }, 0); + } + + close() { + this.assert_ready(); + for (let request_ptr of this.requests_list) { + this.remove_request(request_ptr); + } + _session_cleanup(this.session_ptr); + this.session_ptr = null; + } +} \ No newline at end of file diff --git a/client/javascript/tls_socket.js b/client/javascript/tls_socket.js index aa1b8f5..a55cb4d 100644 --- a/client/javascript/tls_socket.js +++ b/client/javascript/tls_socket.js @@ -1,8 +1,8 @@ //currently broken -class TLSSocket { +class TLSSocket extends CurlSession { constructor(hostname, port, options={}) { - check_loaded(true); + super(); this.hostname = hostname; this.port = port; @@ -15,8 +15,9 @@ class TLSSocket { this.onclose = () => {}; this.connected = false; - this.event_loop = null; + this.recv_loop = null; + this.set_connections(1, 0); this.connect(); } @@ -29,7 +30,7 @@ class TLSSocket { let finish_callback = (error) => { if (error === 0) { this.connected = true; - this.event_loop = setInterval(() => { + this.recv_loop = setInterval(() => { let data = this.recv(); if (data != null) this.onmessage(data); }, 0); @@ -40,9 +41,9 @@ class TLSSocket { } } - this.http_handle = create_handle(this.url, data_callback, finish_callback, headers_callback); + this.http_handle = this.create_request(this.url, data_callback, finish_callback, headers_callback); _tls_socket_set_options(this.http_handle, +this.options.verbose); - start_request(this.http_handle); + this.start_request(this.http_handle); } recv() { @@ -80,10 +81,14 @@ class TLSSocket { } cleanup(error=false) { - if (this.http_handle) _cleanup_handle(this.http_handle); + if (this.http_handle) { + this.remove_request(this.http_handle); + this.http_handle = null; + super.close(); + } else return; - clearInterval(this.event_loop); + clearInterval(this.recv_loop); this.connected = false; if (error) { diff --git a/client/javascript/websocket.js b/client/javascript/websocket.js index 222f795..9e95dc6 100644 --- a/client/javascript/websocket.js +++ b/client/javascript/websocket.js @@ -1,9 +1,9 @@ -class CurlWebSocket { - constructor(url, protocols=[], options={}) { - check_loaded(true); +class CurlWebSocket extends CurlSession { + constructor(url, protocols=[], options={}) { if (!url.startsWith("wss://") && !url.startsWith("ws://")) { throw new SyntaxError("invalid url"); } + super(); this.url = url; this.protocols = protocols; @@ -15,9 +15,11 @@ class CurlWebSocket { this.onclose = () => {}; this.connected = false; - this.event_loop = null; + this.recv_loop = null; + this.http_handle = null; this.recv_buffer = []; + this.set_connections(1, 0); this.connect(); } @@ -27,7 +29,7 @@ class CurlWebSocket { let finish_callback = (error) => { if (error === 0) { this.connected = true; - this.event_loop = setInterval(() => { + this.recv_loop = setInterval(() => { let data = this.recv(); if (data !== null) this.onmessage(data); }, 0); @@ -47,10 +49,10 @@ class CurlWebSocket { request_options._libcurl_verbose = 1; } - this.http_handle = create_handle(this.url, data_callback, finish_callback, headers_callback); + this.http_handle = this.create_request(this.url, data_callback, finish_callback, headers_callback); c_func(_http_set_options, [this.http_handle, JSON.stringify(request_options), null, 0]); _websocket_set_options(this.http_handle); - start_request(this.http_handle); + this.start_request(this.http_handle); } recv() { @@ -104,10 +106,14 @@ class CurlWebSocket { } cleanup(error=0) { - if (this.http_handle) _cleanup_handle(this.http_handle); + if (this.http_handle) { + this.remove_handle(this.http_handle); + this.http_handle = null; + super.close(); + } else return; - clearInterval(this.event_loop); + clearInterval(this.recv_loop); this.connected = false; if (error) { diff --git a/client/libcurl/main.c b/client/libcurl/request.c similarity index 69% rename from client/libcurl/main.c rename to client/libcurl/request.c index e01a2e3..e90b857 100644 --- a/client/libcurl/main.c +++ b/client/libcurl/request.c @@ -14,8 +14,6 @@ void finish_request(CURLMsg *curl_msg); void forward_headers(struct RequestInfo *request_info); -CURLM *multi_handle; -int request_active = 0; struct curl_blob cacert_blob; size_t write_function(char *data, size_t size, size_t nmemb, struct RequestInfo *request_info) { @@ -30,25 +28,7 @@ size_t write_function(char *data, size_t size, size_t nmemb, struct RequestInfo return real_size; } -int active_requests() { - return request_active; -} - -void tick_request() { - CURLMcode mc; - struct CURLMsg *curl_msg; - request_active = 1; - - mc = curl_multi_perform(multi_handle, &request_active); - - int msgq = 0; - curl_msg = curl_multi_info_read(multi_handle, &msgq); - if (curl_msg && curl_msg->msg == CURLMSG_DONE) { - finish_request(curl_msg); - } -} - -CURL* create_handle(const char* url, DataCallback data_callback, EndCallback end_callback, HeadersCallback headers_callback) { +CURL* create_request(const char* url, DataCallback data_callback, EndCallback end_callback, HeadersCallback headers_callback) { CURL *http_handle = curl_easy_init(); //create request metadata struct @@ -57,7 +37,6 @@ CURL* create_handle(const char* url, DataCallback data_callback, EndCallback end request_info->curl_msg = NULL; request_info->headers_list = NULL; request_info->headers_received = 0; - request_info->prevent_cleanup = 0; request_info->end_callback = end_callback; request_info->data_callback = data_callback; request_info->headers_callback = headers_callback; @@ -73,10 +52,6 @@ CURL* create_handle(const char* url, DataCallback data_callback, EndCallback end return http_handle; } -void start_request(CURL* http_handle) { - curl_multi_add_handle(multi_handle, http_handle); -} - void forward_headers(struct RequestInfo *request_info) { request_info->headers_received = 1; (*request_info->headers_callback)(); @@ -96,19 +71,6 @@ void finish_request(CURLMsg *curl_msg) { curl_slist_free_all(request_info->headers_list); } (*request_info->end_callback)(error); - if (request_info->prevent_cleanup) { - return; - } - curl_multi_remove_handle(multi_handle, http_handle); - curl_easy_cleanup(http_handle); - free(request_info); -} - -void cleanup_handle(CURL* http_handle) { - struct RequestInfo *request_info = get_request_info(http_handle); - curl_multi_remove_handle(multi_handle, http_handle); - curl_easy_cleanup(http_handle); - free(request_info); } unsigned char* get_cacert() { @@ -117,8 +79,6 @@ unsigned char* get_cacert() { void init_curl() { curl_global_init(CURL_GLOBAL_DEFAULT); - multi_handle = curl_multi_init(); - cacert_blob.data = _cacert_pem; cacert_blob.len = _cacert_pem_len; cacert_blob.flags = CURL_BLOB_NOCOPY; diff --git a/client/libcurl/request.h b/client/libcurl/request.h new file mode 100644 index 0000000..7530898 --- /dev/null +++ b/client/libcurl/request.h @@ -0,0 +1,3 @@ +#include "curl/multi.h" + +void finish_request(CURLMsg *curl_msg); diff --git a/client/libcurl/session.c b/client/libcurl/session.c new file mode 100644 index 0000000..059f844 --- /dev/null +++ b/client/libcurl/session.c @@ -0,0 +1,52 @@ +#include + +#include "curl/multi.h" +#include "curl/curl.h" + +#include "types.h" +#include "request.h" +#include "util.h" + +struct SessionInfo* session_create() { + struct SessionInfo *session = malloc(sizeof(struct SessionInfo)); + session->multi_handle = curl_multi_init(); + session->request_active = 0; + return session; +} + +void session_perform(struct SessionInfo *session) { + CURLMcode mc; + session->request_active = 0; + mc = curl_multi_perform(session->multi_handle, &session->request_active); + + int msgq = 0; + struct CURLMsg *curl_msg; + curl_msg = curl_multi_info_read(session->multi_handle, &msgq); + if (curl_msg && curl_msg->msg == CURLMSG_DONE) { + finish_request(curl_msg); + } +} + +void session_set_options(struct SessionInfo *session, int connections_limit, int cache_limit) { + curl_multi_setopt(session->multi_handle, CURLMOPT_MAX_TOTAL_CONNECTIONS, connections_limit); + curl_multi_setopt(session->multi_handle, CURLMOPT_MAXCONNECTS, cache_limit); +} + +void session_add_request(struct SessionInfo *session, CURL* http_handle) { + curl_multi_add_handle(session->multi_handle, http_handle); +} + +int session_get_active(struct SessionInfo *session) { + return session->request_active; +} + +void session_remove_request(struct SessionInfo *session, CURL* http_handle) { + struct RequestInfo *request_info = get_request_info(http_handle); + curl_multi_remove_handle(session->multi_handle, http_handle); + curl_easy_cleanup(http_handle); + free(request_info); +} + +void session_cleanup(struct SessionInfo *session) { + curl_multi_cleanup(session->multi_handle); +} \ No newline at end of file diff --git a/client/libcurl/tls_socket.c b/client/libcurl/tls_socket.c index f6a1802..4c95312 100644 --- a/client/libcurl/tls_socket.c +++ b/client/libcurl/tls_socket.c @@ -31,5 +31,4 @@ void tls_socket_set_options(CURL* http_handle, int verbose) { curl_easy_setopt(http_handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); curl_easy_setopt(http_handle, CURLOPT_SSL_ENABLE_ALPN, 0L); curl_easy_setopt(http_handle, CURLOPT_VERBOSE, (long) verbose); - request_info->prevent_cleanup = 1; } diff --git a/client/libcurl/types.h b/client/libcurl/types.h index 3a4e7b1..522c399 100644 --- a/client/libcurl/types.h +++ b/client/libcurl/types.h @@ -6,7 +6,6 @@ typedef void(*HeadersCallback)(); struct RequestInfo { CURL* http_handle; - int prevent_cleanup; int headers_received; struct CURLMsg *curl_msg; struct curl_slist* headers_list; @@ -22,4 +21,9 @@ struct WSResult { int bytes_left; int is_text; char* buffer; +}; + +struct SessionInfo { + CURLM* multi_handle; + int request_active; }; \ No newline at end of file diff --git a/client/libcurl/websocket.c b/client/libcurl/websocket.c index 4e2101b..1784d43 100644 --- a/client/libcurl/websocket.c +++ b/client/libcurl/websocket.c @@ -40,7 +40,6 @@ void close_websocket(CURL* http_handle) { void websocket_set_options(CURL* http_handle) { struct RequestInfo *request_info = get_request_info(http_handle); curl_easy_setopt(http_handle, CURLOPT_CONNECT_ONLY, 2L); - request_info->prevent_cleanup = 1; } int get_result_size (const struct WSResult* result) {