@pgxsinkit/pgwasm
Version:
1,006 lines (995 loc) • 33.9 kB
JavaScript
// @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