/**
 * This file is part of the NocoBase (R) project.
 * Copyright (c) 2020-2024 NocoBase Co., Ltd.
 * Authors: NocoBase Team.
 *
 * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
 * For more information, please refer to: https://www.nocobase.com/agreement.
 */

var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  // If the importer is in node compatibility mode or this is not an ESM
  // file that has been converted to a CommonJS file using a Babel-
  // compatible transform (i.e. "__esModule" has not been set), then set
  // "default" to the CommonJS "module.exports" for node compatibility.
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var Processor_exports = {};
__export(Processor_exports, {
  default: () => Processor
});
module.exports = __toCommonJS(Processor_exports);
var import_sequelize = require("sequelize");
var import_database = require("@nocobase/database");
var import_evaluators = require("@nocobase/evaluators");
var import_utils = require("@nocobase/utils");
var import_set = __toESM(require("lodash/set"));
var import_constants = require("./constants");
var import_timeout_errors = require("./timeout-errors");
const JOB_SAVE_BATCH_SIZE = 100;
const OPEN_SCOPE_PENDING_ERROR = "Pending jobs are not allowed inside an open transaction scope";
class Processor {
  constructor(execution, options) {
    this.execution = execution;
    this.options = options;
    this.logger = options.plugin.getLogger(execution.workflowId);
    this.transaction = options.transaction;
  }
  static StatusMap = {
    [import_constants.JOB_STATUS.PENDING]: import_constants.EXECUTION_STATUS.STARTED,
    [import_constants.JOB_STATUS.RESOLVED]: import_constants.EXECUTION_STATUS.RESOLVED,
    [import_constants.JOB_STATUS.FAILED]: import_constants.EXECUTION_STATUS.FAILED,
    [import_constants.JOB_STATUS.ERROR]: import_constants.EXECUTION_STATUS.ERROR,
    [import_constants.JOB_STATUS.ABORTED]: import_constants.EXECUTION_STATUS.ABORTED,
    [import_constants.JOB_STATUS.CANCELED]: import_constants.EXECUTION_STATUS.CANCELED,
    [import_constants.JOB_STATUS.REJECTED]: import_constants.EXECUTION_STATUS.REJECTED,
    [import_constants.JOB_STATUS.RETRY_NEEDED]: import_constants.EXECUTION_STATUS.RETRY_NEEDED
  };
  logger;
  /**
   * @experimental
   */
  transaction = null;
  /**
   * @experimental
   */
  nodes = [];
  /**
   * @experimental
   */
  nodesMap = /* @__PURE__ */ new Map();
  jobsMapByNodeKey = {};
  jobResultsMapByNodeKey = {};
  jobsToSave = /* @__PURE__ */ new Map();
  scopeTransactions = /* @__PURE__ */ new Map();
  rerunContext = null;
  /**
   * @experimental
   */
  lastSavedJob = null;
  abortController = new AbortController();
  timeoutGuard = null;
  runningRegistered = false;
  abortReason = null;
  aborted = false;
  get abortSignal() {
    return this.abortController.signal;
  }
  setTimeoutGuard(ms) {
    if (this.timeoutGuard) {
      clearTimeout(this.timeoutGuard);
    }
    this.timeoutGuard = setTimeout(() => {
      this.abortExecution(import_constants.EXECUTION_REASON.TIMEOUT);
    }, ms);
  }
  abortExecution(reason) {
    this.aborted = true;
    this.abortReason = reason ?? null;
    if (!this.abortController.signal.aborted) {
      this.abortController.abort(
        reason === import_constants.EXECUTION_REASON.TIMEOUT ? new import_timeout_errors.WorkflowTimeoutError("Workflow execution has been aborted") : new Error("Workflow execution has been aborted")
      );
    }
  }
  isTimeoutAborted() {
    return this.abortSignal.aborted;
  }
  /**
   * Create an independent abort handle for background work that outlives this processor's
   * run loop (e.g. fire-and-forget instructions that resume the job later). It mirrors the
   * current abort state and sets its own timer based on the execution's `expiresAt`, so the
   * timeout still applies after the processor has exited its synchronous run.
   *
   * The caller must invoke `dispose()` once the background work settles to release the timer
   * and the abort listener.
   */
  createBackgroundAbortHandle() {
    const controller = new AbortController();
    const sourceSignal = this.abortSignal;
    let timeoutGuard = null;
    let sourceListener = null;
    const abort = (reason) => {
      if (!controller.signal.aborted) {
        controller.abort((0, import_timeout_errors.isWorkflowTimeoutError)(reason) ? reason : new import_timeout_errors.WorkflowTimeoutError());
      }
    };
    if (sourceSignal.aborted) {
      abort(sourceSignal.reason);
    } else {
      sourceListener = () => abort(sourceSignal.reason);
      sourceSignal.addEventListener("abort", sourceListener, { once: true });
    }
    const remaining = this.execution.expiresAt ? this.execution.expiresAt.getTime() - Date.now() : null;
    if (remaining != null) {
      if (remaining <= 0) {
        abort();
      } else {
        timeoutGuard = setTimeout(abort, remaining);
      }
    }
    return {
      signal: controller.signal,
      dispose: () => {
        if (timeoutGuard) {
          clearTimeout(timeoutGuard);
          timeoutGuard = null;
        }
        if (sourceListener) {
          sourceSignal.removeEventListener("abort", sourceListener);
          sourceListener = null;
        }
      },
      throwIfAborted: () => {
        if (controller.signal.aborted) {
          throw controller.signal.reason ?? new import_timeout_errors.WorkflowTimeoutError();
        }
      }
    };
  }
  /**
   * Reload a job and return it only when it is still pending, otherwise `null`. Background
   * work uses this before resuming so it never overwrites a job that another path (timeout
   * abort, a competing resume) has already settled.
   */
  async findPendingJob(jobId) {
    const job = await this.options.plugin.db.getRepository("jobs").findOne({
      filterByTk: jobId
    });
    return (job == null ? void 0 : job.status) === import_constants.JOB_STATUS.PENDING ? job : null;
  }
  // make dual linked nodes list then cache
  makeNodes(nodes = []) {
    this.nodes = nodes;
    nodes.forEach((node) => {
      this.nodesMap.set(node.id, node);
    });
    nodes.forEach((node) => {
      if (node.upstreamId) {
        node.upstream = this.nodesMap.get(node.upstreamId);
      }
      if (node.downstreamId) {
        node.downstream = this.nodesMap.get(node.downstreamId);
      }
    });
  }
  makeJobs(jobs) {
    for (const job of jobs) {
      const node = this.nodesMap.get(job.nodeId);
      if (!node) {
        this.logger.warn(`node (#${job.nodeId}) not found for job (#${job.id}), this will lead to unexpected error`);
        continue;
      }
      this.jobsMapByNodeKey[node.key] = job;
      this.jobResultsMapByNodeKey[node.key] = job.result;
    }
  }
  async prepare() {
    const {
      execution,
      options: { plugin }
    } = this;
    if (!execution.workflow) {
      execution.workflow = plugin.enabledCache.get(execution.workflowId) || await execution.getWorkflow();
    }
    if (!execution.workflow) {
      throw new Error(`workflow (#${execution.workflowId}) not found for execution (#${execution.id})`);
    }
    const nodes = execution.workflow.nodes || await execution.workflow.getNodes();
    execution.workflow.nodes = nodes;
    this.makeNodes(nodes);
    const JobDBModel = plugin.db.getModel("jobs");
    const jobIds = await JobDBModel.findAll({
      attributes: ["executionId", "nodeId", [(0, import_sequelize.fn)("MAX", (0, import_sequelize.col)("id")), "id"]],
      group: ["executionId", "nodeId"],
      where: {
        executionId: execution.id
      },
      raw: true
    });
    const jobs = await execution.getJobs({
      where: {
        id: jobIds.map((item) => item.id)
      },
      order: [["id", "ASC"]]
    });
    execution.jobs = jobs;
    this.makeJobs(jobs);
  }
  async start() {
    const { execution } = this;
    if (!await this.shouldContinueExecution()) {
      this.logger.warn(`execution was ended with status ${execution.status} before, could not be started again`, {
        workflowId: execution.workflowId
      });
      return;
    }
    this.enterRunningState();
    try {
      await this.prepare();
      if (this.nodes.length) {
        const head = this.nodes.find((item) => !item.upstream);
        if (!head) {
          this.logger.warn(`head node not found for workflow (${execution.workflowId}), could not be started`, {
            workflowId: execution.workflowId
          });
          return this.exit(import_constants.JOB_STATUS.ERROR);
        }
        await this.run(head);
      } else {
        await this.exit(import_constants.JOB_STATUS.RESOLVED);
      }
    } finally {
      this.leaveRunningState();
    }
  }
  async resume(job) {
    const { execution } = this;
    if (!await this.shouldContinueExecution()) {
      this.logger.warn(`execution was ended with status ${execution.status} before, could not be resumed`, {
        workflowId: execution.workflowId
      });
      return;
    }
    this.enterRunningState();
    try {
      await this.prepare();
      const node = this.nodesMap.get(job.nodeId);
      await this.recall(node, job);
    } finally {
      this.leaveRunningState();
    }
  }
  resolveRerun(options = {}) {
    const node = this.getRerunNode(options.nodeId);
    const targetJob = this.jobsMapByNodeKey[node.key];
    if (options.nodeId != null && !targetJob) {
      throw new Error(`job of node (#${node.id}) not found in execution (#${this.execution.id})`);
    }
    if (options.nodeId == null && options.overwrite && !targetJob) {
      throw new Error(`job of head node (#${node.id}) not found in execution (#${this.execution.id})`);
    }
    const input = this.getRerunInput(node);
    return { node, input, targetJob };
  }
  async rerun(options = {}) {
    const { execution } = this;
    if (execution.status !== import_constants.EXECUTION_STATUS.STARTED) {
      throw new Error(`execution (#${execution.id}) is not started`);
    }
    if (!await this.shouldContinueExecution()) {
      this.logger.warn(`execution was ended with status ${execution.status} before, could not be rerun`, {
        workflowId: execution.workflowId
      });
      return;
    }
    this.enterRunningState();
    try {
      await this.prepare();
      const { node, input, targetJob } = this.resolveRerun(options);
      this.rerunContext = {
        overwrite: options.overwrite === true,
        targetJob
      };
      return await this.run(node, input, { rerun: true });
    } finally {
      this.rerunContext = null;
      this.leaveRunningState();
    }
  }
  getRerunNode(nodeId) {
    if (nodeId != null) {
      const node = this.nodesMap.get(nodeId) || this.nodes.find((item) => String(item.id) === String(nodeId));
      if (!node) {
        throw new Error(`node (#${nodeId}) not found in workflow (#${this.execution.workflowId})`);
      }
      return node;
    }
    const head = this.nodes.find((item) => !item.upstream);
    if (!head) {
      throw new Error(`head node not found in workflow (#${this.execution.workflowId})`);
    }
    return head;
  }
  getRerunInput(node) {
    if (!node.upstream) {
      return { result: this.execution.context };
    }
    const upstreamJob = this.jobsMapByNodeKey[node.upstream.key];
    if (!upstreamJob) {
      throw new Error(`upstream job of node (#${node.id}) not found in execution (#${this.execution.id})`);
    }
    return upstreamJob;
  }
  async exec(instruction, node, prevJob, options = {}) {
    let job;
    if (!await this.shouldContinueExecution()) {
      return this.exit();
    }
    try {
      this.logger.debug(`config of node`, { data: node.config, workflowId: node.workflowId });
      job = await instruction(node, prevJob, this, { ...options, signal: this.abortSignal });
      if (job === null) {
        return this.exit();
      }
      if (!job) {
        return this.exit(true);
      }
    } catch (err) {
      if ((0, import_timeout_errors.isWorkflowTimeoutError)(err) || this.abortSignal.aborted && this.aborted) {
        job = {
          result: {
            message: err.message
          },
          status: import_constants.JOB_STATUS.ABORTED
        };
      } else {
        this.logger.error(
          `execution (${this.execution.id}) run instruction [${node.type}] for node (${node.id}) failed: `,
          { error: err, workflowId: node.workflowId }
        );
        job = {
          result: err instanceof Error ? {
            ...err,
            message: err.message
          } : err,
          status: import_constants.JOB_STATUS.ERROR
        };
      }
      if (prevJob instanceof import_database.Model && prevJob.nodeId === node.id) {
        prevJob.set(job);
        job = prevJob;
      }
    }
    if (!(job instanceof import_database.Model)) {
      job.nodeId = node.id;
      job.nodeKey = node.key;
    }
    const savedJob = this.saveJob(job);
    this.logger.info(
      `execution (${this.execution.id}) run instruction [${node.type}] for node (${node.id}) finished as status: ${savedJob.status}`,
      {
        workflowId: node.workflowId
      }
    );
    this.logger.debug(`result of node`, { data: savedJob.result });
    if (this.execution.status === import_constants.EXECUTION_STATUS.ABORTED || this.isTimeoutAborted()) {
      return this.exit(import_constants.JOB_STATUS.ABORTED);
    }
    if (savedJob.status === import_constants.JOB_STATUS.RESOLVED && node.downstream) {
      this.logger.debug(`run next node (${node.downstreamId})`);
      return this.run(node.downstream, savedJob);
    }
    return this.end(node, savedJob);
  }
  async run(node, input, options) {
    const { instructions } = this.options.plugin;
    const instruction = instructions.get(node.type);
    if (!instruction) {
      return Promise.reject(new Error(`instruction [${node.type}] not found for node (#${node.id})`));
    }
    if (typeof instruction.run !== "function") {
      return Promise.reject(new Error("`run` should be implemented for customized execution of the node"));
    }
    this.logger.info(`execution (${this.execution.id}) run instruction [${node.type}] for node (${node.id})`, {
      workflowId: node.workflowId
    });
    return this.exec(instruction.run.bind(instruction), node, input, options);
  }
  // parent node should take over the control
  async end(node, job) {
    this.logger.debug(`branch ended at node (${node.id})`);
    const parentNode = this.findBranchParentNode(node);
    if (parentNode) {
      this.logger.debug(`not on main, recall to parent entry node (${node.id})})`, {
        workflowId: node.workflowId
      });
      await this.recall(parentNode, job);
      return null;
    }
    return this.exit(job.status);
  }
  async recall(node, job) {
    const { instructions } = this.options.plugin;
    const instruction = instructions.get(node.type);
    if (!instruction) {
      return Promise.reject(new Error(`instruction [${node.type}] not found for node (#${node.id})`));
    }
    if (typeof instruction.resume !== "function") {
      return Promise.reject(
        new Error(`"resume" method should be implemented for [${node.type}] instruction of node (#${node.id})`)
      );
    }
    this.logger.info(`execution (${this.execution.id}) resume instruction [${node.type}] for node (${node.id})`, {
      workflowId: node.workflowId
    });
    return this.exec(instruction.resume.bind(instruction), node, job);
  }
  async exit(s) {
    this.leaveRunningState();
    if (s === true) {
      return;
    }
    if ((s == null || s === import_constants.JOB_STATUS.PENDING) && this.hasOpenScopeTransactions()) {
      this.markOpenScopePendingJobsAsError(OPEN_SCOPE_PENDING_ERROR);
      s = import_constants.JOB_STATUS.ERROR;
    }
    const hadScopeTransactions = this.scopeTransactions.size > 0;
    await this.cleanupScopeTransactions({ allowCommit: s === import_constants.JOB_STATUS.RESOLVED });
    if (hadScopeTransactions && s == null && this.execution.status === import_constants.EXECUTION_STATUS.STARTED) {
      s = import_constants.JOB_STATUS.ERROR;
    }
    if (this.jobsToSave.size) {
      const newJobs = [];
      for (const job of this.jobsToSave.values()) {
        if (job.isNewRecord) {
          newJobs.push(job);
        } else {
          const JobCollection = this.options.plugin.db.getCollection("jobs");
          const changes = [];
          if (job.changed("status")) {
            changes.push([`status`, job.status]);
            job.changed("status", false);
          }
          if (job.changed("meta")) {
            changes.push([`meta`, JSON.stringify(job.meta ?? null)]);
            job.changed("meta", false);
          }
          if (job.changed("result")) {
            changes.push([`result`, JSON.stringify(job.result ?? null)]);
            job.changed("result", false);
          }
          if (changes.length) {
            await this.options.plugin.db.sequelize.query(
              `UPDATE ${JobCollection.quotedTableName()} SET ${changes.map(([key]) => `${key} = ?`)} WHERE id='${job.id}'`,
              { replacements: changes.map(([, value]) => value) }
            );
          }
        }
      }
      if (newJobs.length) {
        const JobsModel = this.options.plugin.db.getModel("jobs");
        for (let offset = 0; offset < newJobs.length; offset += JOB_SAVE_BATCH_SIZE) {
          const batch = newJobs.slice(offset, offset + JOB_SAVE_BATCH_SIZE);
          await JobsModel.bulkCreate(
            batch.map((job) => job.toJSON()),
            {
              returning: false
            }
          );
          for (const job of batch) {
            job.isNewRecord = false;
          }
        }
      }
      this.jobsToSave.clear();
    }
    if (typeof s === "number") {
      const status = this.constructor.StatusMap[s] ?? Math.sign(s);
      const values = { status };
      if (status === import_constants.EXECUTION_STATUS.ABORTED && this.abortReason) {
        values.reason = this.abortReason;
      }
      const ExecutionModelClass = this.options.plugin.db.getModel("executions");
      const [affected] = await ExecutionModelClass.update(values, {
        where: {
          id: this.execution.id,
          status: import_constants.EXECUTION_STATUS.STARTED
        },
        individualHooks: true
      });
      if (affected) {
        this.execution.set(values);
      } else {
        await this.execution.reload();
      }
    }
    if (this.execution.status === import_constants.EXECUTION_STATUS.STARTED) {
      this.options.plugin.timeoutManager.scheduleExecutionTimeout(this.execution);
    } else {
      this.options.plugin.timeoutManager.clear(this.execution.id);
      this.options.plugin.timeoutManager.invalidateNextExpiresAtIfMatches(this.execution.expiresAt);
    }
    this.logger.info(`execution (${this.execution.id}) exiting with status ${this.execution.status}`, {
      workflowId: this.execution.workflowId
    });
    return null;
  }
  setScopeTransaction(key, info) {
    this.scopeTransactions.set(key, info);
  }
  getScopeTransactionByKey(key) {
    return this.scopeTransactions.get(key) ?? null;
  }
  clearScopeTransaction(key) {
    this.scopeTransactions.delete(key);
  }
  markScopeTransactionClosing(key, action) {
    const scopeTransaction = this.scopeTransactions.get(key);
    if (scopeTransaction) {
      scopeTransaction.closing = action;
    }
  }
  getScopeTransaction(node, dataSourceName) {
    for (let current = node; current; current = current.upstream) {
      const scopeTransaction = this.scopeTransactions.get(current.key);
      if (!scopeTransaction) {
        continue;
      }
      if (dataSourceName && scopeTransaction.dataSource !== dataSourceName) {
        continue;
      }
      const branchStartNode = this.findBranchStartNode(node, current);
      if ((branchStartNode == null ? void 0 : branchStartNode.branchIndex) !== 1) {
        continue;
      }
      return scopeTransaction.transaction;
    }
    return null;
  }
  isInstructionSync(node) {
    return this.options.plugin.isWorkflowSync(this.execution.workflow) || Boolean(this.getScopeTransaction(node));
  }
  hasOpenScopeTransactions() {
    for (const { transaction } of this.scopeTransactions.values()) {
      if (!transaction.finished) {
        return true;
      }
    }
    return false;
  }
  markOpenScopePendingJobsAsError(message) {
    const pendingJobs = /* @__PURE__ */ new Set();
    for (const job of this.jobsToSave.values()) {
      if (job.status === import_constants.JOB_STATUS.PENDING) {
        pendingJobs.add(job);
      }
    }
    for (const job of Object.values(this.jobsMapByNodeKey)) {
      if (job.status === import_constants.JOB_STATUS.PENDING) {
        pendingJobs.add(job);
      }
    }
    for (const key of this.scopeTransactions.keys()) {
      const job = this.jobsMapByNodeKey[key];
      if ((job == null ? void 0 : job.status) === import_constants.JOB_STATUS.PENDING) {
        pendingJobs.add(job);
      }
    }
    for (const job of pendingJobs) {
      job.set({
        status: import_constants.JOB_STATUS.ERROR,
        result: {
          message
        }
      });
      this.saveJob(job);
    }
  }
  async cleanupScopeTransactions({ allowCommit = false } = {}) {
    if (!this.scopeTransactions.size) {
      return;
    }
    for (const [key, { transaction, dataSource, closing }] of Array.from(this.scopeTransactions.entries()).reverse()) {
      const tx = transaction;
      if (tx.finished) {
        this.scopeTransactions.delete(key);
        continue;
      }
      const action = closing === "commit" && allowCommit ? "commit" : "rollback";
      const actionText = action === "commit" ? "committing" : "rolling back";
      this.logger.warn(
        `scope transaction (${key}) on data source "${dataSource}" was not closed before exit, ${actionText}`,
        {
          workflowId: this.execution.workflowId
        }
      );
      try {
        await tx[action]();
      } catch (error) {
        this.logger.error(`scope transaction (${key}) fallback ${action} failed`, {
          error,
          workflowId: this.execution.workflowId
        });
      } finally {
        this.scopeTransactions.delete(key);
      }
    }
  }
  /**
   * @experimental
   */
  saveJob(payload) {
    var _a;
    const { database } = this.execution.constructor;
    const model = database.getModel("jobs");
    let job;
    if (payload instanceof model) {
      job = payload;
      job.set("updatedAt", /* @__PURE__ */ new Date());
    } else if (((_a = this.rerunContext) == null ? void 0 : _a.overwrite) && this.rerunContext.targetJob && this.rerunContext.targetJob.nodeId === payload.nodeId) {
      job = this.rerunContext.targetJob;
      job.set({
        status: payload.status,
        result: Object.prototype.hasOwnProperty.call(payload, "result") ? payload.result : null,
        meta: Object.prototype.hasOwnProperty.call(payload, "meta") ? payload.meta : null,
        updatedAt: /* @__PURE__ */ new Date()
      });
    } else {
      job = model.build(
        {
          ...payload,
          id: this.options.plugin.snowflake.getUniqueID().toString(),
          createdAt: /* @__PURE__ */ new Date(),
          updatedAt: /* @__PURE__ */ new Date(),
          executionId: this.execution.id
        },
        {
          isNewRecord: true
        }
      );
    }
    this.jobsToSave.set(job.id.toString(), job);
    this.lastSavedJob = job;
    this.jobsMapByNodeKey[job.nodeKey] = job;
    this.jobResultsMapByNodeKey[job.nodeKey] = job.result;
    this.logger.debug(`job added to save list: ${JSON.stringify(job)}`, {
      workflowId: this.execution.workflowId
    });
    return job;
  }
  /**
   * @experimental
   */
  getBranches(node) {
    return this.nodes.filter((item) => item.upstream === node && item.branchIndex !== null).sort((a, b) => Number(a.branchIndex) - Number(b.branchIndex));
  }
  enterRunningState() {
    this.options.plugin.timeoutManager.clear(this.execution.id);
    this.abortReason = null;
    this.aborted = false;
    this.options.plugin.registerRunningExecution(this.execution.id, (reason) => this.abortExecution(reason));
    this.runningRegistered = true;
    const remaining = this.execution.expiresAt ? this.execution.expiresAt.getTime() - Date.now() : null;
    if (remaining == null) {
      return;
    }
    if (remaining <= 0) {
      this.abortExecution(import_constants.EXECUTION_REASON.TIMEOUT);
      return;
    }
    this.setTimeoutGuard(remaining);
  }
  async shouldContinueExecution() {
    return this.options.plugin.timeoutManager.shouldContinue(this.execution);
  }
  leaveRunningState() {
    if (this.timeoutGuard) {
      clearTimeout(this.timeoutGuard);
      this.timeoutGuard = null;
    }
    if (!this.runningRegistered) {
      return;
    }
    this.options.plugin.unregisterRunningExecution(this.execution.id);
    this.runningRegistered = false;
  }
  /**
   * @experimental
   * find the first node in current branch
   */
  findBranchStartNode(node, parent) {
    for (let n = node; n; n = n.upstream) {
      if (!parent) {
        if (n.branchIndex !== null) {
          return n;
        }
      } else {
        if (n.upstream === parent) {
          return n;
        }
      }
    }
    return null;
  }
  /**
   * @experimental
   * find the node start current branch
   */
  findBranchParentNode(node) {
    for (let n = node; n; n = n.upstream) {
      if (n.branchIndex !== null) {
        return n.upstream;
      }
    }
    return null;
  }
  /**
   * @experimental
   */
  findBranchEndNode(node) {
    for (let n = node; n; n = n.downstream) {
      if (!n.downstream) {
        return n;
      }
    }
    return null;
  }
  /**
   * @experimental
   */
  findBranchParentJob(job, node) {
    return this.jobsMapByNodeKey[node.key];
  }
  /**
   * @experimental
   */
  findBranchLastJob(node, job) {
    const allJobs = Object.values(this.jobsMapByNodeKey);
    const branchJobs = [];
    for (let n = this.findBranchEndNode(node); n && n !== node.upstream; n = n.upstream) {
      branchJobs.push(...allJobs.filter((item) => item.nodeId === n.id));
    }
    branchJobs.sort((a, b) => a.id - b.id);
    return branchJobs[branchJobs.length - 1] || null;
  }
  /**
   * @experimental
   */
  getScope(sourceNodeId, includeSelfScope = false) {
    const node = sourceNodeId ? this.nodesMap.get(sourceNodeId) : void 0;
    const systemFns = {};
    const scope = {
      execution: this.execution,
      node
    };
    for (const [name, fn2] of this.options.plugin.functions.getEntities()) {
      (0, import_set.default)(systemFns, name, fn2.bind(scope));
    }
    const $scopes = {};
    for (let n = includeSelfScope ? node : this.findBranchParentNode(node); n; n = this.findBranchParentNode(n)) {
      const instruction = this.options.plugin.instructions.get(n.type);
      if (typeof (instruction == null ? void 0 : instruction.getScope) === "function") {
        $scopes[n.id] = $scopes[n.key] = instruction.getScope(n, this.jobResultsMapByNodeKey[n.key], this);
      }
    }
    const scopes = {
      $context: this.execution.context,
      $jobsMapByNodeKey: this.jobResultsMapByNodeKey,
      $system: systemFns,
      $scopes,
      $env: this.options.plugin.app.environment.getVariables()
    };
    return {
      ...scopes,
      ctx: scopes
      // 2.0
    };
  }
  /**
   * @experimental
   */
  getParsedValue(value, sourceNodeId, { additionalScope = {}, includeSelfScope = false } = {}) {
    const template = (0, import_utils.parse)(value);
    const scope = Object.assign(this.getScope(sourceNodeId, includeSelfScope), additionalScope);
    template.parameters.forEach(({ key }) => {
      (0, import_evaluators.appendArrayColumn)(scope, key);
    });
    return template(scope);
  }
}
