@requence/event-sourcing 0.0.1 → 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.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/dist/chunk-24xjphpk.js +51 -0
  3. package/dist/chunk-24xjphpk.js.map +10 -0
  4. package/dist/chunk-3t1vzbab.js +88 -0
  5. package/dist/chunk-3t1vzbab.js.map +10 -0
  6. package/dist/chunk-6q8mr1sg.js +13 -0
  7. package/dist/chunk-6q8mr1sg.js.map +10 -0
  8. package/dist/chunk-9psg50cb.js +22 -0
  9. package/dist/chunk-9psg50cb.js.map +10 -0
  10. package/dist/chunk-dgzw1nj9.js +50 -0
  11. package/dist/chunk-dgzw1nj9.js.map +10 -0
  12. package/dist/chunk-gq80tgvf.js +70 -0
  13. package/dist/chunk-gq80tgvf.js.map +10 -0
  14. package/dist/chunk-jd4q8fcf.js +11 -0
  15. package/dist/chunk-jd4q8fcf.js.map +10 -0
  16. package/dist/chunk-m07vf8q9.js +210 -0
  17. package/dist/chunk-m07vf8q9.js.map +10 -0
  18. package/dist/chunk-ncpk70p4.js +313 -0
  19. package/dist/chunk-ncpk70p4.js.map +10 -0
  20. package/dist/chunk-pmfzjavb.js +49 -0
  21. package/dist/chunk-pmfzjavb.js.map +10 -0
  22. package/dist/chunk-qdczt68t.js +84 -0
  23. package/dist/chunk-qdczt68t.js.map +10 -0
  24. package/dist/chunk-tfy01fah.js +34 -0
  25. package/dist/chunk-tfy01fah.js.map +10 -0
  26. package/dist/chunk-tkvj6x8k.js +389 -0
  27. package/dist/chunk-tkvj6x8k.js.map +10 -0
  28. package/dist/chunk-ty9bhdvx.js +53 -0
  29. package/dist/chunk-ty9bhdvx.js.map +10 -0
  30. package/dist/chunk-wmz6a1jk.js +233 -0
  31. package/dist/chunk-wmz6a1jk.js.map +10 -0
  32. package/dist/chunk-x90g388y.js +22 -0
  33. package/dist/chunk-x90g388y.js.map +10 -0
  34. package/dist/chunk-z388cd25.js +91 -0
  35. package/dist/chunk-z388cd25.js.map +10 -0
  36. package/dist/chunk-zf3x573k.js +65 -0
  37. package/dist/chunk-zf3x573k.js.map +10 -0
  38. package/dist/createAggregateRoot.d.ts +191 -0
  39. package/dist/createAggregateRoot.d.ts.map +1 -0
  40. package/dist/createAggregateRoot.js +18 -0
  41. package/dist/createAggregateRoot.js.map +9 -0
  42. package/dist/createCheckpointApi.d.ts +38 -0
  43. package/dist/createCheckpointApi.d.ts.map +1 -0
  44. package/dist/createCheckpointApi.js +9 -0
  45. package/dist/createCheckpointApi.js.map +9 -0
  46. package/dist/createEventListener.d.ts +46 -0
  47. package/dist/createEventListener.d.ts.map +1 -0
  48. package/dist/createEventListener.js +12 -0
  49. package/dist/createEventListener.js.map +9 -0
  50. package/dist/createEventStore.d.ts +57 -0
  51. package/dist/createEventStore.d.ts.map +1 -0
  52. package/dist/createEventStore.js +20 -0
  53. package/dist/createEventStore.js.map +9 -0
  54. package/dist/createProcessManager.d.ts +70 -0
  55. package/dist/createProcessManager.d.ts.map +1 -0
  56. package/dist/createProcessManager.js +16 -0
  57. package/dist/createProcessManager.js.map +9 -0
  58. package/dist/createProjection.d.ts +120 -0
  59. package/dist/createProjection.d.ts.map +1 -0
  60. package/dist/createProjection.js +16 -0
  61. package/dist/createProjection.js.map +9 -0
  62. package/dist/drizzle/index.d.ts +449 -0
  63. package/dist/drizzle/index.d.ts.map +1 -0
  64. package/dist/drizzle/index.js +325 -0
  65. package/dist/drizzle/index.js.map +10 -0
  66. package/dist/drizzle/postgres.d.ts +686 -0
  67. package/dist/drizzle/postgres.d.ts.map +1 -0
  68. package/dist/drizzle/postgres.js +18 -0
  69. package/dist/drizzle/postgres.js.map +9 -0
  70. package/dist/errors.d.ts +53 -0
  71. package/dist/errors.d.ts.map +1 -0
  72. package/dist/errors.js +26 -0
  73. package/dist/errors.js.map +9 -0
  74. package/dist/index.d.ts +15 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/dist/index.js +73 -0
  77. package/dist/index.js.map +9 -0
  78. package/dist/lock.d.ts +6 -0
  79. package/dist/lock.d.ts.map +1 -0
  80. package/dist/lock.js +8 -0
  81. package/dist/lock.js.map +9 -0
  82. package/dist/memory/index.d.ts +15 -0
  83. package/dist/memory/index.d.ts.map +1 -0
  84. package/dist/memory/index.js +178 -0
  85. package/dist/memory/index.js.map +10 -0
  86. package/dist/redis/index.d.ts +2 -0
  87. package/dist/redis/index.d.ts.map +1 -0
  88. package/dist/redis/index.js +8 -0
  89. package/dist/redis/index.js.map +9 -0
  90. package/dist/redis/redisLock.d.ts +4 -0
  91. package/dist/redis/redisLock.d.ts.map +1 -0
  92. package/dist/redis/redisLock.js +8 -0
  93. package/dist/redis/redisLock.js.map +9 -0
  94. package/dist/refresh.d.ts +4 -0
  95. package/dist/refresh.d.ts.map +1 -0
  96. package/dist/refresh.js +13 -0
  97. package/dist/refresh.js.map +9 -0
  98. package/dist/replay.d.ts +4 -0
  99. package/dist/replay.d.ts.map +1 -0
  100. package/dist/replay.js +13 -0
  101. package/dist/replay.js.map +9 -0
  102. package/dist/snapshotGenerator.d.ts +79 -0
  103. package/dist/snapshotGenerator.d.ts.map +1 -0
  104. package/dist/snapshotGenerator.js +9 -0
  105. package/dist/snapshotGenerator.js.map +9 -0
  106. package/dist/superjson.d.ts +5 -0
  107. package/dist/superjson.d.ts.map +1 -0
  108. package/dist/superjson.js +12 -0
  109. package/dist/superjson.js.map +9 -0
  110. package/dist/transaction.d.ts +13 -0
  111. package/dist/transaction.d.ts.map +1 -0
  112. package/dist/transaction.js +8 -0
  113. package/dist/transaction.js.map +9 -0
  114. package/dist/utilityTypes.d.js +2 -0
  115. package/dist/utilityTypes.d.js.map +9 -0
  116. package/dist/utils.d.ts +4 -0
  117. package/dist/utils.d.ts.map +1 -0
  118. package/dist/utils.js +10 -0
  119. package/dist/utils.js.map +9 -0
  120. package/dist/wrappedEventBuilder.d.ts +37 -0
  121. package/dist/wrappedEventBuilder.d.ts.map +1 -0
  122. package/dist/wrappedEventBuilder.js +12 -0
  123. package/dist/wrappedEventBuilder.js.map +9 -0
  124. package/package.json +85 -1
  125. package/src/createAggregateRoot.ts +902 -0
  126. package/src/createCheckpointApi.ts +106 -0
  127. package/src/createEventListener.ts +118 -0
  128. package/src/createEventStore.ts +446 -0
  129. package/src/createProcessManager.ts +363 -0
  130. package/src/createProjection.ts +577 -0
  131. package/src/drizzle/index.ts +449 -0
  132. package/src/drizzle/postgres.ts +120 -0
  133. package/src/errors.ts +85 -0
  134. package/src/index.ts +45 -0
  135. package/src/lock.ts +58 -0
  136. package/src/memory/index.ts +224 -0
  137. package/src/redis/index.ts +1 -0
  138. package/src/redis/redisLock.ts +61 -0
  139. package/src/refresh.ts +19 -0
  140. package/src/replay.ts +19 -0
  141. package/src/snapshotGenerator.ts +190 -0
  142. package/src/superjson.ts +11 -0
  143. package/src/tests/aggregateRoot.spec.ts +532 -0
  144. package/src/tests/eventListener.spec.ts +57 -0
  145. package/src/tests/eventStore.spec.ts +86 -0
  146. package/src/tests/processManager.spec.ts +185 -0
  147. package/src/tests/projection.spec.ts +511 -0
  148. package/src/tests/projectionProcessManager.spec.ts +203 -0
  149. package/src/tests/setup.ts +230 -0
  150. package/src/tests/transaction.spec.ts +185 -0
  151. package/src/transaction.ts +93 -0
  152. package/src/utilityTypes.d.ts +110 -0
  153. package/src/utils.ts +43 -0
  154. package/src/wrappedEventBuilder.ts +181 -0
@@ -0,0 +1,185 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+
3
+ import { createUserAggregate, setupEventStore } from './setup.ts'
4
+ import { skipRefreshing } from '../refresh.ts'
5
+
6
+ describe('ProcessManager', () => {
7
+ it('processes events', async () => {
8
+ const usersAggregate = createUserAggregate()
9
+ const { eventStore, pseudoCheckpointStore } =
10
+ setupEventStore(usersAggregate)
11
+
12
+ let called = false
13
+ eventStore.createProcessManager('my-process-manager').withEventHandlers({
14
+ async onUserCreated() {
15
+ called = true
16
+ },
17
+ })
18
+
19
+ await usersAggregate.newStream().create('Torsten').settled()
20
+ expect(Array.from(pseudoCheckpointStore)[0]).toMatchObject({
21
+ lastEventPosition: 0,
22
+ name: 'my-process-manager',
23
+ type: 'processManager',
24
+ })
25
+
26
+ expect(called).toBe(true)
27
+ })
28
+
29
+ it('processes events with state', async () => {
30
+ const usersAggregate = createUserAggregate()
31
+ const { eventStore, pseudoCheckpointStore } =
32
+ setupEventStore(usersAggregate)
33
+
34
+ let called = false
35
+ eventStore
36
+ .createProcessManager('my-process-manager')
37
+ .withState({
38
+ usersCreated: 0,
39
+ })
40
+ .withEventHandlers((state) => ({
41
+ async onUserCreated() {
42
+ state.usersCreated += 1
43
+ called = true
44
+ },
45
+ }))
46
+
47
+ await usersAggregate.newStream().create('Torsten').settled()
48
+
49
+ expect(Array.from(pseudoCheckpointStore)[0]).toMatchObject({
50
+ lastEventPosition: 0,
51
+ name: 'my-process-manager',
52
+ type: 'processManager',
53
+ metadata: { state: { usersCreated: 1 } },
54
+ })
55
+
56
+ expect(called).toBe(true)
57
+ })
58
+
59
+ it('can execute after effect', async () => {
60
+ const usersAggregate = createUserAggregate()
61
+ const { eventStore } = setupEventStore(usersAggregate)
62
+
63
+ const orderOfOperations: string[] = []
64
+ eventStore
65
+ .createProcessManager('userA')
66
+ .withEventHandlers({
67
+ onUserCreated() {
68
+ orderOfOperations.push('created-a')
69
+ },
70
+ })
71
+ .withAfterEffects({
72
+ afterUserCreated() {
73
+ orderOfOperations.push('after-a')
74
+ },
75
+ })
76
+
77
+ eventStore.createProcessManager('userB').withEventHandlers({
78
+ onUserCreated() {
79
+ orderOfOperations.push('created-b')
80
+ },
81
+ })
82
+
83
+ await usersAggregate.newStream().create('Torsten').settled()
84
+
85
+ expect(orderOfOperations).toEqual(['created-a', 'created-b', 'after-a'])
86
+ })
87
+
88
+ it('throws when settled() is called inside event handler', () => {
89
+ const usersAggregate = createUserAggregate()
90
+ const { eventStore } = setupEventStore(usersAggregate)
91
+
92
+ eventStore.createProcessManager('test').withEventHandlers({
93
+ onUserCreated({ streamId }) {
94
+ expect(() => usersAggregate.loadStream(streamId).settled()).toThrow(
95
+ 'aggregateRoot.settled() cannot be called inside a process manager',
96
+ )
97
+ },
98
+ })
99
+
100
+ usersAggregate.newStream().create('Torsten')
101
+ })
102
+
103
+ it('detects infinite loops', async () => {
104
+ const usersAggregate = createUserAggregate()
105
+ const { eventStore } = setupEventStore(usersAggregate)
106
+
107
+ eventStore.createProcessManager('test').withEventHandlers({
108
+ onUserUpdated({ streamId }) {
109
+ usersAggregate.loadStream(streamId).update(41)
110
+ },
111
+ })
112
+
113
+ expect(
114
+ usersAggregate.newStream().create('Torsten').update(40).settled(),
115
+ ).rejects.toThrow(
116
+ /^Infinite loop detected. Process Manager "test" listens and emits same event "UserUpdated" for stream users:(.+), causing a recurring event cycle$/,
117
+ )
118
+ })
119
+
120
+ it('can be refreshed', async () => {
121
+ const usersAggregate = createUserAggregate()
122
+ const { eventStore } = setupEventStore(usersAggregate)
123
+
124
+ let increment = 1
125
+ const processManager = eventStore
126
+ .createProcessManager('test')
127
+ .withState({
128
+ executed: 0,
129
+ })
130
+ .withEventHandlers((state) => ({
131
+ onUserCreated() {
132
+ state.executed += increment
133
+ },
134
+ onUserUpdated() {
135
+ state.executed += increment
136
+ },
137
+ }))
138
+
139
+ await usersAggregate.newStream().create('Torsten').update(40).settled()
140
+ expect(processManager.state()).resolves.toEqual({ executed: 2 })
141
+ increment = 2
142
+ await processManager.refreshState()
143
+ expect(processManager.state()).resolves.toEqual({ executed: 4 })
144
+ })
145
+
146
+ it('will not refresh without state', async () => {
147
+ const usersAggregate = createUserAggregate()
148
+ const { eventStore } = setupEventStore(usersAggregate)
149
+
150
+ let called = 0
151
+ eventStore.createProcessManager('test').withEventHandlers({
152
+ onUserCreated() {
153
+ called += 1
154
+ },
155
+ })
156
+
157
+ await usersAggregate.newStream().create('Torsten').settled()
158
+ expect(called).toBe(1)
159
+
160
+ await eventStore.rebuild()
161
+ expect(called).toBe(1)
162
+ })
163
+
164
+ it('can skip execution while refreshing', async () => {
165
+ const usersAggregate = createUserAggregate()
166
+ const { eventStore } = setupEventStore(usersAggregate)
167
+
168
+ let called = 0
169
+ eventStore
170
+ .createProcessManager('test')
171
+ .withState({})
172
+ .withEventHandlers((_state) => ({
173
+ onUserCreated() {
174
+ skipRefreshing()
175
+ called += 1
176
+ },
177
+ }))
178
+
179
+ await usersAggregate.newStream().create('Torsten').settled()
180
+ expect(called).toBe(1)
181
+
182
+ await eventStore.rebuild()
183
+ expect(called).toBe(1)
184
+ })
185
+ })
@@ -0,0 +1,511 @@
1
+ import { setTimeout } from 'node:timers/promises'
2
+
3
+ import { describe, expect, it } from 'bun:test'
4
+ import { z } from 'zod/v4'
5
+
6
+ import { createAggregateRoot } from '../createAggregateRoot.ts'
7
+ import { createUserAggregate, setupEventStore } from './setup.ts'
8
+ import { ProjectionReplayLoopError } from '../errors.ts'
9
+ import { skipReplay } from '../replay.ts'
10
+
11
+ describe('Projection', () => {
12
+ it('projects', async () => {
13
+ const usersAggregate = createUserAggregate()
14
+ const { eventStore, pseudoCheckpointStore } =
15
+ setupEventStore(usersAggregate)
16
+
17
+ const users = new Map<string, { name: string; age?: number }>()
18
+
19
+ eventStore.createProjection('user').withEventHandlers({
20
+ onUserCreated(event) {
21
+ users.set(event.streamId, { name: event.payload.name })
22
+ },
23
+ onUserUpdated(event) {
24
+ users.get(event.streamId)!.age = event.payload.age
25
+ },
26
+ })
27
+
28
+ await usersAggregate.newStream().create('Torsten').settled()
29
+ expect(users.size).toBe(1)
30
+ const id = Array.from(users.keys()).at(0)!
31
+ await usersAggregate.loadStream(id).update(40).settled()
32
+
33
+ expect(users.get(id)).toEqual({ name: 'Torsten', age: 40 })
34
+ expect(pseudoCheckpointStore).toHaveLength(1)
35
+ expect(Array.from(pseudoCheckpointStore)[0]).toMatchObject({
36
+ type: 'projection',
37
+ name: 'user',
38
+ lastEventPosition: 1,
39
+ })
40
+ let userCount = 0
41
+ eventStore.createProjection('userCount').withEventHandlers({
42
+ onUserCreated() {
43
+ userCount++
44
+ },
45
+ })
46
+
47
+ await eventStore.isReady()
48
+
49
+ expect(userCount).toBe(1)
50
+ expect(pseudoCheckpointStore.size).toBe(2)
51
+ expect(Array.from(pseudoCheckpointStore)[1]).toMatchObject({
52
+ type: 'projection',
53
+ name: 'userCount',
54
+ lastEventPosition: 1,
55
+ })
56
+ })
57
+
58
+ it('projects multiple versions', async () => {
59
+ const aggregate = createAggregateRoot('users').withEvents(
60
+ ({ version }) => ({
61
+ UserCreated: z.object({
62
+ name: z.string(),
63
+ }),
64
+ UserUpdated: [
65
+ z.object({
66
+ age: z.int(),
67
+ }),
68
+ version(1, z.object({ adult: z.boolean() })),
69
+ ],
70
+ }),
71
+ )
72
+
73
+ const streamId = crypto.randomUUID()
74
+
75
+ const { eventStore } = setupEventStore(aggregate, {
76
+ preloadEvents: [
77
+ {
78
+ type: 'UserCreated',
79
+ payload: {
80
+ name: 'Torsten',
81
+ },
82
+ createdAt: new Date(),
83
+ position: 0,
84
+ schemaVersion: 0,
85
+ streamId,
86
+ streamType: 'users',
87
+ streamVersion: 0,
88
+ },
89
+ {
90
+ type: 'UserUpdated',
91
+ payload: {
92
+ age: 40,
93
+ },
94
+ createdAt: new Date(),
95
+ position: 1,
96
+ schemaVersion: 0,
97
+ streamId,
98
+ streamType: 'users',
99
+ streamVersion: 1,
100
+ },
101
+ {
102
+ type: 'UserUpdated',
103
+ payload: {
104
+ adult: true,
105
+ },
106
+ createdAt: new Date(),
107
+ position: 2,
108
+ schemaVersion: 1,
109
+ streamId,
110
+ streamType: 'users',
111
+ streamVersion: 3,
112
+ },
113
+ ],
114
+ })
115
+
116
+ const users = new Map<
117
+ string,
118
+ { name: string; age?: number; adult?: boolean }
119
+ >()
120
+
121
+ eventStore.createProjection('user').withEventHandlers({
122
+ onUserCreated(event) {
123
+ users.set(event.streamId, { name: event.payload.name })
124
+ },
125
+ onUserUpdated(event) {
126
+ if (event.schemaVersion === 0) {
127
+ users.get(event.streamId)!.age = event.payload.age
128
+ } else {
129
+ users.get(event.streamId)!.adult = event.payload.adult
130
+ }
131
+ },
132
+ })
133
+
134
+ await eventStore.isReady()
135
+
136
+ expect(users.get(streamId)).toEqual({
137
+ name: 'Torsten',
138
+ age: 40,
139
+ adult: true,
140
+ })
141
+ })
142
+
143
+ it('projects exclusively', async () => {
144
+ const usersAggregate = createUserAggregate()
145
+ const { eventStore } = setupEventStore(usersAggregate)
146
+
147
+ const users = new Map<string, { name: string; age?: number }>()
148
+
149
+ const orderOfOperations: string[] = []
150
+ eventStore.createProjection('user').withEventHandlers({
151
+ async onUserCreated(event) {
152
+ orderOfOperations.push('creating user')
153
+ users.set(event.streamId, { name: event.payload.name })
154
+ await setTimeout(100)
155
+ orderOfOperations.push('created user')
156
+ },
157
+ async onUserUpdated(event) {
158
+ orderOfOperations.push('updating user')
159
+ users.get(event.streamId)!.age = event.payload.age
160
+ await setTimeout(100)
161
+ orderOfOperations.push('updated user')
162
+ },
163
+ })
164
+
165
+ await usersAggregate
166
+ .newStream()
167
+ .create('Torsten')
168
+ .update(39)
169
+ .update(40)
170
+ .settled()
171
+ expect(users.size).toBe(1)
172
+ const id = Array.from(users.keys()).at(0)!
173
+ expect(users.get(id)).toEqual({ name: 'Torsten', age: 40 })
174
+ expect(orderOfOperations).toEqual([
175
+ 'creating user',
176
+ 'created user',
177
+ 'updating user',
178
+ 'updated user',
179
+ 'updating user',
180
+ 'updated user',
181
+ ])
182
+ })
183
+
184
+ it('projects concurrently', async () => {
185
+ const usersAggregate = createUserAggregate()
186
+ const { eventStore } = setupEventStore(usersAggregate)
187
+
188
+ const users = new Map<string, { name: string; age?: number }>()
189
+ const orderOfOperations: string[] = []
190
+ eventStore
191
+ .createProjection('user')
192
+ .withEventHandlers({
193
+ async onUserCreated(event) {
194
+ orderOfOperations.push('creating user')
195
+ users.set(event.streamId, { name: event.payload.name })
196
+ await setTimeout(100)
197
+ orderOfOperations.push('created user')
198
+ },
199
+ async onUserUpdated(event) {
200
+ orderOfOperations.push('updating user')
201
+ users.get(event.streamId)!.age = event.payload.age
202
+ await setTimeout(100)
203
+ orderOfOperations.push('updated user')
204
+ },
205
+ })
206
+ .concurrent()
207
+
208
+ await usersAggregate
209
+ .newStream()
210
+ .create('Torsten')
211
+ .update(39)
212
+ .update(40)
213
+ .settled()
214
+ expect(users.size).toBe(1)
215
+ const id = Array.from(users.keys()).at(0)!
216
+ expect(users.get(id)).toEqual({ name: 'Torsten', age: 40 })
217
+ expect(orderOfOperations.slice(0, 3)).toEqual([
218
+ 'creating user',
219
+ 'updating user',
220
+ 'updating user',
221
+ ])
222
+ })
223
+
224
+ it('makes snapshots', async () => {
225
+ const usersAggregate = createUserAggregate()
226
+ const {
227
+ eventStore,
228
+ pseudoProjectionAppliedCount,
229
+ pseudoProjectionSnapshotStore,
230
+ } = setupEventStore(usersAggregate)
231
+
232
+ const users = new Map<string, { name: string; age?: number }>()
233
+ let createdHandled = 0
234
+ let updateHandled = 0
235
+ const projection = eventStore
236
+ .createProjection('user')
237
+ .withEventHandlers({
238
+ onUserCreated(event) {
239
+ createdHandled += 1
240
+ users.set(event.streamId, { name: event.payload.name })
241
+ },
242
+ onUserUpdated(event) {
243
+ updateHandled += 1
244
+ users.get(event.streamId)!.age = event.payload.age
245
+ },
246
+ })
247
+ .withSnapshots((s) =>
248
+ s
249
+ .every(2)
250
+ .schema(z.tuple([z.string(), z.int().optional()]))
251
+ .capture((event) => {
252
+ const persistedUser = users.get(event.streamId)
253
+ return persistedUser
254
+ ? [persistedUser.name, persistedUser.age]
255
+ : null
256
+ })
257
+ .apply((snapshot) => {
258
+ users.set(snapshot.projectionId, {
259
+ name: snapshot.state[0],
260
+ age: snapshot.state[1],
261
+ })
262
+ }),
263
+ )
264
+ .withReplay({
265
+ deleteAll() {
266
+ users.clear()
267
+ },
268
+ deleteOne(id) {
269
+ users.delete(id)
270
+ },
271
+ })
272
+
273
+ await usersAggregate
274
+ .newStream()
275
+ .create('Torsten')
276
+ .update(30)
277
+ .update(40)
278
+ .settled()
279
+ const id = Array.from(users.keys()).at(0)!
280
+
281
+ expect(users.get(id)).toEqual({ name: 'Torsten', age: 40 })
282
+ expect(pseudoProjectionAppliedCount.get(`user-${id}`)).toBe(3)
283
+ expect(pseudoProjectionSnapshotStore.size).toBe(1)
284
+ expect(Array.from(pseudoProjectionSnapshotStore).at(0)).toMatchObject({
285
+ state: ['Torsten', 30],
286
+ })
287
+
288
+ await projection.replayUntil(id, 2)
289
+ expect(createdHandled).toBe(1) // only one create bc of replay
290
+ expect(updateHandled).toBe(3) // 3 updates, bc update 1 got snapshotted
291
+ })
292
+
293
+ it('throws when aggregateRoot is accessed inside event handler', () => {
294
+ const usersAggregate = createUserAggregate()
295
+ const { eventStore } = setupEventStore(usersAggregate)
296
+
297
+ eventStore.createProjection('test').withEventHandlers({
298
+ onUserCreated() {
299
+ expect(() => usersAggregate.loadStream('x')).toThrow(
300
+ 'aggregateRoot.loadStream("x") cannot be called inside a projection. Attempted on aggregate stream users within Projection test.',
301
+ )
302
+
303
+ expect(() => usersAggregate.newStream()).toThrow(
304
+ 'aggregateRoot.newStream() cannot be called inside a projection. Attempted on aggregate stream users within Projection test.',
305
+ )
306
+ },
307
+ })
308
+
309
+ usersAggregate.newStream().create('Torsten')
310
+ })
311
+
312
+ it('can rewind', async () => {
313
+ const root = createAggregateRoot('test')
314
+ .withEvents({
315
+ TestCreated: z.object({ name: z.string() }),
316
+ TestUpdated: z.object({ name: z.string() }),
317
+ TestDeleted: z.null(),
318
+ TestRevived: z.null(),
319
+ })
320
+ .withCommands((event) => ({
321
+ create(name: string) {
322
+ return event('TestCreated', { name })
323
+ },
324
+ update(newName: string) {
325
+ return event('TestUpdated', { name: newName })
326
+ },
327
+ delete() {
328
+ return event('TestDeleted')
329
+ },
330
+ revive() {
331
+ return event('TestRevived')
332
+ },
333
+ }))
334
+
335
+ const { eventStore, pseudoEventStore } = setupEventStore(root)
336
+
337
+ const store = new Map<string, { name: string }>()
338
+
339
+ eventStore
340
+ .createProjection('test')
341
+ .withEventHandlers({
342
+ onTestCreated({ streamId, payload }) {
343
+ if (store.has(streamId)) {
344
+ expect.unreachable()
345
+ }
346
+ store.set(streamId, payload)
347
+ },
348
+ onTestUpdated({ streamId, payload }) {
349
+ if (!store.has(streamId)) {
350
+ expect.unreachable()
351
+ }
352
+ store.set(streamId, payload)
353
+ },
354
+ onTestDeleted({ streamId }) {
355
+ if (!store.has(streamId)) {
356
+ expect.unreachable()
357
+ }
358
+ store.delete(streamId)
359
+ },
360
+ async onTestRevived({ streamId }, { replayUntil }) {
361
+ const event = pseudoEventStore.findLast(
362
+ (event) =>
363
+ event.streamId === streamId && event.type === 'TestDeleted',
364
+ )!
365
+ await replayUntil(event.position - 1)
366
+ },
367
+ })
368
+ .withReplay({
369
+ deleteAll() {
370
+ store.clear()
371
+ },
372
+ deleteOne(id) {
373
+ store.delete(id)
374
+ },
375
+ })
376
+
377
+ const stream = root.newStream()
378
+ await stream.create('A').update('B').settled()
379
+
380
+ expect(store.size).toBe(1)
381
+ expect(store.get(stream.streamId)?.name).toBe('B')
382
+
383
+ await stream.delete().settled()
384
+ expect(store.size).toBe(0)
385
+
386
+ await stream.revive().settled()
387
+ expect(store.size).toBe(1)
388
+ expect(store.get(stream.streamId)?.name).toBe('B')
389
+ })
390
+
391
+ it('throws on rewind cycle', async () => {
392
+ const root = createAggregateRoot('test')
393
+ .withEvents({
394
+ TestCreated: z.null(),
395
+ TestUpdated: z.object({ name: z.string() }),
396
+ })
397
+ .withCommands((event) => ({
398
+ *create(name: string) {
399
+ yield event('TestCreated')
400
+ yield event('TestUpdated', { name })
401
+ },
402
+ }))
403
+
404
+ const { eventStore } = setupEventStore(root)
405
+
406
+ eventStore.createProjection('test').withEventHandlers({
407
+ async onTestUpdated({ position }, { replayUntil }) {
408
+ await replayUntil(position)
409
+ },
410
+ })
411
+
412
+ const stream = root.newStream()
413
+ expect(stream.create('A').settled()).rejects.toThrow(
414
+ ProjectionReplayLoopError,
415
+ )
416
+ })
417
+ it('can be replay all after init', async () => {
418
+ const usersAggregate = createUserAggregate()
419
+ const { eventStore } = setupEventStore(usersAggregate)
420
+
421
+ const orderOfOperations: string[] = []
422
+ const testProjection = eventStore
423
+ .createProjection('test')
424
+ .withEventHandlers({
425
+ onUserCreated() {
426
+ orderOfOperations.push('User created')
427
+ },
428
+ onUserUpdated() {
429
+ orderOfOperations.push('User updated')
430
+ },
431
+ })
432
+ .withReplay({
433
+ deleteAll() {
434
+ orderOfOperations.push('User deleted')
435
+ },
436
+ })
437
+
438
+ await usersAggregate.newStream().create('Torsten').update(40).settled()
439
+ await setTimeout(50)
440
+
441
+ await testProjection.replay()
442
+ expect(orderOfOperations).toEqual([
443
+ 'User created',
444
+ 'User updated',
445
+ 'User deleted',
446
+ 'User created',
447
+ 'User updated',
448
+ ])
449
+ })
450
+
451
+ it('can be replay one after init', async () => {
452
+ const usersAggregate = createUserAggregate()
453
+ const { eventStore } = setupEventStore(usersAggregate)
454
+
455
+ const orderOfOperations: string[] = []
456
+ const testProjection = eventStore
457
+ .createProjection('test')
458
+ .withEventHandlers({
459
+ onUserCreated({ streamId }) {
460
+ orderOfOperations.push(`User created ${streamId}`)
461
+ },
462
+ onUserUpdated({ streamId }) {
463
+ orderOfOperations.push(`User updated ${streamId}`)
464
+ },
465
+ })
466
+ .withReplay({
467
+ deleteOne(id) {
468
+ orderOfOperations.push(`User deleted ${id}`)
469
+ },
470
+ })
471
+
472
+ await usersAggregate
473
+ .newStream('my-id')
474
+ .create('Torsten')
475
+ .update(40)
476
+ .settled()
477
+ await setTimeout(50)
478
+
479
+ await testProjection.replayOne('my-id')
480
+ expect(orderOfOperations).toEqual([
481
+ 'User created my-id',
482
+ 'User updated my-id',
483
+ 'User deleted my-id',
484
+ 'User created my-id',
485
+ 'User updated my-id',
486
+ ])
487
+ })
488
+ it('can skip execution while replaying', async () => {
489
+ const usersAggregate = createUserAggregate()
490
+ const { eventStore } = setupEventStore(usersAggregate)
491
+
492
+ let called = 0
493
+ eventStore
494
+ .createProjection('test')
495
+ .withEventHandlers({
496
+ onUserCreated() {
497
+ skipReplay()
498
+ called += 1
499
+ },
500
+ })
501
+ .withReplay({
502
+ deleteAll() {},
503
+ })
504
+
505
+ await usersAggregate.newStream().create('Torsten').settled()
506
+ expect(called).toBe(1)
507
+
508
+ await eventStore.rebuild()
509
+ expect(called).toBe(1)
510
+ })
511
+ })