/**
 * 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 Plugin_exports = {};
__export(Plugin_exports, {
  WORKER_JOB_WORKFLOW_PROCESS: () => WORKER_JOB_WORKFLOW_PROCESS,
  default: () => PluginWorkflowServer
});
module.exports = __toCommonJS(Plugin_exports);
var import_path = __toESM(require("path"));
var import_nodejs_snowflake = require("nodejs-snowflake");
var import_sequelize = require("sequelize");
var import_lru_cache = require("lru-cache");
var import_database = require("@nocobase/database");
var import_server = require("@nocobase/server");
var import_utils = require("@nocobase/utils");
var import_Dispatcher = __toESM(require("./Dispatcher"));
var import_Processor = __toESM(require("./Processor"));
var import_ExecutionTimeoutManager = __toESM(require("./ExecutionTimeoutManager"));
var import_RunningExecutionRegistry = __toESM(require("./RunningExecutionRegistry"));
var import_actions = __toESM(require("./actions"));
var import_nodes = require("./actions/nodes");
var import_workflows = require("./actions/workflows");
var import_constants = require("./constants");
var import_functions = __toESM(require("./functions"));
var import_CollectionTrigger = __toESM(require("./triggers/CollectionTrigger"));
var import_ScheduleTrigger = __toESM(require("./triggers/ScheduleTrigger"));
var import_CalculationInstruction = __toESM(require("./instructions/CalculationInstruction"));
var import_ConditionInstruction = __toESM(require("./instructions/ConditionInstruction"));
var import_EndInstruction = __toESM(require("./instructions/EndInstruction"));
var import_OutputInstruction = __toESM(require("./instructions/OutputInstruction"));
var import_CreateInstruction = __toESM(require("./instructions/CreateInstruction"));
var import_DestroyInstruction = __toESM(require("./instructions/DestroyInstruction"));
var import_QueryInstruction = __toESM(require("./instructions/QueryInstruction"));
var import_UpdateInstruction = __toESM(require("./instructions/UpdateInstruction"));
var import_MultiConditionsInstruction = __toESM(require("./instructions/MultiConditionsInstruction"));
var import_WorkflowRepository = __toESM(require("./repositories/WorkflowRepository"));
const WORKER_JOB_WORKFLOW_PROCESS = "workflow:process";
class PluginWorkflowServer extends import_server.Plugin {
  instructions = new import_utils.Registry();
  triggers = new import_utils.Registry();
  functions = new import_utils.Registry();
  enabledCache = /* @__PURE__ */ new Map();
  snowflake;
  dispatcher = new import_Dispatcher.default(this);
  timeoutManager = new import_ExecutionTimeoutManager.default(this);
  runningExecutionRegistry = new import_RunningExecutionRegistry.default();
  get channelPendingExecution() {
    return `${this.name}.pendingExecution`;
  }
  loggerCache;
  meter = null;
  checker = null;
  taskStatsProviders = /* @__PURE__ */ new Map();
  onBeforeSave = async (instance, { transaction, cycling }) => {
    if (cycling) {
      return;
    }
    const Model = instance.constructor;
    if (!instance.key) {
      instance.set("key", (0, import_utils.uid)());
    }
    if (instance.enabled) {
      instance.set("current", true);
    }
    const previous = await Model.findOne({
      where: {
        key: instance.key,
        current: true,
        id: {
          [import_database.Op.ne]: instance.id
        }
      },
      transaction
    });
    if (!previous) {
      instance.set("current", true);
    } else if (instance.current) {
      await previous.update(
        { enabled: false, current: null },
        {
          transaction,
          cycling: true
        }
      );
      this.toggle(previous, false, { transaction });
    }
  };
  onAfterCreate = async (model, { transaction }) => {
    const WorkflowStatsModel = this.db.getModel("workflowStats");
    let stats = await WorkflowStatsModel.findOne({
      where: { key: model.key },
      transaction
    });
    if (!stats) {
      stats = await model.createStats({ executed: 0 }, { transaction });
    }
    model.stats = stats;
    model.versionStats = await model.createVersionStats({ id: model.id }, { transaction });
    if (model.enabled) {
      this.toggle(model, true, { transaction });
    }
  };
  onAfterUpdate = async (model, { transaction }) => {
    model.stats = await model.getStats({ transaction });
    model.versionStats = await model.getVersionStats({ transaction });
    this.toggle(model, model.enabled, { transaction });
  };
  onAfterDestroy = async (model, { transaction }) => {
    this.toggle(model, false, { transaction });
    const TaskRepo = this.db.getRepository("workflowTasks");
    await TaskRepo.destroy({
      filter: {
        workflowId: model.id
      },
      transaction
    });
  };
  // [Life Cycle]:
  //   * load all workflows in db
  //   * add all hooks for enabled workflows
  //   * add hooks for create/update[enabled]/delete workflow to add/remove specific hooks
  onAfterStart = async () => {
    const collection = this.db.getCollection("workflows");
    const workflows = await collection.repository.find({
      appends: ["versionStats"]
    });
    for (const workflow of workflows) {
      if (workflow.current) {
        workflow.stats = await workflow.getStats();
        if (!workflow.stats) {
          workflow.stats = await workflow.createStats({ executed: 0 });
        }
      }
      if (!workflow.versionStats) {
        workflow.versionStats = await workflow.createVersionStats({ executed: 0 });
      }
      if (workflow.enabled) {
        this.toggle(workflow, true, { silent: true });
      }
    }
    this.checker = setInterval(() => {
      this.dispatcher.recover({ gracePeriod: 6e4 });
    }, 3e5);
    await this.timeoutManager.load();
    this.app.on("workflow:recover", () => {
      this.app.logger.info("workflow:recover");
      this.dispatcher.recover();
    });
    this.dispatcher.setReady(true);
    this.getLogger("dispatcher").info("(starting) check for queueing executions");
    this.dispatcher.recover();
  };
  onBeforeStop = async () => {
    if (this.checker) {
      clearInterval(this.checker);
    }
    await this.timeoutManager.unload();
    await this.dispatcher.beforeStop();
    this.app.logger.info(`stopping workflow plugin before app (${this.app.name}) shutdown...`);
    for (const workflow of this.enabledCache.values()) {
      this.toggle(workflow, false, { silent: true });
    }
    this.loggerCache.clear();
  };
  async handleSyncMessage(message) {
    if (message.type === "statusChange") {
      if (message.enabled) {
        let workflow = this.enabledCache.get(message.workflowId);
        if (workflow) {
          await workflow.reload();
        } else {
          workflow = await this.db.getRepository("workflows").findOne({
            filterByTk: message.workflowId
          });
        }
        if (workflow) {
          this.toggle(workflow, true, { silent: true });
        }
      } else {
        const workflow = this.enabledCache.get(message.workflowId);
        if (workflow) {
          this.toggle(workflow, false, { silent: true });
        }
      }
    }
  }
  serving() {
    return this.app.serving(WORKER_JOB_WORKFLOW_PROCESS);
  }
  async abortExecutionIfExpired(execution, options = {}) {
    return this.timeoutManager.abortExecutionIfExpired(execution, options);
  }
  registerRunningExecution(executionId, abort) {
    return this.runningExecutionRegistry.register(executionId, { abort });
  }
  unregisterRunningExecution(executionId) {
    this.runningExecutionRegistry.unregister(executionId);
  }
  abortRunningExecution(executionId, reason) {
    return this.runningExecutionRegistry.abort(executionId, reason);
  }
  /**
   * @experimental
   */
  getLogger(workflowId = "dispatcher") {
    const now = /* @__PURE__ */ new Date();
    const date = `${now.getFullYear()}-${`0${now.getMonth() + 1}`.slice(-2)}-${`0${now.getDate()}`.slice(-2)}`;
    const key = `${date}-${workflowId}`;
    if (this.loggerCache.has(key)) {
      return this.loggerCache.get(key);
    }
    const logger = this.createLogger({
      dirname: import_path.default.join("workflows", String(workflowId)),
      filename: "%DATE%.log"
    });
    this.loggerCache.set(key, logger);
    return logger;
  }
  /**
   * @experimental
   * @param {WorkflowModel} workflow
   * @returns {boolean}
   */
  isWorkflowSync(workflow) {
    const trigger = this.triggers.get(workflow.type);
    if (!trigger) {
      throw new Error(`invalid trigger type ${workflow.type} of workflow ${workflow.id}`);
    }
    return trigger.sync ?? workflow.sync;
  }
  registerTrigger(type, trigger) {
    if (typeof trigger === "function") {
      this.triggers.register(type, new trigger(this));
    } else if (trigger) {
      this.triggers.register(type, trigger);
    } else {
      throw new Error("invalid trigger type to register");
    }
  }
  registerInstruction(type, instruction) {
    if (typeof instruction === "function") {
      this.instructions.register(type, new instruction(this));
    } else if (instruction) {
      this.instructions.register(type, instruction);
    } else {
      throw new Error("invalid instruction type to register");
    }
  }
  initTriggers(more = {}) {
    this.registerTrigger("collection", import_CollectionTrigger.default);
    this.registerTrigger("schedule", import_ScheduleTrigger.default);
    for (const [name, trigger] of Object.entries(more)) {
      this.registerTrigger(name, trigger);
    }
  }
  initInstructions(more = {}) {
    this.registerInstruction("calculation", import_CalculationInstruction.default);
    this.registerInstruction("condition", import_ConditionInstruction.default);
    this.registerInstruction("multi-conditions", import_MultiConditionsInstruction.default);
    this.registerInstruction("end", import_EndInstruction.default);
    this.registerInstruction("output", import_OutputInstruction.default);
    this.registerInstruction("create", import_CreateInstruction.default);
    this.registerInstruction("destroy", import_DestroyInstruction.default);
    this.registerInstruction("query", import_QueryInstruction.default);
    this.registerInstruction("update", import_UpdateInstruction.default);
    for (const [name, instruction] of Object.entries({ ...more })) {
      this.registerInstruction(name, instruction);
    }
  }
  registerErrorHandlers() {
    const PluginErrorHandler = this.app.pm.get("error-handler");
    if (PluginErrorHandler == null ? void 0 : PluginErrorHandler.errorHandler) {
      PluginErrorHandler.errorHandler.register(
        (err) => err instanceof import_nodes.NodeValidationError || err.name === "NodeValidationError",
        (err, ctx) => {
          ctx.status = err.status;
          ctx.body = {
            errors: Object.values(err.errors).map((message) => ({ message }))
          };
        }
      );
      PluginErrorHandler.errorHandler.register(
        (err) => err instanceof import_workflows.WorkflowValidationError || err.name === "WorkflowValidationError",
        (err, ctx) => {
          ctx.status = err.status;
          ctx.body = {
            errors: Object.values(err.errors).map((message) => ({ message }))
          };
        }
      );
    }
  }
  async beforeLoad() {
    this.db.registerRepositories({
      WorkflowRepository: import_WorkflowRepository.default
    });
    const PluginRepo = this.db.getRepository("applicationPlugins");
    const pluginRecord = await PluginRepo.findOne({
      filter: { name: this.name }
    });
    this.snowflake = new import_nodejs_snowflake.Snowflake({
      custom_epoch: pluginRecord == null ? void 0 : pluginRecord.createdAt.getTime(),
      instance_id: this.app.instanceId
    });
  }
  /**
   * @internal
   */
  async load() {
    const { db, options } = this;
    (0, import_actions.default)(this);
    this.initTriggers(options.triggers);
    this.initInstructions(options.instructions);
    (0, import_functions.default)(this, options.functions);
    this.registerErrorHandlers();
    this.functions.register("instanceId", () => this.app.instanceId);
    this.functions.register("epoch", () => 1605024e3);
    this.functions.register("genSnowflakeId", () => this.app.snowflakeIdGenerator.generate());
    this.loggerCache = new import_lru_cache.LRUCache({
      max: 20,
      updateAgeOnGet: true,
      dispose(logger) {
        const cachedLogger = logger;
        if (!cachedLogger) {
          return;
        }
        cachedLogger.silent = true;
        if (typeof cachedLogger.close === "function") {
          cachedLogger.close();
        }
      }
    });
    this.meter = this.app.telemetry.metric.getMeter();
    if (this.meter) {
      const counter = this.meter.createObservableGauge("workflow.events.counter");
      counter.addCallback((result) => {
        result.observe(this.dispatcher.getEventsCount());
      });
    }
    this.app.acl.registerSnippet({
      name: `pm.${this.name}.workflows`,
      actions: [
        "workflows:*",
        "workflows.nodes:*",
        "executions:list",
        "executions:get",
        "executions:cancel",
        "executions:destroy",
        "flow_nodes:update",
        "flow_nodes:destroy",
        "flow_nodes:destroyBranch",
        "flow_nodes:duplicate",
        "flow_nodes:move",
        "flow_nodes:test",
        "jobs:get",
        "workflowCategories:*"
      ]
    });
    this.app.acl.registerSnippet({
      name: "ui.workflows",
      actions: ["workflows:list"]
    });
    this.app.acl.allow("userWorkflowTasks", "listMine", "loggedIn");
    this.app.acl.allow("userWorkflowTaskStats", "listMine", "loggedIn");
    db.on("workflows.beforeSave", this.onBeforeSave);
    db.on("workflows.afterCreate", this.onAfterCreate);
    db.on("workflows.afterUpdate", this.onAfterUpdate);
    db.on("workflows.afterDestroy", this.onAfterDestroy);
    this.app.on("afterStart", this.onAfterStart);
    this.app.on("beforeStop", this.onBeforeStop);
    this.app.eventQueue.subscribe(this.channelPendingExecution, {
      idle: () => this.serving() && this.dispatcher.idle,
      process: this.dispatcher.onQueueTask
    });
  }
  toggle(workflow, enable, { silent, transaction } = {}) {
    const type = workflow.get("type");
    const trigger = this.triggers.get(type);
    if (!trigger) {
      this.getLogger(workflow.id).error(`trigger type ${workflow.type} of workflow ${workflow.id} is not implemented`, {
        workflowId: workflow.id
      });
      return;
    }
    const next = enable ?? workflow.get("enabled");
    if (next) {
      const prev = workflow.previous();
      if (prev.config) {
        trigger.off({ ...workflow.get(), ...prev });
        this.getLogger(workflow.id).info(`toggle OFF workflow ${workflow.id} based on configuration before updated`, {
          workflowId: workflow.id
        });
      }
      trigger.on(workflow);
      this.getLogger(workflow.id).info(`toggle ON workflow ${workflow.id}`, {
        workflowId: workflow.id
      });
      this.enabledCache.set(workflow.id, workflow);
    } else {
      trigger.off(workflow);
      this.getLogger(workflow.id).info(`toggle OFF workflow ${workflow.id}`, {
        workflowId: workflow.id
      });
      this.enabledCache.delete(workflow.id);
    }
    if (!silent) {
      this.sendSyncMessage(
        {
          type: "statusChange",
          workflowId: workflow.id,
          enabled: next
        },
        { transaction }
      );
    }
  }
  trigger(workflow, context, options = {}) {
    return this.dispatcher.trigger(workflow, context, options);
  }
  async rerun(execution, rerun) {
    return this.dispatcher.enqueue({ executionId: this.getModelId(execution), rerun: rerun ?? {} });
  }
  recover() {
    return this.dispatcher.recover();
  }
  /**
   * Persist the current job state and publish a poke that evaluates that state.
   *
   * PENDING resume calls are valid for instructions such as delay and sequential approval.
   * Callers that aggregate multiple user actions must only call this method when their
   * atomic update changes the job from PENDING to a terminal status.
   * Persistence and queue publication failures are logged and rethrown to awaiting callers.
   */
  async resume(job) {
    var _a;
    const executionId = job.executionId ?? ((_a = job.execution) == null ? void 0 : _a.id);
    if (executionId == null) {
      this.getLogger("dispatcher").warn(`execution id of job (${job.id}) not found, resume ignored`);
      return;
    }
    try {
      if (job.changed()) {
        await job.save();
      }
    } catch (error) {
      this.getLogger("dispatcher").error(
        `persisting job (${job.id}) before resuming execution (${executionId}) failed`,
        { error, executionId, jobId: job.id }
      );
      throw error;
    }
    try {
      await this.dispatcher.enqueue({ executionId, jobId: job.id });
      this.getLogger("dispatcher").info(`execution (${executionId}) resuming from job (${job.id}) published to queue`);
    } catch (error) {
      this.getLogger("dispatcher").error(
        `publishing resume task for execution (${executionId}) from job (${job.id}) failed`,
        { error, executionId, jobId: job.id }
      );
      throw error;
    }
  }
  /**
   * Start a deferred execution
   * @experimental
   */
  async start(execution) {
    if (typeof execution !== "string" && typeof execution !== "number") {
      if (execution.status !== import_constants.EXECUTION_STATUS.QUEUEING && execution.status !== import_constants.EXECUTION_STATUS.STARTED && execution.status != null) {
        return;
      }
      if (execution.status === import_constants.EXECUTION_STATUS.STARTED && execution.startedAt) {
        return;
      }
    }
    return this.dispatcher.enqueue({ executionId: this.getModelId(execution) });
  }
  createProcessor(execution, options = {}) {
    return new import_Processor.default(execution, { ...options, plugin: this });
  }
  getModelId(input) {
    return typeof input === "string" || typeof input === "number" ? input : input.id;
  }
  async execute(workflow, values, options = {}) {
    const trigger = this.triggers.get(workflow.type);
    if (!trigger) {
      throw new Error(`trigger type "${workflow.type}" of workflow ${workflow.id} is not registered`);
    }
    if (!trigger.execute) {
      throw new Error(`"execute" method of trigger ${workflow.type} is not implemented`);
    }
    return trigger.execute(workflow, values, options);
  }
  useDataSourceTransaction(dataSourceName = "main", transaction, create = false) {
    const dataSource = this.app.dataSourceManager.dataSources.get(dataSourceName);
    if (!dataSource) {
      throw new Error(`data source ${dataSourceName} is not found`);
    }
    const { db } = dataSource.collectionManager;
    if (!db) {
      return;
    }
    if (db.sequelize === (transaction == null ? void 0 : transaction.sequelize)) {
      return transaction;
    }
    if (create) {
      return db.sequelize.transaction();
    }
  }
  registerTaskStatsProvider(type, provider) {
    this.taskStatsProviders.set(type, provider);
  }
  async getWorkflowIdsByKey(workflowKey, transaction) {
    const WorkflowRepo = this.db.getRepository("workflows");
    const workflows = await WorkflowRepo.find({
      filter: {
        key: workflowKey
      },
      fields: ["id"],
      transaction
    });
    return workflows.map((workflow) => workflow.id);
  }
  buildTaskStatsFilter(options) {
    var _a, _b, _c;
    const filter = {};
    if ((_a = options.userIds) == null ? void 0 : _a.length) {
      filter.userId = options.userIds;
    }
    if ((_b = options.workflowKeys) == null ? void 0 : _b.length) {
      filter.workflowKey = options.workflowKeys;
    }
    if ((_c = options.types) == null ? void 0 : _c.length) {
      filter.type = options.types;
    }
    return filter;
  }
  normalizeTaskStats(stats = {}) {
    return {
      pending: Number(stats.pending) || 0,
      all: Number(stats.all) || 0
    };
  }
  async upsertTaskStatsRow(row, transaction) {
    const repository = this.db.getRepository("userWorkflowTaskStats");
    const values = {
      userId: row.userId,
      workflowKey: row.workflowKey,
      type: row.type,
      ...this.normalizeTaskStats(row)
    };
    const record = await repository.findOne({
      filter: {
        userId: row.userId,
        workflowKey: row.workflowKey,
        type: row.type
      },
      transaction
    });
    if (record) {
      await record.update(values, { transaction });
      return record;
    }
    return repository.create({
      values,
      transaction
    });
  }
  async refreshUserWorkflowTaskTypeStats(userId, type, { transaction } = {}) {
    const repository = this.db.getRepository("userWorkflowTaskStats");
    const rows = await repository.find({
      filter: {
        userId,
        type
      },
      fields: ["pending", "all"],
      transaction
    });
    const stats = rows.reduce(
      (result, row) => ({
        pending: result.pending + (Number(row.pending) || 0),
        all: result.all + (Number(row.all) || 0)
      }),
      { pending: 0, all: 0 }
    );
    await this.updateTasksStats(userId, type, stats, { transaction });
    return stats;
  }
  async refreshUserWorkflowTaskWorkflowStats(userId, workflowKey, { transaction } = {}) {
    const repository = this.db.getRepository("userWorkflowTaskStats");
    const rows = await repository.find({
      filter: {
        userId,
        workflowKey
      },
      fields: ["pending", "all"],
      transaction
    });
    return rows.reduce(
      (result, row) => ({
        pending: result.pending + (Number(row.pending) || 0),
        all: result.all + (Number(row.all) || 0)
      }),
      { pending: 0, all: 0 }
    );
  }
  sendTaskWorkflowStatsUpdated(userId, workflowKey, stats) {
    if (!userId) {
      return;
    }
    this.app.emit("ws:sendToUser", {
      userId,
      message: {
        type: "workflow:taskWorkflowStats:updated",
        payload: {
          userId,
          workflowKey,
          stats
        }
      }
    });
  }
  async updateTaskStatsByWorkflow(input, { transaction } = {}) {
    await this.upsertTaskStatsRow(
      {
        userId: input.userId,
        workflowKey: input.workflowKey,
        type: input.type,
        ...this.normalizeTaskStats(input.stats)
      },
      transaction
    );
    await this.refreshUserWorkflowTaskTypeStats(input.userId, input.type, { transaction });
    const workflowStats = await this.refreshUserWorkflowTaskWorkflowStats(input.userId, input.workflowKey, {
      transaction
    });
    this.sendTaskWorkflowStatsUpdated(input.userId, input.workflowKey, workflowStats);
  }
  async repairTaskStats(options = {}) {
    const run = async (transaction) => {
      var _a, _b;
      const repository = this.db.getRepository("userWorkflowTaskStats");
      const TaskStatsModel = this.db.getModel("userWorkflowTaskStats");
      const filter = this.buildTaskStatsFilter(options);
      const existedRows = await repository.find({
        filter,
        fields: ["userId", "workflowKey", "type"],
        transaction
      });
      const providers = Array.from(this.taskStatsProviders.entries()).filter(([type]) => {
        var _a2;
        return !((_a2 = options.types) == null ? void 0 : _a2.length) || options.types.includes(type);
      });
      const legacyRows = providers.length ? await this.db.getRepository("userWorkflowTasks").find({
        filter: {
          ...((_a = options.userIds) == null ? void 0 : _a.length) ? { userId: options.userIds } : {},
          type: providers.map(([type]) => type)
        },
        fields: ["userId", "type"],
        transaction
      }) : [];
      const collectedRows = (await Promise.all(
        providers.map(([, provider]) => provider.collectTaskStats({ ...options, transaction }, { plugin: this }))
      )).flat();
      const rows = collectedRows.filter((row) => {
        var _a2, _b2, _c;
        if (((_a2 = options.userIds) == null ? void 0 : _a2.length) && !options.userIds.includes(row.userId)) {
          return false;
        }
        if (((_b2 = options.workflowKeys) == null ? void 0 : _b2.length) && !options.workflowKeys.includes(row.workflowKey)) {
          return false;
        }
        if (((_c = options.types) == null ? void 0 : _c.length) && !options.types.includes(row.type)) {
          return false;
        }
        return true;
      });
      await repository.destroy({
        filter,
        transaction
      });
      const batchSize = 1e3;
      for (let offset = 0; offset < rows.length; offset += batchSize) {
        await TaskStatsModel.bulkCreate(rows.slice(offset, offset + batchSize), {
          returning: false,
          transaction
        });
      }
      const typePairs = /* @__PURE__ */ new Set();
      const workflowPairs = /* @__PURE__ */ new Set();
      if ((_b = options.userIds) == null ? void 0 : _b.length) {
        for (const userId of options.userIds) {
          for (const [type] of providers) {
            typePairs.add(`${userId}\0${type}`);
          }
          for (const workflowKey of options.workflowKeys ?? []) {
            workflowPairs.add(`${userId}\0${workflowKey}`);
          }
        }
      }
      for (const row of legacyRows) {
        typePairs.add(`${row.userId}\0${row.type}`);
      }
      for (const row of [...existedRows, ...rows]) {
        typePairs.add(`${row.userId}\0${row.type}`);
        workflowPairs.add(`${row.userId}\0${row.workflowKey}`);
      }
      const affectedUserIds = Array.from(typePairs, (pair) => Number(pair.slice(0, pair.indexOf("\0"))));
      const affectedTypes = Array.from(typePairs, (pair) => pair.slice(pair.indexOf("\0") + 1));
      const categorizedRows = typePairs.size ? await TaskStatsModel.findAll({
        attributes: ["userId", "type", [(0, import_sequelize.fn)("SUM", (0, import_sequelize.col)("pending")), "pending"], [(0, import_sequelize.fn)("SUM", (0, import_sequelize.col)("all")), "all"]],
        where: {
          userId: Array.from(new Set(affectedUserIds)),
          type: Array.from(new Set(affectedTypes))
        },
        group: ["userId", "type"],
        raw: true,
        transaction
      }) : [];
      const categorizedMap = new Map(
        categorizedRows.map((row) => [
          `${row.userId}\0${row.type}`,
          this.normalizeTaskStats({ pending: row.pending, all: row.all })
        ])
      );
      const categorizedValues = Array.from(typePairs, (pair) => {
        const separatorIndex = pair.indexOf("\0");
        return {
          userId: Number(pair.slice(0, separatorIndex)),
          type: pair.slice(separatorIndex + 1),
          stats: categorizedMap.get(pair) ?? { pending: 0, all: 0 }
        };
      });
      const categorizedUserIds = Array.from(new Set(affectedUserIds));
      const categorizedTypes = Array.from(new Set(affectedTypes));
      if (categorizedUserIds.length && categorizedTypes.length) {
        await this.db.getRepository("userWorkflowTasks").destroy({
          filter: {
            userId: categorizedUserIds,
            type: categorizedTypes
          },
          transaction
        });
      }
      const UserTasksModel = this.db.getModel("userWorkflowTasks");
      for (let offset = 0; offset < categorizedValues.length; offset += batchSize) {
        await UserTasksModel.bulkCreate(categorizedValues.slice(offset, offset + batchSize), {
          returning: false,
          transaction
        });
      }
      if (options.silent) {
        return;
      }
      for (const value of categorizedValues) {
        if (!value.userId) {
          continue;
        }
        this.app.emit("ws:sendToUser", {
          userId: value.userId,
          message: { type: "workflow:tasks:updated", payload: value }
        });
      }
      const affectedWorkflowUserIds = Array.from(workflowPairs, (pair) => Number(pair.slice(0, pair.indexOf("\0"))));
      const affectedWorkflowKeys = Array.from(workflowPairs, (pair) => pair.slice(pair.indexOf("\0") + 1));
      const workflowRows = workflowPairs.size ? await TaskStatsModel.findAll({
        attributes: [
          "userId",
          "workflowKey",
          [(0, import_sequelize.fn)("SUM", (0, import_sequelize.col)("pending")), "pending"],
          [(0, import_sequelize.fn)("SUM", (0, import_sequelize.col)("all")), "all"]
        ],
        where: {
          userId: Array.from(new Set(affectedWorkflowUserIds)),
          workflowKey: Array.from(new Set(affectedWorkflowKeys))
        },
        group: ["userId", "workflowKey"],
        raw: true,
        transaction
      }) : [];
      const workflowMap = new Map(
        workflowRows.map((row) => [
          `${row.userId}\0${row.workflowKey}`,
          this.normalizeTaskStats({ pending: row.pending, all: row.all })
        ])
      );
      for (const pair of workflowPairs) {
        const separatorIndex = pair.indexOf("\0");
        const userId = Number(pair.slice(0, separatorIndex));
        const workflowKey = pair.slice(separatorIndex + 1);
        this.sendTaskWorkflowStatsUpdated(userId, workflowKey, workflowMap.get(pair) ?? { pending: 0, all: 0 });
      }
    };
    if (options.transaction) {
      await run(options.transaction);
      return;
    }
    await this.db.sequelize.transaction(run);
  }
  /**
   * @experimental
   * @deprecated Use updateTaskStatsByWorkflow() when workflowKey is available.
   */
  async updateTasksStats(userId, type, stats = { pending: 0, all: 0 }, { transaction }) {
    const { db } = this.app;
    const repository = db.getRepository("userWorkflowTasks");
    const normalizedStats = this.normalizeTaskStats(stats);
    let record = await repository.findOne({
      filter: {
        userId,
        type
      },
      transaction
    });
    if (record) {
      await record.update(
        {
          stats: normalizedStats
        },
        { transaction }
      );
    } else {
      record = await repository.create({
        values: {
          userId,
          type,
          stats: normalizedStats
        },
        transaction
      });
    }
    if (userId) {
      this.app.emit("ws:sendToUser", {
        userId,
        message: { type: "workflow:tasks:updated", payload: record.get() }
      });
    }
  }
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  WORKER_JOB_WORKFLOW_PROCESS
});
