Refactor event interface to use VariantTag for event types

This commit is contained in:
Pablo Baleztena 2024-10-15 07:45:11 -03:00
parent 4ea00f515b
commit 38e23ba095
5 changed files with 42 additions and 29 deletions

View File

@ -7,7 +7,7 @@ export interface TaggedVariant<TTag extends string> {
export type VariantFromTag< export type VariantFromTag<
TVariant extends TaggedVariant<string>, TVariant extends TaggedVariant<string>,
TTag extends TVariant[typeof VariantTag], TTag extends TVariant[VariantTag],
> = Extract<TVariant, { [VariantTag]: TTag }>; > = Extract<TVariant, { [VariantTag]: TTag }>;
export namespace Variant { export namespace Variant {

View File

@ -1,7 +1,13 @@
import { AsyncResult, MaybePromise, PosixDate } from "@fabric/core"; import {
AsyncResult,
MaybePromise,
PosixDate,
VariantFromTag,
VariantTag,
} from "@fabric/core";
import { StoreQueryError } from "../errors/query-error.js"; import { StoreQueryError } from "../errors/query-error.js";
import { UUID } from "../types/uuid.js"; import { UUID } from "../types/uuid.js";
import { Event, EventFromKey } from "./event.js"; import { Event } from "./event.js";
import { StoredEvent } from "./stored-event.js"; import { StoredEvent } from "./stored-event.js";
export interface EventStore<TEvents extends Event> { export interface EventStore<TEvents extends Event> {
@ -16,9 +22,9 @@ export interface EventStore<TEvents extends Event> {
streamId: UUID, streamId: UUID,
): AsyncResult<StoredEvent<TEvents>[], StoreQueryError>; ): AsyncResult<StoredEvent<TEvents>[], StoreQueryError>;
subscribe<TEventKey extends TEvents["type"]>( subscribe<TEventKey extends TEvents[VariantTag]>(
events: TEventKey[], events: TEventKey[],
subscriber: EventSubscriber<EventFromKey<TEvents, TEventKey>>, subscriber: EventSubscriber<VariantFromTag<TEvents, TEventKey>>,
): void; ): void;
} }

View File

@ -1,11 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { VariantTag } from "@fabric/core";
import { UUID } from "../types/uuid.js"; import { UUID } from "../types/uuid.js";
/** /**
* An event is a tagged variant with a payload and a timestamp. * An event is a tagged variant with a payload and a timestamp.
*/ */
export interface Event<TTag extends string = string, TPayload = any> { export interface Event<TTag extends string = string, TPayload = any> {
readonly type: TTag; readonly [VariantTag]: TTag;
readonly id: UUID; readonly id: UUID;
readonly streamId: UUID; readonly streamId: UUID;
readonly payload: TPayload; readonly payload: TPayload;
@ -13,5 +14,5 @@ export interface Event<TTag extends string = string, TPayload = any> {
export type EventFromKey< export type EventFromKey<
TEvents extends Event, TEvents extends Event,
TKey extends TEvents["type"], TKey extends TEvents[VariantTag],
> = Extract<TEvents, { type: TKey }>; > = Extract<TEvents, { [VariantTag]: TKey }>;

View File

@ -26,7 +26,7 @@ describe("Event Store", () => {
const newUUID = UUIDGeneratorMock.generate(); const newUUID = UUIDGeneratorMock.generate();
const userCreated: UserCreated = { const userCreated: UserCreated = {
type: "UserCreated", _tag: "UserCreated",
id: newUUID, id: newUUID,
streamId: newUUID, streamId: newUUID,
payload: { name: "test" }, payload: { name: "test" },
@ -41,7 +41,7 @@ describe("Event Store", () => {
expect(events[0]).toEqual({ expect(events[0]).toEqual({
id: newUUID, id: newUUID,
streamId: newUUID, streamId: newUUID,
type: "UserCreated", _tag: "UserCreated",
version: BigInt(1), version: BigInt(1),
timestamp: expect.any(PosixDate), timestamp: expect.any(PosixDate),
payload: { name: "test" }, payload: { name: "test" },
@ -52,7 +52,7 @@ describe("Event Store", () => {
const newUUID = UUIDGeneratorMock.generate(); const newUUID = UUIDGeneratorMock.generate();
const userCreated: UserCreated = { const userCreated: UserCreated = {
type: "UserCreated", _tag: "UserCreated",
id: newUUID, id: newUUID,
streamId: newUUID, streamId: newUUID,
payload: { name: "test" }, payload: { name: "test" },
@ -68,7 +68,7 @@ describe("Event Store", () => {
expect(subscriber).toHaveBeenCalledWith({ expect(subscriber).toHaveBeenCalledWith({
id: newUUID, id: newUUID,
streamId: newUUID, streamId: newUUID,
type: "UserCreated", _tag: "UserCreated",
version: BigInt(1), version: BigInt(1),
timestamp: expect.any(PosixDate), timestamp: expect.any(PosixDate),
payload: { name: "test" }, payload: { name: "test" },

View File

@ -1,4 +1,10 @@
import { AsyncResult, MaybePromise, PosixDate, Run } from "@fabric/core"; import {
AsyncResult,
MaybePromise,
PosixDate,
Run,
VariantTag,
} from "@fabric/core";
import { import {
Event, Event,
EventFromKey, EventFromKey,
@ -19,7 +25,7 @@ export class SQLiteEventStore<TEvents extends Event>
private streamVersions = new Map<UUID, bigint>(); private streamVersions = new Map<UUID, bigint>();
private eventSubscribers = new Map< private eventSubscribers = new Map<
TEvents["type"], TEvents[VariantTag],
EventSubscriber<TEvents>[] EventSubscriber<TEvents>[]
>(); >();
@ -34,7 +40,7 @@ export class SQLiteEventStore<TEvents extends Event>
await this.db.run( await this.db.run(
`CREATE TABLE IF NOT EXISTS events ( `CREATE TABLE IF NOT EXISTS events (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
type TEXT NOT NULL, _tag TEXT NOT NULL,
streamId TEXT NOT NULL, streamId TEXT NOT NULL,
version INTEGER NOT NULL, version INTEGER NOT NULL,
timestamp NUMERIC NOT NULL, timestamp NUMERIC NOT NULL,
@ -57,13 +63,13 @@ export class SQLiteEventStore<TEvents extends Event>
{ {
$id: streamId, $id: streamId,
}, },
(event) => ({ (e) => ({
id: event.id, id: e.id,
streamId: event.streamId, streamId: e.streamId,
type: event.type, _tag: e._tag,
version: BigInt(event.version), version: BigInt(e.version),
timestamp: new PosixDate(event.timestamp), timestamp: new PosixDate(e.timestamp),
payload: JSONUtils.parse(event.payload), payload: JSONUtils.parse(e.payload),
}), }),
); );
return events; return events;
@ -95,7 +101,7 @@ export class SQLiteEventStore<TEvents extends Event>
event: StoredEvent<TEvents>, event: StoredEvent<TEvents>,
): AsyncResult<void> { ): AsyncResult<void> {
return AsyncResult.from(async () => { return AsyncResult.from(async () => {
const subscribers = this.eventSubscribers.get(event.type) || []; const subscribers = this.eventSubscribers.get(event[VariantTag]) || [];
await Promise.all(subscribers.map((subscriber) => subscriber(event))); await Promise.all(subscribers.map((subscriber) => subscriber(event)));
}); });
} }
@ -121,13 +127,13 @@ export class SQLiteEventStore<TEvents extends Event>
); );
} }
subscribe<TEventKey extends TEvents["type"]>( subscribe<TEventKey extends TEvents[VariantTag]>(
events: TEventKey[], eventNames: TEventKey[],
subscriber: ( subscriber: (
event: StoredEvent<EventFromKey<TEvents, TEventKey>>, event: StoredEvent<EventFromKey<TEvents, TEventKey>>,
) => MaybePromise<void>, ) => MaybePromise<void>,
): void { ): void {
events.forEach((event) => { eventNames.forEach((event) => {
const subscribers = this.eventSubscribers.get(event) || []; const subscribers = this.eventSubscribers.get(event) || [];
const newSubscribers = [ const newSubscribers = [
...subscribers, ...subscribers,
@ -157,12 +163,12 @@ export class SQLiteEventStore<TEvents extends Event>
timestamp: new PosixDate(), timestamp: new PosixDate(),
}; };
await this.db.runPrepared( await this.db.runPrepared(
`INSERT INTO events (id, streamId, type, version, timestamp, payload) `INSERT INTO events (id, streamId, _tag, version, timestamp, payload)
VALUES ($id, $streamId, $type, $version, $timestamp, $payload)`, VALUES ($id, $streamId, $_tag, $version, $timestamp, $payload)`,
{ {
$id: storedEvent.id, $id: storedEvent.id,
$streamId: streamId, $streamId: streamId,
$type: storedEvent.type, $_tag: storedEvent[VariantTag],
$version: storedEvent.version.toString(), $version: storedEvent.version.toString(),
$timestamp: storedEvent.timestamp.timestamp, $timestamp: storedEvent.timestamp.timestamp,
$payload: JSON.stringify(storedEvent.payload), $payload: JSON.stringify(storedEvent.payload),