Something went wrong. Try again.
TypeScript client for accessing TLE Community.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177import createClient from 'openapi-fetch';import Lacuna from '../lacuna';import { fixNumbers } from './util';import { isBlockingError } from './responses';import type { ResponseEvent } from './responses';import type { paths } from '../types/schema';
export interface ServerRequest { module: string; method: string; params: object; addSession: boolean;}
export interface ServerResponse<T> { result?: T; error?: ServerError;}
export interface ServerError { code: number; message: string; data: any;}
interface RpcErrorBody { id: number; jsonrpc: string; error: ServerError;}
interface RpcResultBody<T> { id: number; jsonrpc: string; result?: T;}
/** * Hard backstop against a subscriber that retries unconditionally. Past this * many attempts `retry()` resolves the response it already has instead of * issuing another request. Handlers should stop well before this on their own * (the built-in RPC limit handler defaults to 5 retries). */const MAX_RETRY_DEPTH = 10;
/** Used whenever we never got a well-formed RPC response back at all. */const networkError = (): ServerError => ({ code: -1, message: 'Could not communicate with server', data: {},});
/** Module names come from the server or from devs who don't know the internals of the library so this * utility can basically treat those things as untrusted user input and clean them. */const sanitizeUrlComponent = (input: string) => { return input.toLowerCase().replace(/[^a-z0-9_]/g, '');};
class Server { lacuna: Lacuna; client: ReturnType<typeof createClient<paths>>;
constructor(lacuna: Lacuna) { this.lacuna = lacuna; this.client = createClient<paths>({ baseUrl: lacuna.config.serverUrl }); }
authHeaders(options: ServerRequest): Record<string, string> { if (!options.addSession) return {};
const sessionId = this.lacuna.session.get();
if (!sessionId) { this.lacuna.log.warn( `${options.module}#${options.method} called with a session required, but no session is set.` ); return {}; }
return { Authorization: `Token ${sessionId}` }; }
async call<T>(options: ServerRequest, attempt = 0): Promise<ServerResponse<T>> { const apiModule = sanitizeUrlComponent(options.module); const apiMethod = sanitizeUrlComponent(options.method); this.lacuna.log.info('Calling', `${apiModule}/${apiMethod}`, options.params);
const path = `/v2/${apiModule}/${apiMethod}` as any;
const { data, error } = await this.client.POST(path, { baseUrl: this.lacuna.config.serverUrl, // Configured serverUrl can change in between requests body: options.params, headers: this.authHeaders(options), });
const response: ServerResponse<T> = data ? { result: fixNumbers((data as RpcResultBody<T>).result) } : { error: (error as RpcErrorBody | undefined)?.error || networkError() };
return this.publish(options, response, attempt, (request, next) => this.call<T>(request, next)); }
/** * Fallback for RPC methods with no working /v2 equivalent. Hits the legacy * non-/v2 JSON-RPC endpoint directly. No endpoint in this library uses it * any more - it's kept as an escape hatch for the /v2 methods the backend * migration hasn't wired up yet (see README "Spec Discrepancies"). */ async callLegacy<T>(options: ServerRequest, attempt = 0): Promise<ServerResponse<T>> { this.lacuna.log.info('Calling (legacy)', `${options.module}/${options.method}`, options.params);
const response = await this.fetchLegacy<T>(options);
return this.publish(options, response, attempt, (request, next) => this.callLegacy<T>(request, next) ); }
private async fetchLegacy<T>(options: ServerRequest): Promise<ServerResponse<T>> { const sessionId = this.lacuna.session.get(); const params = options.addSession && sessionId ? [sessionId, ...(options.params as any[])] : options.params;
const url = new URL(options.module, this.lacuna.config.serverUrl).href;
try { const response = await fetch(url, { method: 'post', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: options.method, params }), });
const body = (await response.json()) as RpcResultBody<T> & Partial<RpcErrorBody>;
if (response.ok) return { result: fixNumbers(body.result) };
return { error: body.error || networkError() }; } catch { return { error: networkError() }; } }
/** * Hands every response to the subscribers registered via Lacuna#onResponse. * * Responses carrying a blocking error code (an RPC limit, a captcha prompt, * or a rejected session) wait for those handlers to finish, and a response a * handler returns - from `retry()`, typically - replaces this one. That's * what lets a script ride out an RPC limit error instead of being stopped by * it. Everything else is published without waiting. */ private publish<T>( request: ServerRequest, response: ServerResponse<T>, attempt: number, again: (request: ServerRequest, attempt: number) => Promise<ServerResponse<T>> ): Promise<ServerResponse<T>> | ServerResponse<T> { const event: ResponseEvent<T> = { request, response, attempt, retry: () => attempt >= MAX_RETRY_DEPTH ? Promise.resolve(response) : again(request, attempt + 1), };
if (isBlockingError(response.error)) { return this.lacuna.responses.dispatchBlocking(event); }
this.lacuna.responses.dispatch(event); return response; }}
export default Server;