NestJS CQRS Saga场景下基于RxJS的事件超时拦截与类型区分实现问询
NestJS CQRS 场景下带类型隔离的RxJS窗口中断实现方案
核心实现约束对齐
首先落地两个必须满足的业务规则:
- 3秒监听窗口从收到对应类型的
ValueHasBeenSet事件的时刻启动,而非全局统一计时 - 窗口的中断逻辑仅对同
type的ValueHasBeenUnset事件生效,不同业务类型的事件流完全隔离,无跨类型干扰 - 不修改原有
ValueHasBeenSet、ValueHasBeenUnset的通用事件定义,仅基于现有携带的type字段做逻辑拆分
可选实现方案
方案1:基于switchMap + takeUntil的轻量实现
这个方案最贴合NestJS CQRS原生事件流的使用习惯,利用RxJS内置的流自动取消特性完成中断逻辑,代码量最少。
核心逻辑:
- 对全局事件流按
type字段分组,实现不同业务类型的流隔离 - 每个分组内单独拆分设置、取消两类事件的子流
- 每收到一次设置事件,就启动一个3秒定时器流,用同分组的取消事件流作为
takeUntil的终止信号
适配后的完整代码:
import { ofType } from '@nestjs/cqrs'; import { groupBy, mergeMap, switchMap, takeUntil, timer, map } from 'rxjs'; import { ValueHasBeenSet, ValueHasBeenUnset } from './event-definitions'; import { SomeCommand } from './commands'; valuePersisted = (events$: Observable<any>): Observable<ICommand> => { return events$.pipe( // 按事件携带的type值分组,从根源避免不同类型事件的串扰 groupBy(event => event.type), // 每个分组独立执行业务逻辑 mergeMap(typeGroup$ => { // 拆分当前分组下的两类事件 const setEventStream = typeGroup$.pipe(ofType(ValueHasBeenSet)); const cancelEventStream = typeGroup$.pipe(ofType(ValueHasBeenUnset)); return setEventStream.pipe( // 每次收到设置事件,启动独立的3秒定时器 switchMap(() => timer(3000).pipe( // 窗口内只要收到同类型的取消事件,立刻中断当前定时器,不输出命令 takeUntil(cancelEventStream), // 定时器正常到期才向外输出待执行的命令 map(() => new SomeCommand()) ) ) ) }) ) }
适用场景
如果业务要求「设置事件在3秒窗口内重复触发时,自动重置3秒计时窗口(从最后一次收到设置事件的时刻重新算3秒)」,这个方案是最优选择,不需要额外写重置逻辑。
方案2:基于race竞态的严格窗口实现
如果业务要求「每个设置事件触发的3秒窗口是独立的,哪怕窗口内重复收到同类型的设置事件,也不会重置计时,严格从第一次设置的时刻算满3秒再判断」,就需要用竞态逻辑替代switchMap的自动重置能力。
核心逻辑:
- 依然按
type字段做全局流分组,保证类型隔离 - 每个分组内,对每一个流过来的事件,做两个分支的竞态判断:
- 分支1:如果当前事件是
ValueHasBeenSet,启动3秒定时器,到期输出SomeCommand - 分支2:如果当前分组内未来3秒内收到
ValueHasBeenUnset事件,就直接废弃当前待触发的命令,不向外输出任何内容
- 分支1:如果当前事件是
适配代码:
import { ofType } from '@nestjs/cqrs'; import { groupBy, mergeMap, race, timer, map, of, ignoreElements, take } from 'rxjs'; import { ValueHasBeenSet, ValueHasBeenUnset } from './event-definitions'; import { SomeCommand } from './commands'; valuePersisted = (events$: Observable<any>): Observable<ICommand> => { return events$.pipe( groupBy(event => event.type), mergeMap(typeGroup$ => typeGroup$.pipe( mergeMap(currentEvent => { // 仅对设置事件启动计时判断 if (!(currentEvent instanceof ValueHasBeenSet)) return of(); // 分支1:3秒定时到期,输出待执行命令 const timeoutTrigger$ = timer(3000).pipe( map(() => new SomeCommand()) ); // 分支2:同类型取消事件先到,无任何输出,中断流程 const cancelTrigger$ = typeGroup$.pipe( ofType(ValueHasBeenUnset), take(1), ignoreElements() ); // 两个分支竞态,谁先触发就执行谁的逻辑 return race(timeoutTrigger$, cancelTrigger$); }) ) ) ) }
适用场景
适合需要严格控制窗口计时、不希望重复设置事件打乱原有计时逻辑的场景,每个设置事件对应的窗口完全独立,不会被后续的重复设置事件干扰。
常见踩坑提醒
- 不要跳过
groupBy分组直接全局监听取消事件:否则只要收到任意类型的取消事件,就会把所有类型下待触发的命令全部中断,完全丢失类型隔离的能力 - 不要把定时器逻辑写到全局流的外层:否则会导致不同类型的事件共享同一个计时窗口,出现blue类型的设置事件等了3秒,却被提前来的yellow类型取消事件打断的异常
- 因为两个事件的类型枚举是有限值(比如固定的
'blue'|'yellow'),groupBy生成的分组流不会出现无限多的问题,不需要手动做分组流的清理,不会有内存泄漏风险
内容的提问来源于stack exchange,提问作者Evgeny Fedorenko
相关产品推荐
相关产品推荐

