@pgxsinkit/pgwasm
Version:
180 lines (164 loc) • 8.17 kB
text/typescript
/**
* The seam between pgwasm and a Postgres build (ADR-0062 decision 3).
*
* pgwasm owns everything above the wire protocol once: queries, transactions, notifications, live
* queries, the persist scheduler, the failure latch, the backup format and the build marker. A build
* provides a boot in two phases (mount storage, then start Postgres), a byte channel per session, and a
* record of its capabilities. Anything that differs between builds is a capability; shared code never
* tests a build's name.
*/
import type { BaseFilesystem } from "../fs/base-filesystem";
/** pgwasm's log level, passed through to the build. */
export type DebugLevel = 0 | 1 | 2 | 3 | 4 | 5;
/** Who a build is. Recorded in every data directory it creates (ADR-0063 decision 1). */
export interface BuildIdentity {
/** Stable name, never renamed; the `storage.build` vocabulary: `"c"`, later `"pgrust"`. */
readonly name: string;
/** On-disk compatibility version. A data directory opens only under the same name AND format. */
readonly dataFormat: number;
/**
* Whether a data directory WITHOUT a marker is this build's. True for the C build only: every data
* directory made before builds were marked was made by it (ADR-0063).
*/
readonly claimsUnmarkedDirectories: boolean;
/** Informational release label, never compared. */
readonly release: string;
}
/** The storage kinds pgwasm knows. A build lists the ones it mounts. */
export type FilesystemKind = "memory" | "idb" | "file" | "vfs";
/** Everything that differs between builds, as data. */
export interface BuildCapabilities {
/** How many sessions `openSession()` hands out at once. */
readonly sessions: number;
/** Needs a cross-origin-isolated context (SharedArrayBuffer) in a browser. */
readonly requiresCrossOriginIsolation: boolean;
/** The storage kinds `boot()` accepts. */
readonly filesystems: readonly FilesystemKind[];
/**
* Whether `exchange()` completes inside the call in this context. A tool that bridges a blocking wasm
* callback to the wire (pg_dump) needs it.
*/
readonly synchronousExchange: boolean;
/** Whether `COPY … FROM/TO '/dev/blob'` works (the `blob` query option). */
readonly blobDevice: boolean;
}
/** Where a data directory lives, as pgwasm resolved it from `dataDir` / `fs`. */
export type StorageRequest =
| { readonly kind: "memory" }
| { readonly kind: "idb"; readonly name: string }
| { readonly kind: "file"; readonly path: string }
| { readonly kind: "vfs"; readonly vfs: BaseFilesystem };
/**
* Files a build installs before Postgres starts, shipped by a build package (for example
* `@pgxsinkit/pgwasm-c/contrib/amcheck`). Pass it in `extensions` like any other extension.
*/
export interface ServerExtension {
readonly kind: "server";
readonly name: string;
/** The {@link BuildIdentity.name} it was compiled for; another build refuses it before boot. */
readonly build: string;
/** A gzipped tar of the extension's `lib/` and `share/` files. */
readonly bundle: URL;
readonly sharedPreloadLibraries?: readonly string[];
}
/**
* One entry of a data directory. `path` is relative to the data directory with a leading `/`
* (`/PG_VERSION`, `/base/1/1259`): the layout Store backups have always had.
*/
export interface DataDirEntry {
readonly path: string;
readonly type: "file" | "directory";
/** Permission bits. */
readonly mode: number;
readonly mtimeSeconds: number;
/** The file's bytes; empty for a directory. */
readonly data: Uint8Array;
}
export interface BootRequest {
readonly storage: StorageRequest;
readonly extensions: readonly ServerExtension[];
readonly user: string;
readonly database: string;
readonly debug: DebugLevel;
}
/** A compiled Postgres, as pgwasm drives it. */
export interface PostgresBuild {
readonly identity: BuildIdentity;
readonly capabilities: BuildCapabilities;
/**
* Optional: settles once everything a {@link boot} would wait on before its own work is ready (artefacts
* fetched and compiled ahead, e.g. by a warm started on an earlier screen). A caller that times a boot
* awaits it first, so the timing measures the boot, never an unfinished warm. Never rejects: a failed
* warm means the build loads its artefacts itself during the boot.
*/
prepare?(): Promise<void>;
/**
* Boot, phase 1: bring the host up and mount the storage (IndexedDB read in, a filesystem initially
* synced). When this resolves, Postgres has not run and nothing in the data directory was written.
*/
boot(request: BootRequest): Promise<MountedDataDirectory>;
}
/** Phase 1 of a boot: storage is mounted, Postgres has not started. */
export interface MountedDataDirectory {
/** A file in the data directory, or `undefined` when it does not exist. Never writes. */
readFile(path: string): Promise<Uint8Array | undefined>;
/** Create a fresh cluster (initdb) in an empty data directory. */
createCluster(): Promise<void>;
/** Write a data-directory image into an empty data directory (a restore, a seed). */
writeEntries(entries: readonly DataDirEntry[]): Promise<void>;
/** Write one file into the data directory. */
writeFile(path: string, data: Uint8Array): Promise<void>;
/** Strictly persist everything written so far. */
persist(): Promise<void>;
/** Boot, phase 2: start Postgres. */
start(options: StartOptions): Promise<RunningPostgres>;
/** Give up without starting: release the storage (locks, handles) and the host. */
release(): Promise<void>;
}
export interface StartOptions {
/** Server settings (GUCs), `shared_preload_libraries` already merged. */
readonly settings: Readonly<Record<string, string>>;
}
/** Phase 2 of a boot: Postgres is running. */
export interface RunningPostgres {
/** A session, already past startup. Calls beyond `capabilities.sessions` reject. */
openSession(): Promise<WireSession>;
/**
* Persist storage after a statement. pgwasm serializes calls and owns the policy: `relaxed` is a
* background persist whose failure pgwasm latches; `false` is awaited by the statement.
*/
persist(relaxed: boolean): Promise<void>;
/** Walk the data directory for a Store backup. pgwasm holds its query lock around the call. */
readEntries(): Promise<DataDirEntry[]>;
/** The `/dev/blob` device, present iff `capabilities.blobDevice`. */
readonly blob: BlobDevice | undefined;
/** Stop Postgres cleanly. Never called after a failure. */
shutdown(): Promise<void>;
/**
* Release the storage and the host. Always the last call, after a failure too. `afterFailedBoot` is
* set when the boot failed after `start()` (a filesystem gets `cleanupFailedInit` instead of `closeFs`).
*/
release(options?: { readonly afterFailedBoot?: boolean }): Promise<void>;
}
/** One session's byte channel carrying the wire protocol. */
export interface WireSession {
/**
* Send frontend bytes and stream every byte of the COMPLETE reply into `onData`; settle when it is
* complete. A build on a real wire frames the reply itself (a Flush after a non-terminal message, then
* read to its terminator). Returns `undefined` when it completed synchronously, which it always does
* when `capabilities.synchronousExchange`. A chunk is only valid during the callback: copy it to keep
* it. A throw is a failure of the build, never an SQL error (those arrive in-band as ErrorResponse),
* and pgwasm fails the instance. A message starting with a 0 byte is a startup packet.
*/
exchange(message: Uint8Array, onData: (chunk: Uint8Array) => void): void | Promise<void>;
/** Set by pgwasm: backend bytes that arrive between exchanges (another session's NOTIFY). */
onUnsolicited: ((chunk: Uint8Array) => void) | undefined;
close(): Promise<void>;
}
/** The `COPY … '/dev/blob'` device. */
export interface BlobDevice {
/** What `COPY … FROM '/dev/blob'` reads, or `undefined` to clear it. */
setReadSource(data: Uint8Array | undefined): void;
/** What `COPY … TO '/dev/blob'` wrote since the last call, or `undefined` when nothing was. */
takeWritten(): Uint8Array<ArrayBuffer>[] | undefined;
}