@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.
- package/CHANGELOG.md +35 -1
- package/dist/errors/error-handler.js +3 -3
- package/dist/errors/errors.d.ts +1 -1
- package/dist/errors/errors.js +3 -3
- package/dist/function/function-runner.d.ts +8 -8
- package/dist/function/function-runner.js +88 -23
- package/dist/function/functions.types.d.ts +104 -33
- package/dist/function/functions.types.js +50 -7
- package/dist/handle-error.d.ts +1 -1
- package/dist/handle-error.js +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +5 -2
- package/dist/middleware/auth-apikey.d.ts +1 -1
- package/dist/middleware/auth-apikey.js +3 -3
- package/dist/middleware/auth-bearer.d.ts +1 -1
- package/dist/middleware/auth-bearer.js +3 -3
- package/dist/middleware/auth-cookie.d.ts +1 -1
- package/dist/middleware/auth-cookie.js +5 -5
- package/dist/middleware/timeout.d.ts +1 -1
- package/dist/middleware/timeout.js +3 -2
- package/dist/middleware-runner.d.ts +6 -20
- package/dist/middleware-runner.js +19 -20
- package/dist/permissions.d.ts +6 -89
- package/dist/permissions.js +24 -81
- package/dist/pikku-state.d.ts +32 -104
- package/dist/pikku-state.js +140 -66
- package/dist/schema.d.ts +8 -6
- package/dist/schema.js +25 -13
- package/dist/services/user-session-service.d.ts +7 -3
- package/dist/services/user-session-service.js +8 -1
- package/dist/services/workflow-service.d.ts +38 -0
- package/dist/services/workflow-service.js +1 -0
- package/dist/types/core.types.d.ts +40 -41
- package/dist/types/core.types.js +4 -15
- package/dist/types/state.types.d.ts +135 -0
- package/dist/types/state.types.js +1 -0
- package/dist/utils.d.ts +9 -1
- package/dist/utils.js +48 -3
- package/dist/wirings/channel/channel-common.js +2 -3
- package/dist/wirings/channel/channel-handler.d.ts +3 -2
- package/dist/wirings/channel/channel-handler.js +7 -10
- package/dist/wirings/channel/channel-runner.d.ts +2 -2
- package/dist/wirings/channel/channel-runner.js +5 -5
- package/dist/wirings/channel/channel.types.d.ts +10 -5
- package/dist/wirings/channel/local/local-channel-runner.d.ts +1 -1
- package/dist/wirings/channel/local/local-channel-runner.js +19 -26
- package/dist/wirings/channel/log-channels.js +1 -1
- package/dist/wirings/channel/serverless/serverless-channel-runner.d.ts +1 -1
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +43 -48
- package/dist/wirings/cli/channel/cli-channel-runner.js +3 -2
- package/dist/wirings/cli/cli-runner.d.ts +5 -5
- package/dist/wirings/cli/cli-runner.js +20 -38
- package/dist/wirings/cli/cli.types.d.ts +16 -11
- package/dist/wirings/forge-node/forge-node.types.d.ts +120 -0
- package/dist/wirings/forge-node/forge-node.types.js +38 -0
- package/dist/wirings/forge-node/index.d.ts +1 -0
- package/dist/wirings/forge-node/index.js +1 -0
- package/dist/wirings/http/http-runner.d.ts +8 -8
- package/dist/wirings/http/http-runner.js +46 -51
- package/dist/wirings/http/http.types.d.ts +19 -11
- package/dist/wirings/http/log-http-routes.js +1 -1
- package/dist/wirings/http/routers/path-to-regex.js +2 -2
- package/dist/wirings/mcp/mcp-runner.d.ts +2 -2
- package/dist/wirings/mcp/mcp-runner.js +33 -40
- package/dist/wirings/mcp/mcp.types.d.ts +6 -0
- package/dist/wirings/queue/queue-runner.d.ts +3 -3
- package/dist/wirings/queue/queue-runner.js +13 -23
- package/dist/wirings/queue/queue.types.d.ts +4 -11
- package/dist/wirings/rpc/index.d.ts +1 -1
- package/dist/wirings/rpc/index.js +1 -1
- package/dist/wirings/rpc/rpc-runner.d.ts +17 -16
- package/dist/wirings/rpc/rpc-runner.js +98 -61
- package/dist/wirings/rpc/rpc-types.d.ts +7 -0
- package/dist/wirings/scheduler/log-schedulers.js +1 -1
- package/dist/wirings/scheduler/scheduler-runner.d.ts +3 -3
- package/dist/wirings/scheduler/scheduler-runner.js +18 -29
- package/dist/wirings/scheduler/scheduler.types.d.ts +3 -8
- package/dist/wirings/trigger/index.d.ts +2 -0
- package/dist/wirings/trigger/index.js +2 -0
- package/dist/wirings/trigger/trigger-runner.d.ts +29 -0
- package/dist/wirings/trigger/trigger-runner.js +57 -0
- package/dist/wirings/trigger/trigger.types.d.ts +42 -0
- package/dist/wirings/trigger/trigger.types.js +1 -0
- package/dist/wirings/workflow/dsl/index.d.ts +5 -0
- package/dist/wirings/workflow/dsl/index.js +4 -0
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +286 -0
- package/dist/wirings/workflow/dsl/workflow-dsl.types.js +5 -0
- package/dist/wirings/workflow/dsl/workflow-runner.d.ts +5 -0
- package/dist/wirings/workflow/dsl/workflow-runner.js +22 -0
- package/dist/wirings/workflow/graph/graph-node.d.ts +122 -0
- package/dist/wirings/workflow/graph/graph-node.js +58 -0
- package/dist/wirings/workflow/graph/graph-runner.d.ts +35 -0
- package/dist/wirings/workflow/graph/graph-runner.js +452 -0
- package/dist/wirings/workflow/graph/index.d.ts +3 -0
- package/dist/wirings/workflow/graph/index.js +3 -0
- package/dist/wirings/workflow/graph/workflow-graph.types.d.ts +95 -0
- package/dist/wirings/workflow/graph/workflow-graph.types.js +15 -0
- package/dist/wirings/workflow/index.d.ts +7 -3
- package/dist/wirings/workflow/index.js +6 -3
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +108 -10
- package/dist/wirings/workflow/pikku-workflow-service.js +274 -165
- package/dist/wirings/workflow/wire-workflow.d.ts +42 -0
- package/dist/wirings/workflow/wire-workflow.js +53 -0
- package/dist/wirings/workflow/workflow-utils.d.ts +23 -0
- package/dist/wirings/workflow/workflow-utils.js +66 -0
- package/dist/wirings/workflow/workflow.types.d.ts +135 -128
- package/package.json +11 -2
- package/src/errors/error-handler.ts +3 -3
- package/src/errors/errors.ts +3 -3
- package/src/factory-functions.test.ts +9 -36
- package/src/function/function-runner.test.ts +58 -69
- package/src/function/function-runner.ts +130 -37
- package/src/function/functions.types.ts +193 -54
- package/src/handle-error.ts +1 -1
- package/src/index.ts +10 -5
- package/src/middleware/auth-apikey.test.ts +360 -0
- package/src/middleware/auth-apikey.ts +3 -7
- package/src/middleware/auth-bearer.test.ts +447 -0
- package/src/middleware/auth-bearer.ts +27 -33
- package/src/middleware/auth-cookie.test.ts +525 -0
- package/src/middleware/auth-cookie.ts +6 -6
- package/src/middleware/timeout.ts +4 -2
- package/src/middleware-runner.test.ts +58 -127
- package/src/middleware-runner.ts +29 -25
- package/src/permissions.test.ts +68 -130
- package/src/permissions.ts +41 -100
- package/src/pikku-state.ts +156 -201
- package/src/schema.ts +42 -13
- package/src/services/user-session-service.ts +14 -4
- package/src/services/workflow-service.ts +74 -0
- package/src/types/core.types.ts +79 -58
- package/src/types/state.types.ts +195 -0
- package/src/utils.ts +59 -4
- package/src/wirings/channel/channel-common.ts +4 -7
- package/src/wirings/channel/channel-handler.ts +17 -23
- package/src/wirings/channel/channel-runner.ts +7 -7
- package/src/wirings/channel/channel.types.ts +14 -11
- package/src/wirings/channel/local/local-channel-runner.test.ts +8 -4
- package/src/wirings/channel/local/local-channel-runner.ts +23 -38
- package/src/wirings/channel/log-channels.ts +1 -1
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +53 -79
- package/src/wirings/cli/channel/cli-channel-runner.ts +4 -3
- package/src/wirings/cli/cli-runner.test.ts +26 -26
- package/src/wirings/cli/cli-runner.ts +34 -71
- package/src/wirings/cli/cli.types.ts +23 -16
- package/src/wirings/forge-node/forge-node.types.ts +135 -0
- package/src/wirings/forge-node/index.ts +1 -0
- package/src/wirings/http/http-runner.test.ts +10 -10
- package/src/wirings/http/http-runner.ts +56 -66
- package/src/wirings/http/http.types.ts +19 -14
- package/src/wirings/http/log-http-routes.ts +1 -1
- package/src/wirings/http/routers/path-to-regex.test.ts +30 -19
- package/src/wirings/http/routers/path-to-regex.ts +2 -2
- package/src/wirings/mcp/mcp-runner.ts +44 -69
- package/src/wirings/mcp/mcp.types.ts +6 -0
- package/src/wirings/queue/queue-runner.test.ts +641 -0
- package/src/wirings/queue/queue-runner.ts +16 -38
- package/src/wirings/queue/queue.types.ts +4 -11
- package/src/wirings/rpc/index.ts +1 -1
- package/src/wirings/rpc/rpc-runner.ts +129 -77
- package/src/wirings/rpc/rpc-types.ts +8 -0
- package/src/wirings/scheduler/log-schedulers.ts +1 -1
- package/src/wirings/scheduler/scheduler-runner.test.ts +627 -0
- package/src/wirings/scheduler/scheduler-runner.ts +29 -55
- package/src/wirings/scheduler/scheduler.types.ts +3 -9
- package/src/wirings/trigger/index.ts +2 -0
- package/src/wirings/trigger/trigger-runner.ts +96 -0
- package/src/wirings/trigger/trigger.types.ts +56 -0
- package/src/wirings/workflow/dsl/index.ts +30 -0
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +312 -0
- package/src/wirings/workflow/dsl/workflow-runner.ts +27 -0
- package/src/wirings/workflow/graph/graph-node.ts +227 -0
- package/src/wirings/workflow/graph/graph-runner.ts +694 -0
- package/src/wirings/workflow/graph/index.ts +3 -0
- package/src/wirings/workflow/graph/workflow-graph.types.ts +126 -0
- package/src/wirings/workflow/index.ts +60 -12
- package/src/wirings/workflow/pikku-workflow-service.ts +401 -195
- package/src/wirings/workflow/wire-workflow.ts +94 -0
- package/src/wirings/workflow/workflow-utils.ts +120 -0
- package/src/wirings/workflow/workflow.types.ts +167 -188
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/wirings/workflow/workflow-runner.d.ts +0 -35
- package/dist/wirings/workflow/workflow-runner.js +0 -68
- package/src/wirings/workflow/workflow-runner.ts +0 -85
|
@@ -1,8 +1,50 @@
|
|
|
1
1
|
import { runPikkuFunc } from '../../function/function-runner.js';
|
|
2
2
|
import { pikkuState } from '../../pikku-state.js';
|
|
3
3
|
import { getDurationInMilliseconds } from '../../time-utils.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { executeGraphStep, runWorkflowGraph } from './graph/graph-runner.js';
|
|
5
|
+
import { PikkuError } from '../../errors/error-handler.js';
|
|
6
|
+
/**
|
|
7
|
+
* Exception thrown when workflow needs to pause for async step
|
|
8
|
+
*/
|
|
9
|
+
export class WorkflowAsyncException extends Error {
|
|
10
|
+
runId;
|
|
11
|
+
stepName;
|
|
12
|
+
constructor(runId, stepName) {
|
|
13
|
+
super(`Workflow paused at step: ${stepName}`);
|
|
14
|
+
this.runId = runId;
|
|
15
|
+
this.stepName = stepName;
|
|
16
|
+
this.name = 'WorkflowAsyncException';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Exception thrown when workflow is cancelled
|
|
21
|
+
*/
|
|
22
|
+
export class WorkflowCancelledException extends Error {
|
|
23
|
+
runId;
|
|
24
|
+
reason;
|
|
25
|
+
constructor(runId, reason) {
|
|
26
|
+
super(reason || 'Workflow cancelled');
|
|
27
|
+
this.runId = runId;
|
|
28
|
+
this.reason = reason;
|
|
29
|
+
this.name = 'WorkflowCancelledException';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Error class for workflow not found
|
|
34
|
+
*/
|
|
35
|
+
export class WorkflowNotFoundError extends PikkuError {
|
|
36
|
+
constructor(name) {
|
|
37
|
+
super(`Workflow not found: ${name}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Error class for workflow not found
|
|
42
|
+
*/
|
|
43
|
+
export class WorkflowRunNotFound extends PikkuError {
|
|
44
|
+
constructor(runId) {
|
|
45
|
+
super(`Workflow run not found: ${runId}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
6
48
|
export class WorkflowServiceNotInitialized extends Error {
|
|
7
49
|
}
|
|
8
50
|
export class WorkflowStepNameNotString extends Error {
|
|
@@ -18,17 +60,36 @@ export class WorkflowStepNameNotString extends Error {
|
|
|
18
60
|
export class PikkuWorkflowService {
|
|
19
61
|
config;
|
|
20
62
|
singletonServices;
|
|
21
|
-
|
|
63
|
+
createWireServices;
|
|
64
|
+
inlineRuns = new Set();
|
|
22
65
|
constructor() { }
|
|
23
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Check if a run is executing inline (without queues)
|
|
68
|
+
*/
|
|
69
|
+
isInline(runId) {
|
|
70
|
+
return this.inlineRuns.has(runId);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Register a run as inline (for graph-runner to use)
|
|
74
|
+
*/
|
|
75
|
+
registerInlineRun(runId) {
|
|
76
|
+
this.inlineRuns.add(runId);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Unregister a run from inline tracking
|
|
80
|
+
*/
|
|
81
|
+
unregisterInlineRun(runId) {
|
|
82
|
+
this.inlineRuns.delete(runId);
|
|
83
|
+
}
|
|
84
|
+
setServices(singletonServices, createWireServices, { workflow }) {
|
|
24
85
|
this.singletonServices = singletonServices;
|
|
25
|
-
this.
|
|
86
|
+
this.createWireServices = createWireServices;
|
|
26
87
|
this.config = {
|
|
27
88
|
retries: workflow?.retries ?? 0,
|
|
28
89
|
retryDelay: workflow?.retryDelay ?? 0,
|
|
29
90
|
orchestratorQueueName: workflow?.orchestratorQueueName ?? 'pikku-workflow-orchestrator',
|
|
30
91
|
stepWorkerQueueName: workflow?.stepWorkerQueueName ?? 'pikku-workflow-step-worker',
|
|
31
|
-
sleeperRPCName: workflow?.sleeperRPCName ?? '
|
|
92
|
+
sleeperRPCName: workflow?.sleeperRPCName ?? 'pikkuWorkflowSleeper',
|
|
32
93
|
};
|
|
33
94
|
}
|
|
34
95
|
// ============================================================================
|
|
@@ -47,7 +108,7 @@ export class PikkuWorkflowService {
|
|
|
47
108
|
* Sets the step result to null and resumes the workflow
|
|
48
109
|
* @param data - Sleeper input data
|
|
49
110
|
*/
|
|
50
|
-
async
|
|
111
|
+
async executeWorkflowSleepCompleted(runId, stepId) {
|
|
51
112
|
await this.setStepResult(stepId, null);
|
|
52
113
|
await this.resumeWorkflow(runId);
|
|
53
114
|
}
|
|
@@ -62,81 +123,91 @@ export class PikkuWorkflowService {
|
|
|
62
123
|
}
|
|
63
124
|
/**
|
|
64
125
|
* Start a new workflow run
|
|
126
|
+
* Automatically detects workflow type (DSL or graph) from meta and executes accordingly
|
|
127
|
+
* @param options.inline - If true, execute workflow directly without queue service
|
|
128
|
+
* @param options.startNode - Starting node ID for graph workflows (from wire config)
|
|
65
129
|
*/
|
|
66
|
-
async startWorkflow(name, input, rpcService) {
|
|
67
|
-
|
|
130
|
+
async startWorkflow(name, input, rpcService, options) {
|
|
131
|
+
// Check meta to determine workflow type
|
|
132
|
+
const meta = pikkuState(null, 'workflows', 'meta');
|
|
133
|
+
const workflowMeta = meta[name];
|
|
134
|
+
if (!workflowMeta) {
|
|
135
|
+
throw new WorkflowNotFoundError(name);
|
|
136
|
+
}
|
|
137
|
+
// Check if this is a graph workflow (source === 'graph')
|
|
138
|
+
if (workflowMeta.source === 'graph') {
|
|
139
|
+
// Graph workflows use the graph scheduler
|
|
140
|
+
return runWorkflowGraph(this, name, input, rpcService, options?.inline, options?.startNode);
|
|
141
|
+
}
|
|
142
|
+
// DSL workflow - check registration exists
|
|
143
|
+
const registrations = pikkuState(null, 'workflows', 'registrations');
|
|
68
144
|
const workflow = registrations.get(name);
|
|
69
145
|
if (!workflow) {
|
|
70
146
|
throw new WorkflowNotFoundError(name);
|
|
71
147
|
}
|
|
72
148
|
// Create workflow run in state
|
|
73
|
-
const runId = await this.createRun(name, input);
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// Queue orchestrator to start the workflow
|
|
78
|
-
await this.resumeWorkflow(runId);
|
|
149
|
+
const runId = await this.createRun(name, input, options?.inline);
|
|
150
|
+
// Register inline run for fast lookup
|
|
151
|
+
if (options?.inline) {
|
|
152
|
+
this.inlineRuns.add(runId);
|
|
79
153
|
}
|
|
80
|
-
|
|
81
|
-
//
|
|
82
|
-
|
|
154
|
+
try {
|
|
155
|
+
// If inline mode or no queue service, execute directly
|
|
156
|
+
// Otherwise, use remote execution (queue-based)
|
|
157
|
+
if (options?.inline || !this.singletonServices?.queueService) {
|
|
158
|
+
await this.runWorkflowJob(runId, rpcService);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
await this.resumeWorkflow(runId);
|
|
162
|
+
}
|
|
163
|
+
return { runId };
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
// Clean up inline tracking
|
|
167
|
+
if (options?.inline) {
|
|
168
|
+
this.inlineRuns.delete(runId);
|
|
169
|
+
}
|
|
83
170
|
}
|
|
84
|
-
return { runId };
|
|
85
171
|
}
|
|
86
172
|
async runWorkflowJob(runId, rpcService) {
|
|
87
|
-
// Get the run
|
|
88
173
|
const run = await this.getRun(runId);
|
|
89
174
|
if (!run) {
|
|
90
175
|
throw new WorkflowRunNotFound(runId);
|
|
91
176
|
}
|
|
92
|
-
|
|
93
|
-
const registrations = pikkuState('workflows', 'registrations');
|
|
177
|
+
const registrations = pikkuState(null, 'workflows', 'registrations');
|
|
94
178
|
const workflow = registrations.get(run.workflow);
|
|
95
179
|
if (!workflow) {
|
|
96
180
|
throw new WorkflowNotFoundError(run.workflow);
|
|
97
181
|
}
|
|
98
|
-
// Use lock to prevent concurrent execution
|
|
99
182
|
await this.withRunLock(runId, async () => {
|
|
100
|
-
|
|
101
|
-
const workflowInteraction = this.createWorkflowInteraction(run.workflow, runId, rpcService);
|
|
102
|
-
// Get function metadata
|
|
103
|
-
const meta = pikkuState('workflows', 'meta');
|
|
183
|
+
const meta = pikkuState(null, 'workflows', 'meta');
|
|
104
184
|
const workflowMeta = meta[run.workflow];
|
|
105
|
-
|
|
185
|
+
const workflowWire = this.createWorkflowWire(run.workflow, runId, rpcService);
|
|
186
|
+
const wire = { workflow: workflowWire };
|
|
106
187
|
try {
|
|
107
|
-
const
|
|
108
|
-
let sessionServices = {};
|
|
109
|
-
const interaction = { workflow: workflowInteraction };
|
|
110
|
-
if (this.createSessionServices) {
|
|
111
|
-
sessionServices = this.createSessionServices(this.singletonServices, interaction, undefined);
|
|
112
|
-
}
|
|
113
|
-
return {
|
|
114
|
-
...this.singletonServices,
|
|
115
|
-
...sessionServices,
|
|
116
|
-
...interaction,
|
|
117
|
-
};
|
|
118
|
-
};
|
|
119
|
-
const result = await runPikkuFunc(PikkuWiringTypes.workflow, workflowMeta.workflowName, workflowMeta.pikkuFuncName, {
|
|
188
|
+
const result = await runPikkuFunc('workflow', workflowMeta.name, workflowMeta.pikkuFuncName, {
|
|
120
189
|
singletonServices: this.singletonServices,
|
|
121
|
-
|
|
122
|
-
|
|
190
|
+
wire,
|
|
191
|
+
createWireServices: this.createWireServices,
|
|
123
192
|
data: () => run.input,
|
|
124
193
|
});
|
|
125
|
-
// Workflow completed successfully
|
|
126
194
|
await this.updateRunStatus(runId, 'completed', result);
|
|
127
195
|
}
|
|
128
196
|
catch (error) {
|
|
129
|
-
// Check if it's a WorkflowAsyncException
|
|
130
197
|
if (error instanceof WorkflowAsyncException) {
|
|
131
198
|
// Normal - workflow paused for step execution
|
|
132
199
|
throw error;
|
|
133
200
|
}
|
|
134
201
|
// Check if it's a WorkflowCancelledException
|
|
135
202
|
if (error instanceof WorkflowCancelledException) {
|
|
136
|
-
// Workflow was cancelled - status
|
|
203
|
+
// Workflow was cancelled - update status and rethrow
|
|
204
|
+
await this.updateRunStatus(runId, 'cancelled', undefined, {
|
|
205
|
+
message: error.message || 'Workflow cancelled',
|
|
206
|
+
stack: '',
|
|
207
|
+
code: 'WORKFLOW_CANCELLED',
|
|
208
|
+
});
|
|
137
209
|
throw error;
|
|
138
210
|
}
|
|
139
|
-
// Real error - mark as failed
|
|
140
211
|
await this.updateRunStatus(runId, 'failed', undefined, {
|
|
141
212
|
message: error.message,
|
|
142
213
|
stack: error.stack,
|
|
@@ -151,56 +222,68 @@ export class PikkuWorkflowService {
|
|
|
151
222
|
* Handles idempotency, RPC execution, result storage, retry logic, and orchestrator triggering
|
|
152
223
|
*/
|
|
153
224
|
async executeWorkflowStep(runId, stepName, rpcName, data, rpcService) {
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
// Log warning if already running (race condition)
|
|
162
|
-
if (stepState.status === 'running') {
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
// If status is 'failed', this is a retry - create new attempt history
|
|
166
|
-
if (stepState.status === 'failed') {
|
|
167
|
-
stepState = await this.createRetryAttempt(stepState.stepId, 'running');
|
|
168
|
-
}
|
|
169
|
-
if (stepState.status !== 'pending') {
|
|
170
|
-
// Mark step as running (pending or failed status)
|
|
171
|
-
await this.setStepRunning(stepState.stepId);
|
|
172
|
-
}
|
|
173
|
-
try {
|
|
174
|
-
// Execute RPC with workflow step context
|
|
175
|
-
const result = await rpcService.rpcWithInteraction(rpcName, data, {
|
|
176
|
-
workflowStep: {
|
|
177
|
-
runId,
|
|
178
|
-
stepId: stepState.stepId,
|
|
179
|
-
attemptCount: stepState.attemptCount,
|
|
180
|
-
},
|
|
181
|
-
});
|
|
182
|
-
// Store result and mark succeeded
|
|
183
|
-
await this.setStepResult(stepState.stepId, result);
|
|
184
|
-
// Resume orchestrator to continue workflow
|
|
185
|
-
try {
|
|
225
|
+
// Use step-level lock to prevent concurrent execution of same step
|
|
226
|
+
await this.withStepLock(runId, stepName, async () => {
|
|
227
|
+
// Get step state
|
|
228
|
+
let stepState = await this.getStepState(runId, stepName);
|
|
229
|
+
// Idempotency - if already succeeded, resume orchestrator and return
|
|
230
|
+
if (stepState.status === 'succeeded') {
|
|
186
231
|
await this.resumeWorkflow(runId);
|
|
232
|
+
return;
|
|
187
233
|
}
|
|
188
|
-
|
|
189
|
-
|
|
234
|
+
// Log warning if already running (race condition)
|
|
235
|
+
if (stepState.status === 'running') {
|
|
236
|
+
return;
|
|
190
237
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
238
|
+
// If status is 'failed', this is a retry - create new attempt history
|
|
239
|
+
if (stepState.status === 'failed') {
|
|
240
|
+
stepState = await this.createRetryAttempt(stepState.stepId, 'running');
|
|
241
|
+
}
|
|
242
|
+
if (stepState.status !== 'pending') {
|
|
243
|
+
// Mark step as running (pending or failed status)
|
|
244
|
+
await this.setStepRunning(stepState.stepId);
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
let result;
|
|
248
|
+
// Check if this is a graph node step (step name starts with 'node:')
|
|
249
|
+
if (stepName.startsWith('node:')) {
|
|
250
|
+
// Get the graph name from the workflow run
|
|
251
|
+
const run = await this.getRun(runId);
|
|
252
|
+
if (!run) {
|
|
253
|
+
throw new Error(`Workflow run not found: ${runId}`);
|
|
254
|
+
}
|
|
255
|
+
const graphName = run.workflow;
|
|
256
|
+
// Execute as graph step with graph wire context
|
|
257
|
+
result = await executeGraphStep(this, rpcService, runId, stepState.stepId, stepName, rpcName, data, graphName);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
// Execute RPC with workflow step context (regular workflow)
|
|
261
|
+
result = await rpcService.rpcWithWire(rpcName, data, {
|
|
262
|
+
workflowStep: {
|
|
263
|
+
runId,
|
|
264
|
+
stepId: stepState.stepId,
|
|
265
|
+
attemptCount: stepState.attemptCount,
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
// Store result and mark succeeded
|
|
270
|
+
await this.setStepResult(stepState.stepId, result);
|
|
271
|
+
// Resume orchestrator to continue workflow
|
|
199
272
|
await this.resumeWorkflow(runId);
|
|
200
273
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
274
|
+
catch (error) {
|
|
275
|
+
// Store error and mark failed
|
|
276
|
+
await this.setStepError(stepState.stepId, error);
|
|
277
|
+
const maxAttempts = (stepState.retries ?? 0) + 1;
|
|
278
|
+
const retriesExhausted = stepState.attemptCount >= maxAttempts;
|
|
279
|
+
if (retriesExhausted) {
|
|
280
|
+
// No more retries - resume orchestrator to mark workflow as failed
|
|
281
|
+
await this.resumeWorkflow(runId);
|
|
282
|
+
}
|
|
283
|
+
// Always throw so queue knows the job failed and can retry if needed
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
});
|
|
204
287
|
}
|
|
205
288
|
/**
|
|
206
289
|
* Orchestrate workflow execution (called by orchestrator)
|
|
@@ -236,7 +319,7 @@ export class PikkuWorkflowService {
|
|
|
236
319
|
try {
|
|
237
320
|
stepState = await this.getStepState(runId, stepName);
|
|
238
321
|
}
|
|
239
|
-
catch
|
|
322
|
+
catch {
|
|
240
323
|
// Step doesn't exist - create it
|
|
241
324
|
stepState = await this.insertStepState(runId, stepName, rpcName, data, stepOptions);
|
|
242
325
|
}
|
|
@@ -260,8 +343,8 @@ export class PikkuWorkflowService {
|
|
|
260
343
|
}
|
|
261
344
|
// Step is pending - schedule it
|
|
262
345
|
await this.setStepScheduled(stepState.stepId);
|
|
263
|
-
// Enqueue step worker
|
|
264
|
-
if (this.singletonServices.queueService) {
|
|
346
|
+
// Enqueue step worker (unless inline mode)
|
|
347
|
+
if (!this.isInline(runId) && this.singletonServices.queueService) {
|
|
265
348
|
// Map step retry options to queue job options
|
|
266
349
|
const retries = stepOptions?.retries ?? 0;
|
|
267
350
|
const retryDelay = stepOptions?.retryDelay;
|
|
@@ -280,41 +363,47 @@ export class PikkuWorkflowService {
|
|
|
280
363
|
? 'exponential'
|
|
281
364
|
: undefined,
|
|
282
365
|
});
|
|
366
|
+
// Pause workflow - step will callback when done
|
|
367
|
+
throw new WorkflowAsyncException(runId, stepName);
|
|
283
368
|
}
|
|
284
369
|
else {
|
|
285
|
-
//
|
|
370
|
+
// Inline or no queue service - execute locally with retry loop
|
|
286
371
|
const retries = stepOptions?.retries ?? 0;
|
|
287
372
|
const retryDelay = stepOptions?.retryDelay;
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
//
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
373
|
+
let currentStepState = stepState;
|
|
374
|
+
while (true) {
|
|
375
|
+
try {
|
|
376
|
+
await this.setStepRunning(currentStepState.stepId);
|
|
377
|
+
const result = await rpcService.rpcWithWire(rpcName, data, {
|
|
378
|
+
workflowStep: {
|
|
379
|
+
runId,
|
|
380
|
+
stepId: currentStepState.stepId,
|
|
381
|
+
attemptCount: currentStepState.attemptCount,
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
await this.setStepResult(currentStepState.stepId, result);
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
// Record the error (marks step as failed)
|
|
389
|
+
await this.setStepError(currentStepState.stepId, error);
|
|
390
|
+
// Check if we should retry
|
|
391
|
+
if (currentStepState.attemptCount < retries) {
|
|
392
|
+
// Create a new pending retry attempt
|
|
393
|
+
currentStepState = await this.createRetryAttempt(currentStepState.stepId, 'pending');
|
|
394
|
+
// Wait for retry delay if specified
|
|
395
|
+
if (retryDelay) {
|
|
396
|
+
await new Promise((resolve) => setTimeout(resolve, getDurationInMilliseconds(retryDelay)));
|
|
397
|
+
}
|
|
398
|
+
// Continue loop to retry
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
// No more retries, fail the workflow
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
311
404
|
}
|
|
312
|
-
// No more retries, fail the workflow
|
|
313
|
-
throw error;
|
|
314
405
|
}
|
|
315
406
|
}
|
|
316
|
-
// Pause workflow - step will callback when done
|
|
317
|
-
throw new WorkflowAsyncException(runId, stepName);
|
|
318
407
|
}
|
|
319
408
|
async inlineStep(runId, stepName, fn, stepOptions) {
|
|
320
409
|
// Check if step already exists
|
|
@@ -322,7 +411,7 @@ export class PikkuWorkflowService {
|
|
|
322
411
|
try {
|
|
323
412
|
stepState = await this.getStepState(runId, stepName);
|
|
324
413
|
}
|
|
325
|
-
catch
|
|
414
|
+
catch {
|
|
326
415
|
// Step doesn't exist - create it (inline, no RPC)
|
|
327
416
|
stepState = await this.insertStepState(runId, stepName, null, // No RPC for inline steps
|
|
328
417
|
null, // No data for inline steps
|
|
@@ -335,26 +424,60 @@ export class PikkuWorkflowService {
|
|
|
335
424
|
// Execute inline function
|
|
336
425
|
const retries = stepOptions?.retries ?? this.getConfig().retries;
|
|
337
426
|
const retryDelay = stepOptions?.retryDelay ?? this.getConfig().retryDelay;
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
427
|
+
let currentStepState = stepState;
|
|
428
|
+
// Check if we're running inline (in-memory) or remote (queue-based)
|
|
429
|
+
if (this.isInline(runId)) {
|
|
430
|
+
// Inline mode - execute with retry loop
|
|
431
|
+
while (true) {
|
|
432
|
+
try {
|
|
433
|
+
await this.setStepRunning(currentStepState.stepId);
|
|
434
|
+
const result = await fn();
|
|
435
|
+
await this.setStepResult(currentStepState.stepId, result);
|
|
436
|
+
return result;
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
// Record the error (marks step as failed)
|
|
440
|
+
await this.setStepError(currentStepState.stepId, error);
|
|
441
|
+
// Check if we should retry
|
|
442
|
+
if (currentStepState.attemptCount < retries) {
|
|
443
|
+
// Create a new pending retry attempt
|
|
444
|
+
currentStepState = await this.createRetryAttempt(currentStepState.stepId, 'pending');
|
|
445
|
+
// Wait for retry delay if specified
|
|
446
|
+
if (retryDelay) {
|
|
447
|
+
await new Promise((resolve) => setTimeout(resolve, getDurationInMilliseconds(retryDelay)));
|
|
448
|
+
}
|
|
449
|
+
// Continue loop to retry
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
// No more retries, fail the workflow
|
|
453
|
+
throw error;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
343
457
|
}
|
|
344
|
-
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
//
|
|
354
|
-
|
|
458
|
+
else {
|
|
459
|
+
// Remote mode - use queue-based retry
|
|
460
|
+
try {
|
|
461
|
+
await this.setStepRunning(currentStepState.stepId);
|
|
462
|
+
const result = await fn();
|
|
463
|
+
await this.setStepResult(currentStepState.stepId, result);
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
// Record the error (marks step as failed)
|
|
468
|
+
await this.setStepError(currentStepState.stepId, error);
|
|
469
|
+
// Check if we should retry
|
|
470
|
+
if (currentStepState.attemptCount < retries) {
|
|
471
|
+
// Create a new pending retry attempt (copies metadata from failed step)
|
|
472
|
+
await this.createRetryAttempt(currentStepState.stepId, 'pending');
|
|
473
|
+
// Schedule orchestrator to retry after delay
|
|
474
|
+
await this.scheduleOrchestratorRetry(runId, retryDelay);
|
|
475
|
+
// Pause workflow - orchestrator will replay and pick up new attempt
|
|
476
|
+
throw new WorkflowAsyncException(runId, stepName);
|
|
477
|
+
}
|
|
478
|
+
// No more retries, fail the workflow
|
|
479
|
+
throw error;
|
|
355
480
|
}
|
|
356
|
-
// No more retries, fail the workflow
|
|
357
|
-
throw error;
|
|
358
481
|
}
|
|
359
482
|
}
|
|
360
483
|
async sleepStep(runId, stepName, duration) {
|
|
@@ -363,7 +486,7 @@ export class PikkuWorkflowService {
|
|
|
363
486
|
try {
|
|
364
487
|
stepState = await this.getStepState(runId, stepName);
|
|
365
488
|
}
|
|
366
|
-
catch
|
|
489
|
+
catch {
|
|
367
490
|
// Step doesn't exist - create it (sleep step, no RPC)
|
|
368
491
|
stepState = await this.insertStepState(runId, stepName, null, // No RPC for sleep steps
|
|
369
492
|
{ duration }, // Store duration as data
|
|
@@ -380,13 +503,15 @@ export class PikkuWorkflowService {
|
|
|
380
503
|
}
|
|
381
504
|
// Step is pending - schedule it
|
|
382
505
|
await this.setStepScheduled(stepState.stepId);
|
|
383
|
-
// Check if
|
|
384
|
-
if (this.singletonServices.schedulerService) {
|
|
385
|
-
// Remote mode -
|
|
506
|
+
// Check if inline mode or no scheduler service
|
|
507
|
+
if (!this.isInline(runId) && this.singletonServices.schedulerService) {
|
|
508
|
+
// Remote mode - schedule sleep via scheduler service
|
|
386
509
|
await this.singletonServices.schedulerService.scheduleRPC(duration, this.getConfig().sleeperRPCName, {
|
|
387
510
|
runId,
|
|
388
511
|
stepId: stepState.stepId,
|
|
389
512
|
});
|
|
513
|
+
// Pause workflow - sleep will callback when done
|
|
514
|
+
throw new WorkflowAsyncException(runId, stepName);
|
|
390
515
|
}
|
|
391
516
|
else {
|
|
392
517
|
// Inline mode - use setTimeout with actual duration
|
|
@@ -394,21 +519,9 @@ export class PikkuWorkflowService {
|
|
|
394
519
|
await this.setStepResult(stepState.stepId, null);
|
|
395
520
|
return;
|
|
396
521
|
}
|
|
397
|
-
// Pause workflow - sleep will callback when done
|
|
398
|
-
throw new WorkflowAsyncException(runId, stepName);
|
|
399
522
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
await this.updateRunStatus(runId, 'cancelled', undefined, {
|
|
403
|
-
message: reason || 'Workflow cancelled by user',
|
|
404
|
-
stack: '',
|
|
405
|
-
code: 'WORKFLOW_CANCELLED',
|
|
406
|
-
});
|
|
407
|
-
// Throw cancellation exception to stop workflow execution
|
|
408
|
-
throw new WorkflowCancelledException(runId, reason);
|
|
409
|
-
}
|
|
410
|
-
createWorkflowInteraction(workflowName, runId, rpcService) {
|
|
411
|
-
const workflowInteraction = {
|
|
523
|
+
createWorkflowWire(workflowName, runId, rpcService) {
|
|
524
|
+
const workflowWire = {
|
|
412
525
|
workflowName,
|
|
413
526
|
runId,
|
|
414
527
|
getRun: async () => (await this.getRun(runId)),
|
|
@@ -427,12 +540,8 @@ export class PikkuWorkflowService {
|
|
|
427
540
|
this.verifyStepName(stepName);
|
|
428
541
|
await this.sleepStep(runId, stepName, getDurationInMilliseconds(duration));
|
|
429
542
|
},
|
|
430
|
-
// Implement workflow.cancel()
|
|
431
|
-
cancel: async (reason) => {
|
|
432
|
-
await this.cancelWorkflow(runId, reason);
|
|
433
|
-
},
|
|
434
543
|
};
|
|
435
|
-
return
|
|
544
|
+
return workflowWire;
|
|
436
545
|
}
|
|
437
546
|
verifyStepName(stepName) {
|
|
438
547
|
if (typeof stepName !== 'string') {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { WorkflowWires } from './workflow.types.js';
|
|
2
|
+
import type { GraphNodeConfig } from './graph/workflow-graph.types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Workflow definition with DSL function
|
|
5
|
+
*/
|
|
6
|
+
export interface WorkflowDefinitionFunc {
|
|
7
|
+
wires: WorkflowWires;
|
|
8
|
+
func: {
|
|
9
|
+
func: (...args: any[]) => any;
|
|
10
|
+
} | ((...args: any[]) => any);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Workflow definition with graph
|
|
14
|
+
*/
|
|
15
|
+
export interface WorkflowDefinitionGraph<T extends Record<string, GraphNodeConfig<string>> = Record<string, GraphNodeConfig<string>>> {
|
|
16
|
+
wires: WorkflowWires;
|
|
17
|
+
graph: T;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Union type for workflow definitions
|
|
21
|
+
*/
|
|
22
|
+
export type WorkflowDefinition<T extends Record<string, GraphNodeConfig<string>> = Record<string, GraphNodeConfig<string>>> = WorkflowDefinitionFunc | WorkflowDefinitionGraph<T>;
|
|
23
|
+
/**
|
|
24
|
+
* Wire a workflow with triggers.
|
|
25
|
+
* Accepts either a DSL function (func) or a graph definition (graph).
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* // DSL workflow
|
|
30
|
+
* wireWorkflow({
|
|
31
|
+
* wires: { http: { route: '/start', method: 'post' } },
|
|
32
|
+
* func: myWorkflowFunc,
|
|
33
|
+
* })
|
|
34
|
+
*
|
|
35
|
+
* // Graph workflow
|
|
36
|
+
* wireWorkflow({
|
|
37
|
+
* wires: { http: { route: '/graph-start', method: 'post' } },
|
|
38
|
+
* graph: myGraphWorkflow,
|
|
39
|
+
* })
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare function wireWorkflow<T extends Record<string, GraphNodeConfig<string>>>(definition: WorkflowDefinitionFunc | WorkflowDefinitionGraph<T>): void;
|