Aller au contenu
Français

In-memory journal

Ce contenu n’est pas encore disponible dans votre langue.

InMemoryJournal stores every event in process memory. It’s the default when you create a system without configuring persistence — fast, zero setup, perfect for tests and dev.

import { ActorSystem, ActorSystemOptions, InMemoryJournal, InMemorySnapshotStore } from 'actor-ts';
// In-memory journal + snapshot store are the DEFAULT — a bare
// ActorSystem.create('demo') already uses them. To set them explicitly
// (or to swap in other stores later), wire them at creation:
const actorSystemOptions = ActorSystemOptions.create().withPersistence({
journal: new InMemoryJournal(),
snapshotStore: new InMemorySnapshotStore(),
});
const system = ActorSystem.create('demo', actorSystemOptions);
// Now `PersistentActor`s in this system write to the in-memory journal.

It’s also the reference semantics every other journal implementation must match — append-only, monotonic sequence numbers, exact-once delivery to subscribers, concurrency check on expected sequence.

The journal contract:

interface Journal {
append<E>(persistenceId: string, events: E[], expectedSeq: number, tags?: string[]): Promise<PersistentEvent<E>[]>;
read<E>(persistenceId: string, fromSeq: number, toSeq?: number): Promise<PersistentEvent<E>[]>;
highestSeq(persistenceId: string): Promise<number>;
delete(persistenceId: string, toSeq: number): Promise<void>;
persistenceIds(): Promise<string[]>;
readonly events?: JournalEventBus;
close?(): Promise<void>;
}

InMemoryJournal implements all of this with a Map<string, PersistentEvent[]> internally. Operations are:

  • append — push events onto the stream’s array. Concurrency check on expectedSeq (throws JournalConcurrencyError if the current head doesn’t match).
  • read — slice the array by sequence number range.
  • highestSeq — read the last entry’s seqNr (or 0).
  • delete — splice off the prefix.
  • persistenceIds — iterate the Map’s keys.

Every append also publishes the event to the in-process JournalEventBus — so push-based queries (see Push-based query) work in real time without polling.

Three legitimate cases:

  1. Tests — fast, no IO, no cleanup needed between tests. Spin up a fresh system, do your assertions, tear down.
  2. Development — when you don’t want to bother with a SQLite file during local iteration.
  3. Throwaway demos — when the data isn’t supposed to survive.

For anything that needs to persist across restarts, switch to SQLite or Cassandra.

import { describe, it, beforeEach, afterEach } from 'bun:test';
import { TestKit } from 'actor-ts';
describe('OrderActor', () => {
let tk: TestKit;
beforeEach(() => {
tk = TestKit.create(); // in-memory journal + snapshot store by default
});
afterEach(async () => { await tk.shutdown(); });
it('replays events on restart', async () => {
const probe = tk.createTestProbe();
// Create + send some commands
let order = tk.system.spawnAnonymous(() => new OrderActor('order-1'));
order.tell({ kind: 'place', sku: 'book-1' });
order.tell({ kind: 'place', sku: 'book-2' });
await probe.expectMessage({ kind: 'placed', sku: 'book-2' });
// Stop + re-spawn — same persistence ID
await tk.system.stop(order);
order = tk.system.spawnAnonymous(() => new OrderActor('order-1'));
order.tell({ kind: 'view', replyTo: probe });
// Recovered state should reflect both placed events
await probe.expectMessage({ items: ['book-1', 'book-2'] });
});
});

A fresh InMemoryJournal per test (via per-test TestKit) makes each case isolated — no state leakage between tests.

// Before (test / dev) — in-memory is the default:
const devOptions = ActorSystemOptions.create().withPersistence({
journal: new InMemoryJournal(),
});
const system = ActorSystem.create('app', devOptions);
// After (production):
import { SqliteJournal, SqliteJournalOptions } from 'actor-ts';
const sqliteJournalOptions = SqliteJournalOptions.create().withPath('/var/lib/my-app/events.db');
const prodOptions = ActorSystemOptions.create().withPersistence({
journal: new SqliteJournal(sqliteJournalOptions),
});
const prodSystem = ActorSystem.create('app', prodOptions);

No code changes inside actors — the Journal interface is the same. Just swap the implementation.

This is why we say “use in-memory for tests and dev” — the production switch is one line, and the test suite continues to run against fast in-memory journals.