@requence/event-sourcing 0.0.1 → 1.0.1
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.
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/dist/chunk-24xjphpk.js +51 -0
- package/dist/chunk-24xjphpk.js.map +10 -0
- package/dist/chunk-3t1vzbab.js +88 -0
- package/dist/chunk-3t1vzbab.js.map +10 -0
- package/dist/chunk-6q8mr1sg.js +13 -0
- package/dist/chunk-6q8mr1sg.js.map +10 -0
- package/dist/chunk-9psg50cb.js +22 -0
- package/dist/chunk-9psg50cb.js.map +10 -0
- package/dist/chunk-dgzw1nj9.js +50 -0
- package/dist/chunk-dgzw1nj9.js.map +10 -0
- package/dist/chunk-gq80tgvf.js +70 -0
- package/dist/chunk-gq80tgvf.js.map +10 -0
- package/dist/chunk-jd4q8fcf.js +11 -0
- package/dist/chunk-jd4q8fcf.js.map +10 -0
- package/dist/chunk-m07vf8q9.js +210 -0
- package/dist/chunk-m07vf8q9.js.map +10 -0
- package/dist/chunk-ncpk70p4.js +313 -0
- package/dist/chunk-ncpk70p4.js.map +10 -0
- package/dist/chunk-pmfzjavb.js +49 -0
- package/dist/chunk-pmfzjavb.js.map +10 -0
- package/dist/chunk-qdczt68t.js +84 -0
- package/dist/chunk-qdczt68t.js.map +10 -0
- package/dist/chunk-tfy01fah.js +34 -0
- package/dist/chunk-tfy01fah.js.map +10 -0
- package/dist/chunk-tkvj6x8k.js +389 -0
- package/dist/chunk-tkvj6x8k.js.map +10 -0
- package/dist/chunk-ty9bhdvx.js +53 -0
- package/dist/chunk-ty9bhdvx.js.map +10 -0
- package/dist/chunk-wmz6a1jk.js +233 -0
- package/dist/chunk-wmz6a1jk.js.map +10 -0
- package/dist/chunk-x90g388y.js +22 -0
- package/dist/chunk-x90g388y.js.map +10 -0
- package/dist/chunk-z388cd25.js +91 -0
- package/dist/chunk-z388cd25.js.map +10 -0
- package/dist/chunk-zf3x573k.js +65 -0
- package/dist/chunk-zf3x573k.js.map +10 -0
- package/dist/createAggregateRoot.d.ts +191 -0
- package/dist/createAggregateRoot.d.ts.map +1 -0
- package/dist/createAggregateRoot.js +18 -0
- package/dist/createAggregateRoot.js.map +9 -0
- package/dist/createCheckpointApi.d.ts +38 -0
- package/dist/createCheckpointApi.d.ts.map +1 -0
- package/dist/createCheckpointApi.js +9 -0
- package/dist/createCheckpointApi.js.map +9 -0
- package/dist/createEventListener.d.ts +46 -0
- package/dist/createEventListener.d.ts.map +1 -0
- package/dist/createEventListener.js +12 -0
- package/dist/createEventListener.js.map +9 -0
- package/dist/createEventStore.d.ts +57 -0
- package/dist/createEventStore.d.ts.map +1 -0
- package/dist/createEventStore.js +20 -0
- package/dist/createEventStore.js.map +9 -0
- package/dist/createProcessManager.d.ts +70 -0
- package/dist/createProcessManager.d.ts.map +1 -0
- package/dist/createProcessManager.js +16 -0
- package/dist/createProcessManager.js.map +9 -0
- package/dist/createProjection.d.ts +120 -0
- package/dist/createProjection.d.ts.map +1 -0
- package/dist/createProjection.js +16 -0
- package/dist/createProjection.js.map +9 -0
- package/dist/drizzle/index.d.ts +449 -0
- package/dist/drizzle/index.d.ts.map +1 -0
- package/dist/drizzle/index.js +325 -0
- package/dist/drizzle/index.js.map +10 -0
- package/dist/drizzle/postgres.d.ts +686 -0
- package/dist/drizzle/postgres.d.ts.map +1 -0
- package/dist/drizzle/postgres.js +18 -0
- package/dist/drizzle/postgres.js.map +9 -0
- package/dist/errors.d.ts +53 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +26 -0
- package/dist/errors.js.map +9 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/index.js.map +9 -0
- package/dist/lock.d.ts +6 -0
- package/dist/lock.d.ts.map +1 -0
- package/dist/lock.js +8 -0
- package/dist/lock.js.map +9 -0
- package/dist/memory/index.d.ts +15 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +178 -0
- package/dist/memory/index.js.map +10 -0
- package/dist/redis/index.d.ts +2 -0
- package/dist/redis/index.d.ts.map +1 -0
- package/dist/redis/index.js +8 -0
- package/dist/redis/index.js.map +9 -0
- package/dist/redis/redisLock.d.ts +4 -0
- package/dist/redis/redisLock.d.ts.map +1 -0
- package/dist/redis/redisLock.js +8 -0
- package/dist/redis/redisLock.js.map +9 -0
- package/dist/refresh.d.ts +4 -0
- package/dist/refresh.d.ts.map +1 -0
- package/dist/refresh.js +13 -0
- package/dist/refresh.js.map +9 -0
- package/dist/replay.d.ts +4 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +13 -0
- package/dist/replay.js.map +9 -0
- package/dist/snapshotGenerator.d.ts +79 -0
- package/dist/snapshotGenerator.d.ts.map +1 -0
- package/dist/snapshotGenerator.js +9 -0
- package/dist/snapshotGenerator.js.map +9 -0
- package/dist/superjson.d.ts +5 -0
- package/dist/superjson.d.ts.map +1 -0
- package/dist/superjson.js +12 -0
- package/dist/superjson.js.map +9 -0
- package/dist/transaction.d.ts +13 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +8 -0
- package/dist/transaction.js.map +9 -0
- package/dist/utilityTypes.d.js +2 -0
- package/dist/utilityTypes.d.js.map +9 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +10 -0
- package/dist/utils.js.map +9 -0
- package/dist/wrappedEventBuilder.d.ts +37 -0
- package/dist/wrappedEventBuilder.d.ts.map +1 -0
- package/dist/wrappedEventBuilder.js +12 -0
- package/dist/wrappedEventBuilder.js.map +9 -0
- package/package.json +87 -1
- package/src/createAggregateRoot.ts +902 -0
- package/src/createCheckpointApi.ts +106 -0
- package/src/createEventListener.ts +118 -0
- package/src/createEventStore.ts +446 -0
- package/src/createProcessManager.ts +363 -0
- package/src/createProjection.ts +577 -0
- package/src/drizzle/index.ts +449 -0
- package/src/drizzle/postgres.ts +120 -0
- package/src/errors.ts +85 -0
- package/src/index.ts +45 -0
- package/src/lock.ts +58 -0
- package/src/memory/index.ts +224 -0
- package/src/redis/index.ts +1 -0
- package/src/redis/redisLock.ts +61 -0
- package/src/refresh.ts +19 -0
- package/src/replay.ts +19 -0
- package/src/snapshotGenerator.ts +190 -0
- package/src/superjson.ts +11 -0
- package/src/tests/aggregateRoot.spec.ts +532 -0
- package/src/tests/eventListener.spec.ts +57 -0
- package/src/tests/eventStore.spec.ts +86 -0
- package/src/tests/processManager.spec.ts +185 -0
- package/src/tests/projection.spec.ts +511 -0
- package/src/tests/projectionProcessManager.spec.ts +203 -0
- package/src/tests/setup.ts +230 -0
- package/src/tests/transaction.spec.ts +185 -0
- package/src/transaction.ts +93 -0
- package/src/utilityTypes.d.ts +110 -0
- package/src/utils.ts +43 -0
- package/src/wrappedEventBuilder.ts +181 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import { setTimeout } from 'node:timers/promises'
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from 'bun:test'
|
|
4
|
+
|
|
5
|
+
import { createAggregateRoot } from '../createAggregateRoot.ts'
|
|
6
|
+
import { setupEventStore } from './setup.ts'
|
|
7
|
+
import {
|
|
8
|
+
CommandError,
|
|
9
|
+
ConcurrencyError,
|
|
10
|
+
EventValidationError,
|
|
11
|
+
} from '../errors.ts'
|
|
12
|
+
import lock from '../lock.ts'
|
|
13
|
+
|
|
14
|
+
describe('AggregateRoot', () => {
|
|
15
|
+
it('adds an event', async () => {
|
|
16
|
+
const aggregateRoot = createAggregateRoot('my-aggregate')
|
|
17
|
+
.withInitialState({
|
|
18
|
+
id: null as null | string,
|
|
19
|
+
userName: null as null | string,
|
|
20
|
+
})
|
|
21
|
+
.withEvents(({ z }) => ({
|
|
22
|
+
UserCreated: z.object({ name: z.string() }),
|
|
23
|
+
UserUpdated: z.object({ newName: z.string() }),
|
|
24
|
+
}))
|
|
25
|
+
.withEventHandlers((state) => ({
|
|
26
|
+
onUserCreated(event) {
|
|
27
|
+
state.id = event.streamId
|
|
28
|
+
state.userName = event.payload.name
|
|
29
|
+
},
|
|
30
|
+
}))
|
|
31
|
+
.withCommands((state, event) => ({
|
|
32
|
+
register<T extends string>(name: T) {
|
|
33
|
+
if (state.id !== null) {
|
|
34
|
+
throw new Error('Cannot create already created user')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return event('UserCreated', { name })
|
|
38
|
+
},
|
|
39
|
+
}))
|
|
40
|
+
|
|
41
|
+
const { pseudoEventStore } = setupEventStore(aggregateRoot)
|
|
42
|
+
const root = await aggregateRoot.newStream().register('Torsten').settled()
|
|
43
|
+
expect(pseudoEventStore).toHaveLength(1)
|
|
44
|
+
expect(await root.state()).toMatchObject({
|
|
45
|
+
userName: 'Torsten',
|
|
46
|
+
})
|
|
47
|
+
const { streamId } = pseudoEventStore[0]
|
|
48
|
+
const root2State = await aggregateRoot.loadStream(streamId).state()
|
|
49
|
+
expect(root2State).toMatchObject({
|
|
50
|
+
userName: 'Torsten',
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
expect(
|
|
54
|
+
aggregateRoot.loadStream(streamId).register('Other').settled(),
|
|
55
|
+
).rejects.toThrow(/Cannot create already created user/)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('allows commands to execute each other', async () => {
|
|
59
|
+
let subCommandCalled = false
|
|
60
|
+
|
|
61
|
+
const aggregateRoot = createAggregateRoot('test')
|
|
62
|
+
.withEvents(({ z }) => ({
|
|
63
|
+
Sub: z.null(),
|
|
64
|
+
}))
|
|
65
|
+
.withCommands((event) => ({
|
|
66
|
+
async sub() {
|
|
67
|
+
subCommandCalled = true
|
|
68
|
+
await setTimeout(20)
|
|
69
|
+
return event('Sub', null)
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
parent() {
|
|
73
|
+
return this.sub()
|
|
74
|
+
},
|
|
75
|
+
}))
|
|
76
|
+
|
|
77
|
+
const { eventStore } = setupEventStore(aggregateRoot)
|
|
78
|
+
|
|
79
|
+
let subEventHandlerCalled = false
|
|
80
|
+
eventStore.createEventListener('test').withEventHandlers({
|
|
81
|
+
onSub() {
|
|
82
|
+
subEventHandlerCalled = true
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
await aggregateRoot.newStream().parent().settled()
|
|
87
|
+
expect(subCommandCalled).toBeTrue()
|
|
88
|
+
expect(subEventHandlerCalled).toBeTrue()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('allows generator commands to yield* into other generator commands', async () => {
|
|
92
|
+
const aggregateRoot = createAggregateRoot('test')
|
|
93
|
+
.withInitialState({
|
|
94
|
+
a: false,
|
|
95
|
+
b: false,
|
|
96
|
+
})
|
|
97
|
+
.withEvents(({ z }) => ({
|
|
98
|
+
EventA: z.null(),
|
|
99
|
+
EventB: z.null(),
|
|
100
|
+
}))
|
|
101
|
+
.withEventHandlers((state) => ({
|
|
102
|
+
onEventA() {
|
|
103
|
+
state.a = true
|
|
104
|
+
},
|
|
105
|
+
onEventB() {
|
|
106
|
+
state.b = true
|
|
107
|
+
},
|
|
108
|
+
}))
|
|
109
|
+
.withCommands((_state, event) => ({
|
|
110
|
+
async *sub() {
|
|
111
|
+
await setTimeout(20)
|
|
112
|
+
yield event('EventA', null)
|
|
113
|
+
yield event('EventB', null)
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
async *parent() {
|
|
117
|
+
yield* this.sub() as any
|
|
118
|
+
},
|
|
119
|
+
}))
|
|
120
|
+
|
|
121
|
+
setupEventStore(aggregateRoot)
|
|
122
|
+
|
|
123
|
+
const root = await aggregateRoot.newStream().parent().settled()
|
|
124
|
+
const state = await root.state()
|
|
125
|
+
expect(state).toEqual({ a: true, b: true })
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('makes snapshots', async () => {
|
|
129
|
+
const aggregateRoot = createAggregateRoot('my-aggregate')
|
|
130
|
+
.withInitialState({
|
|
131
|
+
userName: null as null | string,
|
|
132
|
+
})
|
|
133
|
+
.withEvents(({ z }) => ({
|
|
134
|
+
UserCreated: z.object({
|
|
135
|
+
name: z.string(),
|
|
136
|
+
}),
|
|
137
|
+
UserUpdated: z.object({ newName: z.string() }),
|
|
138
|
+
}))
|
|
139
|
+
.withEventHandlers((state) => ({
|
|
140
|
+
onUserCreated: (event) => {
|
|
141
|
+
state.userName = event.payload.name
|
|
142
|
+
},
|
|
143
|
+
onUserUpdated: (event) => {
|
|
144
|
+
state.userName = event.payload.newName
|
|
145
|
+
},
|
|
146
|
+
}))
|
|
147
|
+
.withSnapshots((state, generate, { z }) =>
|
|
148
|
+
generate
|
|
149
|
+
.every(2)
|
|
150
|
+
.schema(
|
|
151
|
+
z.object({
|
|
152
|
+
currentName: z.string().nullable(),
|
|
153
|
+
}),
|
|
154
|
+
)
|
|
155
|
+
.capture(() => ({
|
|
156
|
+
currentName: state.userName,
|
|
157
|
+
}))
|
|
158
|
+
.apply((snapshot) => {
|
|
159
|
+
state.userName = snapshot.state.currentName
|
|
160
|
+
}),
|
|
161
|
+
)
|
|
162
|
+
.withCommands((_state, event) => ({
|
|
163
|
+
register(name: string) {
|
|
164
|
+
return event('UserCreated', {
|
|
165
|
+
name,
|
|
166
|
+
})
|
|
167
|
+
},
|
|
168
|
+
update(newName: string) {
|
|
169
|
+
return event('UserUpdated', {
|
|
170
|
+
newName,
|
|
171
|
+
})
|
|
172
|
+
},
|
|
173
|
+
}))
|
|
174
|
+
|
|
175
|
+
const { pseudoAggregateRootSnapshotStore, eventStore } =
|
|
176
|
+
setupEventStore(aggregateRoot)
|
|
177
|
+
|
|
178
|
+
eventStore.getAggregateRoot('my-aggregate')
|
|
179
|
+
|
|
180
|
+
const root = aggregateRoot
|
|
181
|
+
.newStream()
|
|
182
|
+
.register('Name1')
|
|
183
|
+
.update('Name2')
|
|
184
|
+
.update('Name3')
|
|
185
|
+
|
|
186
|
+
expect(await root.state()).toEqual({ userName: 'Name3' })
|
|
187
|
+
await root.settled()
|
|
188
|
+
expect(pseudoAggregateRootSnapshotStore.size).toBe(1)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('can yield multiple events', async () => {
|
|
192
|
+
const usersAggregate = createAggregateRoot('users')
|
|
193
|
+
.withInitialState<{ name?: string; age?: number }>({})
|
|
194
|
+
.withEvents(({ z }) => ({
|
|
195
|
+
UserCreated: z.object({
|
|
196
|
+
name: z.string(),
|
|
197
|
+
}),
|
|
198
|
+
UserNameUpdated: z.object({
|
|
199
|
+
name: z.string(),
|
|
200
|
+
}),
|
|
201
|
+
UserAgeUpdated: z.object({
|
|
202
|
+
age: z.int(),
|
|
203
|
+
}),
|
|
204
|
+
}))
|
|
205
|
+
.withEventHandlers((state) => ({
|
|
206
|
+
onUserCreated: (event) => {
|
|
207
|
+
state.name = event.payload.name
|
|
208
|
+
},
|
|
209
|
+
onUserAgeUpdated: (event) => {
|
|
210
|
+
state.age = event.payload.age
|
|
211
|
+
},
|
|
212
|
+
onUserNameUpdated: (event) => {
|
|
213
|
+
state.name = event.payload.name
|
|
214
|
+
},
|
|
215
|
+
}))
|
|
216
|
+
.withCommands((_state, event) => ({
|
|
217
|
+
create(name: string) {
|
|
218
|
+
return event('UserCreated', { name })
|
|
219
|
+
},
|
|
220
|
+
*update(name: string, age: number) {
|
|
221
|
+
yield event('UserNameUpdated', { name })
|
|
222
|
+
yield event('UserAgeUpdated', { age })
|
|
223
|
+
},
|
|
224
|
+
}))
|
|
225
|
+
const { pseudoEventStore } = setupEventStore(usersAggregate)
|
|
226
|
+
const myAggregate = await usersAggregate
|
|
227
|
+
.newStream()
|
|
228
|
+
.create('Torsten')
|
|
229
|
+
.update('Tester', 10)
|
|
230
|
+
.settled()
|
|
231
|
+
|
|
232
|
+
expect(pseudoEventStore).toHaveLength(3)
|
|
233
|
+
expect(await myAggregate.state()).toEqual({
|
|
234
|
+
name: 'Tester',
|
|
235
|
+
age: 10,
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('handles event validation errors', async () => {
|
|
240
|
+
let errorHandlerError: EventValidationError | undefined
|
|
241
|
+
const aggregateRoot = createAggregateRoot('test')
|
|
242
|
+
.withEvents(({ z }) => ({
|
|
243
|
+
Create: z.string().min(10),
|
|
244
|
+
}))
|
|
245
|
+
.withCommands((event) => ({
|
|
246
|
+
create() {
|
|
247
|
+
return event('Create', 'hello').onValidationError(async (error) => {
|
|
248
|
+
await setTimeout(100)
|
|
249
|
+
errorHandlerError = error
|
|
250
|
+
})
|
|
251
|
+
},
|
|
252
|
+
}))
|
|
253
|
+
|
|
254
|
+
setupEventStore(aggregateRoot)
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
await aggregateRoot.newStream().create().settled()
|
|
258
|
+
expect.unreachable()
|
|
259
|
+
} catch (error) {
|
|
260
|
+
expect(error).toBeInstanceOf(CommandError)
|
|
261
|
+
expect((error as CommandError<any>).originalError).toBeInstanceOf(
|
|
262
|
+
EventValidationError,
|
|
263
|
+
)
|
|
264
|
+
expect(errorHandlerError).toEqual(
|
|
265
|
+
(error as CommandError<any>).originalError,
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('throws on state when error in command', async () => {
|
|
271
|
+
const aggregateRoot = createAggregateRoot('user')
|
|
272
|
+
.withEvents(({ z }) => ({
|
|
273
|
+
CreateUser: z.null(),
|
|
274
|
+
}))
|
|
275
|
+
.withCommands((event) => ({
|
|
276
|
+
create() {
|
|
277
|
+
return event('CreateUser', null)
|
|
278
|
+
},
|
|
279
|
+
update() {
|
|
280
|
+
throw new Error('stop')
|
|
281
|
+
},
|
|
282
|
+
}))
|
|
283
|
+
|
|
284
|
+
setupEventStore(aggregateRoot)
|
|
285
|
+
|
|
286
|
+
expect(aggregateRoot.newStream().create().update().state()).rejects.toThrow(
|
|
287
|
+
'stop',
|
|
288
|
+
)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('updates state through chained commands', async () => {
|
|
292
|
+
const aggregateRoot = createAggregateRoot('user')
|
|
293
|
+
.withInitialState({
|
|
294
|
+
alive: false,
|
|
295
|
+
})
|
|
296
|
+
.withEvents(({ z }) => ({
|
|
297
|
+
CreateUser: z.null(),
|
|
298
|
+
KillUser: z.null(),
|
|
299
|
+
ReviveUser: z.null(),
|
|
300
|
+
}))
|
|
301
|
+
.withEventHandlers((state) => ({
|
|
302
|
+
onCreateUser() {
|
|
303
|
+
state.alive = true
|
|
304
|
+
},
|
|
305
|
+
onKillUser() {
|
|
306
|
+
state.alive = false
|
|
307
|
+
},
|
|
308
|
+
onReviveUser() {
|
|
309
|
+
state.alive = true
|
|
310
|
+
},
|
|
311
|
+
}))
|
|
312
|
+
.withCommands((state, event) => ({
|
|
313
|
+
create() {
|
|
314
|
+
if (state.alive) {
|
|
315
|
+
throw new Error('Already alive')
|
|
316
|
+
}
|
|
317
|
+
return event('CreateUser', null)
|
|
318
|
+
},
|
|
319
|
+
kill() {
|
|
320
|
+
if (!state.alive) {
|
|
321
|
+
throw new Error('Already dead')
|
|
322
|
+
}
|
|
323
|
+
return event('KillUser', null)
|
|
324
|
+
},
|
|
325
|
+
revive() {
|
|
326
|
+
if (state.alive) {
|
|
327
|
+
throw new Error('Not killed')
|
|
328
|
+
}
|
|
329
|
+
return event('ReviveUser', null)
|
|
330
|
+
},
|
|
331
|
+
}))
|
|
332
|
+
|
|
333
|
+
const { pseudoEventStore } = setupEventStore(aggregateRoot)
|
|
334
|
+
|
|
335
|
+
await aggregateRoot.newStream().create().kill().revive().settled()
|
|
336
|
+
expect(pseudoEventStore.length).toBe(3)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('resolves state before side effects', async () => {
|
|
340
|
+
const aggregateRoot = createAggregateRoot('test')
|
|
341
|
+
.withInitialState({
|
|
342
|
+
created: false,
|
|
343
|
+
})
|
|
344
|
+
.withEvents(({ z }) => ({
|
|
345
|
+
Create: z.null(),
|
|
346
|
+
}))
|
|
347
|
+
.withEventHandlers((state) => ({
|
|
348
|
+
onCreate() {
|
|
349
|
+
state.created = true
|
|
350
|
+
},
|
|
351
|
+
}))
|
|
352
|
+
.withCommands((_, event) => ({
|
|
353
|
+
create() {
|
|
354
|
+
return event('Create', null)
|
|
355
|
+
},
|
|
356
|
+
}))
|
|
357
|
+
|
|
358
|
+
const { eventStore } = setupEventStore(aggregateRoot)
|
|
359
|
+
let eventHandlerCalled = false
|
|
360
|
+
eventStore.createEventListener('test').withGlobalEventHandler(() => {
|
|
361
|
+
eventHandlerCalled = true
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
const root = aggregateRoot.newStream().create()
|
|
365
|
+
const state = await root.state()
|
|
366
|
+
expect(state.created).toBeTrue()
|
|
367
|
+
expect(eventHandlerCalled).toBeFalse()
|
|
368
|
+
await root.settled()
|
|
369
|
+
expect(eventHandlerCalled).toBeTrue()
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
it('appends events immediately', async () => {
|
|
373
|
+
expect.assertions(1)
|
|
374
|
+
const aggregateRoot = createAggregateRoot('test')
|
|
375
|
+
.withEvents(({ z }) => ({
|
|
376
|
+
Create: z.null(),
|
|
377
|
+
Update: z.null(),
|
|
378
|
+
}))
|
|
379
|
+
.withCommands((event) => ({
|
|
380
|
+
create() {
|
|
381
|
+
return event('Create', null)
|
|
382
|
+
},
|
|
383
|
+
update() {
|
|
384
|
+
return event('Update', null)
|
|
385
|
+
},
|
|
386
|
+
}))
|
|
387
|
+
|
|
388
|
+
const { eventStore, pseudoEventStore } = setupEventStore(aggregateRoot)
|
|
389
|
+
|
|
390
|
+
eventStore.createProcessManager('a').withEventHandlers({
|
|
391
|
+
onCreate(event) {
|
|
392
|
+
aggregateRoot.loadStream(event.streamId).update()
|
|
393
|
+
},
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
eventStore.createProcessManager('b').withEventHandlers({
|
|
397
|
+
onUpdate() {
|
|
398
|
+
expect(pseudoEventStore).toHaveLength(2)
|
|
399
|
+
},
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
await aggregateRoot.newStream().create().settled()
|
|
403
|
+
})
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
describe('hotAggregateRoot', () => {
|
|
407
|
+
it('resolves race conditions internally when executing commands', async () => {
|
|
408
|
+
const aggregateRoot = createAggregateRoot('slow')
|
|
409
|
+
.withEvents(({ z }) => ({
|
|
410
|
+
Created: z.null(),
|
|
411
|
+
}))
|
|
412
|
+
.withCommands((event) => ({
|
|
413
|
+
create: () => event('Created'),
|
|
414
|
+
}))
|
|
415
|
+
|
|
416
|
+
const { eventStore } = setupEventStore(aggregateRoot)
|
|
417
|
+
|
|
418
|
+
let called = 0
|
|
419
|
+
eventStore.createEventListener('test').withEventHandlers({
|
|
420
|
+
onCreated() {
|
|
421
|
+
called += 1
|
|
422
|
+
},
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
const rootA = aggregateRoot.loadStream('A').create()
|
|
426
|
+
const rootB = aggregateRoot.loadStream('A').create()
|
|
427
|
+
|
|
428
|
+
await Promise.all([rootA.settled(), rootB.settled()])
|
|
429
|
+
expect(called).toBe(2)
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
it('resolves race conditions internally when accessing state', async () => {
|
|
433
|
+
const aggregateRoot = createAggregateRoot('slow')
|
|
434
|
+
.withInitialState({
|
|
435
|
+
name: null as string | null,
|
|
436
|
+
})
|
|
437
|
+
.withEvents(({ z }) => ({
|
|
438
|
+
Created: z.string(),
|
|
439
|
+
}))
|
|
440
|
+
.withEventHandlers((state) => ({
|
|
441
|
+
onCreated(event) {
|
|
442
|
+
state.name = event.payload
|
|
443
|
+
},
|
|
444
|
+
}))
|
|
445
|
+
.withCommands((_state, event) => ({
|
|
446
|
+
create: (name: string) => event('Created', name),
|
|
447
|
+
}))
|
|
448
|
+
|
|
449
|
+
setupEventStore(aggregateRoot)
|
|
450
|
+
|
|
451
|
+
await aggregateRoot.loadStream('A').create('Torsten').settled()
|
|
452
|
+
|
|
453
|
+
// if a state call would be blocking, the test would fail due to timeout of 100ms
|
|
454
|
+
await aggregateRoot.loadStream('A').state()
|
|
455
|
+
await aggregateRoot.loadStream('A').state()
|
|
456
|
+
}, 100)
|
|
457
|
+
|
|
458
|
+
it('throws on slow execution of commands', async () => {
|
|
459
|
+
const slowAggregateRoot = createAggregateRoot('slow')
|
|
460
|
+
.withEvents(({ z }) => ({
|
|
461
|
+
Created: z.string(),
|
|
462
|
+
}))
|
|
463
|
+
.withCommands((event) => ({
|
|
464
|
+
async create(delay: number, name: string) {
|
|
465
|
+
await setTimeout(delay)
|
|
466
|
+
return event('Created', name)
|
|
467
|
+
},
|
|
468
|
+
}))
|
|
469
|
+
|
|
470
|
+
const { eventStore } = setupEventStore(slowAggregateRoot, {
|
|
471
|
+
lock: lock(40), // 40ms
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
let called = 0
|
|
475
|
+
let name: string | undefined
|
|
476
|
+
eventStore.createEventListener('test').withEventHandlers({
|
|
477
|
+
onCreated(event) {
|
|
478
|
+
called += 1
|
|
479
|
+
name = event.payload
|
|
480
|
+
},
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
expect(
|
|
484
|
+
Promise.all([
|
|
485
|
+
// locks stream:a, lock auto releases after 40ms while still in create command
|
|
486
|
+
slowAggregateRoot.loadStream('a').create(100, 'A').settled(),
|
|
487
|
+
// waits for lock then emits event
|
|
488
|
+
slowAggregateRoot.loadStream('a').create(5, 'B').settled(),
|
|
489
|
+
]),
|
|
490
|
+
).rejects.toThrow(ConcurrencyError)
|
|
491
|
+
|
|
492
|
+
expect(called).toBe(1)
|
|
493
|
+
expect(name).toBe('B') // 'A' never made it
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
it('auto extends locks', async () => {
|
|
497
|
+
const aggregateRoot = createAggregateRoot('slow')
|
|
498
|
+
.withEvents(({ z }) => ({
|
|
499
|
+
Ping: z.null(),
|
|
500
|
+
}))
|
|
501
|
+
.withCommands((event) => ({
|
|
502
|
+
async *ping() {
|
|
503
|
+
// this delays total execution 10*5 = 50ms
|
|
504
|
+
for (let i = 0; i < 10; i++) {
|
|
505
|
+
await setTimeout(5)
|
|
506
|
+
// yielding event will extend the lock to 40 / 2 = 20ms
|
|
507
|
+
yield event('Ping')
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
}))
|
|
511
|
+
|
|
512
|
+
const { eventStore } = setupEventStore(aggregateRoot, {
|
|
513
|
+
lock: lock(40), // 40ms < 50ms
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
let called = 0
|
|
517
|
+
eventStore.createEventListener('test').withEventHandlers({
|
|
518
|
+
onPing() {
|
|
519
|
+
called += 1
|
|
520
|
+
},
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
expect(
|
|
524
|
+
Promise.all([
|
|
525
|
+
aggregateRoot.loadStream('a').ping().settled(),
|
|
526
|
+
//aggregateRoot.loadStream('a').ping().settled(),
|
|
527
|
+
]),
|
|
528
|
+
).resolves.toHaveLength(1)
|
|
529
|
+
|
|
530
|
+
expect(called).toBe(10)
|
|
531
|
+
})
|
|
532
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { createUserAggregate, setupEventStore } from './setup.ts'
|
|
4
|
+
|
|
5
|
+
describe('EventListeners', () => {
|
|
6
|
+
it('listens to events', async () => {
|
|
7
|
+
const usersAggregate = createUserAggregate()
|
|
8
|
+
const { eventStore } = setupEventStore(usersAggregate)
|
|
9
|
+
|
|
10
|
+
const orderOfOperations: string[] = []
|
|
11
|
+
eventStore
|
|
12
|
+
.createEventListener('listener')
|
|
13
|
+
.withEventHandlers({
|
|
14
|
+
onUserCreated(event) {
|
|
15
|
+
orderOfOperations.push(event.type)
|
|
16
|
+
},
|
|
17
|
+
onUserUpdated(event) {
|
|
18
|
+
orderOfOperations.push(event.type)
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
.withAfterEffects({
|
|
22
|
+
afterUserCreated(event) {
|
|
23
|
+
orderOfOperations.push('After' + event.type)
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
await usersAggregate.newStream().create('Torsten').update(40).settled()
|
|
28
|
+
expect(orderOfOperations).toEqual([
|
|
29
|
+
'UserCreated',
|
|
30
|
+
'UserUpdated',
|
|
31
|
+
'AfterUserCreated',
|
|
32
|
+
])
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('listens to global events', async () => {
|
|
36
|
+
const usersAggregate = createUserAggregate()
|
|
37
|
+
const { eventStore } = setupEventStore(usersAggregate)
|
|
38
|
+
|
|
39
|
+
const orderOfOperations: string[] = []
|
|
40
|
+
eventStore
|
|
41
|
+
.createEventListener('listener')
|
|
42
|
+
.withGlobalEventHandler((event) => {
|
|
43
|
+
orderOfOperations.push(event.type)
|
|
44
|
+
})
|
|
45
|
+
.withGlobalAfterEffect((event) => {
|
|
46
|
+
orderOfOperations.push(`After${event.type}`)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
await usersAggregate.newStream().create('Torsten').update(40).settled()
|
|
50
|
+
expect(orderOfOperations).toEqual([
|
|
51
|
+
'UserCreated',
|
|
52
|
+
'UserUpdated',
|
|
53
|
+
'AfterUserCreated',
|
|
54
|
+
'AfterUserUpdated',
|
|
55
|
+
])
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { createUserAggregate, setupEventStore } from './setup.ts'
|
|
4
|
+
|
|
5
|
+
describe('Event Store', () => {
|
|
6
|
+
it('can rebuild all', async () => {
|
|
7
|
+
const usersAggregate = createUserAggregate()
|
|
8
|
+
|
|
9
|
+
let rebuilding = false
|
|
10
|
+
const { eventStore, pseudoEventStore } = setupEventStore(usersAggregate)
|
|
11
|
+
const users = new Map<string, { name: string; age?: number }>()
|
|
12
|
+
eventStore
|
|
13
|
+
.createProjection('users')
|
|
14
|
+
.withEventHandlers({
|
|
15
|
+
onUserCreated({ streamId, payload }) {
|
|
16
|
+
users.set(streamId, payload)
|
|
17
|
+
},
|
|
18
|
+
onUserUpdated({ streamId, payload }) {
|
|
19
|
+
users.set(streamId, {
|
|
20
|
+
...users.get(streamId)!,
|
|
21
|
+
age: rebuilding ? payload.age + 1 : payload.age,
|
|
22
|
+
})
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
.withReplay({
|
|
26
|
+
deleteAll() {
|
|
27
|
+
users.clear()
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
let knowsUser = false
|
|
32
|
+
eventStore
|
|
33
|
+
.createProcessManager('users')
|
|
34
|
+
.withState({
|
|
35
|
+
knownUsersIds: new Set<string>(),
|
|
36
|
+
})
|
|
37
|
+
.withEventHandlers((state) => ({
|
|
38
|
+
onUserCreated({ streamId }) {
|
|
39
|
+
if (rebuilding) {
|
|
40
|
+
state.knownUsersIds.add(streamId)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
usersAggregate.loadStream(streamId).update(40)
|
|
44
|
+
},
|
|
45
|
+
onUserUpdated({ streamId }) {
|
|
46
|
+
knowsUser = state.knownUsersIds.has(streamId)
|
|
47
|
+
},
|
|
48
|
+
}))
|
|
49
|
+
|
|
50
|
+
const userId = crypto.randomUUID()
|
|
51
|
+
|
|
52
|
+
await usersAggregate.loadStream(userId).create('Torsten').settled()
|
|
53
|
+
expect(users.get(userId)).toEqual({
|
|
54
|
+
name: 'Torsten',
|
|
55
|
+
age: 40,
|
|
56
|
+
})
|
|
57
|
+
expect(knowsUser).toBeFalse()
|
|
58
|
+
expect(pseudoEventStore.length).toBe(2)
|
|
59
|
+
|
|
60
|
+
rebuilding = true
|
|
61
|
+
await eventStore.rebuild()
|
|
62
|
+
expect(pseudoEventStore.length).toBe(2)
|
|
63
|
+
expect(users.get(userId)).toEqual({
|
|
64
|
+
name: 'Torsten',
|
|
65
|
+
age: 41,
|
|
66
|
+
})
|
|
67
|
+
expect(knowsUser).toBeTrue()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('throws on interaction with posponed init', async () => {
|
|
71
|
+
const usersAggregate = createUserAggregate()
|
|
72
|
+
const { eventStore } = setupEventStore(usersAggregate, { autoInit: false })
|
|
73
|
+
|
|
74
|
+
expect(() =>
|
|
75
|
+
usersAggregate.newStream().create('Torsten').settled(),
|
|
76
|
+
).toThrow(
|
|
77
|
+
'Cannot emit events because the event store is not initialized. Set autoInit to true, or explicitly call .init() before emitting events.',
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
eventStore.init()
|
|
81
|
+
|
|
82
|
+
expect(() =>
|
|
83
|
+
usersAggregate.newStream().create('Torsten').settled(),
|
|
84
|
+
).not.toThrow()
|
|
85
|
+
})
|
|
86
|
+
})
|