@tai-kun/surrealdb
Version:
The SurrealDB SDK for JavaScript
324 lines (281 loc) • 8.09 kB
text/typescript
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)
*/
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)
*/
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,
},
});
}