Pi Durable storage
Last updated 29 October 2026
openUrsulaPiStorage() runs Pi Durable’s official node SqliteStorage on a replicated database.
It needs @earendil-works/pi-durable, an optional peer dependency, from 1.0.0 up to but not including 1.4.0. A walkthrough is the Pi Durable quickstart.
openUrsulaPiStorage() by name
From @tonbo/sqlite-ursula/tonbo/pi, since 0.11.1.
function openUrsulaPiStorage(
nameOrUrl: string | URL,
options?: TonboPiStorageOptions,
): Promise<SqliteStorage>
type TonboPiStorageOptions = UrsulaPiStorageOptions & {
apiKey?: string;
apiOrigin?: string;
authOrigin?: string;
};import { openUrsulaPiStorage } from "@tonbo/sqlite-ursula/tonbo/pi";
const harness = await Harness.open(await openUrsulaPiStorage("harness-1"), { models, registry }, context);
harness.resume();It resolves nameOrUrl as resolveDatabase() does, then opens it with the /pi entry’s openUrsulaPiStorage below; the extension keeps the access tokens exchanged for the key fresh. A URL skips the lookup, and a missing dir names the default cache root. Harness.close() closes the storage. It throws as the /tonbo open() does, and the entry re-exports those errors.
TonboPiStorageOptions are the options below plus apiKey, apiOrigin and authOrigin, as TonboOpenOptions has them.
openUrsulaPiStorage() by URL
From @tonbo/sqlite-ursula/pi.
function openUrsulaPiStorage(
url: string | URL,
options?: UrsulaPiStorageOptions,
): Promise<SqliteStorage>
type UrsulaPiStorageOptions = {
dir?: string;
token?: string | TokenProvider;
signal?: AbortSignal;
maxPendingBytes?: number;
recreate?: boolean;
walAutoCheckpointPages?: number;
busyTimeoutMs?: number;
closeTimeoutMs?: number;
};import { openUrsulaPiStorage } from "@tonbo/sqlite-ursula/pi";
import { resolveDatabase } from "@tonbo/sqlite-ursula/tonbo";
// token is undefined while the key's own tokens serve: pass it on as it is.
const { url, token } = await resolveDatabase("harness-1");
const storage = await openUrsulaPiStorage(url, { token, dir: "./harness-1-cache" });It opens the stream at url with the root open(), then runs Pi’s node driver, unmodified, on the local file, and resolves with Pi’s SqliteStorage. To open a Tonbo database by name, pass what resolveDatabase() hands out.
- Every transaction, and every statement outside one, resolves only once
synced()did: a Pi commit resolves once the stream holds it, and a read once what it read is in the stream. - A commit refused with
backlog_fullcommits nothing. From Pi 1.2, the storage waits forsynced()and runs it again. On Pi 1.0 and 1.1 it throws Pi’sStorageRejected, whosecauseis theUrsulaWriteRefusedError, so the session continues. - A reset throws the UrsulaResetError itself, unwrapped, so the session stops. See Resets in a Harness. Other failures throw the package’s other errors.
close()waits up tocloseTimeoutMsforsynced(), then closes. What the stream lacks then stays in the local log for the next open.
Resets in a Harness
When the storage throws an UrsulaResetError, the Harness’s session stops. What to do follows the error’s action:
| Action | Kinds | What to do |
|---|---|---|
reopen | stale, rewound, local_damage, internal | Close the storage, then openUrsulaPiStorage() again and resume the session from it. |
stop | fenced | Another process took the database over. Stop this Harness and alert: reopening in a loop takes the database back from the new owner. |
fix | deleted, stream_state | Fix the cause first, such as a deleted database, then open the storage again. |
Options
UrsulaPiStorageOptions. It takes no readOnly and no other DatabaseSync option.
| Option | Default | What it does |
|---|---|---|
dir | The user cache | As open()’s. |
token | None: URSULA_VFS_TOKEN_FILE | As open()’s. |
signal | 30 seconds | Bounds the open, as open()’s. |
maxPendingBytes | 64 MiB | As open()’s. |
recreate | false | As open()’s. |
walAutoCheckpointPages | Pi's | Passed to Pi's node driver. |
busyTimeoutMs | Pi's | Passed to Pi's node driver. |
closeTimeoutMs | 10 seconds | How long the storage’s close() waits for synced() before it closes anyway. |