@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
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
+ }
@@ -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
+ }