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