Zum Inhalt springen
Deutsch

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.

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 auf expectedSeq (wirft JournalConcurrencyError, 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.

Drei legitime Fälle:

  1. Tests — schnell, kein IO, kein Cleanup zwischen Tests nötig. Ein frisches System hochziehen, Assertions machen, abreißen.
  2. Entwicklung — wenn du dich beim lokalen Iterieren nicht mit einer SQLite-Datei herumschlagen willst.
  3. Wegwerf-Demos — wenn die Daten nicht überleben sollen.

Für alles, was über Neustarts hinweg persistieren muss, wechsle zu SQLite oder 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 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.

// 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.