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", "name": "@valkyr/event-store",
"version": "2.0.0-beta.5", "version": "2.0.0-beta.6",
"exports": { "exports": {
".": "./mod.ts", ".": "./mod.ts",
"./browser": "./adapters/browser/adapter.ts", "./browser": "./adapters/browser/adapter.ts",

8
deno.lock generated
View File

@@ -10,7 +10,6 @@
"npm:eslint@9": "9.33.0", "npm:eslint@9": "9.33.0",
"npm:fake-indexeddb@6": "6.1.0", "npm:fake-indexeddb@6": "6.1.0",
"npm:mongodb@6": "6.18.0", "npm:mongodb@6": "6.18.0",
"npm:nanoid@5": "5.1.5",
"npm:postgres@3": "3.4.7", "npm:postgres@3": "3.4.7",
"npm:prettier@3": "3.6.2", "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", "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", "fast-equals",
"idb", "idb",
"mingo", "mingo",
"nanoid@5.0.2", "nanoid",
"rfdc", "rfdc",
"rxjs" "rxjs"
] ]
@@ -694,10 +693,6 @@
"integrity": "sha512-2ustYUX1R2rL/Br5B/FMhi8d5/QzvkJ912rBYxskcpu0myTHzSZfTr1LAS2Sm7jxRUObRrSBFoyzwAhL49aVSg==", "integrity": "sha512-2ustYUX1R2rL/Br5B/FMhi8d5/QzvkJ912rBYxskcpu0myTHzSZfTr1LAS2Sm7jxRUObRrSBFoyzwAhL49aVSg==",
"bin": true "bin": true
}, },
"nanoid@5.1.5": {
"integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==",
"bin": true
},
"natural-compare@1.4.0": { "natural-compare@1.4.0": {
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
}, },
@@ -894,7 +889,6 @@
"npm:eslint@9", "npm:eslint@9",
"npm:fake-indexeddb@6", "npm:fake-indexeddb@6",
"npm:mongodb@6", "npm:mongodb@6",
"npm:nanoid@5",
"npm:postgres@3", "npm:postgres@3",
"npm:prettier@3", "npm:prettier@3",
"npm:typescript-eslint@8", "npm:typescript-eslint@8",

View File

@@ -1,6 +1,6 @@
import type { AnyEventStore, EventsInsertSettings } from "../libraries/event-store.ts"; import type { AnyEventStore, EventsInsertSettings } from "../libraries/event-store.ts";
import type { Unknown } from "../types/common.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"; 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. * 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. * 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. * @param store - Store this aggregate instance acts against.
*/ */
constructor(store: AnyEventStore) { constructor(store: AnyEventStore) {
this.id = crypto.randomUUID();
this.#store = store; this.#store = store;
} }
@@ -47,20 +48,6 @@ export abstract class AggregateRoot<TEventFactory extends EventFactory> {
// Accessors // 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> { get #self(): typeof AggregateRoot<TEventFactory> {
return this.constructor as 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. * any pending events, make sure to `.save()` before snapshotting.
*/ */
async snapshot() { async snapshot() {
const stream = this.#stream; const stream = this.id;
if (stream === undefined) { if (stream === undefined) {
throw new AggregateSnapshotViolation(this.#self.name); throw new AggregateSnapshotViolation(this.#self.name);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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