Skip to content

dotlabshq/baseworks-eventstore

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@baseworks/eventstore

An append-only, multi-tenant event store over libSQL/SQLite (with a Postgres schema too). Streams, optimistic concurrency, and a global order — the source of truth an event-sourced service reads and rebuilds from.

append(event) ──▶ events table (append-only, per-tenant, globally ordered)
                       │
        readStream / readAll ──▶ fold(projector) ──▶ derived state

Pairs with @baseworks/readmodel: eventstore is the log; readmodel folds it into queryable read models. Zero framework lock-in — it's a repo over any drizzle-shaped client; the same interface is available over HTTP (createEventStoreHttpClient) so a service can front it.

Core model

  • Event envelope — an event grows through three shapes: NewEvent (what a caller records), SealedEvent (+ id, version, time), StoredEvent (+ globalSeq, assigned by the store on write).
  • Stream — the append-only history of one entity, addressed by its id. Within a stream version is strictly increasing (1, 2, 3…).
  • Optimistic concurrencyappend takes the expectedVersion the caller believes the stream is at; if it moved on, the append is rejected with ConcurrencyError. A UNIQUE(tenant, stream_id, version) index is the ultimate backstop.
  • Global orderglobalSeq orders events across all streams, so the whole tenant log replays deterministically.
  • Tenant isolation — every row and every read is scoped by tenant.

Quick start

import { createClient } from '@libsql/client'
import { drizzle } from 'drizzle-orm/libsql'
import { createEventStoreRepo, events, SQLITE_SCHEMA_SQL } from '@baseworks/eventstore'

const client = createClient({ url: process.env.DB_URL! })
await client.executeMultiple(SQLITE_SCHEMA_SQL)        // apply the schema on boot
const store = createEventStoreRepo(drizzle(client, { schema: { events } }), { events })

// append (optimistic concurrency: expectedVersion 0 = a new stream)
await store.append('acme', 'order-1', 0, [
  { type: 'OrderPlaced', streamId: 'order-1', actor: 'u1', payload: { total: 42 } },
])

// read
await store.readStream('acme', 'order-1')             // one entity's history
await store.readAll('acme', { fromGlobalSeq: 0 })      // the whole tenant log, in order

API

createEventStoreRepo(db, { events })EventStoreRepo:

method purpose
append(tenant, streamId, expectedVersion, events) append; → { events, version }, throws ConcurrencyError on conflict
readStream(tenant, streamId, { fromVersion? }) one stream's events
readAll(tenant, { fromGlobalSeq? }) every event in the tenant, in global order
readByCorrelation(tenant, correlationId) events sharing a correlation id (lineage)
streamVersion(tenant, streamId) current version of a stream

Projections (projection.ts) — fold events into derived state:

import { fold, rebuildStream, rebuildAll, type Projector } from '@baseworks/eventstore'

const projector: Projector<State> = { initial, apply }
const state = await rebuildStream(store, projector, 'acme', 'order-1')  // one aggregate

Over HTTP — the same EventStoreRepo interface against an eventstore service:

import { createEventStoreHttpClient } from '@baseworks/eventstore/http-client'
const store = createEventStoreHttpClient('http://events:3001')

Schema

  • SQLITE_SCHEMA_SQL — the canonical DDL string; the single source of truth for the events table shape. Apply it on boot (client.executeMultiple(...)).
  • Drizzle tables: @baseworks/eventstore/schema/sqlite (default) and @baseworks/eventstore/schema/pg (Postgres).

Dependencies

drizzle-orm + zod. No framework, no server — bring your own libsql/pg client.

Develop

pnpm test        # vitest
pnpm build       # tsup → dist

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors