mirror of
https://github.com/ading2210/libcurl.js.git
synced 2025-05-12 22:10:01 -04:00
reorganize client code
This commit is contained in:
parent
9cc0e4178b
commit
717331bfc1
9 changed files with 8 additions and 7 deletions
280
client/javascript/main.js
Normal file
280
client/javascript/main.js
Normal file
|
@ -0,0 +1,280 @@
|
|||
//everything is wrapped in a function to prevent emscripten from polluting the global scope
|
||||
window.libcurl = (function() {
|
||||
|
||||
//emscripten compiled code is inserted here
|
||||
/* __emscripten_output__ */
|
||||
|
||||
//extra client code goes here
|
||||
/* __extra_libraries__ */
|
||||
|
||||
var websocket_url = `wss://${location.hostname}/ws/`;
|
||||
var event_loop = null;
|
||||
var active_requests = 0;
|
||||
var wasm_ready = false;
|
||||
|
||||
function check_loaded() {
|
||||
if (!wasm_ready) {
|
||||
throw new Error("wasm not loaded yet, please call libcurl.load_wasm first");
|
||||
}
|
||||
}
|
||||
|
||||
//a case insensitive dictionary for request headers
|
||||
class HeadersDict {
|
||||
constructor(obj) {
|
||||
for (let key in obj) {
|
||||
this[key] = obj[key];
|
||||
}
|
||||
return new Proxy(this, this);
|
||||
}
|
||||
get(target, prop) {
|
||||
let keys = Object.keys(this);
|
||||
for (let key of keys) {
|
||||
if (key.toLowerCase() === prop.toLowerCase()) {
|
||||
return this[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
set(target, prop, value) {
|
||||
let keys = Object.keys(this);
|
||||
for (let key of keys) {
|
||||
if (key.toLowerCase() === prop.toLowerCase()) {
|
||||
this[key] = value;
|
||||
}
|
||||
}
|
||||
this[prop] = value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function is_str(obj) {
|
||||
return typeof obj === 'string' || obj instanceof String;
|
||||
}
|
||||
|
||||
function allocate_str(str) {
|
||||
return allocate(intArrayFromString(str), ALLOC_NORMAL);
|
||||
}
|
||||
|
||||
function allocate_array(array) {
|
||||
return allocate(array, ALLOC_NORMAL);
|
||||
}
|
||||
|
||||
//low level interface with c code
|
||||
function perform_request(url, params, js_data_callback, js_end_callback, body=null) {
|
||||
let params_str = JSON.stringify(params);
|
||||
let end_callback_ptr;
|
||||
let data_callback_ptr;
|
||||
let url_ptr = allocate_str(url);
|
||||
let params_ptr = allocate_str(params_str);
|
||||
|
||||
let body_ptr = null;
|
||||
let body_length = 0;
|
||||
if (body) { //assume body is an int8array
|
||||
body_ptr = allocate_array(body);
|
||||
body_length = body.length;
|
||||
}
|
||||
|
||||
let end_callback = (error, response_json_ptr) => {
|
||||
let response_json = UTF8ToString(response_json_ptr);
|
||||
let response_info = JSON.parse(response_json);
|
||||
|
||||
Module.removeFunction(end_callback_ptr);
|
||||
Module.removeFunction(data_callback_ptr);
|
||||
if (body_ptr) _free(body_ptr);
|
||||
_free(url_ptr);
|
||||
_free(response_json_ptr);
|
||||
|
||||
if (error != 0) console.error("request failed with error code " + error);
|
||||
active_requests --;
|
||||
js_end_callback(error, response_info);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
end_callback_ptr = Module.addFunction(end_callback, "vii");
|
||||
data_callback_ptr = Module.addFunction(data_callback, "vii");
|
||||
let http_handle = _start_request(url_ptr, params_ptr, data_callback_ptr, end_callback_ptr, body_ptr, body_length);
|
||||
_free(params_ptr);
|
||||
|
||||
active_requests ++;
|
||||
_tick_request();
|
||||
if (!event_loop) {
|
||||
event_loop = setInterval(() => {
|
||||
if (_active_requests() || active_requests) {
|
||||
_tick_request();
|
||||
}
|
||||
else {
|
||||
clearInterval(event_loop);
|
||||
event_loop = null;
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
return http_handle;
|
||||
}
|
||||
|
||||
function merge_arrays(arrays) {
|
||||
let total_len = arrays.reduce((acc, val) => acc + val.length, 0);
|
||||
let new_array = new Uint8Array(total_len);
|
||||
let offset = 0;
|
||||
for (let array of arrays) {
|
||||
new_array.set(array, offset);
|
||||
offset += array.length;
|
||||
}
|
||||
return new_array;
|
||||
}
|
||||
|
||||
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] || "";
|
||||
|
||||
//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()
|
||||
});
|
||||
for (let header_name in response_info.headers) {
|
||||
let header_value = response_info.headers[header_name];
|
||||
response_obj.headers.append(header_name, header_value);
|
||||
}
|
||||
|
||||
return response_obj;
|
||||
}
|
||||
|
||||
async function parse_body(data) {
|
||||
let data_array = null;
|
||||
if (typeof data === "string") {
|
||||
data_array = new TextEncoder().encode(data);
|
||||
}
|
||||
|
||||
else if (data instanceof Blob) {
|
||||
let array_buffer = await data.arrayBuffer();
|
||||
data_array = new Uint8Array(array_buffer);
|
||||
}
|
||||
|
||||
//any typedarray
|
||||
else if (data instanceof ArrayBuffer) {
|
||||
//dataview objects
|
||||
if (ArrayBuffer.isView(data) && data instanceof DataView) {
|
||||
data_array = new Uint8Array(data.buffer);
|
||||
}
|
||||
//regular typed arrays
|
||||
else if (ArrayBuffer.isView(data)) {
|
||||
data_array = Uint8Array.from(data);
|
||||
}
|
||||
//regular arraybuffers
|
||||
else {
|
||||
data_array = new Uint8Array(data);
|
||||
}
|
||||
}
|
||||
|
||||
else if (data instanceof ReadableStream) {
|
||||
let chunks = [];
|
||||
for await (let chunk of data) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
data_array = merge_arrays(chunks);
|
||||
}
|
||||
|
||||
else {
|
||||
throw "invalid data type to be sent";
|
||||
}
|
||||
return data_array;
|
||||
}
|
||||
|
||||
async function create_options(params) {
|
||||
let body = null;
|
||||
if (params.body) {
|
||||
body = await parse_body(params.body);
|
||||
params.body = true;
|
||||
}
|
||||
|
||||
if (!params.headers) params.headers = {};
|
||||
params.headers = new HeadersDict(params.headers);
|
||||
|
||||
if (params.referer) {
|
||||
params.headers["Referer"] = params.referer;
|
||||
}
|
||||
if (!params.headers["User-Agent"]) {
|
||||
params.headers["User-Agent"] = navigator.userAgent;
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
//wrap perform_request in a promise
|
||||
function perform_request_async(url, params, body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let chunks = [];
|
||||
let data_callback = (new_data) => {
|
||||
chunks.push(new_data);
|
||||
};
|
||||
|
||||
let finish_callback = (error, response_info) => {
|
||||
if (error != 0) {
|
||||
reject("libcurl.js encountered an error: " + error);
|
||||
return;
|
||||
}
|
||||
let response_data = merge_arrays(chunks);
|
||||
let response_obj = create_response(response_data, response_info);
|
||||
resolve(response_obj);
|
||||
}
|
||||
perform_request(url, params, data_callback, finish_callback, body);
|
||||
});
|
||||
}
|
||||
|
||||
async function libcurl_fetch(url, params={}) {
|
||||
check_loaded();
|
||||
let body = await create_options(params);
|
||||
return await perform_request_async(url, params, body);
|
||||
}
|
||||
|
||||
function set_websocket_url(url) {
|
||||
check_loaded();
|
||||
if (!Module.websocket) {
|
||||
document.addEventListener("libcurl_load", () => {
|
||||
set_websocket_url(url);
|
||||
});
|
||||
}
|
||||
else Module.websocket.url = url;
|
||||
}
|
||||
|
||||
function main() {
|
||||
console.log("emscripten module loaded");
|
||||
wasm_ready = true;
|
||||
_init_curl();
|
||||
set_websocket_url(websocket_url);
|
||||
|
||||
let load_event = new Event("libcurl_load");
|
||||
document.dispatchEvent(load_event);
|
||||
}
|
||||
|
||||
function load_wasm(url) {
|
||||
wasmBinaryFile = url;
|
||||
createWasm();
|
||||
run();
|
||||
}
|
||||
|
||||
Module.onRuntimeInitialized = main;
|
||||
return {
|
||||
fetch: libcurl_fetch,
|
||||
set_websocket: set_websocket_url,
|
||||
load_wasm: load_wasm,
|
||||
wisp: _wisp_connections,
|
||||
WebSocket: CurlWebSocket
|
||||
}
|
||||
|
||||
})()
|
65
client/javascript/messages.js
Normal file
65
client/javascript/messages.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
const status_messages = {
|
||||
100: "Continue",
|
||||
101: "Switching Protocols",
|
||||
102: "Processing",
|
||||
103: "Early Hints",
|
||||
200: "OK",
|
||||
201: "Created",
|
||||
202: "Accepted",
|
||||
203: "Non-Authoritative Information",
|
||||
204: "No Content",
|
||||
205: "Reset Content",
|
||||
206: "Partial Content",
|
||||
207: "Multi-Status",
|
||||
208: "Already Reported",
|
||||
226: "IM Used",
|
||||
300: "Multiple Choices",
|
||||
301: "Moved Permanently",
|
||||
302: "Found",
|
||||
303: "See Other",
|
||||
304: "Not Modified",
|
||||
305: "Use Proxy",
|
||||
306: "Switch Proxy",
|
||||
307: "Temporary Redirect",
|
||||
308: "Permanent Redirect",
|
||||
400: "Bad Request",
|
||||
401: "Unauthorized",
|
||||
402: "Payment Required",
|
||||
403: "Forbidden",
|
||||
404: "Not Found",
|
||||
405: "Method Not Allowed",
|
||||
406: "Not Acceptable",
|
||||
407: "Proxy Authentication Required",
|
||||
408: "Request Timeout",
|
||||
409: "Conflict",
|
||||
410: "Gone",
|
||||
411: "Length Required",
|
||||
412: "Precondition Failed",
|
||||
413: "Payload Too Large",
|
||||
414: "URI Too Long",
|
||||
415: "Unsupported Media Type",
|
||||
416: "Range Not Satisfiable",
|
||||
417: "Expectation Failed",
|
||||
418: "I'm a teapot",
|
||||
421: "Misdirected Request",
|
||||
422: "Unprocessable Content",
|
||||
423: "Locked",
|
||||
424: "Failed Dependency",
|
||||
425: "Too Early",
|
||||
426: "Upgrade Required",
|
||||
428: "Precondition Required",
|
||||
429: "Too Many Requests",
|
||||
431: "Request Header Fields Too Large",
|
||||
451: "Unavailable For Legal Reasons",
|
||||
500: "Internal Server Error",
|
||||
501: "Not Implemented",
|
||||
502: "Bad Gateway",
|
||||
503: "Service Unavailable",
|
||||
504: "Gateway Timeout",
|
||||
505: "HTTP Version Not Supported",
|
||||
506: "Variant Also Negotiates",
|
||||
507: "Insufficient Storage",
|
||||
508: "Loop Detected",
|
||||
510: "Not Extended",
|
||||
511: "Network Authentication Required"
|
||||
}
|
190
client/javascript/websocket.js
Normal file
190
client/javascript/websocket.js
Normal file
|
@ -0,0 +1,190 @@
|
|||
//class for custom websocket
|
||||
|
||||
class CurlWebSocket extends EventTarget {
|
||||
constructor(url, protocols=[]) {
|
||||
super();
|
||||
if (!url.startsWith("wss://") && !url.startsWith("ws://")) {
|
||||
throw new SyntaxError("invalid url");
|
||||
}
|
||||
|
||||
this.url = url;
|
||||
this.protocols = protocols;
|
||||
this.binaryType = "blob";
|
||||
this.recv_buffer = [];
|
||||
|
||||
//legacy event handlers
|
||||
this.onopen = () => {};
|
||||
this.onerror = () => {};
|
||||
this.onmessage = () => {};
|
||||
this.onclose = () => {};
|
||||
|
||||
this.CONNECTING = 0;
|
||||
this.OPEN = 1;
|
||||
this.CLOSING = 2;
|
||||
this.CLOSED = 3;
|
||||
|
||||
this.connect();
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.status = this.CONNECTING;
|
||||
let data_callback = () => {};
|
||||
let finish_callback = (error, response_info) => {
|
||||
this.finish_callback(error, response_info);
|
||||
}
|
||||
this.http_handle = perform_request(this.url, {}, data_callback, finish_callback, null);
|
||||
this.recv_loop();
|
||||
}
|
||||
|
||||
recv() {
|
||||
let buffer_size = 64*1024;
|
||||
let result_ptr = _recv_from_websocket(this.http_handle, buffer_size);
|
||||
let result_code = _get_result_code(result_ptr);
|
||||
|
||||
if (result_code == 0) { //CURLE_OK - data recieved
|
||||
if (_get_result_closed(result_ptr)) {
|
||||
//this.pass_buffer();
|
||||
this.close_callback();
|
||||
return;
|
||||
}
|
||||
|
||||
let data_size = _get_result_size(result_ptr);
|
||||
let data_ptr = _get_result_buffer(result_ptr);
|
||||
let data_heap = Module.HEAPU8.subarray(data_ptr, data_ptr + data_size);
|
||||
let data = new Uint8Array(data_heap);
|
||||
_free(data_ptr);
|
||||
|
||||
this.recv_buffer.push(data);
|
||||
if (data_size !== buffer_size && !_get_result_bytes_left(result_ptr)) { //message finished
|
||||
let full_data = merge_arrays(this.recv_buffer);
|
||||
let is_text = _get_result_is_text(result_ptr)
|
||||
this.recv_buffer = [];
|
||||
this.recv_callback(full_data, is_text);
|
||||
}
|
||||
}
|
||||
|
||||
if (result_code == 52) { //CURLE_GOT_NOTHING - socket closed
|
||||
this.close_callback();
|
||||
}
|
||||
|
||||
_free(result_ptr);
|
||||
}
|
||||
|
||||
recv_loop() {
|
||||
this.event_loop = setInterval(() => {
|
||||
this.recv();
|
||||
}, 1);
|
||||
}
|
||||
|
||||
recv_callback(data, is_text=false) {
|
||||
let converted;
|
||||
if (is_text) {
|
||||
converted = new TextDecoder().decode(data);
|
||||
}
|
||||
else {
|
||||
if (this.binaryType == "blob") {
|
||||
converted = new Blob(data);
|
||||
}
|
||||
else if (this.binaryType == "arraybuffer") {
|
||||
converted = data.buffer;
|
||||
}
|
||||
else {
|
||||
throw "invalid binaryType string";
|
||||
}
|
||||
}
|
||||
|
||||
let msg_event = new MessageEvent("message", {data: converted});
|
||||
this.onmessage(msg_event);
|
||||
this.dispatchEvent(msg_event);
|
||||
}
|
||||
|
||||
close_callback(error=false) {
|
||||
if (this.status == this.CLOSED) return;
|
||||
this.status = this.CLOSED;
|
||||
|
||||
clearInterval(this.event_loop);
|
||||
_cleanup_websocket();
|
||||
|
||||
if (error) {
|
||||
let error_event = new Event("error");
|
||||
this.dispatchEvent(error_event);
|
||||
this.onerror(error_event);
|
||||
}
|
||||
else {
|
||||
let close_event = new CloseEvent("close");
|
||||
this.dispatchEvent(close_event);
|
||||
this.onclose(close_event);
|
||||
}
|
||||
}
|
||||
|
||||
finish_callback(error, response_info) {
|
||||
this.status = this.OPEN;
|
||||
if (error != 0) this.close_callback(true);
|
||||
let open_event = new Event("open");
|
||||
this.onopen(open_event);
|
||||
this.dispatchEvent(open_event);
|
||||
}
|
||||
|
||||
send(data) {
|
||||
let is_text = false;
|
||||
if (this.status === this.CONNECTING) {
|
||||
throw new DOMException("ws not ready yet");
|
||||
}
|
||||
if (this.status === this.CLOSED) {
|
||||
return;
|
||||
}
|
||||
|
||||
let data_array;
|
||||
if (typeof data === "string") {
|
||||
data_array = new TextEncoder().encode(data);
|
||||
is_text = true;
|
||||
}
|
||||
else if (data instanceof Blob) {
|
||||
data.arrayBuffer().then(array_buffer => {
|
||||
data_array = new Uint8Array(array_buffer);
|
||||
this.send(data_array);
|
||||
});
|
||||
return;
|
||||
}
|
||||
//any typedarray
|
||||
else if (data instanceof ArrayBuffer) {
|
||||
//dataview objects
|
||||
if (ArrayBuffer.isView(data) && data instanceof DataView) {
|
||||
data_array = new Uint8Array(data.buffer);
|
||||
}
|
||||
//regular typed arrays
|
||||
else if (ArrayBuffer.isView(data)) {
|
||||
data_array = Uint8Array.from(data);
|
||||
}
|
||||
//regular arraybuffers
|
||||
else {
|
||||
data_array = new Uint8Array(data);
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw "invalid data type to be sent";
|
||||
}
|
||||
|
||||
let data_ptr = allocate_array(data_array);
|
||||
let data_len = data.length;
|
||||
_send_to_websocket(this.http_handle, data_ptr, data_len, is_text);
|
||||
_free(data_ptr);
|
||||
}
|
||||
|
||||
close() {
|
||||
_close_websocket(this.http_handle);
|
||||
}
|
||||
|
||||
get readyState() {
|
||||
return this.status;
|
||||
}
|
||||
get bufferedAmount() {
|
||||
return 0;
|
||||
}
|
||||
get protocol() {
|
||||
return "";
|
||||
}
|
||||
get extensions() {
|
||||
return "";
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue