@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
package/src/lock.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Mutex } from 'async-mutex'
|
|
2
|
+
|
|
3
|
+
export default function lock(defaultTtl = 5000) {
|
|
4
|
+
const locks = new Map<string, Mutex>()
|
|
5
|
+
return async (key: string | string[]) => {
|
|
6
|
+
const k = Array.isArray(key) ? key.join(':') : key
|
|
7
|
+
if (!locks.has(k)) {
|
|
8
|
+
locks.set(k, new Mutex())
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const mutex = locks.get(k)!
|
|
12
|
+
|
|
13
|
+
const releaseMutex = await mutex.acquire()
|
|
14
|
+
let isReleased = false
|
|
15
|
+
|
|
16
|
+
let timer: ReturnType<typeof setTimeout>
|
|
17
|
+
const startTimer = (ttl: number) => {
|
|
18
|
+
timer = setTimeout(() => {
|
|
19
|
+
if (!isReleased) {
|
|
20
|
+
console.warn('lock ttl expired for key', k)
|
|
21
|
+
isReleased = true
|
|
22
|
+
releaseMutex()
|
|
23
|
+
if (!mutex.isLocked()) {
|
|
24
|
+
locks.delete(k)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}, ttl)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
startTimer(defaultTtl)
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
async extend(ttl?: number) {
|
|
34
|
+
if (isReleased) {
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
clearTimeout(timer)
|
|
38
|
+
startTimer(ttl ?? defaultTtl / 2)
|
|
39
|
+
return true
|
|
40
|
+
},
|
|
41
|
+
async release() {
|
|
42
|
+
if (isReleased) {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
isReleased = true
|
|
47
|
+
clearTimeout(timer)
|
|
48
|
+
releaseMutex()
|
|
49
|
+
|
|
50
|
+
if (!mutex.isLocked()) {
|
|
51
|
+
locks.delete(k)
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type LockCreator = ReturnType<typeof lock>
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { AnyAggregateRoot } from '../createAggregateRoot.ts'
|
|
2
|
+
import type { Checkpoint } from '../createCheckpointApi.ts'
|
|
3
|
+
import {
|
|
4
|
+
type AggregateRootSnapshot,
|
|
5
|
+
ConcurrencyError,
|
|
6
|
+
type Event,
|
|
7
|
+
type ProjectionSnapshot,
|
|
8
|
+
createEventStore as createBaseEventStore,
|
|
9
|
+
isStreamEvents,
|
|
10
|
+
isStreamId,
|
|
11
|
+
} from '../index.ts'
|
|
12
|
+
import type {
|
|
13
|
+
EventStore,
|
|
14
|
+
EventStoreParamsWithAggregateRootSnapshots,
|
|
15
|
+
EventsFromRoot,
|
|
16
|
+
OnProgress,
|
|
17
|
+
} from '../createEventStore.ts'
|
|
18
|
+
import type { BaseOutputEvent, MaybePromise } from '../utilityTypes.js'
|
|
19
|
+
|
|
20
|
+
export { createAggregateRoot } from '../index.ts'
|
|
21
|
+
|
|
22
|
+
export type OnEventsAppended = (
|
|
23
|
+
events: BaseOutputEvent[],
|
|
24
|
+
) => MaybePromise<void>
|
|
25
|
+
|
|
26
|
+
interface MemoryEventStoreParams<Root extends AnyAggregateRoot> {
|
|
27
|
+
aggregateRoots: Root[]
|
|
28
|
+
autoInit?: boolean
|
|
29
|
+
postProcessEvent?: EventStoreParamsWithAggregateRootSnapshots<Root>['postProcessEvent']
|
|
30
|
+
onProjectionReplay?: OnProgress
|
|
31
|
+
onProcessManagerRefresh?: OnProgress
|
|
32
|
+
onEventsAppended?: OnEventsAppended
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createEventStore<const Root extends AnyAggregateRoot>({
|
|
36
|
+
aggregateRoots,
|
|
37
|
+
postProcessEvent,
|
|
38
|
+
onProjectionReplay,
|
|
39
|
+
onProcessManagerRefresh,
|
|
40
|
+
onEventsAppended,
|
|
41
|
+
autoInit,
|
|
42
|
+
}: MemoryEventStoreParams<Root>): EventStore<
|
|
43
|
+
EventsFromRoot<Root>,
|
|
44
|
+
Root,
|
|
45
|
+
true
|
|
46
|
+
> {
|
|
47
|
+
const events: Event[] = []
|
|
48
|
+
const checkpoints = new Set<Checkpoint>()
|
|
49
|
+
const aggregateRootSnapshots = new Map<string, AggregateRootSnapshot>()
|
|
50
|
+
const projectionSnapshots = new Set<ProjectionSnapshot>()
|
|
51
|
+
const projectionAppliedCount = new Map<string, number>()
|
|
52
|
+
|
|
53
|
+
return createBaseEventStore({
|
|
54
|
+
aggregateRoots,
|
|
55
|
+
postProcessEvent,
|
|
56
|
+
onProjectionReplay,
|
|
57
|
+
onProcessManagerRefresh,
|
|
58
|
+
autoInit,
|
|
59
|
+
|
|
60
|
+
async *loadEvents(select, range) {
|
|
61
|
+
let streamTypes: string[] | null = null
|
|
62
|
+
let streamIds: string[] | null = null
|
|
63
|
+
let eventTypes: string[] | null = null
|
|
64
|
+
|
|
65
|
+
if (select) {
|
|
66
|
+
const selects = Array.isArray(select) ? select : [select]
|
|
67
|
+
streamTypes = selects.map((select) => select.stream)
|
|
68
|
+
streamIds = selects
|
|
69
|
+
.map((select) => (isStreamId(select) ? select.id : null))
|
|
70
|
+
.filter(Boolean) as string[]
|
|
71
|
+
eventTypes = selects.flatMap((select) =>
|
|
72
|
+
isStreamEvents(select) ? select.events : [],
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const filteredEvents = events.filter((event) => {
|
|
77
|
+
if (streamTypes?.length && !streamTypes.includes(event.streamType)) {
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (streamIds?.length && !streamIds.includes(event.streamId)) {
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (eventTypes?.length && !eventTypes.includes(event.type)) {
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (range?.from && event.position < range.from) {
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (range?.to && event.position > range.to) {
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return true
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
for (const event of filteredEvents) {
|
|
101
|
+
yield event
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
appendEvents({ id, stream }, newEvents, expectedVersion) {
|
|
106
|
+
const lastEvent = events.findLast(
|
|
107
|
+
(e) => e.streamId === id && e.streamType === stream,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const currentVersion = lastEvent?.streamVersion ?? 0
|
|
111
|
+
|
|
112
|
+
if (currentVersion !== expectedVersion) {
|
|
113
|
+
throw new ConcurrencyError(
|
|
114
|
+
{ id, stream },
|
|
115
|
+
expectedVersion,
|
|
116
|
+
currentVersion,
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const extendedEvents: Event[] = newEvents.map((event, index) => ({
|
|
121
|
+
...event,
|
|
122
|
+
createdAt: new Date(),
|
|
123
|
+
position: events.length + index,
|
|
124
|
+
streamId: id,
|
|
125
|
+
streamType: stream,
|
|
126
|
+
streamVersion: expectedVersion + index + 1,
|
|
127
|
+
}))
|
|
128
|
+
|
|
129
|
+
events.push(...extendedEvents)
|
|
130
|
+
onEventsAppended?.(extendedEvents)
|
|
131
|
+
return extendedEvents
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
checkpoint: {
|
|
135
|
+
get(type, name) {
|
|
136
|
+
for (const checkpoint of checkpoints) {
|
|
137
|
+
if (checkpoint.type === type && checkpoint.name === name) {
|
|
138
|
+
return checkpoint
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
upsert(checkpoint) {
|
|
143
|
+
let persisted: Checkpoint | undefined
|
|
144
|
+
for (const c of checkpoints) {
|
|
145
|
+
if (c.type === checkpoint.type && c.name === checkpoint.name) {
|
|
146
|
+
persisted = c
|
|
147
|
+
break
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (persisted) {
|
|
152
|
+
persisted.lastEventPosition = checkpoint.lastEventPosition
|
|
153
|
+
persisted.metadata = checkpoint.metadata
|
|
154
|
+
} else {
|
|
155
|
+
checkpoints.add(checkpoint)
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
delete(type, name) {
|
|
159
|
+
for (const checkpoint of checkpoints) {
|
|
160
|
+
if (checkpoint.type === type && checkpoint.name === name) {
|
|
161
|
+
checkpoints.delete(checkpoint)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
projectionSnapshot: {
|
|
168
|
+
put(snapshot) {
|
|
169
|
+
projectionSnapshots.add(snapshot)
|
|
170
|
+
},
|
|
171
|
+
get(select) {
|
|
172
|
+
return Array.from(projectionSnapshots)
|
|
173
|
+
.toReversed()
|
|
174
|
+
.find(
|
|
175
|
+
(snapshot) =>
|
|
176
|
+
snapshot.projectionId === select.id &&
|
|
177
|
+
snapshot.projectionType === select.projection,
|
|
178
|
+
)
|
|
179
|
+
},
|
|
180
|
+
delete(target, fromPosition) {
|
|
181
|
+
for (const snapshot of projectionSnapshots) {
|
|
182
|
+
if (target.id && snapshot.projectionId !== target.id) {
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (snapshot.projectionType !== target.projection) {
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (fromPosition && snapshot.lastEventPosition < fromPosition) {
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
projectionSnapshots.delete(snapshot)
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
incrementAppliedCount(select) {
|
|
198
|
+
const key = `${select.projection}-${select.id}`
|
|
199
|
+
const nextCount = (projectionAppliedCount.get(key) ?? 0) + 1
|
|
200
|
+
projectionAppliedCount.set(key, nextCount)
|
|
201
|
+
return nextCount
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
aggregateRootSnapshot: {
|
|
206
|
+
put(snapshot) {
|
|
207
|
+
aggregateRootSnapshots.set(
|
|
208
|
+
`${snapshot.streamType}:${snapshot.streamId}`,
|
|
209
|
+
snapshot,
|
|
210
|
+
)
|
|
211
|
+
},
|
|
212
|
+
get(select) {
|
|
213
|
+
return aggregateRootSnapshots.get(`${select.stream}:${select.id}`)
|
|
214
|
+
},
|
|
215
|
+
delete(target, fromStreamVersion) {
|
|
216
|
+
const key = `${target.stream}:${target.id}`
|
|
217
|
+
const snapshot = aggregateRootSnapshots.get(key)
|
|
218
|
+
if (snapshot && snapshot.streamVersion > fromStreamVersion) {
|
|
219
|
+
aggregateRootSnapshots.delete(key)
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
}) as any
|
|
224
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './redisLock.ts'
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Redis } from 'ioredis'
|
|
2
|
+
|
|
3
|
+
import type { LockCreator } from '../lock.ts'
|
|
4
|
+
|
|
5
|
+
const extendScript = `
|
|
6
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
7
|
+
return redis.call("pexpire", KEYS[1], ARGV[2])
|
|
8
|
+
else
|
|
9
|
+
return 0
|
|
10
|
+
end
|
|
11
|
+
`
|
|
12
|
+
|
|
13
|
+
const deleteScript = `
|
|
14
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
15
|
+
return redis.call("del", KEYS[1])
|
|
16
|
+
else
|
|
17
|
+
return 0
|
|
18
|
+
end
|
|
19
|
+
`
|
|
20
|
+
|
|
21
|
+
export function redisLock(client: Redis, defaultTtl = 5000): LockCreator {
|
|
22
|
+
return async (rawKey) => {
|
|
23
|
+
const key = Array.isArray(rawKey) ? rawKey.join(':') : rawKey
|
|
24
|
+
const token = crypto.randomUUID()
|
|
25
|
+
|
|
26
|
+
while (true) {
|
|
27
|
+
const acquired = await client.set(key, token, 'PX', defaultTtl, 'NX')
|
|
28
|
+
|
|
29
|
+
if (acquired !== 'OK') {
|
|
30
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let isReleased = false
|
|
35
|
+
return {
|
|
36
|
+
async extend(ttl) {
|
|
37
|
+
const extended = await client.eval(
|
|
38
|
+
extendScript,
|
|
39
|
+
1,
|
|
40
|
+
key,
|
|
41
|
+
token,
|
|
42
|
+
ttl ?? defaultTtl / 2,
|
|
43
|
+
)
|
|
44
|
+
if (extended === 0) {
|
|
45
|
+
isReleased = true
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
return true
|
|
49
|
+
},
|
|
50
|
+
async release() {
|
|
51
|
+
if (isReleased) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
isReleased = true
|
|
56
|
+
await client.eval(deleteScript, 1, key, token)
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/refresh.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
+
|
|
3
|
+
import { RefreshingSkipError } from './errors.ts'
|
|
4
|
+
|
|
5
|
+
const refreshStore = new AsyncLocalStorage<boolean>()
|
|
6
|
+
|
|
7
|
+
export function isRefreshing() {
|
|
8
|
+
return refreshStore.getStore() ?? false
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function withRefreshing<T extends () => any>(handler: T) {
|
|
12
|
+
return refreshStore.run(true, handler)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function skipRefreshing() {
|
|
16
|
+
if (isRefreshing()) {
|
|
17
|
+
throw new RefreshingSkipError()
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/replay.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
+
|
|
3
|
+
import { ReplaySkipError } from './errors.ts'
|
|
4
|
+
|
|
5
|
+
const replayStore = new AsyncLocalStorage<boolean>()
|
|
6
|
+
|
|
7
|
+
export function isReplaying() {
|
|
8
|
+
return replayStore.getStore() ?? false
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function withReplaying<T extends () => any>(handler: T) {
|
|
12
|
+
return replayStore.run(true, handler)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function skipReplay() {
|
|
16
|
+
if (isReplaying()) {
|
|
17
|
+
throw new ReplaySkipError()
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { z } from 'zod/v4'
|
|
2
|
+
|
|
3
|
+
import { ValidationError } from './errors.ts'
|
|
4
|
+
import type {
|
|
5
|
+
BaseOutputEvent,
|
|
6
|
+
Keys,
|
|
7
|
+
Max,
|
|
8
|
+
MaybePromise,
|
|
9
|
+
Merge,
|
|
10
|
+
} from './utilityTypes.js'
|
|
11
|
+
|
|
12
|
+
type RegisteredSnapshotSchemas = { schemaVersion: number; schema: z.ZodType }
|
|
13
|
+
|
|
14
|
+
type LatestSnapshotSchemaInput<
|
|
15
|
+
T extends RegisteredSnapshotSchemas,
|
|
16
|
+
M = Max<T['schemaVersion']>,
|
|
17
|
+
> = T extends { schemaVersion: M } ? z.input<T['schema']> : never
|
|
18
|
+
|
|
19
|
+
type OutputSnapshot<T extends RegisteredSnapshotSchemas> = T extends {
|
|
20
|
+
schemaVersion: infer V
|
|
21
|
+
schema: infer S
|
|
22
|
+
}
|
|
23
|
+
? {
|
|
24
|
+
schemaVersion: V
|
|
25
|
+
state: z.output<S>
|
|
26
|
+
createdAt: Date
|
|
27
|
+
}
|
|
28
|
+
: never
|
|
29
|
+
|
|
30
|
+
type SnapshotApplier<Base, T extends RegisteredSnapshotSchemas> = (
|
|
31
|
+
snapshot: OutputSnapshot<T> & Base,
|
|
32
|
+
) => MaybePromise<void>
|
|
33
|
+
type SnapshotCapturer<T extends RegisteredSnapshotSchemas> = (
|
|
34
|
+
event: BaseOutputEvent,
|
|
35
|
+
) => MaybePromise<LatestSnapshotSchemaInput<T> | undefined | null | void>
|
|
36
|
+
|
|
37
|
+
export type SnapshotGenerator<
|
|
38
|
+
Base,
|
|
39
|
+
Schemas extends RegisteredSnapshotSchemas = never,
|
|
40
|
+
Flags extends {
|
|
41
|
+
every: boolean
|
|
42
|
+
apply: boolean
|
|
43
|
+
capture: boolean
|
|
44
|
+
schema: boolean
|
|
45
|
+
} = {
|
|
46
|
+
every: false
|
|
47
|
+
apply: true
|
|
48
|
+
capture: true
|
|
49
|
+
schema: false
|
|
50
|
+
},
|
|
51
|
+
> = {
|
|
52
|
+
every(
|
|
53
|
+
count: number,
|
|
54
|
+
): Omit<
|
|
55
|
+
SnapshotGenerator<Base, Schemas, Merge<Flags, { every: true }>>,
|
|
56
|
+
Keys<Merge<Flags, { every: true }>>
|
|
57
|
+
>
|
|
58
|
+
schema<Schema extends z.ZodType, V extends number = 0>(
|
|
59
|
+
schema: Schema,
|
|
60
|
+
schemaVersion?: V,
|
|
61
|
+
): Omit<
|
|
62
|
+
SnapshotGenerator<
|
|
63
|
+
Base,
|
|
64
|
+
Schemas | { schemaVersion: V; schema: Schema },
|
|
65
|
+
Merge<Flags, { apply: false; capture: false }>
|
|
66
|
+
>,
|
|
67
|
+
Keys<Merge<Flags, { apply: false; capture: false }>>
|
|
68
|
+
>
|
|
69
|
+
apply(
|
|
70
|
+
applier: SnapshotApplier<Base, Schemas>,
|
|
71
|
+
): Omit<
|
|
72
|
+
SnapshotGenerator<
|
|
73
|
+
Base,
|
|
74
|
+
Schemas,
|
|
75
|
+
Merge<Flags, { apply: true; schema: true }>
|
|
76
|
+
>,
|
|
77
|
+
Keys<Merge<Flags, { apply: true; schema: true }>>
|
|
78
|
+
>
|
|
79
|
+
capture(
|
|
80
|
+
capturer: SnapshotCapturer<Schemas>,
|
|
81
|
+
): Omit<
|
|
82
|
+
SnapshotGenerator<
|
|
83
|
+
Base,
|
|
84
|
+
Schemas,
|
|
85
|
+
Merge<Flags, { capture: true; schema: true }>
|
|
86
|
+
>,
|
|
87
|
+
Keys<Merge<Flags, { capture: true; schema: true }>>
|
|
88
|
+
>
|
|
89
|
+
readonly complete: Flags['schema'] extends true
|
|
90
|
+
? Flags['capture'] extends true
|
|
91
|
+
? Flags['apply'] extends true
|
|
92
|
+
? true
|
|
93
|
+
: false
|
|
94
|
+
: false
|
|
95
|
+
: false
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export type InternalSnapshotGenerator = {
|
|
99
|
+
internalApply: (snapshot: OutputSnapshot<any>) => Promise<void>
|
|
100
|
+
internalCapture: (
|
|
101
|
+
event: BaseOutputEvent,
|
|
102
|
+
applied: number,
|
|
103
|
+
) => Promise<null | { state: any; version: number }>
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type CompleteSnapshotGenerator = Omit<
|
|
107
|
+
SnapshotGenerator<
|
|
108
|
+
any,
|
|
109
|
+
RegisteredSnapshotSchemas,
|
|
110
|
+
{ every: true; apply: true; capture: true; schema: true }
|
|
111
|
+
>,
|
|
112
|
+
'schema' | 'capture' | 'apply' | 'every'
|
|
113
|
+
>
|
|
114
|
+
|
|
115
|
+
export default function createSnapshotGenerator<Base>(
|
|
116
|
+
schemas: Array<RegisteredSnapshotSchemas> = [],
|
|
117
|
+
config: { every: number } | null = null,
|
|
118
|
+
applier:
|
|
119
|
+
| ((snapshot: OutputSnapshot<any>) => MaybePromise<void>)
|
|
120
|
+
| null = null,
|
|
121
|
+
capturer: ((event: BaseOutputEvent) => any) | null = null,
|
|
122
|
+
): CompleteSnapshotGenerator {
|
|
123
|
+
return {
|
|
124
|
+
async internalApply(snapshot) {
|
|
125
|
+
const result = z
|
|
126
|
+
.union(schemas.map(({ schema }) => schema))
|
|
127
|
+
.safeParse(snapshot.state)
|
|
128
|
+
|
|
129
|
+
if (!result.success) {
|
|
130
|
+
throw new ValidationError(
|
|
131
|
+
`loaded snapshot version ${snapshot.schemaVersion} does not comply with registered schema`,
|
|
132
|
+
result.error,
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await applier!(snapshot)
|
|
137
|
+
},
|
|
138
|
+
async internalCapture(event, applied) {
|
|
139
|
+
if (!config || applied % config.every !== 0) {
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const latestSchema = schemas
|
|
144
|
+
.slice(1)
|
|
145
|
+
.reduce(
|
|
146
|
+
(latest, schema) =>
|
|
147
|
+
schema.schemaVersion > latest.schemaVersion ? schema : latest,
|
|
148
|
+
schemas[0],
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
const result = latestSchema.schema.safeParse(await capturer!(event))
|
|
152
|
+
|
|
153
|
+
if (!result.success) {
|
|
154
|
+
throw new ValidationError(
|
|
155
|
+
`generated snapshot does not comply with schema for version ${latestSchema.schemaVersion}`,
|
|
156
|
+
result.error,
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
state: result.data,
|
|
162
|
+
version: latestSchema.schemaVersion,
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
every(count) {
|
|
166
|
+
return createSnapshotGenerator(
|
|
167
|
+
schemas,
|
|
168
|
+
{ every: count },
|
|
169
|
+
applier,
|
|
170
|
+
capturer,
|
|
171
|
+
)
|
|
172
|
+
},
|
|
173
|
+
schema(schema, schemaVersion) {
|
|
174
|
+
schemas.push({ schemaVersion: schemaVersion ?? 0, schema })
|
|
175
|
+
return createSnapshotGenerator(schemas, config, null, null) as any
|
|
176
|
+
},
|
|
177
|
+
apply(applier) {
|
|
178
|
+
return createSnapshotGenerator(schemas, config, applier as any, capturer)
|
|
179
|
+
},
|
|
180
|
+
capture(capturer) {
|
|
181
|
+
return createSnapshotGenerator(schemas, config, applier, capturer as any)
|
|
182
|
+
},
|
|
183
|
+
complete: true,
|
|
184
|
+
} as SnapshotGenerator<
|
|
185
|
+
Base,
|
|
186
|
+
RegisteredSnapshotSchemas,
|
|
187
|
+
{ capture: true; apply: true; every: true; schema: true }
|
|
188
|
+
> &
|
|
189
|
+
InternalSnapshotGenerator
|
|
190
|
+
}
|
package/src/superjson.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import SuperJSON from 'superjson'
|
|
2
|
+
|
|
3
|
+
export const superjson = new SuperJSON()
|
|
4
|
+
|
|
5
|
+
export function extendTypes(handler: (superjson: SuperJSON) => void) {
|
|
6
|
+
handler(superjson)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function clone<T>(obj: T): T {
|
|
10
|
+
return superjson.deserialize(superjson.serialize(obj))
|
|
11
|
+
}
|