@pgxsinkit/pgwasm
Version:
135 lines (125 loc) • 5.42 kB
text/typescript
// Began as a copy of `@electric-sql/pglite` (taken under its PostgreSQL License option, © ElectricSQL
// — see NOTICE). Owned outright (ADR-0062); compatibility with PGlite is an anti-goal — evolve freely.
import type { DataDirEntry, PostgresBuild, ServerExtension } from "./build/seam";
import { readDataDirArchive } from "./core/data-dir-archive";
import { BUILD_MARKER_PATH, checkBuildMarker, encodeBuildMarker, findEntry, PG_VERSION_PATH } from "./core/marker";
import { PgwasmInstance } from "./core/pgwasm";
import { resolveStorage } from "./core/storage";
import {
BackupFormatError,
CrossOriginIsolationRequiredError,
ExtensionBuildMismatchError,
UnsupportedFilesystemError,
} from "./errors";
import type { Extension, Extensions, PgwasmOptions, PgwasmWithExtensions } from "./interface";
function isServerExtension(extension: Extension | ServerExtension): extension is ServerExtension {
return "kind" in extension && extension.kind === "server";
}
function assertBuild(build: unknown): asserts build is PostgresBuild {
const candidate = build as Partial<PostgresBuild> | null | undefined;
if (
typeof candidate !== "object" ||
candidate === null ||
typeof candidate.boot !== "function" ||
typeof candidate.identity?.name !== "string" ||
!Array.isArray(candidate.capabilities?.filesystems)
) {
throw new TypeError("createPgwasm needs a Postgres build in `build`, e.g. `cBuild` from @pgxsinkit/pgwasm-c.");
}
}
/** Merge the server extensions' preload libraries into the settings. */
function mergeSettings(
settings: Readonly<Record<string, string>> | undefined,
serverExtensions: readonly ServerExtension[],
): Record<string, string> {
const merged: Record<string, string> = { ...settings };
const preload = serverExtensions.flatMap((extension) => extension.sharedPreloadLibraries ?? []);
if (preload.length > 0) {
const existing = (merged["shared_preload_libraries"] ?? "")
.split(",")
.map((library) => library.trim())
.filter((library) => library !== "");
merged["shared_preload_libraries"] = [...new Set([...existing, ...preload])].join(",");
}
return merged;
}
/**
* A Store backup's entries, checked against the build before anything is booted, with the build
* marker added when the backup has none (it predates marking and is the C build's).
*/
async function restoreEntries(build: PostgresBuild, loadDataDir: Blob | File): Promise<readonly DataDirEntry[]> {
const entries = await readDataDirArchive(loadDataDir);
if (findEntry(entries, PG_VERSION_PATH) === undefined) {
throw new BackupFormatError("The Store backup holds no data directory (it has no PG_VERSION).");
}
const marker = findEntry(entries, BUILD_MARKER_PATH);
checkBuildMarker(build.identity, marker?.data, true, "backup");
if (marker !== undefined) return entries;
return [
...entries,
{
path: BUILD_MARKER_PATH,
type: "file",
mode: 0o600,
mtimeSeconds: Math.floor(Date.now() / 1000),
data: encodeBuildMarker(build.identity),
},
];
}
/**
* Start a database on a Postgres build. Resolves once it is ready.
*
* Everything that can be checked before booting is: the `dataDir` scheme, the build's supported
* storage and cross-origin isolation, each server extension's build, and a Store backup's recorded
* build (ADR-0063). The booted data directory's recorded build is checked before anything is written.
*/
export async function createPgwasm<E extends Extensions = Record<never, never>>(
options: PgwasmOptions<E>,
): Promise<PgwasmWithExtensions<E>> {
const build: unknown = options.build;
assertBuild(build);
const identity = build.identity;
const capabilities = build.capabilities;
const storage = resolveStorage(options.dataDir, options.fs);
if (!capabilities.filesystems.includes(storage.kind)) {
throw new UnsupportedFilesystemError(storage.kind, identity, capabilities.filesystems);
}
if (
capabilities.requiresCrossOriginIsolation &&
(globalThis as { crossOriginIsolated?: boolean }).crossOriginIsolated === false
) {
throw new CrossOriginIsolationRequiredError(identity);
}
const serverExtensions: ServerExtension[] = [];
const clientExtensions: [string, Extension][] = [];
for (const [key, extension] of Object.entries(options.extensions ?? {})) {
if (isServerExtension(extension)) {
if (extension.build !== identity.name) {
throw new ExtensionBuildMismatchError(extension.name, extension.build, identity);
}
serverExtensions.push(extension);
} else {
clientExtensions.push([key, extension]);
}
}
const restore = options.loadDataDir === undefined ? undefined : await restoreEntries(build, options.loadDataDir);
const plan = {
build,
storage,
serverExtensions,
clientExtensions,
restore,
settings: mergeSettings(options.settings, serverExtensions),
user: options.username ?? "postgres",
database: options.database ?? "postgres",
username: options.username,
debug: options.debug ?? 0,
relaxedDurability: options.relaxedDurability ?? false,
parsers: options.parsers,
serializers: options.serializers,
} as const;
const instance = new PgwasmInstance(plan);
await instance.boot(plan);
// The namespaces were attached under their keys during boot.
return instance as PgwasmInstance & PgwasmWithExtensions<E>;
}