@pikku/core 0.11.0 → 0.11.2

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.
Files changed (184) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/dist/errors/error-handler.js +3 -3
  3. package/dist/errors/errors.d.ts +1 -1
  4. package/dist/errors/errors.js +3 -3
  5. package/dist/function/function-runner.d.ts +8 -8
  6. package/dist/function/function-runner.js +88 -23
  7. package/dist/function/functions.types.d.ts +104 -33
  8. package/dist/function/functions.types.js +50 -7
  9. package/dist/handle-error.d.ts +1 -1
  10. package/dist/handle-error.js +1 -1
  11. package/dist/index.d.ts +6 -2
  12. package/dist/index.js +5 -2
  13. package/dist/middleware/auth-apikey.d.ts +1 -1
  14. package/dist/middleware/auth-apikey.js +3 -3
  15. package/dist/middleware/auth-bearer.d.ts +1 -1
  16. package/dist/middleware/auth-bearer.js +3 -3
  17. package/dist/middleware/auth-cookie.d.ts +1 -1
  18. package/dist/middleware/auth-cookie.js +5 -5
  19. package/dist/middleware/timeout.d.ts +1 -1
  20. package/dist/middleware/timeout.js +3 -2
  21. package/dist/middleware-runner.d.ts +6 -20
  22. package/dist/middleware-runner.js +19 -20
  23. package/dist/permissions.d.ts +6 -89
  24. package/dist/permissions.js +24 -81
  25. package/dist/pikku-state.d.ts +32 -104
  26. package/dist/pikku-state.js +140 -66
  27. package/dist/schema.d.ts +8 -6
  28. package/dist/schema.js +25 -13
  29. package/dist/services/user-session-service.d.ts +7 -3
  30. package/dist/services/user-session-service.js +8 -1
  31. package/dist/services/workflow-service.d.ts +38 -0
  32. package/dist/services/workflow-service.js +1 -0
  33. package/dist/types/core.types.d.ts +40 -41
  34. package/dist/types/core.types.js +4 -15
  35. package/dist/types/state.types.d.ts +135 -0
  36. package/dist/types/state.types.js +1 -0
  37. package/dist/utils.d.ts +9 -1
  38. package/dist/utils.js +48 -3
  39. package/dist/wirings/channel/channel-common.js +2 -3
  40. package/dist/wirings/channel/channel-handler.d.ts +3 -2
  41. package/dist/wirings/channel/channel-handler.js +7 -10
  42. package/dist/wirings/channel/channel-runner.d.ts +2 -2
  43. package/dist/wirings/channel/channel-runner.js +5 -5
  44. package/dist/wirings/channel/channel.types.d.ts +10 -5
  45. package/dist/wirings/channel/local/local-channel-runner.d.ts +1 -1
  46. package/dist/wirings/channel/local/local-channel-runner.js +19 -26
  47. package/dist/wirings/channel/log-channels.js +1 -1
  48. package/dist/wirings/channel/serverless/serverless-channel-runner.d.ts +1 -1
  49. package/dist/wirings/channel/serverless/serverless-channel-runner.js +43 -48
  50. package/dist/wirings/cli/channel/cli-channel-runner.js +3 -2
  51. package/dist/wirings/cli/cli-runner.d.ts +5 -5
  52. package/dist/wirings/cli/cli-runner.js +20 -38
  53. package/dist/wirings/cli/cli.types.d.ts +16 -11
  54. package/dist/wirings/forge-node/forge-node.types.d.ts +120 -0
  55. package/dist/wirings/forge-node/forge-node.types.js +38 -0
  56. package/dist/wirings/forge-node/index.d.ts +1 -0
  57. package/dist/wirings/forge-node/index.js +1 -0
  58. package/dist/wirings/http/http-runner.d.ts +8 -8
  59. package/dist/wirings/http/http-runner.js +46 -51
  60. package/dist/wirings/http/http.types.d.ts +19 -11
  61. package/dist/wirings/http/log-http-routes.js +1 -1
  62. package/dist/wirings/http/routers/path-to-regex.js +2 -2
  63. package/dist/wirings/mcp/mcp-runner.d.ts +2 -2
  64. package/dist/wirings/mcp/mcp-runner.js +33 -40
  65. package/dist/wirings/mcp/mcp.types.d.ts +6 -0
  66. package/dist/wirings/queue/queue-runner.d.ts +3 -3
  67. package/dist/wirings/queue/queue-runner.js +13 -23
  68. package/dist/wirings/queue/queue.types.d.ts +4 -11
  69. package/dist/wirings/rpc/index.d.ts +1 -1
  70. package/dist/wirings/rpc/index.js +1 -1
  71. package/dist/wirings/rpc/rpc-runner.d.ts +17 -16
  72. package/dist/wirings/rpc/rpc-runner.js +98 -61
  73. package/dist/wirings/rpc/rpc-types.d.ts +7 -0
  74. package/dist/wirings/scheduler/log-schedulers.js +1 -1
  75. package/dist/wirings/scheduler/scheduler-runner.d.ts +3 -3
  76. package/dist/wirings/scheduler/scheduler-runner.js +18 -29
  77. package/dist/wirings/scheduler/scheduler.types.d.ts +3 -8
  78. package/dist/wirings/trigger/index.d.ts +2 -0
  79. package/dist/wirings/trigger/index.js +2 -0
  80. package/dist/wirings/trigger/trigger-runner.d.ts +29 -0
  81. package/dist/wirings/trigger/trigger-runner.js +57 -0
  82. package/dist/wirings/trigger/trigger.types.d.ts +42 -0
  83. package/dist/wirings/trigger/trigger.types.js +1 -0
  84. package/dist/wirings/workflow/dsl/index.d.ts +5 -0
  85. package/dist/wirings/workflow/dsl/index.js +4 -0
  86. package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +286 -0
  87. package/dist/wirings/workflow/dsl/workflow-dsl.types.js +5 -0
  88. package/dist/wirings/workflow/dsl/workflow-runner.d.ts +5 -0
  89. package/dist/wirings/workflow/dsl/workflow-runner.js +22 -0
  90. package/dist/wirings/workflow/graph/graph-node.d.ts +122 -0
  91. package/dist/wirings/workflow/graph/graph-node.js +58 -0
  92. package/dist/wirings/workflow/graph/graph-runner.d.ts +35 -0
  93. package/dist/wirings/workflow/graph/graph-runner.js +452 -0
  94. package/dist/wirings/workflow/graph/index.d.ts +3 -0
  95. package/dist/wirings/workflow/graph/index.js +3 -0
  96. package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +95 -0
  97. package/dist/wirings/workflow/graph/workflow-graph.types.js +15 -0
  98. package/dist/wirings/workflow/index.d.ts +7 -3
  99. package/dist/wirings/workflow/index.js +6 -3
  100. package/dist/wirings/workflow/pikku-workflow-service.d.ts +108 -10
  101. package/dist/wirings/workflow/pikku-workflow-service.js +274 -165
  102. package/dist/wirings/workflow/wire-workflow.d.ts +42 -0
  103. package/dist/wirings/workflow/wire-workflow.js +53 -0
  104. package/dist/wirings/workflow/workflow-utils.d.ts +23 -0
  105. package/dist/wirings/workflow/workflow-utils.js +66 -0
  106. package/dist/wirings/workflow/workflow.types.d.ts +135 -128
  107. package/package.json +11 -2
  108. package/src/errors/error-handler.ts +3 -3
  109. package/src/errors/errors.ts +3 -3
  110. package/src/factory-functions.test.ts +9 -36
  111. package/src/function/function-runner.test.ts +58 -69
  112. package/src/function/function-runner.ts +130 -37
  113. package/src/function/functions.types.ts +193 -54
  114. package/src/handle-error.ts +1 -1
  115. package/src/index.ts +10 -5
  116. package/src/middleware/auth-apikey.test.ts +360 -0
  117. package/src/middleware/auth-apikey.ts +3 -7
  118. package/src/middleware/auth-bearer.test.ts +447 -0
  119. package/src/middleware/auth-bearer.ts +27 -33
  120. package/src/middleware/auth-cookie.test.ts +525 -0
  121. package/src/middleware/auth-cookie.ts +6 -6
  122. package/src/middleware/timeout.ts +4 -2
  123. package/src/middleware-runner.test.ts +58 -127
  124. package/src/middleware-runner.ts +29 -25
  125. package/src/permissions.test.ts +68 -130
  126. package/src/permissions.ts +41 -100
  127. package/src/pikku-state.ts +156 -201
  128. package/src/schema.ts +42 -13
  129. package/src/services/user-session-service.ts +14 -4
  130. package/src/services/workflow-service.ts +74 -0
  131. package/src/types/core.types.ts +79 -58
  132. package/src/types/state.types.ts +195 -0
  133. package/src/utils.ts +59 -4
  134. package/src/wirings/channel/channel-common.ts +4 -7
  135. package/src/wirings/channel/channel-handler.ts +17 -23
  136. package/src/wirings/channel/channel-runner.ts +7 -7
  137. package/src/wirings/channel/channel.types.ts +14 -11
  138. package/src/wirings/channel/local/local-channel-runner.test.ts +8 -4
  139. package/src/wirings/channel/local/local-channel-runner.ts +23 -38
  140. package/src/wirings/channel/log-channels.ts +1 -1
  141. package/src/wirings/channel/serverless/serverless-channel-runner.ts +53 -79
  142. package/src/wirings/cli/channel/cli-channel-runner.ts +4 -3
  143. package/src/wirings/cli/cli-runner.test.ts +26 -26
  144. package/src/wirings/cli/cli-runner.ts +34 -71
  145. package/src/wirings/cli/cli.types.ts +23 -16
  146. package/src/wirings/forge-node/forge-node.types.ts +135 -0
  147. package/src/wirings/forge-node/index.ts +1 -0
  148. package/src/wirings/http/http-runner.test.ts +10 -10
  149. package/src/wirings/http/http-runner.ts +56 -66
  150. package/src/wirings/http/http.types.ts +19 -14
  151. package/src/wirings/http/log-http-routes.ts +1 -1
  152. package/src/wirings/http/routers/path-to-regex.test.ts +30 -19
  153. package/src/wirings/http/routers/path-to-regex.ts +2 -2
  154. package/src/wirings/mcp/mcp-runner.ts +44 -69
  155. package/src/wirings/mcp/mcp.types.ts +6 -0
  156. package/src/wirings/queue/queue-runner.test.ts +641 -0
  157. package/src/wirings/queue/queue-runner.ts +16 -38
  158. package/src/wirings/queue/queue.types.ts +4 -11
  159. package/src/wirings/rpc/index.ts +1 -1
  160. package/src/wirings/rpc/rpc-runner.ts +129 -77
  161. package/src/wirings/rpc/rpc-types.ts +8 -0
  162. package/src/wirings/scheduler/log-schedulers.ts +1 -1
  163. package/src/wirings/scheduler/scheduler-runner.test.ts +627 -0
  164. package/src/wirings/scheduler/scheduler-runner.ts +29 -55
  165. package/src/wirings/scheduler/scheduler.types.ts +3 -9
  166. package/src/wirings/trigger/index.ts +2 -0
  167. package/src/wirings/trigger/trigger-runner.ts +96 -0
  168. package/src/wirings/trigger/trigger.types.ts +56 -0
  169. package/src/wirings/workflow/dsl/index.ts +30 -0
  170. package/src/wirings/workflow/dsl/workflow-dsl.types.ts +312 -0
  171. package/src/wirings/workflow/dsl/workflow-runner.ts +27 -0
  172. package/src/wirings/workflow/graph/graph-node.ts +227 -0
  173. package/src/wirings/workflow/graph/graph-runner.ts +694 -0
  174. package/src/wirings/workflow/graph/index.ts +3 -0
  175. package/src/wirings/workflow/graph/workflow-graph.types.ts +126 -0
  176. package/src/wirings/workflow/index.ts +60 -12
  177. package/src/wirings/workflow/pikku-workflow-service.ts +401 -195
  178. package/src/wirings/workflow/wire-workflow.ts +94 -0
  179. package/src/wirings/workflow/workflow-utils.ts +120 -0
  180. package/src/wirings/workflow/workflow.types.ts +167 -188
  181. package/tsconfig.tsbuildinfo +1 -1
  182. package/dist/wirings/workflow/workflow-runner.d.ts +0 -35
  183. package/dist/wirings/workflow/workflow-runner.js +0 -68
  184. package/src/wirings/workflow/workflow-runner.ts +0 -85
@@ -0,0 +1,122 @@
1
+ import type { GraphNodeConfig, NextConfig, RefValue } from './workflow-graph.types.js';
2
+ /**
3
+ * Standard RPC handler interface (matches generated FlattenedRPCMap entries)
4
+ */
5
+ export interface RPCHandler<I = any, O = any> {
6
+ input: I;
7
+ output: O;
8
+ }
9
+ /**
10
+ * Compute output types for all nodes based on their func (RPC name)
11
+ */
12
+ export type NodeOutputMap<Nodes extends Record<string, {
13
+ func: string;
14
+ }>, RPCMap extends Record<string, RPCHandler>> = {
15
+ [K in keyof Nodes]: Nodes[K]['func'] extends keyof RPCMap ? RPCMap[Nodes[K]['func']]['output'] : unknown;
16
+ };
17
+ /**
18
+ * Node definition with RPC name reference
19
+ */
20
+ export interface GraphNodeDef<RPCMap extends Record<string, RPCHandler>, NodeIds extends string = string, NodeOutputs extends Record<string, any> = Record<string, any>> {
21
+ /** RPC function name (must be a key in RPCMap) */
22
+ func: keyof RPCMap & string;
23
+ /** Input mapping - reference outputs from other nodes with autocomplete */
24
+ input?: (ref: <N extends keyof NodeOutputs & string>(nodeId: N, path: keyof NodeOutputs[N] & string) => RefValue) => Record<string, unknown>;
25
+ /** Next node(s) to execute */
26
+ next?: NextConfig<NodeIds>;
27
+ /** Error handling - node(s) to execute on error */
28
+ onError?: NodeIds | NodeIds[];
29
+ }
30
+ /**
31
+ * Helper to create a type-safe graph definition with RPC autocomplete.
32
+ * Pass node IDs as first type parameter for type-safe next/ref/output key completion.
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * // Import the typed graph from generated types
37
+ * import { pikkuWorkflowGraph, wireWorkflow } from './.pikku/workflow/pikku-workflow-types.gen.js'
38
+ *
39
+ * export const myGraph = pikkuWorkflowGraph({
40
+ * name: 'myWorkflow',
41
+ * nodes: {
42
+ * entry: 'createUserProfile', // autocompletes RPC names
43
+ * sendWelcome: 'sendEmail',
44
+ * },
45
+ * config: {
46
+ * entry: { next: 'sendWelcome' },
47
+ * sendWelcome: {
48
+ * input: (ref) => ({
49
+ * to: ref('entry', 'email'), // 'entry' and 'email' both autocomplete
50
+ * }),
51
+ * },
52
+ * },
53
+ * })
54
+ *
55
+ * wireWorkflow({
56
+ * wires: { http: { route: '/start', method: 'post' } },
57
+ * graph: myGraph,
58
+ * })
59
+ * ```
60
+ */
61
+ /**
62
+ * Type to compute output types for all nodes based on their func mapping
63
+ */
64
+ type ComputeNodeOutputs<FuncMap extends Record<string, string>, RPCMap extends Record<string, RPCHandler>> = {
65
+ [K in keyof FuncMap]: FuncMap[K] extends keyof RPCMap ? RPCMap[FuncMap[K]]['output'] : unknown;
66
+ };
67
+ /**
68
+ * Type to compute input types for all nodes based on their func mapping
69
+ */
70
+ type ComputeNodeInputs<FuncMap extends Record<string, string>, RPCMap extends Record<string, RPCHandler>> = {
71
+ [K in keyof FuncMap]: FuncMap[K] extends keyof RPCMap ? RPCMap[FuncMap[K]]['input'] : unknown;
72
+ };
73
+ /**
74
+ * Typed ref value - carries the type of the referenced field as a phantom type.
75
+ * At runtime this is just RefValue, but TypeScript tracks the type T.
76
+ */
77
+ export type TypedRef<T> = RefValue & {
78
+ __phantomType?: T;
79
+ };
80
+ /**
81
+ * Map input type fields to allow TypedRef of matching type as an alternative
82
+ */
83
+ type InputWithRefs<T> = {
84
+ [K in keyof T]: T[K] | TypedRef<T[K]>;
85
+ };
86
+ export declare function createGraph<RPCMap extends Record<string, RPCHandler>>(): <const FuncMap extends Record<string, keyof RPCMap & string>>(funcMap: FuncMap, nodesOrBuilder?: GraphNodeConfigMap<FuncMap, RPCMap> | ((nodes: FuncMap) => GraphNodeConfigMap<FuncMap, RPCMap>)) => Record<Extract<keyof FuncMap, string>, GraphNodeConfig<Extract<keyof FuncMap, string>>>;
87
+ /**
88
+ * Type helper for node configuration map
89
+ */
90
+ type GraphNodeConfigMap<FuncMap extends Record<string, string>, RPCMap extends Record<string, RPCHandler>> = {
91
+ [K in Extract<keyof FuncMap, string>]?: {
92
+ next?: NextConfig<Extract<keyof FuncMap, string>>;
93
+ input?: (ref: <N extends Extract<keyof FuncMap, string>, P extends keyof ComputeNodeOutputs<FuncMap, RPCMap>[N] & string>(nodeId: N, path: P) => TypedRef<ComputeNodeOutputs<FuncMap, RPCMap>[N][P]>) => InputWithRefs<ComputeNodeInputs<FuncMap, RPCMap>[K]>;
94
+ onError?: Extract<keyof FuncMap, string> | Extract<keyof FuncMap, string>[];
95
+ };
96
+ };
97
+ /**
98
+ * Untyped graph for use without RPC map.
99
+ * Pass node IDs as type parameter for type-safe next/ref, or omit for untyped usage.
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * // Typed usage - next and ref are type-checked
104
+ * graph<'entry' | 'sendWelcome'>({
105
+ * entry: { func: 'createUserProfile', next: 'sendWelcome' },
106
+ * sendWelcome: { func: 'sendEmail', input: (ref) => ({ to: ref('entry', 'email') }) },
107
+ * })
108
+ *
109
+ * // Untyped usage - no type checking on next/ref
110
+ * graph({
111
+ * entry: { func: 'createUserProfile', next: 'sendWelcome' },
112
+ * sendWelcome: { func: 'sendEmail' },
113
+ * })
114
+ * ```
115
+ */
116
+ export declare function graph<NodeIds extends string = string>(nodes: Record<NodeIds, {
117
+ func: string;
118
+ next?: NextConfig<NodeIds>;
119
+ input?: (ref: (nodeId: NodeIds, path: string) => RefValue) => Record<string, unknown>;
120
+ onError?: NodeIds | NodeIds[];
121
+ }>): Record<NodeIds, GraphNodeConfig<NodeIds>>;
122
+ export {};
@@ -0,0 +1,58 @@
1
+ export function createGraph() {
2
+ return (funcMap, nodesOrBuilder) => {
3
+ // If no nodes provided, return just the funcMap converted to graph nodes
4
+ if (!nodesOrBuilder) {
5
+ const result = {};
6
+ for (const [nodeId, rpcName] of Object.entries(funcMap)) {
7
+ result[nodeId] = {
8
+ func: rpcName,
9
+ };
10
+ }
11
+ return result;
12
+ }
13
+ const nodes = typeof nodesOrBuilder === 'function'
14
+ ? nodesOrBuilder(funcMap)
15
+ : nodesOrBuilder;
16
+ const result = {};
17
+ for (const [nodeId, def] of Object.entries(nodes)) {
18
+ result[nodeId] = {
19
+ func: funcMap[nodeId],
20
+ input: def?.input,
21
+ next: def?.next,
22
+ onError: def?.onError,
23
+ };
24
+ }
25
+ return result;
26
+ };
27
+ }
28
+ /**
29
+ * Untyped graph for use without RPC map.
30
+ * Pass node IDs as type parameter for type-safe next/ref, or omit for untyped usage.
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * // Typed usage - next and ref are type-checked
35
+ * graph<'entry' | 'sendWelcome'>({
36
+ * entry: { func: 'createUserProfile', next: 'sendWelcome' },
37
+ * sendWelcome: { func: 'sendEmail', input: (ref) => ({ to: ref('entry', 'email') }) },
38
+ * })
39
+ *
40
+ * // Untyped usage - no type checking on next/ref
41
+ * graph({
42
+ * entry: { func: 'createUserProfile', next: 'sendWelcome' },
43
+ * sendWelcome: { func: 'sendEmail' },
44
+ * })
45
+ * ```
46
+ */
47
+ export function graph(nodes) {
48
+ const result = {};
49
+ for (const [nodeId, def] of Object.entries(nodes)) {
50
+ result[nodeId] = {
51
+ func: def.func,
52
+ input: def.input,
53
+ next: def.next,
54
+ onError: def.onError,
55
+ };
56
+ }
57
+ return result;
58
+ }
@@ -0,0 +1,35 @@
1
+ import type { PikkuWorkflowService } from '../pikku-workflow-service.js';
2
+ import type { GraphNodeConfig, WorkflowGraphDefinition } from './workflow-graph.types.js';
3
+ /**
4
+ * Add a workflow graph to the system
5
+ * This is called by the generated workflow wirings
6
+ */
7
+ export declare const addWorkflowGraph: (workflowName: string, graphResult: {
8
+ graph: Record<string, GraphNodeConfig<string>>;
9
+ wires?: any;
10
+ }) => void;
11
+ /**
12
+ * Get a registered workflow graph by name
13
+ */
14
+ export declare function getWorkflowGraph(name: string): WorkflowGraphDefinition<any> | undefined;
15
+ /**
16
+ * Continue graph execution
17
+ * Non-blocking - finds pending nodes, resolves inputs, queues them for execution
18
+ */
19
+ export declare function continueGraph(workflowService: PikkuWorkflowService, runId: string, graphName: string): Promise<void>;
20
+ /**
21
+ * Execute a graph step with wire context.
22
+ * Called by the step worker when executing graph nodes.
23
+ */
24
+ export declare function executeGraphStep(workflowService: PikkuWorkflowService, rpcService: any, runId: string, stepId: string, stepName: string, rpcName: string, data: any, graphName: string): Promise<any>;
25
+ /**
26
+ * Handle node completion - re-triggers graph continuation
27
+ */
28
+ export declare function onGraphNodeComplete(workflowService: PikkuWorkflowService, runId: string, graphName: string): Promise<void>;
29
+ /**
30
+ * Start a workflow graph execution
31
+ * @param startNode - Optional starting node ID (from wire config). If not provided, uses wires.api from graph definition.
32
+ */
33
+ export declare function runWorkflowGraph(workflowService: PikkuWorkflowService, graphName: string, triggerInput: any, rpcService?: any, inline?: boolean, startNode?: string): Promise<{
34
+ runId: string;
35
+ }>;
@@ -0,0 +1,452 @@
1
+ import { createRef, isRef } from './workflow-graph.types.js';
2
+ import { pikkuState } from '../../../pikku-state.js';
3
+ /**
4
+ * Add a workflow graph to the system
5
+ * This is called by the generated workflow wirings
6
+ */
7
+ export const addWorkflowGraph = (workflowName, graphResult) => {
8
+ // Get workflow metadata from inspector
9
+ const meta = pikkuState(null, 'workflows', 'meta');
10
+ const workflowMeta = meta[workflowName];
11
+ if (!workflowMeta) {
12
+ throw new Error(`Workflow metadata not found for '${workflowName}'. Make sure to run the CLI to generate metadata.`);
13
+ }
14
+ // Store workflow graph definition in state
15
+ const registrations = pikkuState(null, 'workflows', 'graphRegistrations');
16
+ registrations.set(workflowName, {
17
+ name: workflowName,
18
+ wires: graphResult.wires || {},
19
+ graph: graphResult.graph,
20
+ });
21
+ };
22
+ /**
23
+ * Get a registered workflow graph by name
24
+ */
25
+ export function getWorkflowGraph(name) {
26
+ const registrations = pikkuState(null, 'workflows', 'graphRegistrations');
27
+ return registrations.get(name);
28
+ }
29
+ /**
30
+ * Resolve next config to array of node IDs
31
+ * For branching (Record), uses the branch key from step
32
+ */
33
+ function resolveNextFromConfig(next, branchKey) {
34
+ if (!next)
35
+ return [];
36
+ if (typeof next === 'string')
37
+ return [next];
38
+ if (Array.isArray(next))
39
+ return next;
40
+ // Record (branching) - use branch key set by graph.branch()
41
+ if (!branchKey || !(branchKey in next))
42
+ return [];
43
+ const branchNext = next[branchKey];
44
+ return Array.isArray(branchNext) ? branchNext : [branchNext];
45
+ }
46
+ /**
47
+ * Check if a value is a template
48
+ */
49
+ function isTemplate(value) {
50
+ return (typeof value === 'object' &&
51
+ value !== null &&
52
+ '$template' in value &&
53
+ typeof value.$template === 'object');
54
+ }
55
+ /**
56
+ * Create a template function for use in input callbacks
57
+ */
58
+ function createTemplate(templateStr, refs) {
59
+ const parts = [];
60
+ const expressions = [];
61
+ const regex = /\$(\d+)/g;
62
+ let lastIndex = 0;
63
+ let match;
64
+ while ((match = regex.exec(templateStr)) !== null) {
65
+ parts.push(templateStr.slice(lastIndex, match.index));
66
+ const refIndex = parseInt(match[1], 10);
67
+ const refValue = refs[refIndex];
68
+ if (refValue) {
69
+ expressions.push({ $ref: refValue.nodeId, path: refValue.path });
70
+ }
71
+ else {
72
+ expressions.push({ $ref: 'unknown' });
73
+ }
74
+ lastIndex = regex.lastIndex;
75
+ }
76
+ parts.push(templateStr.slice(lastIndex));
77
+ return { $template: { parts, expressions } };
78
+ }
79
+ /**
80
+ * Evaluate a node's input callback to get the input mapping
81
+ */
82
+ function evaluateInputCallback(node) {
83
+ if (!node.input)
84
+ return {};
85
+ const ref = (targetNodeId, path) => createRef(targetNodeId, path);
86
+ const template = (templateStr, refs) => createTemplate(templateStr, refs);
87
+ // Call with both ref and template - input callback may accept 1 or 2 params
88
+ return node.input(ref, template);
89
+ }
90
+ /**
91
+ * Extract node IDs referenced in an input mapping (including from templates)
92
+ */
93
+ function extractReferencedNodeIds(inputMapping) {
94
+ const nodeIds = [];
95
+ for (const value of Object.values(inputMapping)) {
96
+ if (isRef(value)) {
97
+ nodeIds.push(value.nodeId);
98
+ }
99
+ else if (isTemplate(value)) {
100
+ for (const expr of value.$template.expressions) {
101
+ nodeIds.push(expr.$ref);
102
+ }
103
+ }
104
+ }
105
+ return [...new Set(nodeIds)];
106
+ }
107
+ /**
108
+ * Resolve a template value using node results
109
+ */
110
+ function resolveTemplate(template, nodeResults) {
111
+ const { parts, expressions } = template.$template;
112
+ let result = '';
113
+ for (let i = 0; i < parts.length; i++) {
114
+ result += parts[i];
115
+ if (i < expressions.length) {
116
+ const expr = expressions[i];
117
+ const nodeResult = nodeResults[expr.$ref];
118
+ const value = expr.path
119
+ ? getValueAtPath(nodeResult, expr.path)
120
+ : nodeResult;
121
+ result += String(value ?? '');
122
+ }
123
+ }
124
+ return result;
125
+ }
126
+ /**
127
+ * Resolve input mapping using node results
128
+ */
129
+ function resolveInputMapping(inputMapping, nodeResults) {
130
+ const resolved = {};
131
+ for (const [key, value] of Object.entries(inputMapping)) {
132
+ if (isRef(value)) {
133
+ const nodeResult = nodeResults[value.nodeId];
134
+ resolved[key] = value.path
135
+ ? getValueAtPath(nodeResult, value.path)
136
+ : nodeResult;
137
+ }
138
+ else if (isTemplate(value)) {
139
+ resolved[key] = resolveTemplate(value, nodeResults);
140
+ }
141
+ else {
142
+ resolved[key] = value;
143
+ }
144
+ }
145
+ return resolved;
146
+ }
147
+ /**
148
+ * Get value at a dot-notation path from an object
149
+ */
150
+ function getValueAtPath(obj, path) {
151
+ if (!path)
152
+ return obj;
153
+ const parts = path.split('.');
154
+ let current = obj;
155
+ for (const part of parts) {
156
+ if (current == null)
157
+ return undefined;
158
+ current = current[part];
159
+ }
160
+ return current;
161
+ }
162
+ /**
163
+ * Get the RPC name from a graph node's func
164
+ * Supports both function references (func.name) and string RPC names
165
+ */
166
+ function getRpcName(node) {
167
+ const func = node.func;
168
+ // If func is a string, use it directly as the RPC name
169
+ if (typeof func === 'string') {
170
+ return func;
171
+ }
172
+ // Otherwise, it's a function reference - use its name
173
+ return func?.name || 'unknown';
174
+ }
175
+ /**
176
+ * Queue a graph node for execution
177
+ */
178
+ async function queueGraphNode(workflowService, runId, _graphName, nodeId, rpcName, input) {
179
+ // Step name convention: node:<nodeId>
180
+ // Graph name is stored as the workflow name on the run
181
+ await workflowService.insertStepState(runId, `node:${nodeId}`, rpcName, input, { retries: 3 });
182
+ await workflowService.resumeWorkflow(runId);
183
+ }
184
+ /**
185
+ * Continue graph execution
186
+ * Non-blocking - finds pending nodes, resolves inputs, queues them for execution
187
+ */
188
+ export async function continueGraph(workflowService, runId, graphName) {
189
+ const definition = getWorkflowGraph(graphName);
190
+ if (!definition) {
191
+ throw new Error(`Workflow graph '${graphName}' not found`);
192
+ }
193
+ const graph = definition.graph;
194
+ // Get completed node IDs + branch keys (lightweight, no results)
195
+ const { completedNodeIds, branchKeys } = await workflowService.getCompletedGraphState(runId);
196
+ // Find candidate next nodes from completed nodes
197
+ const candidateNodes = [];
198
+ for (const nodeId of completedNodeIds) {
199
+ const node = graph[nodeId];
200
+ if (!node?.next)
201
+ continue;
202
+ const nextNodes = resolveNextFromConfig(node.next, branchKeys[nodeId]);
203
+ candidateNodes.push(...nextNodes);
204
+ }
205
+ if (candidateNodes.length === 0 && completedNodeIds.length > 0) {
206
+ // No more nodes to run - graph complete
207
+ await workflowService.updateRunStatus(runId, 'completed');
208
+ return;
209
+ }
210
+ // Filter to only nodes that don't have a step yet
211
+ const nodesToQueue = await workflowService.getNodesWithoutSteps(runId, candidateNodes);
212
+ // Queue each node for execution
213
+ for (const nodeId of nodesToQueue) {
214
+ const node = graph[nodeId];
215
+ if (!node)
216
+ continue;
217
+ // Evaluate input callback to get the mapping
218
+ const inputMapping = evaluateInputCallback(node);
219
+ // Only fetch results for nodes referenced in this node's input
220
+ const referencedNodeIds = extractReferencedNodeIds(inputMapping);
221
+ const nodeResults = await workflowService.getNodeResults(runId, referencedNodeIds);
222
+ const resolvedInput = resolveInputMapping(inputMapping, nodeResults);
223
+ const rpcName = getRpcName(node);
224
+ await queueGraphNode(workflowService, runId, graphName, nodeId, rpcName, resolvedInput);
225
+ }
226
+ }
227
+ /**
228
+ * Execute a graph step with wire context.
229
+ * Called by the step worker when executing graph nodes.
230
+ */
231
+ export async function executeGraphStep(workflowService, rpcService, runId, stepId, stepName, rpcName, data, graphName) {
232
+ const nodeId = stepName.replace(/^node:/, '');
233
+ const wireState = {};
234
+ const graphWire = {
235
+ runId,
236
+ graphName,
237
+ nodeId,
238
+ branch: (key) => {
239
+ wireState.branchKey = key;
240
+ },
241
+ setState: (name, value) => workflowService.updateRunState(runId, name, value),
242
+ getState: () => workflowService.getRunState(runId),
243
+ };
244
+ try {
245
+ // Execute the RPC with graph wire context
246
+ const result = await rpcService.rpcWithWire(rpcName, data, {
247
+ graph: graphWire,
248
+ });
249
+ // If branch was called, store the branch key
250
+ if (wireState.branchKey) {
251
+ await workflowService.setBranchTaken(stepId, wireState.branchKey);
252
+ }
253
+ return result;
254
+ }
255
+ catch (error) {
256
+ // Check if this node has an onError handler
257
+ const definition = getWorkflowGraph(graphName);
258
+ if (definition) {
259
+ const node = definition.graph[nodeId];
260
+ if (node?.onError) {
261
+ // Route to error handler nodes
262
+ const errorNodes = Array.isArray(node.onError)
263
+ ? node.onError
264
+ : [node.onError];
265
+ for (const errorNodeId of errorNodes) {
266
+ const errorNode = definition.graph[errorNodeId];
267
+ if (errorNode) {
268
+ const errorRpcName = getRpcName(errorNode);
269
+ // Queue error node with the error as input
270
+ await queueGraphNode(workflowService, runId, graphName, errorNodeId, errorRpcName, { error: { message: error.message } });
271
+ }
272
+ }
273
+ // Don't rethrow - error was handled by routing to error nodes
274
+ return;
275
+ }
276
+ }
277
+ // No error handler - rethrow
278
+ throw error;
279
+ }
280
+ }
281
+ /**
282
+ * Handle node completion - re-triggers graph continuation
283
+ */
284
+ export async function onGraphNodeComplete(workflowService, runId, graphName) {
285
+ await continueGraph(workflowService, runId, graphName);
286
+ }
287
+ /**
288
+ * Execute a graph node inline (without queue)
289
+ */
290
+ async function executeGraphNodeInline(workflowService, rpcService, runId, graphName, nodeId, input, graph) {
291
+ const node = graph[nodeId];
292
+ if (!node)
293
+ return;
294
+ const rpcName = getRpcName(node);
295
+ const stepName = `node:${nodeId}`;
296
+ // Insert step state
297
+ const stepState = await workflowService.insertStepState(runId, stepName, rpcName, input, { retries: 3 });
298
+ await workflowService.setStepRunning(stepState.stepId);
299
+ // Execute with graph wire context
300
+ const wireState = {};
301
+ const graphWire = {
302
+ runId,
303
+ graphName,
304
+ nodeId,
305
+ branch: (key) => {
306
+ wireState.branchKey = key;
307
+ },
308
+ setState: (name, value) => workflowService.updateRunState(runId, name, value),
309
+ getState: () => workflowService.getRunState(runId),
310
+ };
311
+ try {
312
+ const result = await rpcService.rpcWithWire(rpcName, input, {
313
+ graph: graphWire,
314
+ });
315
+ // If branch was called, store the branch key
316
+ if (wireState.branchKey) {
317
+ await workflowService.setBranchTaken(stepState.stepId, wireState.branchKey);
318
+ }
319
+ await workflowService.setStepResult(stepState.stepId, result);
320
+ }
321
+ catch (error) {
322
+ await workflowService.setStepError(stepState.stepId, error);
323
+ // Check if this node has an onError handler
324
+ const definition = getWorkflowGraph(graphName);
325
+ if (definition) {
326
+ const node = definition.graph[nodeId];
327
+ if (node?.onError) {
328
+ // Route to error handler nodes (inline)
329
+ const errorNodes = Array.isArray(node.onError)
330
+ ? node.onError
331
+ : [node.onError];
332
+ await Promise.all(errorNodes.map((errorNodeId) => executeGraphNodeInline(workflowService, rpcService, runId, graphName, errorNodeId, { error: { message: error.message } }, graph)));
333
+ return;
334
+ }
335
+ }
336
+ // No error handler - rethrow
337
+ throw error;
338
+ }
339
+ }
340
+ /**
341
+ * Continue graph execution inline (without queue)
342
+ * Executes nodes in parallel where possible using Promise.all
343
+ */
344
+ async function continueGraphInline(workflowService, rpcService, runId, graphName, graph, triggerInput) {
345
+ while (true) {
346
+ // Get completed node IDs + branch keys (lightweight, no results)
347
+ const { completedNodeIds, branchKeys } = await workflowService.getCompletedGraphState(runId);
348
+ // Find candidate next nodes from completed nodes
349
+ const candidateNodes = [];
350
+ for (const nodeId of completedNodeIds) {
351
+ const node = graph[nodeId];
352
+ if (!node?.next)
353
+ continue;
354
+ const nextNodes = resolveNextFromConfig(node.next, branchKeys[nodeId]);
355
+ candidateNodes.push(...nextNodes);
356
+ }
357
+ if (candidateNodes.length === 0 && completedNodeIds.length > 0) {
358
+ // No more nodes to run - graph complete
359
+ await workflowService.updateRunStatus(runId, 'completed');
360
+ return;
361
+ }
362
+ // Filter to only nodes that don't have a step yet
363
+ const nodesToExecute = await workflowService.getNodesWithoutSteps(runId, candidateNodes);
364
+ if (nodesToExecute.length === 0) {
365
+ // No more nodes to execute - if we've completed at least one node, we're done
366
+ // This handles branching where only some paths are taken
367
+ if (completedNodeIds.length > 0) {
368
+ await workflowService.updateRunStatus(runId, 'completed');
369
+ }
370
+ return;
371
+ }
372
+ // Execute all nodes in parallel
373
+ await Promise.all(nodesToExecute.map(async (nodeId) => {
374
+ const node = graph[nodeId];
375
+ if (!node)
376
+ return;
377
+ // Evaluate input callback to get the mapping
378
+ const inputMapping = evaluateInputCallback(node);
379
+ // Only fetch results for nodes referenced in this node's input (excluding trigger)
380
+ const referencedNodeIds = extractReferencedNodeIds(inputMapping).filter((id) => id !== 'trigger');
381
+ const fetchedResults = await workflowService.getNodeResults(runId, referencedNodeIds);
382
+ // Merge fetched results with trigger input
383
+ const nodeResults = { trigger: triggerInput, ...fetchedResults };
384
+ const resolvedInput = resolveInputMapping(inputMapping, nodeResults);
385
+ await executeGraphNodeInline(workflowService, rpcService, runId, graphName, nodeId, resolvedInput, graph);
386
+ }));
387
+ }
388
+ }
389
+ /**
390
+ * Start a workflow graph execution
391
+ * @param startNode - Optional starting node ID (from wire config). If not provided, uses wires.api from graph definition.
392
+ */
393
+ export async function runWorkflowGraph(workflowService, graphName, triggerInput, rpcService, inline, startNode) {
394
+ const definition = getWorkflowGraph(graphName);
395
+ if (!definition) {
396
+ throw new Error(`Workflow graph '${graphName}' not found`);
397
+ }
398
+ // Use startNode from caller if provided, otherwise use wires.api from graph definition
399
+ const apiStartNode = startNode ?? definition.wires?.api;
400
+ const entryNodes = apiStartNode ? [apiStartNode] : [];
401
+ if (entryNodes.length === 0) {
402
+ throw new Error(`Workflow graph '${graphName}' has no wires.api defined and no startNode was provided`);
403
+ }
404
+ const graph = definition.graph;
405
+ const runId = await workflowService.createRun(graphName, triggerInput, inline);
406
+ // Register as inline for fast lookup
407
+ if (inline) {
408
+ workflowService.registerInlineRun(runId);
409
+ }
410
+ // Create nodeResults with trigger for resolving entry node inputs
411
+ const triggerNodeResults = { trigger: triggerInput };
412
+ try {
413
+ if (inline && rpcService) {
414
+ // Inline mode - execute entry nodes in parallel
415
+ await Promise.all(entryNodes.map(async (nodeId) => {
416
+ const node = graph[nodeId];
417
+ if (!node)
418
+ return;
419
+ // Evaluate and resolve entry node input
420
+ const inputMapping = evaluateInputCallback(node);
421
+ const resolvedInput = Object.keys(inputMapping).length > 0
422
+ ? resolveInputMapping(inputMapping, triggerNodeResults)
423
+ : triggerInput;
424
+ await executeGraphNodeInline(workflowService, rpcService, runId, graphName, nodeId, resolvedInput, graph);
425
+ }));
426
+ // Continue executing remaining nodes inline
427
+ await continueGraphInline(workflowService, rpcService, runId, graphName, graph, triggerInput);
428
+ }
429
+ else {
430
+ // Queue-based mode
431
+ for (const nodeId of entryNodes) {
432
+ const node = graph[nodeId];
433
+ if (!node)
434
+ continue;
435
+ // Evaluate and resolve entry node input
436
+ const inputMapping = evaluateInputCallback(node);
437
+ const resolvedInput = Object.keys(inputMapping).length > 0
438
+ ? resolveInputMapping(inputMapping, triggerNodeResults)
439
+ : triggerInput;
440
+ const rpcName = getRpcName(node);
441
+ await queueGraphNode(workflowService, runId, graphName, nodeId, rpcName, resolvedInput);
442
+ }
443
+ }
444
+ return { runId };
445
+ }
446
+ finally {
447
+ // Clean up inline tracking
448
+ if (inline) {
449
+ workflowService.unregisterInlineRun(runId);
450
+ }
451
+ }
452
+ }
@@ -0,0 +1,3 @@
1
+ export * from './workflow-graph.types.js';
2
+ export * from './graph-node.js';
3
+ export * from './graph-runner.js';
@@ -0,0 +1,3 @@
1
+ export * from './workflow-graph.types.js';
2
+ export * from './graph-node.js';
3
+ export * from './graph-runner.js';