@bimetal/data
v0.9.0
Published
Event-sourcing data core for calendar applications: CalendarStore, undo/redo, pluggable storage
Downloads
733
Maintainers
Readme
@bimetal/data
Event-sourcing data core for calendar applications. Immutable, append-only, pluggable storage.
Installation
npm install @bimetal/dataQuick Start
import { createCalendarStore, createEvent, updateEvent, deleteEvent } from '@bimetal/data';
import { createDateTime, createTimeRange } from '@bimetal/core';
const store = createCalendarStore();
// Create
const event = {
id: 'e1',
title: 'Meeting',
timeRange: createTimeRange(
createDateTime(2026, 3, 15, 9, 0, 'Europe/Berlin'),
createDateTime(2026, 3, 15, 10, 0, 'Europe/Berlin'),
),
allDay: false,
metadata: { color: '#007AFF', category: 'Work' },
};
await store.dispatch(createEvent(event));
// Update
await store.dispatch(updateEvent('e1', { title: 'Updated Meeting' }));
// Delete
await store.dispatch(deleteEvent('e1'));
// Undo (compensating event, append-only)
await store.undo('e1');
// Read
store.getEvents(); // CalendarEvent[]
store.getEventsInRange(start, end); // filtered by TimeRange
store.getHistory('e1'); // DomainEvent[] — full timeline
// React to changes
store.subscribe(state => console.log(state.events));Event Sourcing
State is never mutated directly. Every change produces an immutable domain event:
| Command | Domain Event |
|---------|-------------|
| createEvent(event) | CalendarEventCreated |
| updateEvent(id, changes) | CalendarEventUpdated (carries previous for undo) |
| deleteEvent(id) | CalendarEventDeleted (carries snapshot for restore) |
| store.undo(id) | CalendarEventReverted (compensating event) |
Current state is a projection over the event stream. The event stream is the source of truth.
Pluggable Storage
EventStore is an interface. Default: InMemoryEventStore.
import { createCalendarStore } from '@bimetal/data';
import type { EventStore } from '@bimetal/data';
// Custom storage backend
const myStore: EventStore = {
async append(streamId, events, expectedVersion) { /* ... */ },
async read(streamId, fromVersion?) { /* ... */ },
subscribe(handler, streamId?) { /* ... */ },
};
const store = createCalendarStore({ eventStore: myStore });Command ID and Tracing
The command id is stored as causationId on resulting domain events for tracing. It does not provide deduplication — repeated dispatches of the same command will produce duplicate events.
Optimistic Concurrency
append() takes an expectedVersion. If another write happened in between, ConcurrencyError is thrown.
Configuration
createCalendarStore({
defaultStreamId: 'my-calendar',
clock: { now: () => Date.now() }, // injectable for tests
generateId: () => crypto.randomUUID(), // injectable for determinism
eventStore: createInMemoryEventStore(), // pluggable backend
});