UNPKG

@pgxsinkit/pgwasm

Version:
1,006 lines (995 loc) • 33.9 kB
// @bun import { gzip2, gunzipIfCompressed2, TarFormatError2, writeTar2, readTar2 } from "./chunk-0w227vw2.js"; import { PgwasmError2, UnsupportedDataDirError2, OpfsAhpRemovedError2, UnsupportedFilesystemError2, CrossOriginIsolationRequiredError2, ExtensionBuildMismatchError2, BuildMismatchError2, DataFormatMismatchError2, BuildMarkerUnreadableError2, DataDirExistsError2, BackupFormatError2, PgwasmClosedError2, PgwasmFailedError2, UnsupportedFeatureError2, makeQueryError } from "./chunk-cpsnmzrf.js"; import { toPostgresName, quoteIdentifier, parseResults, parseDescribeStatementResults } from "./chunk-tdvrvgt5.js"; import { parsers, serializers, toText, arraySerializer, arrayParser } from "./chunk-d82gg1hx.js"; import { Parser2 } from "./chunk-at1py5s5.js"; import { DatabaseError2, NotificationResponseMessage, NoticeMessage, serialize2, registerProtocolAccess } from "./chunk-74gcrk47.js"; // packages/pgwasm/src/templating.ts var TemplateType = { part: "part", container: "container" }; function isTemplateValue(value) { return typeof value === "object" && value !== null && "_templateType" in value; } function addToLastAndPushWithSuffix(arr, suffix, ...values) { const lastArrIdx = arr.length - 1; const lastValIdx = values.length - 1; if (lastValIdx === -1) return; if (lastValIdx === 0) { arr[lastArrIdx] = `${arr[lastArrIdx] ?? ""}${values[0] ?? ""}${suffix}`; return; } arr[lastArrIdx] = `${arr[lastArrIdx] ?? ""}${values[0] ?? ""}`; arr.push(...values.slice(1, lastValIdx)); arr.push(`${values[lastValIdx] ?? ""}${suffix}`); } function sql(strings, ...values) { const parsedStrings = [strings[0] ?? ""]; const parsedRaw = [strings.raw[0] ?? ""]; const parsedValues = []; for (let i = 0;i < values.length; i++) { const value = values[i]; const nextString = strings[i + 1] ?? ""; const nextRaw = strings.raw[i + 1] ?? ""; if (isTemplateValue(value) && value._templateType === TemplateType.part) { addToLastAndPushWithSuffix(parsedStrings, nextString, value.str); addToLastAndPushWithSuffix(parsedRaw, nextRaw, value.str); continue; } if (isTemplateValue(value) && value._templateType === TemplateType.container) { addToLastAndPushWithSuffix(parsedStrings, nextString, ...value.strings); addToLastAndPushWithSuffix(parsedRaw, nextRaw, ...value.strings.raw); parsedValues.push(...value.values); continue; } parsedStrings.push(nextString); parsedRaw.push(nextRaw); parsedValues.push(value); } const templateStrings = Object.assign(parsedStrings, { raw: parsedRaw }); return { _templateType: TemplateType.container, strings: templateStrings, values: parsedValues }; } function identifier2(strings, ...values) { return { _templateType: TemplateType.part, str: `"${String.raw(strings, ...values)}"` }; } function raw2(strings, ...values) { return { _templateType: TemplateType.part, str: String.raw(strings, ...values) }; } function query(strings, ...values) { const { strings: queryStringParts, values: params } = sql(strings, ...values); return { query: [queryStringParts[0], ...params.flatMap((_, idx) => [`$${idx + 1}`, queryStringParts[idx + 1]])].join(""), params }; } // packages/pgwasm/src/core/data-dir-archive.ts function normalizeDataDirPath(name) { const segments = name.split("/").filter((segment) => segment !== "" && segment !== "."); if (segments.includes("..")) { throw new BackupFormatError2(`the backup member "${name}" points outside the data directory`); } return `/${segments.join("/")}`; } async function readDataDirArchive(archive) { const bytes = await gunzipIfCompressed2(new Uint8Array(await archive.arrayBuffer())); let members; try { members = readTar2(bytes); } catch (error) { if (error instanceof TarFormatError2) { throw new BackupFormatError2(`the Store backup is not a readable tarball: ${error.message}`, { cause: error }); } throw error; } const entries = []; for (const entry of members) { const path = normalizeDataDirPath(entry.name); if (path === "/") continue; entries.push({ path, type: entry.type, mode: entry.mode, mtimeSeconds: entry.mtimeSeconds, data: entry.data }); } return entries; } async function writeDataDirArchive(entries, baseName, compression = "auto") { const tarball = writeTar2(entries.map((entry) => ({ name: entry.path, type: entry.type, mode: entry.mode, mtimeSeconds: entry.mtimeSeconds, data: entry.data }))); if (compression === "none") { return new File([tarball.slice()], `${baseName}.tar`, { type: "application/x-tar" }); } return new File([await gzip2(tarball)], `${baseName}.tar.gz`, { type: "application/x-gzip" }); } // packages/pgwasm/src/core/marker.ts var BUILD_MARKER_PATH = "/PGWASM_BUILD"; var PG_VERSION_PATH = "/PG_VERSION"; var MARKER_FORMAT = 1; var encoder = new TextEncoder; var decoder = new TextDecoder; function encodeBuildMarker(build) { return encoder.encode(`${JSON.stringify({ pgwasm: MARKER_FORMAT, build: build.name, dataFormat: build.dataFormat })} `); } function decodeBuildMarker(bytes) { const raw = decoder.decode(bytes); let parsed; try { parsed = JSON.parse(raw); } catch { throw new BuildMarkerUnreadableError2(raw, "not JSON"); } if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { throw new BuildMarkerUnreadableError2(raw, "not a JSON object"); } const { pgwasm, build, dataFormat } = parsed; if (typeof pgwasm !== "number" || !Number.isInteger(pgwasm) || pgwasm < 1) { throw new BuildMarkerUnreadableError2(raw, "no marker format version"); } if (pgwasm > MARKER_FORMAT) { throw new BuildMarkerUnreadableError2(raw, `marker format ${pgwasm} is newer than this pgwasm reads`); } if (typeof build !== "string" || build === "") { throw new BuildMarkerUnreadableError2(raw, "no build name"); } if (typeof dataFormat !== "number" || !Number.isInteger(dataFormat) || dataFormat < 1) { throw new BuildMarkerUnreadableError2(raw, "no data format"); } return { build, dataFormat }; } function checkBuildMarker(build, marker, hasCluster, source) { if (marker === undefined) { if (hasCluster && !build.claimsUnmarkedDirectories) { throw new BuildMismatchError2(build, "unmarked", source); } return; } const recorded = decodeBuildMarker(marker); if (recorded.build !== build.name) { const found = recorded; throw new BuildMismatchError2(build, found, source); } if (recorded.dataFormat !== build.dataFormat) { throw new DataFormatMismatchError2(build, recorded.dataFormat, source); } } function findEntry(entries, path) { return entries.find((entry) => entry.path === path && entry.type === "file"); } // packages/pgwasm/src/core/mutex.ts class Mutex { #tail = Promise.resolve(); runExclusive(fn) { const run = this.#tail.then(fn); this.#tail = run.then(() => { return; }, () => { return; }); return run; } } // packages/pgwasm/src/core/storage.ts function resolveStorage(dataDir, fs) { if (fs !== undefined) { if (dataDir !== undefined) { throw new UnsupportedDataDirError2(dataDir, "`dataDir` and `fs` are exclusive; pass one of them"); } return { kind: "vfs", vfs: fs }; } if (dataDir === undefined || dataDir.startsWith("memory://")) { return { kind: "memory" }; } if (dataDir.startsWith("idb://")) { const name = dataDir.slice("idb://".length); if (name === "") throw new UnsupportedDataDirError2(dataDir, "an IndexedDB store needs a name (idb://<name>)"); return { kind: "idb", name }; } if (dataDir.startsWith("file://")) { const path = dataDir.slice("file://".length); if (path === "") throw new UnsupportedDataDirError2(dataDir, "a file store needs a path (file://<path>)"); return { kind: "file", path }; } if (dataDir.startsWith("opfs-ahp://")) { throw new OpfsAhpRemovedError2(dataDir); } const scheme = /^([a-z][a-z0-9+.-]*):\/\//i.exec(dataDir)?.[1]; throw new UnsupportedDataDirError2(dataDir, scheme === undefined ? "a scheme is required: memory://, idb://<name> or file://<path>" : `unknown scheme "${scheme}://" (supported: memory://, idb://<name>, file://<path>)`); } function describeStorage(storage) { switch (storage.kind) { case "memory": return { kind: "memory" }; case "idb": return { kind: "idb", name: storage.name }; case "file": return { kind: "file", path: storage.path }; case "vfs": { const { name, persistent } = storage.vfs.description; return persistent === undefined ? { kind: "vfs", name } : { kind: "vfs", name, persistent }; } } } function backupBaseName(storage) { const location = storage.kind === "idb" ? storage.name : storage.kind === "file" ? storage.path : ""; const last = location.split("/").filter((segment) => segment !== "").pop(); return last ?? "pgdata"; } // packages/pgwasm/src/core/pgwasm.ts function newQuery(throwOnError = false, onNotice) { return { results: [], throwOnError, onNotice, databaseError: null }; } function describeException(error) { if (typeof error === "object" && error !== null && "message" in error) { const { name, message } = error; return `${typeof name === "string" ? name : "Error"}: ${String(message)}`; } return String(error); } function concatChunks(chunks) { if (chunks.length === 1) return chunks[0] ?? new Uint8Array(0); const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0); const out = new Uint8Array(total); let offset = 0; for (const chunk of chunks) { out.set(chunk, offset); offset += chunk.byteLength; } return out; } class PgwasmInstance { build; storage; debug; waitReady; #capabilities; #relaxedDurability; #resolveReady = () => { return; }; #rejectReady = () => { return; }; #ready = false; #closing = false; #closed = false; #failure; #persistFailure; #persistScheduled = false; #queryMutex = new Mutex; #transactionMutex = new Mutex; #listenMutex = new Mutex; #persistMutex = new Mutex; #running; #session; #extensionsClose = []; #parser = new Parser2; #unsolicitedParser = new Parser2; #currentQuery = newQuery(); #notifyListeners = new Map; #globalNotifyListeners = new Set; #parsers; #serializers; #arrayTypesInitialized = false; #inTransaction = false; constructor(plan) { this.build = plan.build.identity; this.#capabilities = plan.build.capabilities; this.storage = describeStorage(plan.storage); this.debug = plan.debug; this.#relaxedDurability = plan.relaxedDurability; this.#parsers = { ...parsers, ...plan.parsers }; this.#serializers = { ...serializers, ...plan.serializers }; this.waitReady = new Promise((resolve, reject) => { this.#resolveReady = resolve; this.#rejectReady = reject; }); this.waitReady.catch(() => { return; }); registerProtocolAccess(this, { execProtocol: (message, options) => this.#execProtocol(message, options), execProtocolStream: (message, options) => this.#execProtocolStream(message, options), execProtocolRaw: (message, options) => this.#execProtocolRaw(message, options), execProtocolRawStream: (message, options) => this.#execProtocolRawStream(message, options), runExclusiveSession: (fn) => this.#runExclusiveSession(fn), capabilities: this.#capabilities }); } async boot(plan) { try { await this.#boot(plan); this.#resolveReady(); } catch (error) { this.#rejectReady(error); throw error; } } async#boot(plan) { const inits = []; for (const [key, extension] of plan.clientExtensions) { const result = await extension.setup(this); if (result.namespace !== undefined) { if (key in this) { throw new PgwasmError2(`The extension key "${key}" collides with a pgwasm member; use another key.`); } Object.defineProperty(this, key, { value: result.namespace, enumerable: true }); } if (result.init) inits.push(result.init); if (result.close) this.#extensionsClose.push(result.close); } const identity = plan.build.identity; const mounted = await plan.build.boot({ storage: plan.storage, extensions: plan.serverExtensions, user: plan.user, database: plan.database, debug: plan.debug }); let running; try { const marker = await mounted.readFile(BUILD_MARKER_PATH); const hasCluster = await mounted.readFile(PG_VERSION_PATH) !== undefined; checkBuildMarker(identity, marker, hasCluster, "data-directory"); if (plan.restore !== undefined) { if (hasCluster) throw new DataDirExistsError2; this.#log("pgwasm: creating the data directory from a Store backup"); await mounted.writeEntries(plan.restore); await mounted.persist(); } else if (!hasCluster) { this.#log("pgwasm: no database in the data directory, creating one"); await mounted.createCluster(); if (marker === undefined) await mounted.writeFile(BUILD_MARKER_PATH, encodeBuildMarker(identity)); await mounted.persist(); } running = await mounted.start({ settings: plan.settings }); } catch (error) { try { await mounted.release(); } catch {} throw error; } this.#running = running; try { const session = await running.openSession(); session.onUnsolicited = (chunk) => this.#handleUnsolicited(chunk); this.#session = session; this.#ready = true; if (plan.username !== undefined) { await this.exec(`SET ROLE ${quoteIdentifier(plan.username)}`); } await this.#initArrayTypes(); for (const init of inits) { await init(); } } catch (error) { this.#ready = false; this.#closed = true; try { await this.#persistMutex.runExclusive(async () => {}); await running.release({ afterFailedBoot: true }); } catch {} throw error; } } get ready() { return this.#ready && !this.#closing && !this.#closed && this.#failure === undefined; } get closed() { return this.#closed; } async#checkReady() { if (this.#closing) throw new PgwasmClosedError2("closing"); if (this.#closed) throw new PgwasmClosedError2("closed"); if (!this.#ready) await this.waitReady; } #checkOpenState() { if (this.#closing) throw new PgwasmClosedError2("closing"); if (this.#closed) throw new PgwasmClosedError2("closed"); if (this.#failure) throw this.#failure.error; } #checkPersistLatch() { if (this.#persistFailure) throw this.#persistFailure.error; } #fail(cause) { if (this.#failure) return this.#failure.error; const reported = this.#currentQuery.databaseError; const error = new PgwasmFailedError2("pgwasm failed and must be closed: " + (reported ? `Postgres reported ${reported.severity ?? "an error"}: ${reported.message}; the build then threw ` : "the build threw ") + describeException(cause), { cause }); this.#failure = { error }; return error; } #log(...args) { if (this.debug > 0) console.log(...args); } #exchange(message, onData) { this.#checkOpenState(); const session = this.#session; if (session === undefined) throw new PgwasmClosedError2("closed"); let pending; try { pending = session.exchange(message, onData); } catch (error) { throw this.#fail(error); } if (pending === undefined) return; return pending.then(undefined, (error) => { throw this.#fail(error); }); } #parseChunk(chunk) { this.#parser.parse(chunk, (message) => { const handled = this.#handleMessage(message); if (handled) this.#currentQuery.results.push(handled); }); } #handleUnsolicited(chunk) { this.#unsolicitedParser.parse(chunk, (message) => { if (message instanceof NotificationResponseMessage) this.#dispatchNotification(message); }); } #handleMessage(message) { if (this.#currentQuery.databaseError) return null; if (message instanceof DatabaseError2) { if (this.#currentQuery.throwOnError) this.#currentQuery.databaseError = message; } else if (message instanceof NoticeMessage) { if (this.debug > 0) console.warn(message); this.#currentQuery.onNotice?.(message); } else if (message instanceof NotificationResponseMessage) { this.#dispatchNotification(message); } return message; } #dispatchNotification(message) { const listeners = this.#notifyListeners.get(message.channel); if (listeners) { for (const callback of listeners) queueMicrotask(() => callback(message.payload)); } for (const callback of this.#globalNotifyListeners) { queueMicrotask(() => callback(message.channel, message.payload)); } } async#execProtocolRaw(message, { persist = true } = {}) { this.#checkPersistLatch(); const chunks = []; const pending = this.#exchange(message, (chunk) => { chunks.push(chunk.slice()); this.#parseChunk(chunk); }); if (pending !== undefined) await pending; if (persist) await this.#persistAfterStatement(); return concatChunks(chunks); } async#execProtocolRawStream(message, { persist = true, onRawData }) { this.#checkPersistLatch(); const pending = this.#exchange(message, onRawData); if (pending !== undefined) await pending; if (persist) await this.#persistAfterStatement(); } async#execProtocol(message, { persist = true, throwOnError = true, onNotice } = {}) { this.#currentQuery = newQuery(throwOnError, onNotice); try { const data = await this.#execProtocolRaw(message, { persist }); const databaseError = this.#currentQuery.databaseError; const result = { messages: this.#currentQuery.results, data }; if (throwOnError && databaseError) { this.#parser = new Parser2; throw databaseError; } return result; } finally { this.#currentQuery = newQuery(); } } async#execProtocolStream(message, { persist = true, throwOnError = true, onNotice } = {}) { this.#currentQuery = newQuery(throwOnError, onNotice); try { this.#checkPersistLatch(); const pending = this.#exchange(message, (chunk) => this.#parseChunk(chunk)); if (pending !== undefined) await pending; if (persist) await this.#persistAfterStatement(); const databaseError = this.#currentQuery.databaseError; const results = this.#currentQuery.results; if (throwOnError && databaseError) { this.#parser = new Parser2; throw databaseError; } return results; } finally { this.#currentQuery = newQuery(); } } async#persistAfterStatement() { this.#checkOpenState(); this.#checkPersistLatch(); const running = this.#running; if (running === undefined || this.#persistScheduled) return; this.#persistScheduled = true; const run = () => this.#persistMutex.runExclusive(async () => { this.#persistScheduled = false; await running.persist(this.#relaxedDurability); }); if (this.#relaxedDurability) { run().catch((error) => { this.#persistFailure ??= { error }; }); } else { await run(); } } #runExclusiveQuery(fn) { return this.#queryMutex.runExclusive(async () => { this.#checkPersistLatch(); return await fn(); }); } async#execNoPersist(message, options) { return await this.#execProtocolStream(message, { persist: false, ...options?.onNotice ? { onNotice: options.onNotice } : {} }); } async#handleBlob(blob) { const device = this.#running?.blob; if (blob === undefined) { device?.setReadSource(undefined); return; } if (device === undefined) { throw new UnsupportedFeatureError2(`The "${this.build.name}" Postgres build has no /dev/blob device, so the \`blob\` query option is not available.`); } device.setReadSource(new Uint8Array(await blob.arrayBuffer())); } #cleanupBlob() { this.#running?.blob?.setReadSource(undefined); } #takeWrittenBlob() { const chunks = this.#running?.blob?.takeWritten(); return chunks === undefined ? undefined : new Blob(chunks); } #serializeParams(params, dataTypeIDs, options) { return params.map((param, index) => { if (param === null || param === undefined) return null; const oid = dataTypeIDs[index] ?? 0; const serializer = options?.serializers?.[oid] ?? this.#serializers[oid]; return serializer ? serializer(param) : toText(param); }); } async#runQuery(sql, params = [], options) { return await this.#runExclusiveQuery(async () => { this.#log("runQuery", sql, params, options); await this.#handleBlob(options?.blob); let results = []; try { const parsed = await this.#execNoPersist(serialize2.parse({ text: sql, ...options?.paramTypes ? { types: options.paramTypes } : {} }), options); const dataTypeIDs = parseDescribeStatementResults(await this.#execNoPersist(serialize2.describe({ type: "S" }), options)); const values = this.#serializeParams(params, dataTypeIDs, options); results = [ ...parsed, ...await this.#execNoPersist(serialize2.bind({ values }), options), ...await this.#execNoPersist(serialize2.describe({ type: "P" }), options), ...await this.#execNoPersist(serialize2.execute({}), options) ]; } catch (error) { if (error instanceof DatabaseError2) { throw makeQueryError({ error, query: sql, params, options }); } throw error; } finally { results.push(...await this.#execNoPersist(serialize2.sync(), options)); } this.#cleanupBlob(); if (!this.#inTransaction) await this.#persistAfterStatement(); const blob = this.#takeWrittenBlob(); return parseResults(results, this.#parsers, options, blob)[0]; }); } async#runExec(sql, options) { return await this.#runExclusiveQuery(async () => { this.#log("runExec", sql, options); await this.#handleBlob(options?.blob); let results = []; try { results = await this.#execNoPersist(serialize2.query(sql), options); } catch (error) { if (error instanceof DatabaseError2) { throw makeQueryError({ error, query: sql, params: undefined, options }); } throw error; } finally { results.push(...await this.#execNoPersist(serialize2.sync(), options)); } this.#cleanupBlob(); if (!this.#inTransaction) await this.#persistAfterStatement(); const blob = this.#takeWrittenBlob(); return parseResults(results, this.#parsers, options, blob); }); } async query(sql, params, options) { await this.#checkReady(); return await this.#transactionMutex.runExclusive(() => this.#runQuery(sql, params, options)); } async sql(sqlStrings, ...params) { const { query: query2, params: actualParams } = query(sqlStrings, ...params); return await this.query(query2, actualParams); } async exec(sql, options) { await this.#checkReady(); return await this.#transactionMutex.runExclusive(() => this.#runExec(sql, options)); } async transaction(callback) { await this.#checkReady(); return await this.#transactionMutex.runExclusive(async () => { await this.#runExec("BEGIN"); this.#inTransaction = true; let closed = false; const checkClosed = () => { if (closed) throw new PgwasmError2("Transaction is closed"); }; const tx = { query: async (sql, params, options) => { checkClosed(); return await this.#runQuery(sql, params, options); }, sql: async (sqlStrings, ...params) => { checkClosed(); const { query: query2, params: actualParams } = query(sqlStrings, ...params); return await this.#runQuery(query2, actualParams); }, exec: async (sql, options) => { checkClosed(); return await this.#runExec(sql, options); }, rollback: async () => { checkClosed(); await this.#runExec("ROLLBACK"); closed = true; }, listen: async (channel, callback) => { checkClosed(); return await this.listen(channel, callback, tx); }, get closed() { return closed; } }; try { const result = await callback(tx); this.#inTransaction = false; if (!closed) { closed = true; await this.#runExec("COMMIT"); } else { await this.#persistAfterStatement(); } return result; } catch (error) { this.#inTransaction = false; if (!closed) { closed = true; await this.#runExec("ROLLBACK"); } else { try { await this.#persistAfterStatement(); } catch {} } throw error; } }); } async runExclusive(fn) { return await this.#runExclusiveQuery(fn); } async#runExclusiveSession(fn) { await this.#checkReady(); return await this.#transactionMutex.runExclusive(() => this.#queryMutex.runExclusive(async () => { this.#checkOpenState(); this.#checkPersistLatch(); return await fn(); })); } async#initArrayTypes({ force = false } = {}) { if (this.#arrayTypesInitialized && !force) return; this.#arrayTypesInitialized = true; const types = await this.query(` SELECT b.oid, b.typarray FROM pg_catalog.pg_type a LEFT JOIN pg_catalog.pg_type b ON b.oid = a.typelem WHERE a.typcategory = 'A' GROUP BY b.oid, b.typarray ORDER BY b.oid `); for (const type of types.rows) { this.#serializers[type.typarray] = (value) => arraySerializer(value, this.#serializers[type.oid], type.typarray); this.#parsers[type.typarray] = (value) => arrayParser(value, this.#parsers[type.oid], type.typarray); } } async refreshArrayTypes() { await this.#initArrayTypes({ force: true }); } async listen(channel, callback, tx) { return await this.#withListenLocks(tx, (exec) => this.#subscribe(channel, callback, exec)); } async unlisten(channel, callback, tx) { await this.#withListenLocks(tx, (exec) => this.#unsubscribe(channel, callback, exec)); } async#withListenLocks(tx, fn) { if (tx) return await this.#listenMutex.runExclusive(() => fn((sql) => tx.exec(sql))); await this.#checkReady(); return await this.#transactionMutex.runExclusive(() => this.#listenMutex.runExclusive(() => fn((sql) => this.#runExec(sql)))); } async#subscribe(channel, callback, exec) { const pgChannel = toPostgresName(channel); let listeners = this.#notifyListeners.get(pgChannel); if (!listeners) { listeners = new Set; this.#notifyListeners.set(pgChannel, listeners); } listeners.add(callback); try { await exec(`LISTEN ${channel}`); } catch (error) { listeners.delete(callback); if (listeners.size === 0) this.#notifyListeners.delete(pgChannel); throw error; } return async (unlistenTx) => { await this.unlisten(channel, callback, unlistenTx); }; } async#unsubscribe(channel, callback, exec) { const pgChannel = toPostgresName(channel); const cleanUp = async () => { await exec(`UNLISTEN ${channel}`); if (this.#notifyListeners.get(pgChannel)?.size === 0) this.#notifyListeners.delete(pgChannel); }; if (callback) { this.#notifyListeners.get(pgChannel)?.delete(callback); if (this.#notifyListeners.get(pgChannel)?.size === 0) await cleanUp(); } else { await cleanUp(); } } onNotification(callback) { this.#globalNotifyListeners.add(callback); return () => { this.#globalNotifyListeners.delete(callback); }; } async dumpDataDir(compression) { await this.#checkReady(); const running = this.#running; if (running === undefined) throw new PgwasmClosedError2("closed"); const entries = await this.#queryMutex.runExclusive(async () => { this.#checkOpenState(); return await running.readEntries(); }); return await writeDataDirArchive(entries, backupBaseName(this.storage), compression); } async close() { await this.#checkReady(); this.#closing = true; const failure = this.#failure; const running = this.#running; const session = this.#session; let closeFailure; let finalPersistFailure; try { await this.#transactionMutex.runExclusive(() => this.#queryMutex.runExclusive(async () => { for (const closeFn of this.#extensionsClose) { try { await closeFn(); } catch (error) { closeFailure ??= { error }; } } if (failure || running === undefined) return; try { await session?.close(); await running.shutdown(); } catch (error) { closeFailure ??= { error }; } try { await this.#persistMutex.runExclusive(() => running.persist(false)); this.#persistFailure = undefined; } catch (error) { finalPersistFailure = { error }; this.#persistFailure = { error }; } })); } finally { try { await this.#persistMutex.runExclusive(async () => {}); await running?.release(); } finally { this.#closed = true; this.#closing = false; this.#ready = false; this.#session = undefined; this.#running = undefined; } } if (failure) throw failure.error; if (finalPersistFailure) throw finalPersistFailure.error; if (closeFailure) throw closeFailure.error; } async[Symbol.asyncDispose]() { await this.close(); } } // packages/pgwasm/src/create.ts function isServerExtension(extension) { return "kind" in extension && extension.kind === "server"; } function assertBuild(build) { const candidate = build; 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."); } } function mergeSettings(settings, serverExtensions) { const merged = { ...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; } async function restoreEntries(build, loadDataDir) { const entries = await readDataDirArchive(loadDataDir); if (findEntry(entries, PG_VERSION_PATH) === undefined) { throw new BackupFormatError2("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: 384, mtimeSeconds: Math.floor(Date.now() / 1000), data: encodeBuildMarker(build.identity) } ]; } async function createPgwasm2(options) { const build = 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 UnsupportedFilesystemError2(storage.kind, identity, capabilities.filesystems); } if (capabilities.requiresCrossOriginIsolation && globalThis.crossOriginIsolated === false) { throw new CrossOriginIsolationRequiredError2(identity); } const serverExtensions = []; const clientExtensions = []; for (const [key, extension] of Object.entries(options.extensions ?? {})) { if (isServerExtension(extension)) { if (extension.build !== identity.name) { throw new ExtensionBuildMismatchError2(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 }; const instance = new PgwasmInstance(plan); await instance.boot(plan); return instance; } export { identifier2, raw2, createPgwasm2 }; //# debugId=EB995FC77D1C6A9B64756E2164756E21