create session class and refactor js to use it

This commit is contained in:
ading2210 2024-03-14 13:13:46 -04:00
parent a282734c13
commit 0a5ace96fb
14 changed files with 359 additions and 192 deletions

View file

@ -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);
});

141
client/javascript/http.js Normal file
View file

@ -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;
}
}

View file

@ -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()},

View file

@ -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;
}
}

View file

@ -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) {

View file

@ -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) {