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 { ActorSystem, ActorSystemOptions, InMemoryJournal, InMemorySnapshotStore } from 'actor-ts';
// In-Memory-Journal + Snapshot-Store sind der DEFAULT — ein bloßes// ActorSystem.create('demo') nutzt sie bereits. Um sie explizit zu// setzen (oder später andere Stores einzuwechseln), beim Erstellen verdrahten:const actorSystemOptions = ActorSystemOptions.create().withPersistence({ journal: new InMemoryJournal(), snapshotStore: new InMemorySnapshotStore(),});const system = ActorSystem.create('demo', actorSystemOptions);
// 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>(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 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).delete— 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 } from 'actor-ts';
describe('OrderActor', () => { let tk: TestKit;
beforeEach(() => { tk = TestKit.create(); // In-Memory-Journal + Snapshot-Store per Default });
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(() => 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' });
// Stoppen + neu spawnen — gleiche Persistence-ID await tk.system.stop(order); order = tk.system.spawnAnonymous(() => new OrderActor('order-1')); order.tell({ kind: 'view', replyTo: probe });
// Wiederhergestellter State sollte beide platzierten Events widerspiegeln await probe.expectMessage({ 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) — In-Memory ist der Default:const devOptions = ActorSystemOptions.create().withPersistence({ journal: new InMemoryJournal(),});const system = ActorSystem.create('app', devOptions);
// Nachher (Produktion):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);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.
