UNPKG

@pgxsinkit/pgwasm

Version:
135 lines (125 loc) • 5.42 kB
// 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>; }