Skip to content
English

SqliteJournal

Defined in: src/persistence/journals/SqliteJournal.ts:56

Journal backed by SQLite — zero-dependency, single-file persistence.

Works on all three runtimes via the SqliteDriver abstraction in src/runtime/sqlite/ — Bun (bun:sqlite), Node (better-sqlite3, or the built-in node:sqlite when it is not installed) and Deno (node:sqlite). Every driver shares the same prepared-statement + transaction shape, so the journal code itself is unchanged across runtimes.

Construction is lazy: the native DB is opened on the first append / read / highestSeq / delete / persistenceIds call. This keeps new SqliteJournal(SqliteJournalOptions.create().withPath(path)) sync-friendly (matches the pre-abstraction shape) while still supporting the async driver-resolution flow Node requires.

new SqliteJournal(options?): SqliteJournal

Defined in: src/persistence/journals/SqliteJournal.ts:73

SqliteJournalOptions = {}

SqliteJournal

readonly events: JournalEventBus

Defined in: src/persistence/journals/SqliteJournal.ts:67

In-process event bus — published-to inside append so the query layer can do sub-poll-interval push delivery in the same process. Cross-process subscribers (separate Bun/Node instance reading the same SQLite file) still need to poll; that’s the inherent limit of in-process notifications.

Journal.events

get serializer(): Serializer<unknown> | undefined

Defined in: src/persistence/journals/SqliteJournal.ts:84

The configured payload serializer — read by SqliteQuery so tag reads decode like the journal.

Serializer<unknown> | undefined

append<E>(persistenceId, events, expectedSeq, tags?): Promise<PersistentEvent<E>[]>

Defined in: src/persistence/journals/SqliteJournal.ts:86

Append events to the stream of persistenceId, enforcing optimistic concurrency: the current highest sequence number MUST equal expectedSeq or the call throws JournalConcurrencyError. Returns the written events with their assigned sequence numbers.

E

string

readonly E[]

number

readonly string[]

Promise<PersistentEvent<E>[]>

Journal.append


close(): Promise<void>

Defined in: src/persistence/journals/SqliteJournal.ts:237

Best-effort teardown; idempotent.

Promise<void>

Journal.close


delete(persistenceId, toSeq): Promise<void>

Defined in: src/persistence/journals/SqliteJournal.ts:190

Delete events up to and including toSeq — used when compacting past a snapshot. Only ever a prefix, so what survives is a suffix that read still returns contiguously, and sequence numbers never rewind: highestSeq keeps reporting the high-water mark afterwards.

string

number

Promise<void>

Journal.delete


highestSeq(persistenceId): Promise<number>

Defined in: src/persistence/journals/SqliteJournal.ts:177

Current highest sequence number for persistenceId — 0 if no events exist.

string

Promise<number>

Journal.highestSeq


persistenceIds(): Promise<string[]>

Defined in: src/persistence/journals/SqliteJournal.ts:209

Persistence IDs currently known to the journal (useful for projections).

Promise<string[]>

Journal.persistenceIds


persistenceIdsPaginated(afterPersistenceId, limit): Promise<string[]>

Defined in: src/persistence/journals/SqliteJournal.ts:219

One ascending page of persistence ids: those strictly greater than afterPersistenceId — all of them when it is undefined — capped at limit. Returning fewer than limit means the journal is exhausted.

Optional on purpose. Not every store can enumerate ids in order: DynamoDB reaches partition keys only through a full table scan, and MongoDB’s distinct has no cursor. A journal without a sorted index over its ids omits this, and the query layer falls back to persistenceIds() plus an in-process slice — correct, just not cheaper than the full list. Implement it wherever a sorted key exists; that is what keeps currentPersistenceIdsPaginated from materialising a million rows to hand back the first 256.

Which ascending order is the backend’s business. afterPersistenceId is compared in the same order the page is sorted by, so Postgres’ collated ORDER BY and SQLite’s byte-wise one are both fine — a paginated walk only needs the order to be total and stable within one journal. What a journal must not do is mix two orders across calls, which would make the cursor skip ids.

string | undefined

number

Promise<string[]>

Journal.persistenceIdsPaginated


read<E>(persistenceId, fromSeq, toSeq?): Promise<PersistentEvent<E>[]>

Defined in: src/persistence/journals/SqliteJournal.ts:146

Return the events in [fromSeq, …, toSeq], ascending by sequence number. toSeq defaults to the current highest sequence number. Both bounds are inclusive — fromSeq is the first event returned, not an “after” cursor.

Ordering and contiguity are part of the contract, and replay enforces them (#122). Consecutive entries must differ by exactly one, every sequenceNr must be a safe integer ≥ 1, and nothing may fall outside the requested window. delete compacts a prefix, never a hole in the middle, so a gap inside the returned slice can only mean a defect — a missing ORDER BY, a half-written append, a store someone else can write. replayState raises JournalIntegrityError instead of folding it, because an actor that recovers from a shuffled or holed stream reaches a state that never existed and then fails every later persist with a JournalConcurrencyError that has no visible cause.

E

string

number

number

Promise<PersistentEvent<E>[]>

Journal.read