Pi Durable storage

Last updated 29 October 2026

View .md

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_full commits nothing. From Pi 1.2, the storage waits for synced() and runs it again. On Pi 1.0 and 1.1 it throws Pi’s StorageRejected, whose cause is the UrsulaWriteRefusedError, 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 to closeTimeoutMs for synced(), 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:

ActionKindsWhat to do
reopenstale, rewound, local_damage, internalClose the storage, then openUrsulaPiStorage() again and resume the session from it.
stopfencedAnother process took the database over. Stop this Harness and alert: reopening in a loop takes the database back from the new owner.
fixdeleted, stream_stateFix the cause first, such as a deleted database, then open the storage again.

Options

UrsulaPiStorageOptions. It takes no readOnly and no other DatabaseSync option.

OptionDefaultWhat it does
dirThe user cacheAs open()’s.
tokenNone: URSULA_VFS_TOKEN_FILEAs open()’s.
signal30 secondsBounds the open, as open()’s.
maxPendingBytes64 MiBAs open()’s.
recreatefalseAs open()’s.
walAutoCheckpointPagesPi'sPassed to Pi's node driver.
busyTimeoutMsPi'sPassed to Pi's node driver.
closeTimeoutMs10 secondsHow long the storage’s close() waits for synced() before it closes anyway.