feat: add epoch event stream to aggregate id

This commit is contained in:
2025-08-12 05:07:16 +02:00
parent d3b08b0caa
commit 393afd58d6
16 changed files with 34 additions and 72 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@valkyr/event-store",
"version": "2.0.0-beta.5",
"version": "2.0.0-beta.6",
"exports": {
".": "./mod.ts",
"./browser": "./adapters/browser/adapter.ts",

8
deno.lock generated
View File

@@ -10,7 +10,6 @@
"npm:eslint@9": "9.33.0",
"npm:fake-indexeddb@6": "6.1.0",
"npm:mongodb@6": "6.18.0",
"npm:nanoid@5": "5.1.5",
"npm:postgres@3": "3.4.7",
"npm:prettier@3": "3.6.2",
"npm:typescript-eslint@8": "8.39.0_eslint@9.33.0_typescript@5.9.2_@typescript-eslint+parser@8.39.0__eslint@9.33.0__typescript@5.9.2",
@@ -299,7 +298,7 @@
"fast-equals",
"idb",
"mingo",
"nanoid@5.0.2",
"nanoid",
"rfdc",
"rxjs"
]
@@ -694,10 +693,6 @@
"integrity": "sha512-2ustYUX1R2rL/Br5B/FMhi8d5/QzvkJ912rBYxskcpu0myTHzSZfTr1LAS2Sm7jxRUObRrSBFoyzwAhL49aVSg==",
"bin": true
},
"nanoid@5.1.5": {
"integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==",
"bin": true
},
"natural-compare@1.4.0": {
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
},
@@ -894,7 +889,6 @@
"npm:eslint@9",
"npm:fake-indexeddb@6",
"npm:mongodb@6",
"npm:nanoid@5",
"npm:postgres@3",
"npm:prettier@3",
"npm:typescript-eslint@8",

View File

@@ -1,6 +1,6 @@
import type { AnyEventStore, EventsInsertSettings } from "../libraries/event-store.ts";
import type { Unknown } from "../types/common.ts";
import { AggregateSnapshotViolation, AggregateStreamViolation } from "./errors.ts";
import { AggregateSnapshotViolation } from "./errors.ts";
import { EventFactory } from "./event-factory.ts";
/**
@@ -27,7 +27,7 @@ export abstract class AggregateRoot<TEventFactory extends EventFactory> {
/**
* Primary unique identifier for the stream the aggregate belongs to.
*/
#stream?: string;
id: string;
/**
* List of pending records to push to the parent event store.
@@ -40,6 +40,7 @@ export abstract class AggregateRoot<TEventFactory extends EventFactory> {
* @param store - Store this aggregate instance acts against.
*/
constructor(store: AnyEventStore) {
this.id = crypto.randomUUID();
this.#store = store;
}
@@ -47,20 +48,6 @@ export abstract class AggregateRoot<TEventFactory extends EventFactory> {
// Accessors
// -------------------------------------------------------------------------
set id(value: string) {
if (this.#stream !== undefined) {
throw new AggregateStreamViolation(this.constructor.name);
}
this.#stream = value;
}
get id(): string {
if (this.#stream === undefined) {
this.#stream = crypto.randomUUID();
}
return this.#stream;
}
get #self(): typeof AggregateRoot<TEventFactory> {
return this.constructor as typeof AggregateRoot<TEventFactory>;
}
@@ -147,7 +134,7 @@ export abstract class AggregateRoot<TEventFactory extends EventFactory> {
* any pending events, make sure to `.save()` before snapshotting.
*/
async snapshot() {
const stream = this.#stream;
const stream = this.id;
if (stream === undefined) {
throw new AggregateSnapshotViolation(this.#self.name);
}

View File

@@ -1,7 +1,6 @@
import z, { ZodType } from "zod";
import { EventValidationError } from "./errors.ts";
import { makeId } from "./nanoid.ts";
import { getLogicalTimestamp } from "./time.ts";
import { toPrettyErrorLines } from "./zod.ts";
@@ -41,8 +40,8 @@ export class Event<TEventState extends EventState = EventState> {
const timestamp = getLogicalTimestamp();
const record = {
id: makeId(),
stream: payload.stream ?? makeId(),
id: crypto.randomUUID(),
stream: payload.stream ?? crypto.randomUUID(),
type: this.state.type,
data: "data" in payload ? payload.data : null,
meta: "meta" in payload ? payload.meta : null,
@@ -134,13 +133,13 @@ export type EventRecord<TEvent extends EventState = EventState> = {
/**
* A unique event identifier.
*/
id: string;
id: UUID;
/**
* Event streams are used to group related events together. This identifier
* is used to identify the stream to which the event belongs.
*/
stream: string;
stream: UUID;
/**
* Type refers to the purpose of the event in a past tense descibing something
@@ -203,3 +202,5 @@ export type EventStatus = {
*/
outdated: boolean;
};
export type UUID = `${string}-${string}-${string}-${string}-${string}`;

View File

@@ -1,10 +0,0 @@
import { nanoid } from "nanoid";
/**
* Generate a new nanoid.
*
* @param size - Size of the id. Default: 11
*/
export function makeId(size: number = 11): string {
return nanoid(size);
}

View File

@@ -19,6 +19,7 @@ export function makeAggregateReducer<
},
reduce(events: TEventFactory["$events"][number]["$record"][], snapshot?: Unknown) {
const instance = aggregate.from(store, snapshot);
instance.id = events[0].stream;
for (const event of events) {
instance.with(event);
}

1
mod.ts
View File

@@ -3,7 +3,6 @@ export * from "./libraries/errors.ts";
export * from "./libraries/event.ts";
export * from "./libraries/event-factory.ts";
export * from "./libraries/event-store.ts";
export * from "./libraries/nanoid.ts";
export * from "./libraries/projector.ts";
export * from "./libraries/queue.ts";
export * from "./libraries/reducer.ts";

View File

@@ -2,7 +2,6 @@
"dependencies": {
"@valkyr/db": "1.0.1",
"mongodb": "6",
"nanoid": "5",
"postgres": "3",
"zod": "4"
},

View File

@@ -2,7 +2,6 @@ import { assertEquals, assertObjectMatch, assertRejects } from "@std/assert";
import { it } from "@std/testing/bdd";
import { EventInsertionError, EventValidationError } from "../../libraries/errors.ts";
import { makeId } from "../../libraries/nanoid.ts";
import type { Events } from "../mocks/events.ts";
import { describe } from "../utilities/describe.ts";
@@ -58,7 +57,7 @@ export default describe<Events>(".addEvent", (getEventStore) => {
it("should insert and project 'user:created' event", async () => {
const { store, projector } = await getEventStore();
const stream = makeId();
const stream = crypto.randomUUID();
const event = store.event({
stream,
type: "user:created",
@@ -93,7 +92,7 @@ export default describe<Events>(".addEvent", (getEventStore) => {
},
});
const stream = makeId();
const stream = crypto.randomUUID();
const event = store.event({
stream,
type: "user:created",
@@ -121,7 +120,7 @@ export default describe<Events>(".addEvent", (getEventStore) => {
it("should insert 'user:created' and add it to 'tenant:xyz' relation", async () => {
const { store, projector } = await getEventStore();
const key = `tenant:${makeId()}`;
const key = `tenant:${crypto.randomUUID()}`;
projector.on("user:created", async ({ stream }) => {
await store.relations.insert(key, stream);
@@ -171,7 +170,7 @@ export default describe<Events>(".addEvent", (getEventStore) => {
it("should insert 'user:email-set' and remove it from 'tenant:xyz' relations", async () => {
const { store, projector } = await getEventStore();
const key = `tenant:${makeId()}`;
const key = `tenant:${crypto.randomUUID()}`;
projector.on("user:created", async ({ stream }) => {
await store.relations.insert(key, stream);

View File

@@ -1,6 +1,5 @@
import { assertEquals, assertObjectMatch, assertRejects } from "@std/assert";
import { it } from "@std/testing/bdd";
import { nanoid } from "nanoid";
import { EventValidationError } from "../../mod.ts";
import type { Events } from "../mocks/events.ts";
@@ -10,7 +9,7 @@ import { describe } from "../utilities/describe.ts";
export default describe<Events>(".addSequence", (getEventStore) => {
it("should insert 'user:created', 'user:name:given-set', and 'user:email-set' in a sequence of events", async () => {
const { store } = await getEventStore();
const stream = nanoid();
const stream = crypto.randomUUID();
const events = [
store.event({
@@ -63,7 +62,7 @@ export default describe<Events>(".addSequence", (getEventStore) => {
it("should not commit any events when insert fails", async () => {
const { store } = await getEventStore();
const stream = nanoid();
const stream = crypto.randomUUID();
await assertRejects(
async () =>

View File

@@ -1,6 +1,5 @@
import { assertEquals, assertNotEquals, assertObjectMatch } from "@std/assert";
import { it } from "@std/testing/bdd";
import { nanoid } from "nanoid";
import type { Events } from "../mocks/events.ts";
import { userReducer } from "../mocks/user-reducer.ts";
@@ -9,7 +8,7 @@ import { describe } from "../utilities/describe.ts";
export default describe<Events>(".createSnapshot", (getEventStore) => {
it("should create a new snapshot", async () => {
const { store } = await getEventStore();
const stream = nanoid();
const stream = crypto.randomUUID();
await store.pushEvent(
store.event({

View File

@@ -1,8 +1,6 @@
import { assertEquals } from "@std/assert";
import { it } from "@std/testing/bdd";
import { nanoid } from "nanoid";
import { makeId } from "../../libraries/nanoid.ts";
import type { Events } from "../mocks/events.ts";
import { userPostReducer } from "../mocks/user-posts-reducer.ts";
import { userReducer } from "../mocks/user-reducer.ts";
@@ -12,8 +10,8 @@ export default describe<Events>(".makeReducer", (getEventStore) => {
it("should create a 'user' reducer and only reduce filtered events", async () => {
const { store } = await getEventStore();
const streamA = nanoid();
const streamB = nanoid();
const streamA = crypto.randomUUID();
const streamB = crypto.randomUUID();
await store.pushEvent(
store.event({
@@ -95,15 +93,15 @@ export default describe<Events>(".makeReducer", (getEventStore) => {
it("should create a 'post:count' reducer and retrieve post correct post count", async () => {
const { store, projector } = await getEventStore();
const auditor = nanoid();
const auditor = crypto.randomUUID();
projector.on("post:created", async ({ stream, meta: { auditor } }) => {
await store.relations.insert(`user:${auditor}:posts`, stream);
});
const post1 = makeId();
const post2 = makeId();
const post3 = makeId();
const post1 = crypto.randomUUID();
const post2 = crypto.randomUUID();
const post3 = crypto.randomUUID();
await store.pushEvent(
store.event({

View File

@@ -1,7 +1,6 @@
import { assertEquals, assertObjectMatch } from "@std/assert";
import { it } from "@std/testing/bdd";
import { makeId } from "../../libraries/nanoid.ts";
import type { Events } from "../mocks/events.ts";
import { describe } from "../utilities/describe.ts";
@@ -9,7 +8,7 @@ export default describe<Events>("projector.once", (getEventStore) => {
it("should handle successfull projection", async () => {
const { store, projector } = await getEventStore();
const stream = makeId();
const stream = crypto.randomUUID();
const event = store.event({
stream,
type: "user:created",
@@ -51,7 +50,7 @@ export default describe<Events>("projector.once", (getEventStore) => {
it("should handle failed projection", async () => {
const { store, projector } = await getEventStore();
const stream = makeId();
const stream = crypto.randomUUID();
const event = store.event({
stream,
type: "user:created",

View File

@@ -1,6 +1,5 @@
import { assertEquals } from "@std/assert";
import { it } from "@std/testing/bdd";
import { nanoid } from "nanoid";
import type { Events } from "../../mocks/events.ts";
import { describe } from "../../utilities/describe.ts";
@@ -10,7 +9,7 @@ export default describe<Events>("relations", (getEventStore) => {
const { store } = await getEventStore();
const key = "sample";
const stream = nanoid();
const stream = crypto.randomUUID();
await store.relations.insert(key, stream);
@@ -21,7 +20,7 @@ export default describe<Events>("relations", (getEventStore) => {
const { store } = await getEventStore();
const key = "sample";
const stream = nanoid();
const stream = crypto.randomUUID();
await store.relations.insertMany([
{ key, stream },

View File

@@ -1,6 +1,5 @@
import { assertEquals } from "@std/assert";
import { it } from "@std/testing/bdd";
import { nanoid } from "nanoid";
import type { Events } from "../mocks/events.ts";
import { userReducer } from "../mocks/user-reducer.ts";
@@ -9,7 +8,7 @@ import { describe } from "../utilities/describe.ts";
export default describe<Events>(".reduce", (getEventStore) => {
it("should return reduced state", async () => {
const { store } = await getEventStore();
const stream = nanoid();
const stream = crypto.randomUUID();
await store.pushEvent(
store.event({
@@ -51,7 +50,7 @@ export default describe<Events>(".reduce", (getEventStore) => {
it("should return snapshot if it exists and no new events were found", async () => {
const { store } = await getEventStore();
const stream = nanoid();
const stream = crypto.randomUUID();
await store.pushEvent(
store.event({
@@ -94,7 +93,7 @@ export default describe<Events>(".reduce", (getEventStore) => {
});
it("should return undefined if stream does not have events", async () => {
const stream = nanoid();
const stream = crypto.randomUUID();
const { store } = await getEventStore();
const state = await store.reduce({ name: "user", stream, reducer: userReducer });

View File

@@ -1,6 +1,5 @@
import { assertObjectMatch } from "@std/assert";
import { it } from "@std/testing/bdd";
import { nanoid } from "nanoid";
import type { Events } from "../mocks/events.ts";
import { describe } from "../utilities/describe.ts";
@@ -8,7 +7,7 @@ import { describe } from "../utilities/describe.ts";
export default describe<Events>(".replayEvents", (getEventStore) => {
it("should replay events", async () => {
const { store, projector } = await getEventStore();
const stream = nanoid();
const stream = crypto.randomUUID();
const record: Record<string, any> = {};