@objectstack/service-automation 7.4.0 → 7.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1635,6 +1635,142 @@ function registerNotifyNode(engine, ctx) {
1635
1635
  ctx.logger.info("[Notify] 1 built-in node executor registered (notify)");
1636
1636
  }
1637
1637
 
1638
+ // src/builtin/wait-node.ts
1639
+ import { defineActionDescriptor as defineActionDescriptor7 } from "@objectstack/spec/automation";
1640
+ function registerWaitNode(engine, ctx) {
1641
+ const getJobService = () => {
1642
+ try {
1643
+ return ctx.getService("job");
1644
+ } catch {
1645
+ return void 0;
1646
+ }
1647
+ };
1648
+ engine.registerNodeExecutor({
1649
+ type: "wait",
1650
+ descriptor: defineActionDescriptor7({
1651
+ type: "wait",
1652
+ version: "1.0.0",
1653
+ name: "Wait",
1654
+ description: "Pause the flow until a timer elapses or a named signal arrives.",
1655
+ icon: "timer-reset",
1656
+ category: "logic",
1657
+ source: "builtin",
1658
+ // Durable pause — the run suspends and resumes later (timer/signal).
1659
+ supportsPause: true,
1660
+ isAsync: true
1661
+ }),
1662
+ async execute(node, variables, _context) {
1663
+ const loose = node.config ?? {};
1664
+ const wec = node.waitEventConfig ?? {};
1665
+ const eventType = String(wec.eventType ?? loose.eventType ?? "timer");
1666
+ const runId = variables.get("$runId");
1667
+ if (eventType === "timer") {
1668
+ const durationMs = parseIsoDuration(wec.timerDuration ?? loose.timerDuration ?? loose.duration) ?? (typeof wec.timeoutMs === "number" ? wec.timeoutMs : void 0) ?? (typeof loose.timeoutMs === "number" ? loose.timeoutMs : void 0);
1669
+ const job = getJobService();
1670
+ if (job && runId != null && durationMs && durationMs > 0) {
1671
+ const jobName = `flow-wait:${String(runId)}:${node.id}`;
1672
+ const at = new Date(Date.now() + durationMs).toISOString();
1673
+ try {
1674
+ await job.schedule(jobName, { type: "once", at }, async () => {
1675
+ try {
1676
+ await engine.resume(String(runId));
1677
+ } finally {
1678
+ try {
1679
+ await job.cancel?.(jobName);
1680
+ } catch {
1681
+ }
1682
+ }
1683
+ });
1684
+ return { success: true, suspend: true, correlation: jobName };
1685
+ } catch (err) {
1686
+ ctx.logger.warn(
1687
+ `[wait] node '${node.id}': failed to schedule timer resume (${err?.message ?? err}); suspending without auto-resume (resume it via resume(runId))`
1688
+ );
1689
+ }
1690
+ } else if (!job) {
1691
+ ctx.logger.warn(
1692
+ `[wait] node '${node.id}': no job service registered \u2014 suspending without an auto-resume timer (resume it via resume(runId), or install the job service for durable timers)`
1693
+ );
1694
+ }
1695
+ return { success: true, suspend: true, correlation: `timer:${node.id}` };
1696
+ }
1697
+ const signal = String(wec.signalName ?? loose.signalName ?? loose.signal ?? `wait:${node.id}`);
1698
+ return { success: true, suspend: true, correlation: signal };
1699
+ }
1700
+ });
1701
+ ctx.logger.info("[Wait Node] 1 built-in node executor registered");
1702
+ }
1703
+ function parseIsoDuration(input) {
1704
+ if (typeof input === "number" && Number.isFinite(input)) return input > 0 ? input : void 0;
1705
+ if (typeof input !== "string") return void 0;
1706
+ const s = input.trim();
1707
+ if (!s) return void 0;
1708
+ if (/^\d+(?:\.\d+)?$/.test(s)) {
1709
+ const n = Number(s);
1710
+ return n > 0 ? n : void 0;
1711
+ }
1712
+ const m = /^P(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/.exec(s);
1713
+ if (!m) return void 0;
1714
+ const [, w, d, h, min, sec] = m;
1715
+ if (!w && !d && !h && !min && !sec) return void 0;
1716
+ const totalSec = Number(w ?? 0) * 7 * 86400 + Number(d ?? 0) * 86400 + Number(h ?? 0) * 3600 + Number(min ?? 0) * 60 + Number(sec ?? 0);
1717
+ const ms = totalSec * 1e3;
1718
+ return ms > 0 ? ms : void 0;
1719
+ }
1720
+
1721
+ // src/builtin/subflow-node.ts
1722
+ import { defineActionDescriptor as defineActionDescriptor8 } from "@objectstack/spec/automation";
1723
+ var MAX_SUBFLOW_DEPTH = 16;
1724
+ function registerSubflowNode(engine, ctx) {
1725
+ engine.registerNodeExecutor({
1726
+ type: "subflow",
1727
+ descriptor: defineActionDescriptor8({
1728
+ type: "subflow",
1729
+ version: "1.0.0",
1730
+ name: "Subflow",
1731
+ description: "Invoke another flow as a reusable step and capture its output.",
1732
+ icon: "workflow",
1733
+ category: "logic",
1734
+ source: "builtin"
1735
+ }),
1736
+ async execute(node, variables, context) {
1737
+ const cfg = node.config ?? {};
1738
+ const flowName = typeof cfg.flowName === "string" ? cfg.flowName : typeof cfg.flow === "string" ? cfg.flow : void 0;
1739
+ if (!flowName) {
1740
+ return { success: false, error: `subflow '${node.id}': config.flowName is required` };
1741
+ }
1742
+ const depth = Number(context?.$subflowDepth ?? 0);
1743
+ if (depth >= MAX_SUBFLOW_DEPTH) {
1744
+ return {
1745
+ success: false,
1746
+ error: `subflow '${flowName}': max nesting depth (${MAX_SUBFLOW_DEPTH}) exceeded \u2014 recursive subflow?`
1747
+ };
1748
+ }
1749
+ const rawInput = cfg.input && typeof cfg.input === "object" ? cfg.input : {};
1750
+ const params = interpolate(rawInput, variables, context ?? {});
1751
+ const childContext = {
1752
+ ...context ?? {},
1753
+ $subflowDepth: depth + 1,
1754
+ params
1755
+ };
1756
+ const child = await engine.execute(flowName, childContext);
1757
+ if (child.status === "paused") {
1758
+ return {
1759
+ success: false,
1760
+ error: `subflow '${flowName}' suspended at a pausing node \u2014 a nested approval/screen/wait pause from a subflow is not yet supported`
1761
+ };
1762
+ }
1763
+ if (!child.success) {
1764
+ return { success: false, error: `subflow '${flowName}' failed: ${child.error ?? "unknown error"}` };
1765
+ }
1766
+ const outVar = typeof cfg.outputVariable === "string" && cfg.outputVariable ? cfg.outputVariable : void 0;
1767
+ if (outVar) variables.set(outVar, child.output ?? null);
1768
+ return { success: true, output: { output: child.output ?? null } };
1769
+ }
1770
+ });
1771
+ ctx.logger.info("[Subflow Node] 1 built-in node executor registered");
1772
+ }
1773
+
1638
1774
  // src/builtin/index.ts
1639
1775
  function installBuiltinNodes(engine, ctx) {
1640
1776
  registerLogicNodes(engine, ctx);
@@ -1643,6 +1779,8 @@ function installBuiltinNodes(engine, ctx) {
1643
1779
  registerHttpNodes(engine, ctx);
1644
1780
  registerConnectorNodes(engine, ctx);
1645
1781
  registerNotifyNode(engine, ctx);
1782
+ registerWaitNode(engine, ctx);
1783
+ registerSubflowNode(engine, ctx);
1646
1784
  const types = engine.getRegisteredNodeTypes();
1647
1785
  ctx.logger.info(
1648
1786
  `[Automation] ${types.length} built-in node executors installed: ${types.join(", ")}`