UNPKG

@qubit-rs/client

Version:

Seamless RPC for Rust & TypeScript

369 lines (360 loc) 10 kB
// src/util/promise_manager.ts function create_promise_manager() { const promises = {}; return { /** Send some payload, for a given ID */ wait_for: (id) => { return new Promise((resolve) => { promises[id] = resolve; }); }, /** Resolve a response based on an ID */ resolve: (response) => { const handler = promises[response.id]; if (handler) { delete promises[response.id]; handler(response); } } }; } // src/jsonrpc/index.ts function create_payload(id, method, params) { return { jsonrpc: "2.0", method, id, params }; } function parse_response(response) { try { if (typeof response === "string") { response = JSON.parse(response); } if (response?.jsonrpc !== "2.0") { throw new Error("invalid value for `jsonrpc`"); } if ("params" in response && "subscription" in response.params && "result" in response.params) { return { type: "message", id: response.params.subscription, value: response.params.result }; } if (typeof response?.id !== "number" && typeof response?.id !== "string" && response?.id !== null) { throw new Error("missing `id` field from response"); } if ("result" in response && !("error" in response)) { return { type: "ok", id: response.id, value: response.result }; } if ("error" in response && !("result" in response)) { if (typeof response.error?.code === "number" && typeof response.error?.message === "string") { return { type: "error", id: response.id, value: response.error }; } throw new Error("malformed error object in response"); } throw new Error("invalid response object"); } catch (e) { console.error("Error encountered whilst parsing response"); console.error(e); return null; } } // src/util/socket.ts function create_socket(host, on_message, options) { let socket_open = false; let next_timeout = 0.5; let queue = []; const WS = options?.WebSocket || WebSocket; let socket; function new_socket() { socket = new WS(host); socket.addEventListener("open", () => { socket_open = true; next_timeout = 0.5; for (const payload of queue) { socket.send(payload); } queue = []; }); socket.addEventListener("message", (e) => { const message = parse_response(e.data); if (message) { on_message(message); } }); socket.addEventListener("close", () => { socket_open = false; setTimeout(() => { next_timeout *= 2; new_socket(); }, next_timeout * 1e3); }); } new_socket(); return { send: (payload) => { if (!socket_open) { queue.push(payload); } else { socket.send(payload); } } }; } // src/util/subscription_manager.ts function create_subscription_manager() { const subscriptions = {}; function get_subscription(id) { let subscription = subscriptions[id]; if (!subscription) { subscription = subscriptions[id] = { queue: [] }; } return subscription; } return { /** Handle an incoming message */ handle: (message) => { const subscription = get_subscription(message.id); if (!subscription.handler) { subscription.queue.push(message.value); return; } subscription.handler(message.value); }, /** Register a handler for when new data arrives for a given subscription. */ register: (id, handler) => { const subscription = get_subscription(id); if (subscription.handler) { console.error(`attempted to subscribe to a subscription multiple times (subscription ID: ${id})`); return; } subscription.handler = handler; for (const value of subscription.queue) { handler(value); } subscription.queue = []; }, /** Remove the given subscription. */ remove: (id) => { delete subscriptions[id]; } }; } // src/transport/ws.ts function ws(host, socket_options) { const subscriptions = create_subscription_manager(); const requests = create_promise_manager(); const socket = create_socket( host, (message) => { if (message.type === "message") { subscriptions.handle(message); } else if ("id" in message) { requests.resolve(message); } }, socket_options ); const send_request = (id, payload) => { socket.send(payload); return requests.wait_for(id); }; return { query: (id, payload) => send_request(id, JSON.stringify(payload)), mutate: (id, payload) => send_request(id, JSON.stringify(payload)), subscribe: (id, on_data) => { if (on_data) { subscriptions.register(id, on_data); } return () => { subscriptions.remove(id); }; } }; } // src/transport/http.ts function http(host, http_options) { const fetch_impl = http_options?.fetch || fetch; return { query: async (_id, payload) => { const temp_url = new URL(host, "http://example.com"); temp_url.searchParams.set("input", encodeURIComponent(JSON.stringify(payload))); const url = `${host.replace(/\?.*$/, "")}?${temp_url.searchParams.toString()}`; const res = await fetch_impl(url, { method: "GET", mode: "cors" }); const body = await res.json(); return parse_response(body); }, mutate: async (_id, payload) => { const res = await fetch_impl(host, { method: "POST", mode: "cors", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); const body = await res.json(); return parse_response(body); } }; } // src/transport/multi.ts function multi(host, options) { const http_client = http(host, options?.http); const ws_client = ws(host, options?.ws); return { query: (id, payload) => { return http_client.query(id, payload); }, mutate: (id, payload) => { return http_client.mutate(id, payload); }, subscribe: (id, on_data) => { return ws_client.subscribe(id, on_data); } }; } // src/path_builder.ts function proxy_builder(builder) { return new Proxy({}, { get(_target, property) { return builder(property); } }); } function wrap_handlers(handlers, path) { const wrapped = {}; for (const [key, handler] of Object.entries(handlers)) { wrapped[key] = (...args) => { return handler(path, ...args); }; } return wrapped; } function create_path_builder(handlers) { return proxy_builder((property) => { const path = []; if (typeof property === "string") { path.push(property); } const proxy = new Proxy(wrap_handlers(handlers, path), { get: (target, property2, proxy2) => { if (typeof property2 !== "string") { console.warn("attempted to access non-string property:", property2); return proxy2; } if (property2 in target) { return target[property2]; } path.push(property2); return proxy2; } }); return proxy; }); } // src/client.ts function sync_promise(implementation) { const callback_promise = implementation(); return () => { callback_promise.then((callback) => { callback(); }); }; } function get_handlers(handler) { let on_data = (_) => { }; let on_error = (_) => { }; let on_end = () => { }; if (typeof handler === "function") { on_data = handler; } else { if (handler?.on_data) { on_data = handler.on_data; } if (handler?.on_error) { on_error = handler.on_error; } if (handler?.on_end) { on_end = handler.on_end; } } return { on_data, on_error, on_end }; } function build_client(client, plugins) { let next_id = 0; async function send(method, sender, args) { const id = next_id++; const payload = create_payload(id, method.join("."), args); const response = await sender(id, payload); if (response === null) { throw new Error("malformed response from the API"); } if (response.type !== "ok") { throw response.value; } return response.value; } return create_path_builder({ ...plugins ?? {}, query: (method, ...args) => { return send(method, client.query, args); }, mutate: async (method, ...args) => { return send(method, client.mutate, args); }, subscribe: (method, ...args) => { const { on_data, on_error, on_end } = get_handlers(args.pop()); const p = send(method, client.mutate, args); const transport_unsubscribe = sync_promise(async () => { const subscription_id = await p; let count = 0; let required_count = null; if (typeof subscription_id !== "string" && typeof subscription_id !== "number") { on_error(new Error("cannot subscribe to subscription")); return () => { }; } if (!client.subscribe) { on_error(new Error("client does not support subscriptions")); return () => { }; } return client.subscribe(subscription_id, (data) => { if (typeof data === "object" && "close_stream" in data && data.close_stream === subscription_id) { required_count = data.count; } else { count += 1; if (on_data) { on_data(data); } } if (count === required_count) { unsubscribe(); } }); }); const unsubscribe = async () => { const unsubscribe_method = method.slice(0, -1); const subscription_id = await p; unsubscribe_method.push(`${method.at(-1)}_unsub`); send(unsubscribe_method, client.query, [subscription_id]); transport_unsubscribe(); on_end(); }; return unsubscribe; } }); } export { build_client, http, multi, ws };