command-queue.js 2.58 KB
/**
 * CommandQueue - 多 Lane 命令队列
 *
 * 参考 openclaw/src/process/command-queue.ts
 *
 * 核心概念:
 *   Lane       = 逻辑隔离的命令通道 (如 "main", "subagent", "prewarm")
 *   Generation = 每次 clearAll 后递增,旧任务被 CommandLaneClearedError 拒绝
 *
 * 每个 lane 内的任务严格串行执行。
 * 不同 lane 之间可以并行。
 * clearAll() 会使尚在队列中的任务全部失败,防止 "脏" 回调污染新状态。
 */

class CommandLaneClearedError extends Error {
  /**
   * @param {string} [lane]
   */
  constructor(lane) {
    super(lane ? `Command lane "${lane}" cleared` : 'Command lane cleared');
    this.name = 'CommandLaneClearedError';
    this.lane = lane;
  }
}

class CommandQueue {
  constructor() {
    /** @type {Map<string, Promise<void>>} lane → tail promise */
    this._lanes = new Map();
    this._generation = 0;
  }

  /**
   * 向指定 lane 入队一个任务
   *
   * 任务会在该 lane 的前一个任务完成后执行。
   * 如果在排队期间调用了 clearAll(),任务会被 CommandLaneClearedError 拒绝。
   *
   * @template T
   * @param {string} lane - lane 名称
   * @param {() => Promise<T>} task - 异步任务
   * @returns {Promise<T>}
   */
  enqueue(lane, task) {
    const gen = this._generation;

    const prior = this._lanes.get(lane) ?? Promise.resolve();

    const wrapped = prior
      .catch(() => undefined)
      .then(() => {
        // generation 变了 → 队列已被清空,拒绝执行
        if (this._generation !== gen) {
          throw new CommandLaneClearedError(lane);
        }
        return task();
      });

    // 追踪 lane 的尾部 promise(吞掉错误防止 unhandled rejection)
    const tracked = wrapped.catch(() => undefined).finally(() => {
      // 如果这是该 lane 最后一个任务,清理引用
      if (this._lanes.get(lane) === tracked) {
        this._lanes.delete(lane);
      }
    });
    this._lanes.set(lane, tracked);

    return wrapped;
  }

  /**
   * 清除所有 lane,递增 generation
   *
   * 已排队但未执行的任务会收到 CommandLaneClearedError。
   * 正在执行的任务不受影响(但其后续排队任务会被拒绝)。
   */
  clearAll() {
    this._generation++;
    this._lanes.clear();
  }

  /**
   * 清除特定 lane
   * @param {string} lane
   */
  clearLane(lane) {
    this._lanes.delete(lane);
  }

  /** 当前 generation */
  get generation() {
    return this._generation;
  }

  /** lane 数量 */
  get size() {
    return this._lanes.size;
  }
}

module.exports = { CommandQueue, CommandLaneClearedError };