# Event Store Event store solution written in deno for use in TypeScript projects to manage and distribute events from a central repository to one or more distibuted services. ## Quick Start The following provides a quick introduction on how to get started. ### Event Store Once we have defined our configs and printed our events we create a new event store instance. Currently we have support for `sqlite`, `postgres`, and `valkyr/db` which all works the same way. So for this example we will use the `sqlite` store. - Browser _(TODO)_ - Mongo _(TODO)_ - [Postgres](./adapters/postgres) ### Reducers Event reducers takes a entity stream and reduces it to a wanted state. This is required when we want to perform write side business logic on the current state of our streams. Using read stores for this is not ideal as the read side data may not be up to date. ```ts import { makeReducer } from "@valkyr/event-store"; import type { EventRecord } from "./generated/events.ts"; const reducer = makeReducer< { name: string; email: string; }, EventRecord >( (state, event) => { switch (event.type) { case "user:created": { state.name = `${event.data.name.given} ${event.data.name.family}`; state.email = event.data.email; break; } case "user:email-set": { state.email = event.data.email; break; } } return state; }, "user", () => ({ name: "", email: "", }), ); ``` ### Aggreates Event aggregates takes a entity stream and reduces it to a wanted state. It works on the same conceptual grounds as the standard reducer but resolved states using an aggregate instead of folding onto a state object. The benefit of this is that we can create various helper methods on the aggregate that can help us navigate and query the aggregated state. ```ts import { AggregateRoot, makeAggregateReducer } from "@valkyr/event-store"; import type { EventRecord } from "./generated/events.ts"; import { eventStore } from "./event-store.ts"; export class User extends AggregateRoot { name!: Name; email!: string; // ------------------------------------------------------------------------- // Factories // ------------------------------------------------------------------------- static #reducer = makeAggregateReducer(User, "user"); static async getById(userId: string): Promise { return eventStore.reduce({ stream: userId, reducer: this.#reducer }); } // ------------------------------------------------------------------------- // Folder // ------------------------------------------------------------------------- with(event: EventRecord) { switch (event.type) { case "user:created": { this.name = event.data.name; this.email = event.data.email; break; } case "user:email-set": { this.email = event.data.email; break; } } } // ------------------------------------------------------------------------- // Utilities // ------------------------------------------------------------------------- fullName() { return `${this.name.given} ${this.name.family}`; } } type Name = { given: string; family: string; }; ``` ### Projectors Projectors serves as a bridge between the write side and read side of your application. Think of them as event handlers that listens for an event and creates new read side records by pushing that data to one or more data stores or apis which is queried by your users. A projector is registered for a specific event type, and can have multiple handlers. They also come with three different types of listeners, `once`, `on`, and `all`. ```ts import { projector } from "./event-store.ts"; projector.on("user:created", async (record) => { // do something with the event record ... }); ``` ### Hydration in Event Processing When handling events in a distributed system or during event replay operations, it is important to differentiate between **new events** and **rehydrated events**. - **New Events (`hydrate: false`)**: These events are being processed for the first time. They will trigger all projection handlers, including `.once()`, `.on()`, and `.all()`. - **Rehydrated Events (`hydrate: true`)**: These events are being replayed, either as part of a stream synchronization, system recovery, or reprocessing in a distributed environment. They **will not trigger** `.once()` handlers to avoid redundant side effects but will still be processed by `.on()` and `.all()` handlers where applicable. This mechanism ensures that critical one-time operations (such as sending emails or initiating external API calls) are **not repeated** unnecessarily while still allowing stateful projections to update their read models correctly. #### `.once("user:created", (event) => Promise)` This handler tells the projection that an event is only ever processed when the event is originating directly from the local event store. A useful pattern for when you want the event handler to submit data to a third party service such as sending an email or submitting third party orders. We disallow `hydrate` and `outdated` as these events represents events that has already been processed. #### `.on("user:created", (event) => Promise)` This method tells the projection to allow events directly from the event store as well as events coming through hydration via sync, manual or automatic stream rehydration operations. This is the default pattern used for most events. This is where you usually project the latest data to your read side models and data stores. We allow `hydrate` as they serve to keep the read side up to date with the latest events. We disallow `outdated` as we do not want the latest data to be overridden by outdated ones. NOTE! The nature of this pattern means that outdated events are never run by this projection. Make sure to handle `outdated` events if you have processing requirements that needs to know about every unknown events that has occurred in the event stream. #### `.all("user:created", (event) => Promise)` This method is a catch all for events that does not fall under the stricter definitions of once and on patterns. This is a good place to deal with data that does not depend on a strict order of events.