| ... | @@ -162,130 +162,125 @@ function mapCommandToEvent(command: OrchestrationCommand): Omit<OrchestrationEve | ... | @@ -162,130 +162,125 @@ function mapCommandToEvent(command: OrchestrationCommand): Omit<OrchestrationEve |
| 162 | | 162 | |
| 163 | const decodeUnknownCommand = Schema.decodeUnknownEffect(OrchestrationCommandSchema); | 163 | const decodeUnknownCommand = Schema.decodeUnknownEffect(OrchestrationCommandSchema); |
| 164 | | 164 | |
| 165 | const makeOrchestrationEngine = | 165 | const makeOrchestrationEngine = Effect.gen(function* () { |
| 166 | Effect.gen(function* () { | 166 | const logger = createLogger("orchestration"); |
| 167 | const logger = createLogger("orchestration"); | 167 | const eventStore = yield* OrchestrationEventRepository; |
| 168 | const eventStore = yield* OrchestrationEventRepository; | | |
| 169 | | 168 | |
| 170 | let readModel = createEmptyReadModel(new Date().toISOString()); | 169 | let readModel = createEmptyReadModel(new Date().toISOString()); |
| 171 | | 170 | |
| 172 | const commandQueue = yield* Queue.unbounded<CommandEnvelope>(); | 171 | const commandQueue = yield* Queue.unbounded<CommandEnvelope>(); |
| 173 | const readModelPubSub = yield* PubSub.unbounded<OrchestrationReadModel>(); | 172 | const readModelPubSub = yield* PubSub.unbounded<OrchestrationReadModel>(); |
| 174 | const eventPubSub = yield* PubSub.unbounded<OrchestrationEvent>(); | 173 | const eventPubSub = yield* PubSub.unbounded<OrchestrationEvent>(); |
| 175 | | 174 | |
| 176 | const readModelListeners = new Set<(snapshot: OrchestrationReadModel) => void>(); | 175 | const readModelListeners = new Set<(snapshot: OrchestrationReadModel) => void>(); |
| 177 | const domainEventListeners = new Set<(event: OrchestrationEvent) => void>(); | 176 | const domainEventListeners = new Set<(event: OrchestrationEvent) => void>(); |
| 178 | | 177 | |
| 179 | const notifyReadModelListeners = (snapshot: OrchestrationReadModel) => | 178 | const notifyReadModelListeners = (snapshot: OrchestrationReadModel) => |
| 180 | Effect.forEach(readModelListeners, (listener) => | 179 | Effect.forEach(readModelListeners, (listener) => |
| 181 | Effect.try({ | 180 | Effect.try({ |
| 182 | try: () => listener(snapshot), | 181 | try: () => listener(snapshot), |
| 183 | catch: toListenerCallbackError("read-model"), | 182 | catch: toListenerCallbackError("read-model"), |
| 184 | }), | 183 | }), |
| 185 | ).pipe(Effect.asVoid); | 184 | ).pipe(Effect.asVoid); |
| 186 | | 185 | |
| 187 | const notifyDomainEventListeners = (event: OrchestrationEvent) => | 186 | const notifyDomainEventListeners = (event: OrchestrationEvent) => |
| 188 | Effect.forEach(domainEventListeners, (listener) => | 187 | Effect.forEach(domainEventListeners, (listener) => |
| 189 | Effect.try({ | 188 | Effect.try({ |
| 190 | try: () => listener(event), | 189 | try: () => listener(event), |
| 191 | catch: toListenerCallbackError("domain-event"), | 190 | catch: toListenerCallbackError("domain-event"), |
| 192 | }), | 191 | }), |
| 193 | ).pipe(Effect.asVoid); | 192 | ).pipe(Effect.asVoid); |
| 194 | | 193 | |
| 195 | const processEnvelope = (envelope: CommandEnvelope): Effect.Effect<void> => | 194 | const processEnvelope = (envelope: CommandEnvelope): Effect.Effect<void> => |
| 196 | Effect.gen(function* () { | 195 | Effect.gen(function* () { |
| 197 | const eventBase = mapCommandToEvent(envelope.command); | 196 | const eventBase = mapCommandToEvent(envelope.command); |
| 198 | const savedEvent = yield* eventStore.append(eventBase); | 197 | const savedEvent = yield* eventStore.append(eventBase); |
| 199 | readModel = yield* reduceEvent(readModel, savedEvent); | 198 | readModel = yield* reduceEvent(readModel, savedEvent); |
| 200 | | 199 | |
| 201 | const snapshot = readModel; | 200 | const snapshot = readModel; |
| 202 | yield* Effect.all([ | 201 | yield* Effect.all([ |
| 203 | PubSub.publish(eventPubSub, savedEvent), | 202 | PubSub.publish(eventPubSub, savedEvent), |
| 204 | PubSub.publish(readModelPubSub, snapshot), | 203 | PubSub.publish(readModelPubSub, snapshot), |
| 205 | ]); | 204 | ]); |
| 206 | | 205 | |
| 207 | yield* notifyDomainEventListeners(savedEvent); | 206 | yield* notifyDomainEventListeners(savedEvent); |
| 208 | yield* notifyReadModelListeners(snapshot); | 207 | yield* notifyReadModelListeners(snapshot); |
| 209 | | 208 | |
| 210 | yield* Deferred.succeed(envelope.result, { sequence: savedEvent.sequence }); | 209 | yield* Deferred.succeed(envelope.result, { sequence: savedEvent.sequence }); |
| 211 | }).pipe( | 210 | }).pipe(Effect.catch((error) => Deferred.fail(envelope.result, error).pipe(Effect.asVoid))); |
| 212 | Effect.catch((error) => Deferred.fail(envelope.result, error).pipe(Effect.asVoid)), | | |
| 213 | ); | | |
| 214 | | 211 | |
| 215 | const bootstrapReadModel: Effect.Effect<void, OrchestrationDispatchError> = Effect.gen( | 212 | const bootstrapReadModel: Effect.Effect<void, OrchestrationDispatchError> = Effect.gen( |
| 216 | function* () { | 213 | function* () { |
| 217 | const existingEvents = yield* eventStore.readAll(); | 214 | const existingEvents = yield* eventStore.readAll(); |
| 218 | for (const event of existingEvents) { | 215 | for (const event of existingEvents) { |
| 219 | readModel = yield* reduceEvent(readModel, event); | 216 | readModel = yield* reduceEvent(readModel, event); |
| 220 | } | 217 | } |
| 221 | }, | 218 | }, |
| 222 | ); | 219 | ); |
| 223 | | 220 | |
| 224 | yield* bootstrapReadModel; | 221 | yield* bootstrapReadModel; |
| 225 | | 222 | |
| 226 | const worker = Effect.forever(Queue.take(commandQueue).pipe(Effect.flatMap(processEnvelope))); | 223 | const worker = Effect.forever(Queue.take(commandQueue).pipe(Effect.flatMap(processEnvelope))); |
| 227 | yield* Effect.forkScoped(worker); | 224 | yield* Effect.forkScoped(worker); |
| 228 | logger.info("orchestration engine started", { | 225 | logger.info("orchestration engine started", { |
| 229 | sequence: readModel.sequence, | 226 | sequence: readModel.sequence, |
| 230 | }); | 227 | }); |
| 231 | | 228 | |
| 232 | const getSnapshot: OrchestrationEngineShape["getSnapshot"] = () => | 229 | const getSnapshot: OrchestrationEngineShape["getSnapshot"] = () => |
| 233 | Effect.sync((): OrchestrationReadModel => readModel); | 230 | Effect.sync((): OrchestrationReadModel => readModel); |
| 234 | | 231 | |
| 235 | const replayEvents: OrchestrationEngineShape["replayEvents"] = (fromSequenceExclusive) => | 232 | const replayEvents: OrchestrationEngineShape["replayEvents"] = (fromSequenceExclusive) => |
| 236 | eventStore.readFromSequence(fromSequenceExclusive); | 233 | eventStore.readFromSequence(fromSequenceExclusive); |
| 237 | | 234 | |
| 238 | const dispatch: OrchestrationEngineShape["dispatch"] = (command) => | 235 | const dispatch: OrchestrationEngineShape["dispatch"] = (command) => |
| 239 | Effect.gen(function* () { | 236 | Effect.gen(function* () { |
| 240 | const result = yield* Deferred.make<{ sequence: number }, OrchestrationDispatchError>(); | 237 | const result = yield* Deferred.make<{ sequence: number }, OrchestrationDispatchError>(); |
| 241 | yield* Queue.offer(commandQueue, { command, result }); | 238 | yield* Queue.offer(commandQueue, { command, result }); |
| 242 | return yield* Deferred.await(result); | 239 | return yield* Deferred.await(result); |
| 243 | }); | 240 | }); |
| 244 | | 241 | |
| 245 | const dispatchUnknown: OrchestrationEngineShape["dispatchUnknown"] = (command) => | 242 | const dispatchUnknown: OrchestrationEngineShape["dispatchUnknown"] = (command) => |
| 246 | Effect.gen(function* () { | 243 | Effect.gen(function* () { |
| 247 | const payload = | 244 | const payload = |
| 248 | typeof command === "string" | 245 | typeof command === "string" |
| 249 | ? yield* Effect.try({ | 246 | ? yield* Effect.try({ |
| 250 | try: () => JSON.parse(command) as unknown, | 247 | try: () => JSON.parse(command) as unknown, |
| 251 | catch: toOrchestrationJsonParseError, | 248 | catch: toOrchestrationJsonParseError, |
| 252 | }) | 249 | }) |
| 253 | : command; | 250 | : command; |
| 254 | | 251 | |
| 255 | const decoded = yield* decodeUnknownCommand(payload).pipe( | 252 | const decoded = yield* decodeUnknownCommand(payload).pipe( |
| 256 | Effect.mapError(toOrchestrationCommandDecodeError), | 253 | Effect.mapError(toOrchestrationCommandDecodeError), |
| 257 | ); | 254 | ); |
| 258 | | 255 | |
| 259 | return yield* dispatch(decoded); | 256 | return yield* dispatch(decoded); |
| 260 | }); | 257 | }); |
| 261 | | 258 | |
| 262 | const subscribeToReadModel: OrchestrationEngineShape["subscribeToReadModel"] = (callback) => | 259 | const subscribeToReadModel: OrchestrationEngineShape["subscribeToReadModel"] = (callback) => |
| 263 | Effect.sync(() => { | 260 | Effect.sync(() => { |
| 264 | readModelListeners.add(callback); | 261 | readModelListeners.add(callback); |
| 265 | return () => { | 262 | return () => { |
| 266 | readModelListeners.delete(callback); | 263 | readModelListeners.delete(callback); |
| 267 | }; | 264 | }; |
| 268 | }); | 265 | }); |
| 269 | | 266 | |
| 270 | const subscribeToDomainEvents: OrchestrationEngineShape["subscribeToDomainEvents"] = ( | 267 | const subscribeToDomainEvents: OrchestrationEngineShape["subscribeToDomainEvents"] = (callback) => |
| 271 | callback, | 268 | Effect.sync(() => { |
| 272 | ) => | 269 | domainEventListeners.add(callback); |
| 273 | Effect.sync(() => { | 270 | return () => { |
| 274 | domainEventListeners.add(callback); | 271 | domainEventListeners.delete(callback); |
| 275 | return () => { | 272 | }; |
| 276 | domainEventListeners.delete(callback); | 273 | }); |
| 277 | }; | | |
| 278 | }); | | |
| 279 | | 274 | |
| 280 | return { | 275 | return { |
| 281 | getSnapshot, | 276 | getSnapshot, |
| 282 | replayEvents, | 277 | replayEvents, |
| 283 | dispatchUnknown, | 278 | dispatchUnknown, |
| 284 | dispatch, | 279 | dispatch, |
| 285 | subscribeToReadModel, | 280 | subscribeToReadModel, |
| 286 | subscribeToDomainEvents, | 281 | subscribeToDomainEvents, |
| 287 | } satisfies OrchestrationEngineShape; | 282 | } satisfies OrchestrationEngineShape; |
| 288 | }); | 283 | }); |
| 289 | | 284 | |
| 290 | export const OrchestrationEngineLive = Layer.effect( | 285 | export const OrchestrationEngineLive = Layer.effect( |
| 291 | OrchestrationEngineService, | 286 | OrchestrationEngineService, |