@eventualize/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,290 @@
1
+ import IEvDbStorageSnapshotAdapter from '@eventualize/types/IEvDbStorageSnapshotAdapter';
2
+ import IEvDbStorageStreamAdapter from '@eventualize/types/IEvDbStorageStreamAdapter';
3
+ import IEvDbEventPayload from "@eventualize/types/IEvDbEventPayload";
4
+ import EvDbStreamCursor from '@eventualize/types/EvDbStreamCursor';
5
+ import EvDbStreamAddress from '@eventualize/types/EvDbStreamAddress';
6
+ import EVDbMessagesProducer from '@eventualize/types/EvDbMessagesProducer';
7
+
8
+ import EvDbStream from './EvDbStream.js';
9
+ import { EvDbView } from './EvDbView.js';
10
+ import { ViewFactory, createViewFactory, EvDbStreamEventHandlersMap } from './EvDbViewFactory.js';
11
+ import EvDbEvent from '@eventualize/types/EvDbEvent.js';
12
+
13
+ /**
14
+ * Configuration for creating a stream factory
15
+ */
16
+ export interface EvDbStreamFactoryConfig<TEvents extends IEvDbEventPayload, TStreamType extends string> {
17
+ streamType: TStreamType;
18
+ viewFactories: ViewFactory<any, TEvents>[];
19
+ eventTypes: EventTypeConfig<TEvents>[];
20
+ viewNames: string[]; // Track view names for accessor creation
21
+ }
22
+
23
+ /**
24
+ * Configuration for each event type
25
+ */
26
+ export interface EventTypeConfig<
27
+ TEvent extends IEvDbEventPayload,
28
+ > {
29
+ eventClass: new (...args: any[]) => TEvent;
30
+ eventName: string;
31
+ eventMessagesProducer?: EVDbMessagesProducer;
32
+ }
33
+
34
+ /**
35
+ * Type helper to extract event methods
36
+ */
37
+ type EventMethods<TEvents extends IEvDbEventPayload> = {
38
+ [K in TEvents as `appendEvent${K['payloadType']}`]: (event: K) => Promise<void>;
39
+ };
40
+
41
+ /**
42
+ * Type helper to create view accessors map
43
+ */
44
+ type ViewAccessors<TViews extends Record<string, EvDbView<any>>> = {
45
+ readonly views: TViews;
46
+ };
47
+
48
+ /**
49
+ * Combined stream type with event methods and view accessors
50
+ */
51
+ export type StreamWithEventMethods<TEvents extends IEvDbEventPayload, TViews extends Record<string, EvDbView<any>> = {}> =
52
+ EvDbStream & EventMethods<TEvents> & ViewAccessors<TViews>;
53
+
54
+ /**
55
+ * Stream Factory - creates stream instances with configured views and dynamic event methods
56
+ */
57
+ export class EvDbStreamFactory<TEvents extends IEvDbEventPayload, TStreamType extends string, TViews extends Record<string, EvDbView<any>> = {}> {
58
+ private DynamicStreamClass: new (
59
+ streamType: string,
60
+ views: EvDbView<any>[],
61
+ streamStorageAdapter: IEvDbStorageStreamAdapter,
62
+ streamId: string,
63
+ lastStreamOffset: number,
64
+ ) => StreamWithEventMethods<TEvents, TViews>;
65
+
66
+ constructor(private readonly config: EvDbStreamFactoryConfig<TEvents, TStreamType>) {
67
+ this.DynamicStreamClass = this.createDynamicStreamClass();
68
+ }
69
+
70
+ /**
71
+ * Creates a dynamic stream class with event-specific methods and view accessors
72
+ */
73
+ private createDynamicStreamClass() {
74
+ const eventTypes = this.config.eventTypes;
75
+ const viewNames = this.config.viewNames;
76
+
77
+ const messagesProducer: EVDbMessagesProducer = (event: EvDbEvent, viewsState: Readonly<Record<string, unknown>>) => {
78
+ const eventType = eventTypes.find(e => e.eventName === event.eventType);
79
+ if (!eventType || !eventType.eventMessagesProducer) return [];
80
+ return eventType.eventMessagesProducer(event, viewsState);
81
+ }
82
+
83
+ class DynamicStream extends EvDbStream {
84
+ public readonly views: Record<string, EvDbView<any>> = {};
85
+
86
+ constructor(
87
+ streamType: string,
88
+ views: EvDbView<any>[],
89
+ streamStorageAdapter: IEvDbStorageStreamAdapter,
90
+ streamId: string,
91
+ lastStreamOffset: number,
92
+ ) {
93
+ super(streamType, views, streamStorageAdapter, streamId, lastStreamOffset, messagesProducer);
94
+
95
+ // Create view accessors
96
+ views.forEach((view, index) => {
97
+ const viewName = viewNames[index];
98
+ if (viewName) {
99
+ this.views[viewName] = view;
100
+ }
101
+ });
102
+ }
103
+ }
104
+
105
+ // Add dynamic methods for each event type
106
+ eventTypes.forEach(({ eventName }) => {
107
+ const methodName = `appendEvent${eventName}`;
108
+ (DynamicStream.prototype as any)[methodName] = async function (event: IEvDbEventPayload) {
109
+ return this.appendEvent(event);
110
+ };
111
+ });
112
+
113
+ return DynamicStream as any;
114
+ }
115
+
116
+ /**
117
+ * Creates a stream instance with all configured views and dynamic event methods
118
+ */
119
+ public create(
120
+ streamId: string,
121
+ streamStorageAdapter: IEvDbStorageStreamAdapter,
122
+ snapshotStorageAdapter: IEvDbStorageSnapshotAdapter,
123
+ lastStreamOffset: number = 0
124
+ ): StreamWithEventMethods<TEvents, TViews> {
125
+ const views = this.createViews(streamId, snapshotStorageAdapter);
126
+
127
+ return new this.DynamicStreamClass(
128
+ this.config.streamType,
129
+ views,
130
+ streamStorageAdapter,
131
+ streamId,
132
+ lastStreamOffset,
133
+ );
134
+ }
135
+
136
+ private createViews(streamId: string, snapshotStorageAdapter: IEvDbStorageSnapshotAdapter): Array<EvDbView<any>> {
137
+ const views = this.config.viewFactories.map(factory =>
138
+ factory.create(streamId, snapshotStorageAdapter)
139
+ );
140
+ return views;
141
+ }
142
+
143
+ private getViews(streamId: string, snapshotStorageAdapter: IEvDbStorageSnapshotAdapter): Promise<EvDbView<any>>[] {
144
+ const getViewPromises = this.config.viewFactories.map(viewFactory =>
145
+ viewFactory.get(streamId, snapshotStorageAdapter)
146
+ );
147
+ return getViewPromises;
148
+ }
149
+
150
+ /**
151
+ * Fetches from storage a stream instance with all configured views and dynamic event methods
152
+ */
153
+ public async get(
154
+ streamId: string,
155
+ streamStorageAdapter: IEvDbStorageStreamAdapter,
156
+ snapshotStorageAdapter: IEvDbStorageSnapshotAdapter
157
+ ): Promise<StreamWithEventMethods<TEvents, TViews>> {
158
+ const streamAddress = new EvDbStreamAddress(this.config.streamType, streamId);
159
+
160
+ const views = await Promise.all(this.getViews(streamId, snapshotStorageAdapter));
161
+
162
+ if (!views.length) {
163
+ const lastStreamOffset = await streamStorageAdapter.getLastOffsetAsync(streamAddress);
164
+ const stream = this.create(streamId, streamStorageAdapter, snapshotStorageAdapter, lastStreamOffset);
165
+ return stream;
166
+ }
167
+
168
+ const lowestViewOffset = views.reduce((lowestOffset: number, currentView: EvDbView<any>) =>
169
+ Math.min(lowestOffset, currentView.storeOffset),
170
+ Number.MAX_VALUE
171
+ );
172
+
173
+ const streamCursor = new EvDbStreamCursor(streamAddress, lowestViewOffset + 1);
174
+ const events = await streamStorageAdapter.getEventsAsync(streamCursor);
175
+
176
+ let streamOffset = lowestViewOffset;
177
+ for await (const event of events) {
178
+ views.forEach(view => view.applyEvent(event));
179
+ streamOffset = event.streamCursor.offset;
180
+ }
181
+
182
+ return new this.DynamicStreamClass(
183
+ this.config.streamType,
184
+ views,
185
+ streamStorageAdapter,
186
+ streamId,
187
+ streamOffset
188
+ );
189
+ }
190
+
191
+ public getStreamType(): TStreamType {
192
+ return this.config.streamType;
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Factory function to create a StreamFactory
198
+ */
199
+ export function createEvDbStreamFactory<TEvents extends IEvDbEventPayload, TStreamType extends string, TViews extends Record<string, EvDbView<any>> = {}>(
200
+ config: EvDbStreamFactoryConfig<TEvents, TStreamType>
201
+ ): EvDbStreamFactory<TEvents, TStreamType, TViews> {
202
+ return new EvDbStreamFactory(config);
203
+ }
204
+
205
+ /**
206
+ * Fluent builder for creating stream factories with inferred event types
207
+ */
208
+ export class StreamFactoryBuilder<
209
+ TStreamType extends string,
210
+ TEvents extends IEvDbEventPayload = never,
211
+ TViews extends Record<string, EvDbView<any>> = {}
212
+ > {
213
+ private viewFactories: ViewFactory<any, TEvents>[] = [];
214
+ private eventTypes: EventTypeConfig<any>[] = [];
215
+ private viewNames: string[] = [];
216
+
217
+ constructor(private streamType: TStreamType) { }
218
+
219
+ /**
220
+ * Register event type for dynamic method generation - infers the event name from class name
221
+ */
222
+ withEventType<TEvent extends IEvDbEventPayload>(
223
+ eventClass: new (...args: any[]) => TEvent,
224
+ eventMessagesProducer?: EVDbMessagesProducer): StreamFactoryBuilder<
225
+ TStreamType,
226
+ TEvents | TEvent,
227
+ TViews
228
+ > {
229
+ // Use the class name as the event name
230
+ const eventName = eventClass.name;
231
+
232
+ this.eventTypes.push({ eventClass, eventName, eventMessagesProducer } as EventTypeConfig<TEvent>);
233
+ return this as any;
234
+ }
235
+
236
+ /**
237
+ * Add a view with inline handler definition
238
+ * This can only be called AFTER withEventType calls to ensure type safety
239
+ */
240
+ public withView<TViewName extends string, TState>(
241
+ viewName: TViewName,
242
+ stateClass: new (...args: any[]) => TState,
243
+ handlers: EvDbStreamEventHandlersMap<TState, TEvents>
244
+ ): StreamFactoryBuilder<TStreamType, TEvents, TViews & Record<TViewName, EvDbView<TState>>> {
245
+ // Create default state instance
246
+ const defaultState = new stateClass();
247
+
248
+ // Create the view factory
249
+ const viewFactory = createViewFactory<TState, TEvents>({
250
+ viewName,
251
+ streamType: this.streamType,
252
+ defaultState,
253
+ handlers
254
+ });
255
+
256
+ this.viewFactories.push(viewFactory);
257
+ this.viewNames.push(viewName);
258
+ return this as any;
259
+ }
260
+
261
+ /**
262
+ * Add a pre-created view factory (legacy support)
263
+ */
264
+ public withViewFactory<TViewName extends string, TState>(
265
+ viewName: TViewName,
266
+ viewFactory: ViewFactory<TState, TEvents>
267
+ ): StreamFactoryBuilder<TStreamType, TEvents, TViews & Record<TViewName, EvDbView<TState>>> {
268
+ this.viewFactories.push(viewFactory);
269
+ this.viewNames.push(viewName);
270
+ return this as any;
271
+ }
272
+
273
+ /**
274
+ * Build the stream factory
275
+ */
276
+ public build() {
277
+ const factory = new EvDbStreamFactory({
278
+ streamType: this.streamType,
279
+ viewFactories: this.viewFactories,
280
+ eventTypes: this.eventTypes,
281
+ viewNames: this.viewNames
282
+ }) as EvDbStreamFactory<TEvents, TStreamType, TViews>;
283
+
284
+ // Return factory with type helper for stream type extraction
285
+ return Object.assign(factory, {
286
+ // This is a type-only property for extracting the stream type
287
+ StreamType: null as unknown as StreamWithEventMethods<TEvents, TViews>
288
+ });
289
+ }
290
+ }
@@ -0,0 +1,100 @@
1
+ import IEvDbViewStore, { IEvDbViewStoreGeneric } from "@eventualize/types/IEvDbViewStore";
2
+ import EvDbViewAddress from "@eventualize/types/EvDbViewAddress";
3
+ import IEvDbStorageSnapshotAdapter from "@eventualize/types/IEvDbStorageSnapshotAdapter";
4
+ import EvDbEvent from "@eventualize/types/src/EvDbEvent";
5
+ import { EvDbStoredSnapshotData } from "@eventualize/types/EvDbStoredSnapshotData";
6
+ import { EvDbStoredSnapshotResult } from "@eventualize/types/EvDbStoredSnapshotResult";
7
+ import IEvDbEventMetadata from "@eventualize/types/IEvDbEventMetadata";
8
+ import IEvDbEventPayload from "@eventualize/types/IEvDbEventPayload";
9
+
10
+
11
+ export abstract class EvDbViewRaw implements IEvDbViewStore {
12
+ private _memoryOffset: number;
13
+ private _storeOffset: number;
14
+ private _storedAt: Date;
15
+
16
+ protected constructor(
17
+ private readonly _storageAdapter: IEvDbStorageSnapshotAdapter,
18
+ public readonly address: EvDbViewAddress,
19
+ snapshot: EvDbStoredSnapshotResult<any>
20
+
21
+ ) {
22
+ const storeOffset = snapshot.offset ?? 0;
23
+ this._memoryOffset = storeOffset;
24
+ this._storeOffset = storeOffset;
25
+
26
+ this._storedAt = snapshot.storedAt ?? new Date();
27
+ }
28
+ public abstract getSnapshotData(): EvDbStoredSnapshotData;
29
+
30
+ get storedAt(): Date { return this._storedAt };
31
+ get storeOffset(): number { return this._storeOffset };
32
+ get memoryOffset(): number { return this._memoryOffset };
33
+
34
+ shouldStoreSnapshot(offsetGapFromLastSave: number, durationSinceLastSaveMs: number): boolean {
35
+ return true;
36
+ }
37
+ applyEvent(e: EvDbEvent): void {
38
+ const offset = e.streamCursor.offset;
39
+ if (this.memoryOffset >= offset) {
40
+ return;
41
+ }
42
+ this.onApplyEvent(e);
43
+ this._memoryOffset = offset;
44
+ }
45
+
46
+ async store(): Promise<void> {
47
+ const eventsSinceLatestSnapshot = this.memoryOffset - this.storeOffset;
48
+ const secondsSinceLatestSnapshot = new Date().getTime() - this.storedAt.getTime();
49
+ if (!this.shouldStoreSnapshot(eventsSinceLatestSnapshot, secondsSinceLatestSnapshot)) {
50
+ return;
51
+ }
52
+ const snapshotData = this.getSnapshotData();
53
+ await this._storageAdapter.storeSnapshotAsync(snapshotData);
54
+ this._storeOffset = this._memoryOffset;
55
+ }
56
+
57
+ protected abstract onApplyEvent(e: EvDbEvent): void;
58
+ }
59
+
60
+ export abstract class EvDbView<TState> extends EvDbViewRaw implements IEvDbViewStoreGeneric<TState> {
61
+ protected getDefaultState(): TState {
62
+ return this.defaultState;
63
+ };
64
+ protected _state: TState = this.getDefaultState();
65
+ get state(): TState { return this._state }
66
+ // public getState(): TState {
67
+ // return this._state;
68
+ // }
69
+
70
+ public constructor(
71
+ address: EvDbViewAddress,
72
+ storageAdapter: IEvDbStorageSnapshotAdapter,
73
+ snapshot: EvDbStoredSnapshotResult<TState>,
74
+ public readonly defaultState: TState
75
+ ) {
76
+ super(storageAdapter, address, snapshot);
77
+ if (snapshot.offset === 0)
78
+ this._state = this.getDefaultState();
79
+ else
80
+ this._state = snapshot.state;
81
+
82
+
83
+ }
84
+
85
+ getSnapshotData(): EvDbStoredSnapshotData {
86
+ return EvDbStoredSnapshotData.fromAddress(
87
+ this.address,
88
+ this.memoryOffset,
89
+ this.storeOffset,
90
+ this._state
91
+ );
92
+ }
93
+
94
+ public abstract handleOnApply(oldState: TState, event: IEvDbEventPayload, metadata: IEvDbEventMetadata): TState
95
+
96
+ protected onApplyEvent(e: EvDbEvent): void {
97
+ this._state = this.handleOnApply(this._state, e.payload, e);
98
+ }
99
+
100
+ }
@@ -0,0 +1,123 @@
1
+ import { EvDbView } from './EvDbView.js';
2
+ import IEvDbEventPayload from "@eventualize/types/IEvDbEventPayload";
3
+ import IEvDbEventMetadata from '@eventualize/types/IEvDbEventMetadata';
4
+ import IEvDbStorageSnapshotAdapter from '@eventualize/types/IEvDbStorageSnapshotAdapter';
5
+ import EvDbViewAddress from '@eventualize/types/EvDbViewAddress';
6
+ import EvDbStreamAddress from '@eventualize/types/EvDbStreamAddress';
7
+ import { EvDbStoredSnapshotResult } from '@eventualize/types/EvDbStoredSnapshotResult';
8
+
9
+ /**
10
+ * Handler function type for applying an event to state
11
+ */
12
+ export type EvDbViewEventHandler<TState, TEvent extends IEvDbEventPayload> = (
13
+ oldState: TState,
14
+ event: TEvent,
15
+ metadata: IEvDbEventMetadata
16
+ ) => TState;
17
+
18
+ /**
19
+ * Map of event handlers - one handler per event type in the union
20
+ * Key is the payloadType string, value is the handler function
21
+ */
22
+ export type EvDbStreamEventHandlersMap<TState, TEvents extends IEvDbEventPayload> = {
23
+ [K in TEvents['payloadType']]: EvDbViewEventHandler<
24
+ TState,
25
+ Extract<TEvents, { payloadType: K }>
26
+ >;
27
+ };
28
+ /**
29
+ * Configuration for creating a view
30
+ */
31
+ export interface ViewConfig<TState, TEvents extends IEvDbEventPayload> {
32
+ viewName: string;
33
+ streamType: string;
34
+ defaultState: TState;
35
+ handlers: Partial<EvDbStreamEventHandlersMap<TState, TEvents>>;
36
+ }
37
+
38
+ /**
39
+ * Generic View class that uses the handlers map
40
+ */
41
+ class GenericView<TState, TEvents extends IEvDbEventPayload> extends EvDbView<TState> {
42
+
43
+ constructor(
44
+ viewAddress: EvDbViewAddress,
45
+ storageAdapter: IEvDbStorageSnapshotAdapter,
46
+ snapshot: EvDbStoredSnapshotResult<TState>,
47
+ public readonly config: ViewConfig<TState, TEvents>
48
+ ) {
49
+ super(viewAddress, storageAdapter, snapshot, config.defaultState);
50
+ this.config = config;
51
+ }
52
+
53
+
54
+ /**
55
+ * Dynamically applies events based on the handlers map
56
+ */
57
+ public handleOnApply(oldState: TState, event: TEvents, metadata: IEvDbEventMetadata): TState {
58
+ const payloadType = event.payloadType as keyof typeof this.config.handlers;
59
+ const handler = this.config.handlers[payloadType];
60
+
61
+ if (!handler) {
62
+ // console.warn(`No handler found for event type: ${event.payloadType}`);
63
+ return oldState;
64
+ }
65
+
66
+ return handler(oldState, event as any, metadata);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * View Factory - creates view instances with the handlers map
72
+ */
73
+ export class ViewFactory<TState, TEvents extends IEvDbEventPayload> {
74
+ constructor(private readonly config: ViewConfig<TState, TEvents>) { }
75
+
76
+ /**
77
+ * Creates a view instance
78
+ */
79
+ public create(
80
+ streamId: string,
81
+ storageAdapter: IEvDbStorageSnapshotAdapter
82
+ ): EvDbView<TState> {
83
+ const streamAddress = new EvDbStreamAddress(this.config.streamType, streamId);
84
+ const viewAddress = new EvDbViewAddress(streamAddress, this.config.viewName);
85
+
86
+ return new GenericView<TState, TEvents>(
87
+ viewAddress,
88
+ storageAdapter,
89
+ EvDbStoredSnapshotResult.getEmptyState<TState>(),
90
+ this.config
91
+ );
92
+ }
93
+
94
+ /**
95
+ * Get a view instance from event store
96
+ */
97
+ public async get(
98
+ streamId: string,
99
+ storageAdapter: IEvDbStorageSnapshotAdapter
100
+ ): Promise<EvDbView<TState>> {
101
+ const streamAddress = new EvDbStreamAddress(this.config.streamType, streamId);
102
+ const viewAddress = new EvDbViewAddress(streamAddress, this.config.viewName);
103
+
104
+ const snapshot = await storageAdapter.getSnapshotAsync(viewAddress)
105
+
106
+ return new GenericView<TState, TEvents>(
107
+ viewAddress,
108
+ storageAdapter,
109
+ snapshot,
110
+ this.config
111
+ );
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Factory function to create a ViewFactory
117
+ */
118
+ export function createViewFactory<TState, TEvents extends IEvDbEventPayload>(
119
+ config: ViewConfig<TState, TEvents>
120
+ ): ViewFactory<TState, TEvents> {
121
+ return new ViewFactory(config);
122
+ }
123
+
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "moduleResolution": "node",
7
+ "module": "es2020",
8
+ "target": "es2020",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ },
12
+ "references": [
13
+ {
14
+ "path": "../types"
15
+ }
16
+ ],
17
+ "include": ["src/**/*"]
18
+ }