UNPKG

@tai-kun/surrealdb

Version:

The SurrealDB SDK for JavaScript

324 lines (281 loc) 8.09 kB
import { type ConnectionInfo, type ConnectionState, type EngineAbc, type EngineAbcConfig, type EngineEventMap, processEndpoint, type ProcessEndpointOptions, } from "@tai-kun/surrealdb/engine"; import { CircularEngineReferenceError, Closed, ConnectionConflictError, ConnectionUnavailableError, EngineNotFoundError, RpcResponseError, SurrealTypeError, unreachable, } from "@tai-kun/surrealdb/errors"; import type { Formatter } from "@tai-kun/surrealdb/formatter"; import type { RpcMethod, RpcParams, RpcResponse, RpcResult, } from "@tai-kun/surrealdb/types"; import { getTimeoutSignal, mutex, type StatefulPromise, TaskEmitter, type TaskListener, type TaskListenerOptions, } from "@tai-kun/surrealdb/utils"; export type CreateEngine = (config: EngineAbcConfig) => | EngineAbc | PromiseLike<EngineAbc>; export type ClientEngines = { readonly [_ in string]?: CreateEngine | string | undefined; }; export interface ClientConfig { readonly engines: ClientEngines; readonly formatter: Formatter; readonly disableDefaultErrorHandler?: boolean | undefined; } export interface ClientConnectOptions extends ProcessEndpointOptions { readonly signal?: AbortSignal | undefined; } export interface ClientCloseOptions { readonly force?: boolean | undefined; readonly signal?: AbortSignal | undefined; } export interface ClientRpcOptions { readonly signal?: AbortSignal | undefined; } export default class BasicClient { protected readonly ee: TaskEmitter<EngineEventMap> = new TaskEmitter(); protected readonly fmt: Formatter; protected eng: EngineAbc | null = null; private readonly _engines: ClientEngines; constructor(config: ClientConfig) { const { engines, formatter, disableDefaultErrorHandler, } = config; this.fmt = formatter; this._engines = engines; if (!disableDefaultErrorHandler) { this.ee.on("error", (_, e) => { if (e.fatal) { console.error("[@tai-kun/surrealdb]", "FATAL", e); this.close({ force: true }).then(null, reason => { console.error("[@tai-kun/surrealdb]", reason); }); } else { console.warn("[@tai-kun/surrealdb]", "WARNING", e); } }); } } protected async createEngine(scheme: string): Promise<EngineAbc> { let engine = this._engines[scheme]; const seen: string[] = []; while (typeof engine === "string") { if (seen.includes(engine)) { throw new CircularEngineReferenceError(seen); } seen.push(engine); engine = this._engines[engine]; } if (!engine) { throw new EngineNotFoundError(scheme); } return await engine({ emitter: this.ee, formatter: this.fmt, }); } /** * [API Reference](https://tai-kun.github.io/surrealdb.js/v2/guides/connecting/#state) */ get state(): ConnectionState { return this.eng?.state ?? "closed"; } /** * [API Reference](https://tai-kun.github.io/surrealdb.js/v2/guides/connecting/#endpoint) */ get endpoint(): URL | null | undefined { return this.eng?.endpoint; } /** * [API Reference](https://tai-kun.github.io/surrealdb.js/v2/guides/connecting/#namespace) */ get namespace(): string | null | undefined { return this.eng?.namespace; } /** * [API Reference](https://tai-kun.github.io/surrealdb.js/v2/guides/connecting/#database) */ get database(): string | null | undefined { return this.eng?.database; } /** * [API Reference](https://tai-kun.github.io/surrealdb.js/v2/guides/connecting/#token) */ get token(): string | null | undefined { return this.eng?.token; } /** * [API Reference](https://tai-kun.github.io/surrealdb.js/v2/guides/connecting/#getconnectioninfo) */ getConnectionInfo(): ConnectionInfo | undefined { return this.eng?.getConnectionInfo(); } /** * [API Reference](https://tai-kun.github.io/surrealdb.js/v2/guides/connecting/#on) */ on<TEvent extends keyof EngineEventMap>( event: TEvent, listener: TaskListener<EngineEventMap[TEvent]>, ): void { this.ee.on(event, listener); } /** * [API Reference](https://tai-kun.github.io/surrealdb.js/v2/guides/connecting/#off) */ off<TEvent extends keyof EngineEventMap>( event: TEvent, listener: TaskListener<EngineEventMap[TEvent]>, ): void { // 誤ってすべてのイベントリスナーを解除してしまわないようにするため、 // listener が無い場合はエラーを投げる。 if (typeof listener !== "function") { throw new SurrealTypeError("Function", listener); } this.ee.off(event, listener); } /** * [API Reference](https://tai-kun.github.io/surrealdb.js/v2/guides/connecting/#once) */ once<TEvent extends keyof EngineEventMap>( event: TEvent, options?: TaskListenerOptions | undefined, ): StatefulPromise<EngineEventMap[TEvent]> { return this.ee.once(event, options); } /** * [API Reference](https://tai-kun.github.io/surrealdb.js/v2/guides/connecting/#connect) */ @mutex connect( endpoint: string | URL, options: ClientConnectOptions | undefined = {}, ): Promise<void> { try { const conn = this.getConnectionInfo(); endpoint = processEndpoint(endpoint, options); if (conn?.state === "open") { if (conn.endpoint.href === endpoint.href) { return Promise.resolve(); } throw new ConnectionConflictError(conn.endpoint, endpoint); } if (this.eng) { unreachable(conn as never); } const scheme = endpoint.protocol.slice(0, -1 /* remove `:` */); const { signal = getTimeoutSignal(15_000) } = options; return (async () => { try { this.eng = await this.createEngine(scheme); await this.eng.connect({ endpoint, signal }); } catch (e) { this.eng = null; throw e; } })(); } catch (e) { return Promise.reject(e); } } /** * [API Reference](https://tai-kun.github.io/surrealdb.js/v2/guides/connecting/#close) */ @mutex close(options: ClientCloseOptions | undefined = {}): Promise<void> { if (!this.eng) { return Promise.resolve(); } const eng = this.eng; this.eng = null; try { if (options.force) { this.ee.abort(new Closed("force close")); } return (async () => { try { await eng.close({ signal: options.signal || getTimeoutSignal(15_000), }); } finally { await this.ee.idle(); // エラーを投げない。 } })(); } catch (e) { return Promise.reject(e); } } async rpc<TMethod extends RpcMethod, TResult extends RpcResult<TMethod>>( method: TMethod, params: RpcParams<TMethod>, options: ClientRpcOptions | undefined = {}, ): Promise<TResult> { const { signal = getTimeoutSignal(5_000) } = options; if (this.eng?.state !== "open") { await this.ee.once("open", { signal }); } if (!this.eng) { throw new ConnectionUnavailableError({ cause: "The engine is not set.", }); } return await rpc({ engine: this.eng, signal, method, params, }); } } async function rpc< TMethod extends RpcMethod, TResult extends RpcResult<TMethod>, >( args: { readonly engine: EngineAbc; readonly signal: AbortSignal; readonly method: TMethod; readonly params: RpcParams<TMethod>; }, ): Promise<TResult> { const resp: RpcResponse<any> = await args.engine.rpc({ signal: args.signal, // @ts-expect-error request: { method: args.method, params: args.params, }, }); if ("result" in resp) { return resp.result; } throw new RpcResponseError(resp, { cause: { method: args.method, // TODO(tai-kun): params には機微情報が含まれている可能性があるので、method のみにしておく? params: args.params, }, }); }