@minamorl/root-core
v0.1.0
Published
An event-sourcing kernel with algebraic normalization for TypeScript. State is derived from a normalized event log — never mutated directly.
Readme
@minamorl/root-core
An event-sourcing kernel with algebraic normalization for TypeScript. State is derived from a normalized event log — never mutated directly.
Install
npm install @minamorl/root-coreCore Concept
All state changes are expressed as a sequence of three event types:
type Event =
| { type: "Create"; id: string; value: unknown }
| { type: "Update"; id: string; value: unknown }
| { type: "Delete"; id: string };Events are normalized by a rewrite system that collapses redundant operations, then replayed to produce state. The fundamental invariant:
state(events) === state(rewrite(events)) // for all event sequencesQuick Start
import { Root, Patch, rewrite } from "@minamorl/root-core";
const root = new Root(rewrite, { enforce: (p) => p });
root.commit({ type: "Create", id: "1", value: { name: "Alice" } });
root.commit({ type: "Update", id: "1", value: { name: "Bob" } });
root.state(); // { "1": { name: "Bob" } }
root.history(); // normalized: [Create("1", {name:"Bob"})]Architecture
types.ts Event ADT: Create | Update | Delete
↓
state.ts Pure replay: Event[] → Record<string, unknown>
↓
rewrite.ts Normalization via 4 rewrite laws
↓
invert.ts Inverse computation: events + base → undo events
↓
patch.ts Patch: immutable event bundle with compose/invert
↓
root.ts Root: facade with commit/undo/redo/subscribe/compact
↓
schema-registry.ts Schema metadata layer (Zod + SQL column meta)API
rewrite(events: readonly Event[]): Event[]
Normalizes an event sequence by applying four rewrite laws:
| Law | Rule | Effect |
|-----|------|--------|
| L1 | Update without prior Create | Discarded |
| L2 | Delete removes all history for that id | Cleared |
| L3 | Create → Update* → Delete | Collapses to nothing |
| L4 | Sequential Updates on same id | Collapses to last |
Returns only Create (plus at most one Update) per surviving id. Idempotent: rewrite(rewrite(x)) equals rewrite(x).
state(events: readonly Event[]): Record<string, unknown>
Replays events sequentially to produce a { id: value } state map.
invert(events: Event[], base: Record<string, unknown>): Event[]
Computes inverse events relative to a base state:
Create(id)→Delete(id)if id was absent in base, elseUpdate(id, oldValue)Update(id)→Update(id, oldValue)if existed in baseDelete(id)→Create(id, oldValue)if existed in base
Patch
Immutable event bundle.
class Patch {
static from(events: readonly Event[], rewrite: RewriteFn): Patch;
compose(other: Patch): Patch;
toNormalForm(): Event[];
toEvents(): readonly Event[];
invert(base: Record<string, unknown>): Patch;
}Root
Main facade. Manages event history, normalization, undo/redo, and push-based subscriptions.
class Root {
constructor(
rewrite: (es: readonly Event[]) => Event[],
law: { enforce(p: Patch): Patch }
);
commit(input: Patch | Event | readonly Event[]): void;
state(): Record<string, unknown>;
history(): readonly Event[];
undo(p: Patch): void;
redo(p: Patch): void;
subscribe(fn: Subscriber): () => void;
compact(): void;
}| Method | Description |
|--------|-------------|
| commit(input) | Accepts a Patch, single Event, or Event array. Normalizes and appends to history. Notifies subscribers. |
| state() | Returns current state by replaying normalized history. |
| history() | Returns the normalized event log. |
| undo(patch) | Reverts a previously committed patch using its base-state snapshot. |
| redo(patch) | Re-commits a patch. |
| subscribe(fn) | Registers a push callback. Immediately emits { type: "Snapshot" }. Returns unsubscribe. |
| compact() | Collapses entire history to Create-only events from current state. |
Law enforcement is pluggable — the law parameter can transform or reject patches before they are committed.
SchemaRegistry
Type-safe registry mapping entity ids to Zod schemas and SQL table metadata.
class SchemaRegistry {
register(entry: RootSchemaEntry): void;
get(id: string): RootSchemaEntry | undefined;
list(): RootSchemaEntry[];
}
interface RootSchemaEntry {
id: string;
schema: ZodTypeAny;
meta: TableMeta;
}
interface TableMeta {
table: string;
version: string;
columns: Record<string, ColumnMeta>;
}Used by adapters (e.g., @minamorl/root-adapters) for database projection.
Design Decisions
- Normalization is a rewrite system — history is always stored in normal form
- Law enforcement is pluggable — transform or reject patches at commit time
- Undo uses base-state snapshots stored in a WeakMap keyed by Patch instance
- Values are
unknown— deliberately untyped at the core level; Zod validation lives in SchemaRegistry - Push-based reactivity — subscribers receive individual events or Snapshot markers
Dependencies
zod— type-only import for SchemaRegistry
License
MIT
