@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.
Files changed (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/dist/chunk-24xjphpk.js +51 -0
  4. package/dist/chunk-24xjphpk.js.map +10 -0
  5. package/dist/chunk-3t1vzbab.js +88 -0
  6. package/dist/chunk-3t1vzbab.js.map +10 -0
  7. package/dist/chunk-6q8mr1sg.js +13 -0
  8. package/dist/chunk-6q8mr1sg.js.map +10 -0
  9. package/dist/chunk-9psg50cb.js +22 -0
  10. package/dist/chunk-9psg50cb.js.map +10 -0
  11. package/dist/chunk-dgzw1nj9.js +50 -0
  12. package/dist/chunk-dgzw1nj9.js.map +10 -0
  13. package/dist/chunk-gq80tgvf.js +70 -0
  14. package/dist/chunk-gq80tgvf.js.map +10 -0
  15. package/dist/chunk-jd4q8fcf.js +11 -0
  16. package/dist/chunk-jd4q8fcf.js.map +10 -0
  17. package/dist/chunk-m07vf8q9.js +210 -0
  18. package/dist/chunk-m07vf8q9.js.map +10 -0
  19. package/dist/chunk-ncpk70p4.js +313 -0
  20. package/dist/chunk-ncpk70p4.js.map +10 -0
  21. package/dist/chunk-pmfzjavb.js +49 -0
  22. package/dist/chunk-pmfzjavb.js.map +10 -0
  23. package/dist/chunk-qdczt68t.js +84 -0
  24. package/dist/chunk-qdczt68t.js.map +10 -0
  25. package/dist/chunk-tfy01fah.js +34 -0
  26. package/dist/chunk-tfy01fah.js.map +10 -0
  27. package/dist/chunk-tkvj6x8k.js +389 -0
  28. package/dist/chunk-tkvj6x8k.js.map +10 -0
  29. package/dist/chunk-ty9bhdvx.js +53 -0
  30. package/dist/chunk-ty9bhdvx.js.map +10 -0
  31. package/dist/chunk-wmz6a1jk.js +233 -0
  32. package/dist/chunk-wmz6a1jk.js.map +10 -0
  33. package/dist/chunk-x90g388y.js +22 -0
  34. package/dist/chunk-x90g388y.js.map +10 -0
  35. package/dist/chunk-z388cd25.js +91 -0
  36. package/dist/chunk-z388cd25.js.map +10 -0
  37. package/dist/chunk-zf3x573k.js +65 -0
  38. package/dist/chunk-zf3x573k.js.map +10 -0
  39. package/dist/createAggregateRoot.d.ts +191 -0
  40. package/dist/createAggregateRoot.d.ts.map +1 -0
  41. package/dist/createAggregateRoot.js +18 -0
  42. package/dist/createAggregateRoot.js.map +9 -0
  43. package/dist/createCheckpointApi.d.ts +38 -0
  44. package/dist/createCheckpointApi.d.ts.map +1 -0
  45. package/dist/createCheckpointApi.js +9 -0
  46. package/dist/createCheckpointApi.js.map +9 -0
  47. package/dist/createEventListener.d.ts +46 -0
  48. package/dist/createEventListener.d.ts.map +1 -0
  49. package/dist/createEventListener.js +12 -0
  50. package/dist/createEventListener.js.map +9 -0
  51. package/dist/createEventStore.d.ts +57 -0
  52. package/dist/createEventStore.d.ts.map +1 -0
  53. package/dist/createEventStore.js +20 -0
  54. package/dist/createEventStore.js.map +9 -0
  55. package/dist/createProcessManager.d.ts +70 -0
  56. package/dist/createProcessManager.d.ts.map +1 -0
  57. package/dist/createProcessManager.js +16 -0
  58. package/dist/createProcessManager.js.map +9 -0
  59. package/dist/createProjection.d.ts +120 -0
  60. package/dist/createProjection.d.ts.map +1 -0
  61. package/dist/createProjection.js +16 -0
  62. package/dist/createProjection.js.map +9 -0
  63. package/dist/drizzle/index.d.ts +449 -0
  64. package/dist/drizzle/index.d.ts.map +1 -0
  65. package/dist/drizzle/index.js +325 -0
  66. package/dist/drizzle/index.js.map +10 -0
  67. package/dist/drizzle/postgres.d.ts +686 -0
  68. package/dist/drizzle/postgres.d.ts.map +1 -0
  69. package/dist/drizzle/postgres.js +18 -0
  70. package/dist/drizzle/postgres.js.map +9 -0
  71. package/dist/errors.d.ts +53 -0
  72. package/dist/errors.d.ts.map +1 -0
  73. package/dist/errors.js +26 -0
  74. package/dist/errors.js.map +9 -0
  75. package/dist/index.d.ts +15 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +73 -0
  78. package/dist/index.js.map +9 -0
  79. package/dist/lock.d.ts +6 -0
  80. package/dist/lock.d.ts.map +1 -0
  81. package/dist/lock.js +8 -0
  82. package/dist/lock.js.map +9 -0
  83. package/dist/memory/index.d.ts +15 -0
  84. package/dist/memory/index.d.ts.map +1 -0
  85. package/dist/memory/index.js +178 -0
  86. package/dist/memory/index.js.map +10 -0
  87. package/dist/redis/index.d.ts +2 -0
  88. package/dist/redis/index.d.ts.map +1 -0
  89. package/dist/redis/index.js +8 -0
  90. package/dist/redis/index.js.map +9 -0
  91. package/dist/redis/redisLock.d.ts +4 -0
  92. package/dist/redis/redisLock.d.ts.map +1 -0
  93. package/dist/redis/redisLock.js +8 -0
  94. package/dist/redis/redisLock.js.map +9 -0
  95. package/dist/refresh.d.ts +4 -0
  96. package/dist/refresh.d.ts.map +1 -0
  97. package/dist/refresh.js +13 -0
  98. package/dist/refresh.js.map +9 -0
  99. package/dist/replay.d.ts +4 -0
  100. package/dist/replay.d.ts.map +1 -0
  101. package/dist/replay.js +13 -0
  102. package/dist/replay.js.map +9 -0
  103. package/dist/snapshotGenerator.d.ts +79 -0
  104. package/dist/snapshotGenerator.d.ts.map +1 -0
  105. package/dist/snapshotGenerator.js +9 -0
  106. package/dist/snapshotGenerator.js.map +9 -0
  107. package/dist/superjson.d.ts +5 -0
  108. package/dist/superjson.d.ts.map +1 -0
  109. package/dist/superjson.js +12 -0
  110. package/dist/superjson.js.map +9 -0
  111. package/dist/transaction.d.ts +13 -0
  112. package/dist/transaction.d.ts.map +1 -0
  113. package/dist/transaction.js +8 -0
  114. package/dist/transaction.js.map +9 -0
  115. package/dist/utilityTypes.d.js +2 -0
  116. package/dist/utilityTypes.d.js.map +9 -0
  117. package/dist/utils.d.ts +4 -0
  118. package/dist/utils.d.ts.map +1 -0
  119. package/dist/utils.js +10 -0
  120. package/dist/utils.js.map +9 -0
  121. package/dist/wrappedEventBuilder.d.ts +37 -0
  122. package/dist/wrappedEventBuilder.d.ts.map +1 -0
  123. package/dist/wrappedEventBuilder.js +12 -0
  124. package/dist/wrappedEventBuilder.js.map +9 -0
  125. package/package.json +87 -1
  126. package/src/createAggregateRoot.ts +902 -0
  127. package/src/createCheckpointApi.ts +106 -0
  128. package/src/createEventListener.ts +118 -0
  129. package/src/createEventStore.ts +446 -0
  130. package/src/createProcessManager.ts +363 -0
  131. package/src/createProjection.ts +577 -0
  132. package/src/drizzle/index.ts +449 -0
  133. package/src/drizzle/postgres.ts +120 -0
  134. package/src/errors.ts +85 -0
  135. package/src/index.ts +45 -0
  136. package/src/lock.ts +58 -0
  137. package/src/memory/index.ts +224 -0
  138. package/src/redis/index.ts +1 -0
  139. package/src/redis/redisLock.ts +61 -0
  140. package/src/refresh.ts +19 -0
  141. package/src/replay.ts +19 -0
  142. package/src/snapshotGenerator.ts +190 -0
  143. package/src/superjson.ts +11 -0
  144. package/src/tests/aggregateRoot.spec.ts +532 -0
  145. package/src/tests/eventListener.spec.ts +57 -0
  146. package/src/tests/eventStore.spec.ts +86 -0
  147. package/src/tests/processManager.spec.ts +185 -0
  148. package/src/tests/projection.spec.ts +511 -0
  149. package/src/tests/projectionProcessManager.spec.ts +203 -0
  150. package/src/tests/setup.ts +230 -0
  151. package/src/tests/transaction.spec.ts +185 -0
  152. package/src/transaction.ts +93 -0
  153. package/src/utilityTypes.d.ts +110 -0
  154. package/src/utils.ts +43 -0
  155. 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
+ })