@tai-kun/surrealdb
Version:
The SurrealDB SDK for JavaScript
295 lines (250 loc) • 7.18 kB
text/typescript
import type ClientBase from "@tai-kun/surrealdb/basic-client";
import type {
ClientCloseOptions,
ClientConfig,
} from "@tai-kun/surrealdb/basic-client";
import { SurrealValueError } from "../errors";
import { mutex } from "../utils";
import createSurql, {
type CreateSurqlConfig,
type Surql,
} from "./surql/create-surql";
type ClientConstructor = new(config: ClientConfig) => ClientBase;
/**
* @experimental
*/
export interface PoolSetupOptions {
readonly signal?: AbortSignal | undefined;
}
type Alias = { readonly [name: string]: string | URL };
/**
* @experimental
*/
export interface PoolInit<
TClientConstructor extends ClientConstructor,
TAlias extends Alias,
> extends ClientConfig, CreateSurqlConfig {
readonly Client: TClientConstructor;
readonly alias?: TAlias | undefined;
readonly setup?: (
this: InstanceType<TClientConstructor>,
endpoint: URL,
options: PoolSetupOptions,
alias: keyof TAlias | undefined,
) => PromiseLike<void>;
}
/**
* @experimental
*/
export type PoolSurreal<TClientConstructor extends ClientConstructor> =
& InstanceType<TClientConstructor>
& Disposable
& { disconnect(options?: ClientCloseOptions | undefined): void };
type Endpoint<TAlias extends Alias> = Extract<keyof TAlias, string>;
/**
* @experimental
*/
export interface PoolInstance<
TClientConstructor extends ClientConstructor,
TAlias extends Alias,
> extends AsyncDisposable {
get(
endpoint: Endpoint<TAlias> | URL,
options?: PoolSetupOptions | undefined,
): Promise<PoolSurreal<TClientConstructor>>;
close(
endpoint: Endpoint<TAlias> | URL,
options?: ClientCloseOptions | undefined,
): Promise<void>;
closeAll(options?: ClientCloseOptions | undefined): Promise<void>;
}
/**
* @experimental
*/
export interface PoolOptions<
TClientConstructor extends ClientConstructor,
TAlias extends Alias,
> {
readonly alias?: TAlias | undefined;
readonly setup?: PoolInit<TClientConstructor, TAlias>["setup"];
readonly closeDelay?: number | undefined;
}
/**
* @experimental
*/
export interface InitializedPool<
TClientConstructor extends ClientConstructor,
TAlias extends Alias,
> {
Pool: new<const TPoolAlias extends Alias = {}>(
options?: PoolOptions<TClientConstructor, TPoolAlias> | undefined,
) => PoolInstance<
TClientConstructor,
Omit<TAlias, keyof TPoolAlias> & TPoolAlias
>;
surql: Surql;
}
/**
* @experimental
*/
export default function initPool<
TClientConstructor extends ClientConstructor,
const TAlias extends Alias = {},
>(
init: PoolInit<TClientConstructor, TAlias>,
): InitializedPool<TClientConstructor, TAlias> {
const {
alias,
setup = async function(endpoint, options) {
if (this.state === "open") {
return;
}
await this.connect(endpoint, options);
},
Client,
formatter,
varPrefix,
...others
} = init;
// @ts-expect-error
class Surreal extends Client implements Disposable {
constructor(private dispose: () => void) {
super({
formatter,
...others,
});
}
disconnect(): void {
this.dispose();
}
[Symbol.dispose || Symbol.for("Symbol.dispose")](): void {
this.disconnect();
}
}
// @ts-expect-error
class Pool implements PoolInstance<ClientConstructor, Alias> {
private map = new Map<string, [
number,
PoolSurreal<ClientConstructor>,
ReturnType<typeof setTimeout>?,
]>();
private closing = new Set<
(options: ClientCloseOptions | undefined) => Promise<void>
>();
private setup: NonNullable<PoolInit<ClientConstructor, Alias>["setup"]>;
private alias: Alias;
private closeDelay: number;
constructor(
options: PoolOptions<ClientConstructor, Alias> | undefined = {},
) {
this.setup = (options.setup || setup) as NonNullable<
PoolInit<ClientConstructor, Alias>["setup"]
>;
this.alias = { ...alias, ...options.alias };
this.closeDelay = Math.max(0, options.closeDelay ?? 30e3);
}
private getUrl(endpoint: Endpoint<Alias> | URL): URL | undefined {
if (typeof endpoint === "string") {
return this.alias[endpoint] === undefined
? undefined
: new URL(this.alias[endpoint]);
}
return new URL(endpoint); // copy
}
private getKey(endpoint: Endpoint<Alias> | URL): string | undefined {
return this.getUrl(endpoint)?.toString();
}
async get(
endpoint: Endpoint<Alias> | URL,
options: PoolSetupOptions | undefined = {},
): Promise<PoolSurreal<ClientConstructor>> {
const url = this.getUrl(endpoint);
const alias = typeof endpoint === "string" ? endpoint : undefined;
if (!url) {
throw new SurrealValueError("predefined endpoint", url, {
cause: {
alias,
},
});
}
const key = url.toString();
if (this.map.has(key)) {
const val = this.map.get(key)!;
clearTimeout(val[2]);
delete val[2];
const db = val[1];
await this.setup.call(db, url, options, alias);
val[0] += 1;
return db;
}
const db = new Surreal(() => {
const val = this.map.get(key);
if (!val) {
return;
}
val[0] -= 1;
if (val[0] > 0 || 2 in val) {
return;
}
let promise: Promise<void> | undefined;
const close = async (options?: ClientCloseOptions | undefined) => {
clearTimeout(val[2]);
this.map.delete(key);
promise ||= (async () => {
try {
await val[1].close(options);
} catch (e) {
// TODO(tai-kun): TaskEmitter を使って利用者がエラーを受け取れるようにする。
console.error(e);
} finally {
this.closing.delete(close);
}
})();
return promise;
};
val[2] = setTimeout(close, this.closeDelay);
this.closing.add(close);
}) as unknown as PoolSurreal<ClientConstructor>;
await this.setup.call(db, url, options, alias);
this.map.set(key, [1, db]);
return db;
}
async close(
endpoint: Endpoint<Alias> | URL,
options?: ClientCloseOptions | undefined,
): Promise<void> {
const key = this.getKey(endpoint);
const val = this.map.get(key!);
if (!val) {
return;
}
const [, db] = val;
if (db.state === "closed" || db.state === "closing") {
return;
}
await db.close(options);
}
async closeAll(options?: ClientCloseOptions | undefined): Promise<void> {
for (const [, db] of this.map.values()) {
db.disconnect();
}
await Promise.all([...this.closing].map(async close => {
await close(options);
}));
}
async [Symbol.asyncDispose || Symbol.for("Symbol.asyncDispose")]() {
await this.closeAll();
}
}
return {
// @ts-expect-error
Pool,
surql: createSurql({
formatter,
varPrefix,
}),
};
}