@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.
- package/LICENSE +21 -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 +85 -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,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
|
+
})
|