In-Memory-Journal
InMemoryJournal speichert jedes Event im Prozess-Speicher. Es
ist der Default, wenn du ein System ohne konfigurierte
Persistenz erstellst — schnell, null Setup, perfekt für Tests und
Dev.
import { InMemoryJournal, InMemorySnapshotStore, PersistenceExtensionId, ActorSystem } from 'actor-ts';
const system = ActorSystem.create('demo');system.extension(PersistenceExtensionId).configure({ journal: new InMemoryJournal(), snapshotStore: new InMemorySnapshotStore(),});
// Jetzt schreiben `PersistentActor`s in diesem System in das In-Memory-Journal.Es ist außerdem die Referenz-Semantik, der jede andere Journal-Implementierung entsprechen muss — Append-only, monotone Sequenznummern, Exactly-once-Delivery an Subscriber, Concurrency-Check auf erwarteter Sequenz.
Was es tut
Abschnitt betitelt „Was es tut“Der Journal-Vertrag:
interface Journal { append<E>(pid: string, events: E[], expectedSeq: number, tags?: string[]): Promise<PersistentEvent<E>[]>; read<E>(pid: string, fromSeq: number, toSeq?: number): Promise<PersistentEvent<E>[]>; highestSeq(pid: string): Promise<number>; deleteEventsUpTo(pid: string, seqNr: number): Promise<void>; persistenceIds(): AsyncIterable<string>; readonly events: JournalEventBus;}InMemoryJournal implementiert all das intern mit einer
Map<string, PersistentEvent[]>. Operationen sind:
append— Events auf das Array des Streams pushen. Concurrency-Check aufexpectedSeq(wirftJournalConcurrencyError, wenn der aktuelle Head nicht passt).read— das Array nach Sequenznummern-Bereich slicen.highestSeq— die seqNr des letzten Eintrags lesen (oder 0).deleteEventsUpTo— das Präfix abschneiden.persistenceIds— über die Keys der Map iterieren.
Jedes append publiziert das Event außerdem in den In-Process-
JournalEventBus — sodass Push-basierte Queries (siehe
Push-basierte Query) in
Echtzeit ohne Polling funktionieren.
Wann verwenden
Abschnitt betitelt „Wann verwenden“Drei legitime Fälle:
- Tests — schnell, kein IO, kein Cleanup zwischen Tests nötig. Ein frisches System hochziehen, Assertions machen, abreißen.
- Entwicklung — wenn du dich beim lokalen Iterieren nicht mit einer SQLite-Datei herumschlagen willst.
- Wegwerf-Demos — wenn die Daten nicht überleben sollen.
Für alles, was über Neustarts hinweg persistieren muss, wechsle zu SQLite oder Cassandra.
Test-Pattern
Abschnitt betitelt „Test-Pattern“import { describe, it, beforeEach, afterEach } from 'bun:test';import { TestKit, InMemoryJournal, InMemorySnapshotStore, PersistenceExtensionId } from 'actor-ts';
describe('OrderActor', () => { let tk: TestKit;
beforeEach(() => { tk = TestKit.create(); tk.system.extension(PersistenceExtensionId).configure({ journal: new InMemoryJournal(), snapshotStore: new InMemorySnapshotStore(), }); });
afterEach(async () => { await tk.shutdown(); });
it('replays events on restart', async () => { const probe = tk.createTestProbe();
// Anlegen + ein paar Commands senden let order = tk.system.spawnAnonymous(Props.create(() => new OrderActor('order-1'))); order.tell({ kind: 'place', sku: 'book-1' }); order.tell({ kind: 'place', sku: 'book-2' }); await probe.expectMsg({ kind: 'placed', sku: 'book-2' });
// Stoppen + neu spawnen — gleiche Persistence-ID await tk.system.stop(order); order = tk.system.spawnAnonymous(Props.create(() => new OrderActor('order-1'))); order.tell({ kind: 'view', replyTo: probe });
// Wiederhergestellter State sollte beide platzierten Events widerspiegeln await probe.expectMsg({ items: ['book-1', 'book-2'] }); });});Ein frisches InMemoryJournal pro Test (über TestKit pro Test)
macht jeden Fall isoliert — kein State-Leakage zwischen Tests.
Was es nicht tut
Abschnitt betitelt „Was es nicht tut“Auf ein echtes Journal umsteigen
Abschnitt betitelt „Auf ein echtes Journal umsteigen“// Vorher (Test / Dev):system.extension(PersistenceExtensionId).configure({ journal: new InMemoryJournal(),});
// Nachher (Produktion):import { SqliteJournal } from 'actor-ts';system.extension(PersistenceExtensionId).configure({ journal: new SqliteJournal({ path: '/var/lib/my-app/events.db' }),});Keine Code-Änderungen innerhalb der Actors — das Journal-
Interface ist dasselbe. Tausche einfach die Implementierung aus.
Deshalb sagen wir “verwende In-Memory für Tests und Dev” — der Produktions-Switch ist eine Zeile, und die Test-Suite läuft weiter gegen schnelle In-Memory-Journals.
Wie geht’s weiter
Abschnitt betitelt „Wie geht’s weiter“- Persistenz im Überblick — das größere Bild.
- SQLite-Journal — der Single-Node-Produktions-Default.
- Cassandra-Journal — für Multi-Node-Produktion.
- PersistentActor — was die Events schreibt.
- Snapshot Stores — In-Memory — der zugehörige In-Memory-Snapshot-Store.