@molecule/api-project-archive-external-state-pglite
Provider bond · project-archive-external-state · API (Node) · v1.0.1 · Apache-2.0
Back up and restore a project's PGlite database as part of a project archive — captures it with PGlite's own dumpDataDir, proves the dump loads back with the same row counts before the caller destroys anything, and restores it with loadDataDir after a checksum check. In-memory databases are declared, never inferred.
npm install @molecule/api-project-archive-external-state-pglitenpm · Source on GitHub · Implements @molecule/api-project-archive
How it works
@molecule/api-project-archive-external-state-pglite is a provider bond on the API (Node) side: it implements the project-archive-external-state core interface (@molecule/api-project-archive) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
import { setExternalStateProvider } from '@molecule/api-project-archive'
import { createPgliteExternalStateProvider } from '@molecule/api-project-archive-external-state-pglite'
setExternalStateProvider(
createPgliteExternalStateProvider({
// The same dataDir each project's pool was created with.
dataDirs: (projectId) => [`/var/lib/app/${projectId}/pglite`],
}),
)Works with: @molecule/api-database-pglite, @molecule/api-project-archive
Reference
Capture and restore a project's PGlite databases for
@molecule/api-project-archive.
It dumps each data directory with PGlite's own dumpDataDir and loads it
back with the loadDataDir option, so a restore reproduces the database
exactly — the same files, not a replay of SQL.
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { setExternalStateProvider } from '@molecule/api-project-archive'
import { createPgliteExternalStateProvider } from '@molecule/api-project-archive-external-state-pglite'
setExternalStateProvider(
createPgliteExternalStateProvider({
// The same dataDir each project's pool was created with.
dataDirs: (projectId) => [`/var/lib/app/${projectId}/pglite`],
}),
)
// A project whose database lives in memory: declare it. The archive then
// records it as having nothing durable instead of silently leaving it out.
createPgliteExternalStateProvider({
dataDirs: (projectId) => (isEphemeral(projectId) ? ['memory://'] : []),
})
Type
provider
Installation
npm install @molecule/api-project-archive-external-state-pglite @molecule/api-database-pglite @molecule/api-project-archive
API
Interfaces
DataDirDump
What one dump produced.
interface DataDirDump {
/** The gzip-compressed tar PGlite's `dumpDataDir('gzip')` returned. */
bytes: Uint8Array
/** Row counts read under the same lock, immediately before the dump. */
tables: TableCounts
/** `server_version` of the instance that wrote the dump. */
postgresVersion: string
}
LoadOutcome
Outcome of loading a tar into a folder.
interface LoadOutcome {
/** Row counts of the loaded database. */
tables: TableCounts
}
PgliteExternalStateConfig
How this deployment finds a project's PGlite databases.
interface PgliteExternalStateConfig {
/**
* The data directory of every PGlite database belonging to `projectId` —
* the same strings handed to `createPool({ dataDir })` in
* `@molecule/api-database-pglite`.
*
* **This is a DECLARATION, not a search.** An empty array means the project
* genuinely owns none — and it is the ONLY way to say that. Each entry is
* either:
*
* - a filesystem path (relative paths resolve against the working directory,
* exactly as the database bond resolves them). It must already hold a
* PGlite data directory at capture time; a missing or empty folder is an
* ERROR, never an absence, because a path template one directory off would
* otherwise capture nothing and let the caller destroy the only copy.
* - `'memory://'` — the project's database lives in memory and has nothing
* durable to capture. Capture records that explicitly (a record with no
* part) so the archive says so, instead of the database silently not
* appearing.
*
* Any other scheme (`idb://…`) is refused: it cannot be reached from a server.
*
* @param projectId - The project being archived or restored.
* @returns Its data directories; `[]` when it owns none.
*/
dataDirs: (projectId: string) => readonly string[] | Promise<readonly string[]>
/**
* How long (ms) to wait for the database's single connection before giving
* up, when the app's own pool is busy with it. Capture takes the same lock
* the app's pool uses, so a dump never runs in the middle of a transaction.
* Defaults to 30 000.
*/
acquireTimeoutMillis?: number
/**
* Largest uncompressed data directory (bytes) a restore will unpack. Guards
* against a decompression bomb in a tampered archive. Defaults to 2 GiB.
*/
maxDataDirBytes?: number
}
Types
TableCounts
Row count per user table, keyed "schema"."table", plus the current value of
every user sequence, keyed sequence "schema"."name" — all as decimal strings.
type TableCounts = Record<string, string>
Classes
JoinedInstanceError
Raised when a restore target turned out to be open in another pool of this process — the load was ignored and the folder belongs to someone else, so it must not be cleaned up.
class JoinedInstanceError extends Error {
/**
* Build the error for a folder another pool holds.
*
* @param dataDir - The folder another pool holds.
*/
constructor(dataDir: string) {
super(
`the PGlite data directory ${dataDir} is already open in this process, so the archived ` +
`database cannot be loaded into it. End the pools using it and restore again.`,
)
this.name = 'JoinedInstanceError'
}
}
Functions
sha256Hex(bytes)
Hex SHA-256 of some bytes.
function sha256Hex(bytes: Uint8Array<ArrayBufferLike>): string
bytes— The bytes to hash.
Returns: The lowercase hex digest.
countTables(instance)
Count the rows of every user table and read every user sequence (everything outside the system schemas), in a stable order.
function countTables(instance: PgliteInstance): Promise<TableCounts>
instance— An open instance whose connection the caller holds.
Returns: Row counts keyed by quoted schema.table.
canonicalCounts(counts)
Canonical string form of a set of table counts, for comparison.
function canonicalCounts(counts: TableCounts): string
counts— The counts.
Returns: A string that is equal for equal count sets regardless of key order.
dumpDataDir(dataDir, timeoutMillis)
Dump one data directory through the app's shared instance.
Joins (or opens) the instance for dataDir in the database bond's registry,
waits for its single connection, and reads the row counts and the dump while
holding it — so nothing commits between the two and no transaction is open
during the dump.
function dumpDataDir(dataDir: string, timeoutMillis: number): Promise<DataDirDump>
dataDir— Absolute path of an existing PGlite data directory.timeoutMillis— How long to wait for the connection.
Returns: The dump and what it should contain.
inflateDump(gz, maxBytes)
Decompress a dump into the tar PGlite's loadDataDir takes.
Done here with node's zlib rather than by PGlite: PGlite 0.5.8 inflates a gzip dump on a web stream whose failure is not tied to any promise, so a truncated gzip crashes the whole process instead of rejecting. Handing it a plain tar that zlib has already validated avoids that path entirely.
function inflateDump(
gz: Uint8Array<ArrayBufferLike>,
maxBytes: number,
): Promise<Uint8Array<ArrayBuffer>>
gz— The gzip bytes.maxBytes— Largest uncompressed size accepted.
Returns: The uncompressed tar.
countLoadedDump(tar)
Load a tar into a fresh in-memory instance and count its rows — proof that a dump loads and holds what was counted when it was taken.
function countLoadedDump(tar: Uint8Array<ArrayBuffer>): Promise<TableCounts>
tar— The uncompressed data-directory tar.
Returns: The row counts of the loaded database.
loadDataDir(dataDir, tar, timeoutMillis)
Load a tar into an empty folder, through the database bond's registry, and count what landed.
function loadDataDir(
dataDir: string,
tar: Uint8Array<ArrayBuffer>,
timeoutMillis: number,
): Promise<LoadOutcome>
dataDir— Absolute path of the (empty or absent) target folder.tar— The uncompressed data-directory tar.timeoutMillis— How long to wait for the connection.
Returns: The counts of the loaded database.
createPgliteExternalStateProvider(config)
Create the provider.
function createPgliteExternalStateProvider(
config: PgliteExternalStateConfig,
): ProjectExternalStateProvider
config— How to find a project's PGlite databases.
Returns: A provider ready to bond with setExternalStateProvider.
Constants
KIND
Recorded on every record this provider produces; routes restores back here.
const KIND: 'pglite'
DUMP_FORMAT
Recorded in each record so a future format change can be told apart.
const DUMP_FORMAT: 'pglite-datadir-tar-gzip'
Core Interface
Implements @molecule/api-project-archive interface.
Bond Wiring
Setup function to register this provider with the bond system:
import { bond } from '@molecule/api-bond'
import { provider } from '@molecule/api-project-archive-external-state-pglite'
export function setupProjectArchiveExternalStatePglite(): void {
bond('project-archive-external-state', 'pglite', provider)
}
Injection Notes
Requirements
Peer dependencies:
@molecule/api-database-pglite^1.0.0@molecule/api-project-archive^1.0.1
Runtime Dependencies
@molecule/api-database-pglite@molecule/api-project-archive
Bond the SAME copy of @molecule/api-database-pglite your app uses (it
is a peer dependency for that reason). Capture and restore go through that
package's instance registry: when the app's pool has a folder open, capture
JOINS that instance and waits for its single connection, so a dump never runs
mid-transaction and a second PGlite is never opened on the same folder (two
instances on one folder overwrite each other's files). Never point another
PROCESS at a folder while it is being captured or restored — PGlite does not
lock its data directory.
Verified before the caller may destroy anything. Capture counts every
table's rows under the connection lock, dumps, then loads the dump into a
scratch in-memory instance and checks the counts match. The record carries
the dump's SHA-256 and byte size; restore refuses bytes that do not match
them, refuses to unpack more than maxDataDirBytes (default 2 GiB), and
checks the restored database's row counts against the recorded ones.
Restore only into an empty or absent folder. A folder that already holds anything is refused, so a live database is never overwritten. If a restore fails, the folder is put back the way it was found (removed, or emptied). End the app's pools on that folder before restoring.
Absence is DECLARED, never inferred. Only an empty dataDirs result says
a project owns no database. A declared folder with no PGlite data directory
in it (no PG_VERSION) is an ERROR — opening PGlite there would create a
fresh empty database and capture that. 'memory://' is the explicit way to
say "nothing durable"; idb:// and other schemes are refused.
If the data directory lives inside the project's source tree and is committed, you do not need this package — whatever archives the source tree already carries it.