Event store architecture
The event store records an ordered history for each asset. It assigns an ID across the whole log and a version within each asset stream. Consumers decide what events mean and how to rebuild state from them.
Workspace map
createEventStore wraps an EventLog in the public store. The writer returns a Result for each append and notifies subscribe listeners of accepted appends and subscribeRejections listeners of rejected ones. The reader is a read-only view of the log. subscribe can filter by a literal event type prefix and does not replay earlier events; a consumer reads history to build its initial view.
EventLog stays internal: callers pick a backend through its factory and never see the contract the two backends share.
An optional schema map checks eventType and eventData when appending at the TypeScript boundary. Reads still return Event with an unknown payload, so consumers validate stored data before treating it as a typed domain event.
| Backend | Factory | Storage | Runtime |
|---|---|---|---|
| Memory | persistence.memory() | One store instance | Browser or Node.js |
| SQLite | await persistence.sqlite(location?) | SQLite connection, optionally backed by a file | Node.js |
| IndexedDB | await persistence.indexeddb({ name }) | IndexedDB database | Browser |
The IndexedDB backend is a MemoryEventLog loaded from the database and given an EventJournal. The memory log calls the journal before it changes, so a failed write leaves the log as it was.
The main entry loads the Node-only SQLite backend when its factory is called. The separate @jolly-pixel/event-store/node entry exposes the SQLite factory directly. SqliteDatabase.open owns the whole opening: parent directories, vacuum mode, journal pragmas and schema.
Appending an event
eventId orders accepted events across all assets. eventVersion increases within one asset stream. Each event also records assetType, eventType, eventData, actor, and createdAt. Both backends store JSON values rather than references to the caller's objects; a rejected append consumes neither position. Subscriber callbacks run after the backend accepts an event.
Reading history
list can exclude versions at or below fromVersion. listAll can exclude IDs at or below fromEventId, filter by event type prefix, and limit the result. Checkpoint reads retain each stream's newest matching checkpoint and every later event. A stream without a matching checkpoint remains whole. The read returns surviving events in global eventId order, which is eventVersion order when assetId scopes it to one stream; its prefix filter applies after the checkpoint boundary is chosen. The checkpoint read and compact share each backend's head lookup.
Checkpoints and compaction
The caller supplies checkpoint event types because only the consumer knows which events can replace earlier state. compact permanently removes events before the newest matching checkpoint in each asset stream. Streams without a checkpoint stay intact. An event a tag points at survives, so a stream can keep gaps below its checkpoint. Surviving IDs and versions do not change, and the report counts removed events and assets with checkpoints. SQLite can run VACUUM after a deletion when reclaim is true; the memory backend has no file to reclaim.
Details: API and data model, memory persistence, SQLite persistence, and glossary.