@qubit-rs/client
Version:
Seamless RPC for Rust & TypeScript
369 lines (355 loc) • 11.4 kB
JavaScript
;Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// 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 (_optionalChain([response, 'optionalAccess', _2 => _2.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 _optionalChain([response, 'optionalAccess', _3 => _3.id]) !== "number" && typeof _optionalChain([response, 'optionalAccess', _4 => _4.id]) !== "string" && _optionalChain([response, 'optionalAccess', _5 => _5.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 _optionalChain([response, 'access', _6 => _6.error, 'optionalAccess', _7 => _7.code]) === "number" && typeof _optionalChain([response, 'access', _8 => _8.error, 'optionalAccess', _9 => _9.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 = _optionalChain([options, 'optionalAccess', _10 => _10.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 = _optionalChain([http_options, 'optionalAccess', _11 => _11.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, _optionalChain([options, 'optionalAccess', _12 => _12.http]));
const ws_client = ws(host, _optionalChain([options, 'optionalAccess', _13 => _13.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 (_optionalChain([handler, 'optionalAccess', _14 => _14.on_data])) {
on_data = handler.on_data;
}
if (_optionalChain([handler, 'optionalAccess', _15 => _15.on_error])) {
on_error = handler.on_error;
}
if (_optionalChain([handler, 'optionalAccess', _16 => _16.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({
..._nullishCoalesce(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;
}
});
}
exports.build_client = build_client; exports.http = http; exports.multi = multi; exports.ws = ws;