p-queue 并发设为 1,active 回调却启动了两项任务
p-queue 9.3.3 的 active 事件在占用并发名额前触发。从监听器同步添加任务可能重入调度,需调整入队时机或按迁移要求升级。
concurrency: 1 已经设置,两个任务的执行区间却发生重叠。如果第二个任务是在 active 事件监听器里同步调用 queue.add() 加入的,应先检查调度重入,而不是继续调小并发值。
p-queue 9.3.3 先触发 active,再执行取出的任务。此时第一个任务还没增加内部 pending 计数,监听器重新入队会看到“仍有空位”,于是又启动一个任务。事件触发次数多本身不能证明超限,真正要比较的是任务体同时运行的数量。
将新任务的生产移出 active 监听器
active 适合记录状态,不能在存在这条旧实现路径时把它当成同步生产任务的钩子。已知任务可以在普通控制流程中加入;需要由运行事件产生后续任务时,可以延后入队,避开当前同步调度栈:
queue.once('active', () => {
queueMicrotask(() => {
queue.add(nextWork).catch(handleTaskError);
});
});
这里的 queue 是现有 9.3.3 实例,nextWork 与 handleTaskError 是应用自己的函数。使用 once 是为了只派生一次;每次 active 都无限增加新任务,会造成另一种队列增长。微任务中的入队也必须处理返回 Promise,不能把拒绝留成未处理异常。
这只能避开同步重入,不会等待 nextWork 完成。业务如果需要等待整条任务链,应显式保存并等待派生任务的 Promise。不能在派生任务尚未加入前就调用一次 onIdle,然后把它当成未来所有工作都已结束。
更容易维护的做法,是让生产者直接决定任务依赖关系,active 只做观察。若监听器还有状态更新或统计用途,迁移入队位置时保留这些行为,不必删除全部监听器。
正式 10.0.0 已调整顺序,也更换了事件接口
10.0.0 会先启动任务,再发 active 事件。任务占用的并发名额已经可见,同步监听器不再看到旧的空位。但它要求 Node 22 或更新,并从旧事件接口切换为 EventTarget。
因此,不能只更新版本后原样保留 .once('active', ...)。新接口使用 addEventListener('active', listener, { once: true }),其他 completed、error 等监听器也要按对应事件参数迁移。浏览器与 jsdom 集成还需检查各自的 EventTarget 环境。
对暂时保留 9.x 的项目,可以先调整生产流程;升级时再依次验证 Node 要求、事件接口和任务取消行为。不要为了序列化任务而在任务内部再等待同一个单并发队列中的任务,这会让当前任务占着唯一名额等待下一项,形成死锁。
验收时让两个任务停留在可控制的等待点,分别记录开始与结束,确认实际运行峰值不超过配置。还应覆盖任务拒绝、后续任务正常开始,以及所有派生任务都被等待的情况。只查看 queue.concurrency 仍等于 1,不能证明执行过程符合限制。


