@pikku/core 0.10.1 → 0.11.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/function/function-runner.js +2 -2
  3. package/dist/function/functions.types.d.ts +1 -0
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.js +2 -0
  6. package/dist/middleware/auth-apikey.d.ts +1 -0
  7. package/dist/middleware/auth-bearer.d.ts +1 -0
  8. package/dist/middleware/auth-cookie.d.ts +1 -0
  9. package/dist/middleware/timeout.d.ts +1 -0
  10. package/dist/middleware-runner.js +1 -0
  11. package/dist/permissions.js +1 -0
  12. package/dist/pikku-state.d.ts +7 -2
  13. package/dist/pikku-state.js +4 -0
  14. package/dist/services/index.d.ts +1 -0
  15. package/dist/services/index.js +1 -0
  16. package/dist/services/scheduler-service.d.ts +63 -0
  17. package/dist/services/scheduler-service.js +6 -0
  18. package/dist/time-utils.d.ts +14 -0
  19. package/dist/time-utils.js +62 -0
  20. package/dist/types/core.types.d.ts +24 -2
  21. package/dist/types/core.types.js +1 -0
  22. package/dist/wirings/channel/channel-common.d.ts +28 -0
  23. package/dist/wirings/channel/channel-common.js +42 -0
  24. package/dist/wirings/channel/channel-handler.js +37 -11
  25. package/dist/wirings/channel/channel-runner.js +9 -4
  26. package/dist/wirings/channel/channel.types.d.ts +8 -2
  27. package/dist/wirings/channel/local/local-channel-handler.js +3 -0
  28. package/dist/wirings/channel/local/local-channel-runner.js +47 -14
  29. package/dist/wirings/channel/serverless/serverless-channel-runner.js +90 -41
  30. package/dist/wirings/cli/channel/cli-channel-runner.js +10 -1
  31. package/dist/wirings/cli/cli-runner.d.ts +1 -1
  32. package/dist/wirings/cli/cli-runner.js +1 -1
  33. package/dist/wirings/http/http-runner.js +0 -1
  34. package/dist/wirings/mcp/mcp-runner.js +3 -2
  35. package/dist/wirings/queue/queue-runner.js +6 -3
  36. package/dist/wirings/queue/queue.types.d.ts +1 -3
  37. package/dist/wirings/rpc/index.d.ts +1 -1
  38. package/dist/wirings/rpc/index.js +1 -1
  39. package/dist/wirings/rpc/rpc-runner.d.ts +15 -0
  40. package/dist/wirings/rpc/rpc-runner.js +38 -3
  41. package/dist/wirings/rpc/rpc-types.d.ts +1 -0
  42. package/dist/wirings/workflow/index.d.ts +6 -0
  43. package/dist/wirings/workflow/index.js +6 -0
  44. package/dist/wirings/workflow/pikku-workflow-service.d.ts +152 -0
  45. package/dist/wirings/workflow/pikku-workflow-service.js +448 -0
  46. package/dist/wirings/workflow/workflow-runner.d.ts +35 -0
  47. package/dist/wirings/workflow/workflow-runner.js +68 -0
  48. package/dist/wirings/workflow/workflow.types.d.ts +247 -0
  49. package/dist/wirings/workflow/workflow.types.js +1 -0
  50. package/package.json +2 -3
  51. package/src/function/function-runner.ts +2 -2
  52. package/src/function/functions.types.ts +1 -0
  53. package/src/index.ts +2 -0
  54. package/src/middleware-runner.ts +1 -0
  55. package/src/permissions.ts +1 -0
  56. package/src/pikku-state.ts +14 -2
  57. package/src/services/index.ts +1 -0
  58. package/src/services/scheduler-service.ts +76 -0
  59. package/src/time-utils.ts +75 -0
  60. package/src/types/core.types.ts +30 -1
  61. package/src/wirings/channel/channel-common.ts +80 -0
  62. package/src/wirings/channel/channel-handler.ts +48 -18
  63. package/src/wirings/channel/channel-runner.ts +15 -4
  64. package/src/wirings/channel/channel.types.ts +12 -2
  65. package/src/wirings/channel/local/local-channel-handler.ts +3 -0
  66. package/src/wirings/channel/local/local-channel-runner.ts +48 -36
  67. package/src/wirings/channel/serverless/serverless-channel-runner.ts +134 -66
  68. package/src/wirings/cli/channel/cli-channel-runner.ts +13 -1
  69. package/src/wirings/cli/cli-runner.ts +2 -2
  70. package/src/wirings/http/http-runner.ts +0 -2
  71. package/src/wirings/mcp/mcp-runner.ts +5 -2
  72. package/src/wirings/queue/queue-runner.ts +14 -6
  73. package/src/wirings/queue/queue.types.ts +1 -4
  74. package/src/wirings/rpc/index.ts +1 -1
  75. package/src/wirings/rpc/rpc-runner.ts +57 -4
  76. package/src/wirings/rpc/rpc-types.ts +1 -0
  77. package/src/wirings/workflow/index.ts +22 -0
  78. package/src/wirings/workflow/pikku-workflow-service.ts +756 -0
  79. package/src/wirings/workflow/workflow-runner.ts +85 -0
  80. package/src/wirings/workflow/workflow.types.ts +332 -0
  81. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,152 @@
1
+ import { CoreConfig, CoreSingletonServices, CreateSessionServices, SerializedError } from '../../types/core.types.js';
2
+ import type { StepState, WorkflowRun, WorkflowStatus, WorkflowService, WorkflowStepOptions } from './workflow.types.js';
3
+ export declare class WorkflowServiceNotInitialized extends Error {
4
+ }
5
+ export declare class WorkflowStepNameNotString extends Error {
6
+ constructor(stepName: any);
7
+ }
8
+ /**
9
+ * Abstract workflow state service
10
+ * Implementations provide pluggable storage backends (SQLite, PostgreSQL, etc.)
11
+ * Combines orchestration and step execution
12
+ */
13
+ export declare abstract class PikkuWorkflowService implements WorkflowService {
14
+ private config;
15
+ private singletonServices;
16
+ private createSessionServices;
17
+ constructor();
18
+ setServices(singletonServices: CoreSingletonServices, createSessionServices: CreateSessionServices, { workflow }: CoreConfig): void;
19
+ /**
20
+ * Create a new workflow run
21
+ * @param name - Workflow name
22
+ * @param input - Input data for the workflow
23
+ * @returns Run ID
24
+ */
25
+ abstract createRun(workflowName: string, input: any): Promise<string>;
26
+ /**
27
+ * Get a workflow run by ID
28
+ * @param id - Run ID
29
+ * @returns Workflow run or null if not found
30
+ */
31
+ abstract getRun(id: string): Promise<WorkflowRun | null>;
32
+ /**
33
+ * Get workflow run history (all step attempts in chronological order)
34
+ * @param runId - Run ID
35
+ * @returns Array of step states with step names, ordered oldest to newest
36
+ */
37
+ abstract getRunHistory(runId: string): Promise<Array<StepState & {
38
+ stepName: string;
39
+ }>>;
40
+ /**
41
+ * Update workflow run status
42
+ * @param id - Run ID
43
+ * @param status - New status
44
+ */
45
+ abstract updateRunStatus(id: string, status: WorkflowStatus, output?: any, error?: SerializedError): Promise<void>;
46
+ /**
47
+ * Insert initial step state (called by orchestrator)
48
+ * Creates pending step in both workflow_step and workflow_step_history
49
+ * @param runId - Run ID
50
+ * @param stepName - Step cache key
51
+ * @param rpcName - RPC function name
52
+ * @param data - Step input data
53
+ * @param stepOptions - Step options (retries, retryDelay)
54
+ * @returns Step state with generated stepId
55
+ */
56
+ abstract insertStepState(runId: string, stepName: string, rpcName: string | null, data: any, stepOptions?: WorkflowStepOptions): Promise<StepState>;
57
+ /**
58
+ * Get step state by cache key (read-only)
59
+ * @param runId - Run ID
60
+ * @param stepName - Step cache key (from workflow.do)
61
+ * @returns Step state with attemptCount calculated from history
62
+ */
63
+ abstract getStepState(runId: string, stepName: string): Promise<StepState>;
64
+ /**
65
+ * Mark step as running
66
+ * Updates both workflow_step and workflow_step_history
67
+ * @param stepId - Step ID
68
+ */
69
+ abstract setStepRunning(stepId: string): Promise<void>;
70
+ /**
71
+ * Mark step as scheduled (queued for execution)
72
+ * Updates both workflow_step and workflow_step_history
73
+ * @param stepId - Step ID
74
+ */
75
+ abstract setStepScheduled(stepId: string): Promise<void>;
76
+ /**
77
+ * Store step result and mark as succeeded
78
+ * Updates both workflow_step and workflow_step_history
79
+ * @param stepId - Step ID
80
+ * @param result - Step result
81
+ */
82
+ abstract setStepResult(stepId: string, result: any): Promise<void>;
83
+ /**
84
+ * Store step error and mark as failed
85
+ * Updates both workflow_step and workflow_step_history
86
+ * @param stepId - Step ID
87
+ * @param error - Error object
88
+ */
89
+ abstract setStepError(stepId: string, error: Error): Promise<void>;
90
+ /**
91
+ * Create a new retry attempt for a failed step
92
+ * Inserts new pending step in both workflow_step and workflow_step_history
93
+ * Resets status to 'pending' with new stepId
94
+ * Copies metadata (rpcName, data, retries, retryDelay) from failed attempt
95
+ * @param failedStepId - Failed step ID to copy from
96
+ * @returns New step state for the retry attempt
97
+ */
98
+ abstract createRetryAttempt(failedStepId: string, status: 'pending' | 'running'): Promise<StepState>;
99
+ /**
100
+ * Execute function within a run lock to prevent concurrent modifications
101
+ * @param id - Run ID
102
+ * @param fn - Function to execute
103
+ * @returns Function result
104
+ */
105
+ abstract withRunLock<T>(id: string, fn: () => Promise<T>): Promise<T>;
106
+ /**
107
+ * Close any open connections
108
+ */
109
+ abstract close(): Promise<void>;
110
+ /**
111
+ * Resume a paused workflow by triggering the orchestrator
112
+ * @param runId - Run ID
113
+ */
114
+ resumeWorkflow(runId: string): Promise<void>;
115
+ /**
116
+ * Execute a workflow sleep step completion
117
+ * Sets the step result to null and resumes the workflow
118
+ * @param data - Sleeper input data
119
+ */
120
+ executeWorkflowSleep(runId: string, stepId: string): Promise<void>;
121
+ /**
122
+ * Schedule orchestrator retry with delay
123
+ * @param runId - Run ID
124
+ * @param retryDelay - Delay in milliseconds or duration string (optional)
125
+ */
126
+ protected scheduleOrchestratorRetry(runId: string, retryDelay?: number | string): Promise<void>;
127
+ /**
128
+ * Start a new workflow run
129
+ */
130
+ startWorkflow<I>(name: string, input: I, rpcService: any): Promise<{
131
+ runId: string;
132
+ }>;
133
+ runWorkflowJob(runId: string, rpcService: any): Promise<void>;
134
+ /**
135
+ * Execute a single workflow step (called by worker)
136
+ * Handles idempotency, RPC execution, result storage, retry logic, and orchestrator triggering
137
+ */
138
+ executeWorkflowStep(runId: string, stepName: string, rpcName: string, data: any, rpcService: any): Promise<void>;
139
+ /**
140
+ * Orchestrate workflow execution (called by orchestrator)
141
+ * Runs workflow job and handles async exceptions
142
+ */
143
+ orchestrateWorkflow(runId: string, rpcService: any): Promise<void>;
144
+ private verifyQueueService;
145
+ private rpcStep;
146
+ private inlineStep;
147
+ private sleepStep;
148
+ private cancelWorkflow;
149
+ private createWorkflowInteraction;
150
+ private verifyStepName;
151
+ private getConfig;
152
+ }
@@ -0,0 +1,448 @@
1
+ import { runPikkuFunc } from '../../function/function-runner.js';
2
+ import { pikkuState } from '../../pikku-state.js';
3
+ import { getDurationInMilliseconds } from '../../time-utils.js';
4
+ import { PikkuWiringTypes, } from '../../types/core.types.js';
5
+ import { WorkflowAsyncException, WorkflowCancelledException, WorkflowNotFoundError, WorkflowRunNotFound, } from './workflow-runner.js';
6
+ export class WorkflowServiceNotInitialized extends Error {
7
+ }
8
+ export class WorkflowStepNameNotString extends Error {
9
+ constructor(stepName) {
10
+ super(`Workflow step name must be a string. Received: ${typeof stepName}`);
11
+ }
12
+ }
13
+ /**
14
+ * Abstract workflow state service
15
+ * Implementations provide pluggable storage backends (SQLite, PostgreSQL, etc.)
16
+ * Combines orchestration and step execution
17
+ */
18
+ export class PikkuWorkflowService {
19
+ config;
20
+ singletonServices;
21
+ createSessionServices;
22
+ constructor() { }
23
+ setServices(singletonServices, createSessionServices, { workflow }) {
24
+ this.singletonServices = singletonServices;
25
+ this.createSessionServices = createSessionServices;
26
+ this.config = {
27
+ retries: workflow?.retries ?? 0,
28
+ retryDelay: workflow?.retryDelay ?? 0,
29
+ orchestratorQueueName: workflow?.orchestratorQueueName ?? 'pikku-workflow-orchestrator',
30
+ stepWorkerQueueName: workflow?.stepWorkerQueueName ?? 'pikku-workflow-step-worker',
31
+ sleeperRPCName: workflow?.sleeperRPCName ?? 'pikkuWorkflowStepSleeper',
32
+ };
33
+ }
34
+ // ============================================================================
35
+ // Workflow Lifecycle Methods
36
+ // ============================================================================
37
+ /**
38
+ * Resume a paused workflow by triggering the orchestrator
39
+ * @param runId - Run ID
40
+ */
41
+ async resumeWorkflow(runId) {
42
+ const queueService = this.verifyQueueService();
43
+ await queueService.add(this.getConfig().orchestratorQueueName, { runId });
44
+ }
45
+ /**
46
+ * Execute a workflow sleep step completion
47
+ * Sets the step result to null and resumes the workflow
48
+ * @param data - Sleeper input data
49
+ */
50
+ async executeWorkflowSleep(runId, stepId) {
51
+ await this.setStepResult(stepId, null);
52
+ await this.resumeWorkflow(runId);
53
+ }
54
+ /**
55
+ * Schedule orchestrator retry with delay
56
+ * @param runId - Run ID
57
+ * @param retryDelay - Delay in milliseconds or duration string (optional)
58
+ */
59
+ async scheduleOrchestratorRetry(runId, retryDelay) {
60
+ const queueService = this.verifyQueueService();
61
+ await queueService.add(this.getConfig().orchestratorQueueName, { runId }, retryDelay ? { delay: getDurationInMilliseconds(retryDelay) } : undefined);
62
+ }
63
+ /**
64
+ * Start a new workflow run
65
+ */
66
+ async startWorkflow(name, input, rpcService) {
67
+ const registrations = pikkuState('workflows', 'registrations');
68
+ const workflow = registrations.get(name);
69
+ if (!workflow) {
70
+ throw new WorkflowNotFoundError(name);
71
+ }
72
+ // Create workflow run in state
73
+ const runId = await this.createRun(name, input);
74
+ // If queue service is available, use remote execution (queue-based)
75
+ // Otherwise, execute directly (inline/synchronous)
76
+ if (this.singletonServices?.queueService) {
77
+ // Queue orchestrator to start the workflow
78
+ await this.resumeWorkflow(runId);
79
+ }
80
+ else {
81
+ // No queue service - execute directly via runWorkflowJob
82
+ await this.runWorkflowJob(runId, rpcService);
83
+ }
84
+ return { runId };
85
+ }
86
+ async runWorkflowJob(runId, rpcService) {
87
+ // Get the run
88
+ const run = await this.getRun(runId);
89
+ if (!run) {
90
+ throw new WorkflowRunNotFound(runId);
91
+ }
92
+ // Get workflow registration
93
+ const registrations = pikkuState('workflows', 'registrations');
94
+ const workflow = registrations.get(run.workflow);
95
+ if (!workflow) {
96
+ throw new WorkflowNotFoundError(run.workflow);
97
+ }
98
+ // Use lock to prevent concurrent execution
99
+ await this.withRunLock(runId, async () => {
100
+ // Create workflow interaction object
101
+ const workflowInteraction = this.createWorkflowInteraction(run.workflow, runId, rpcService);
102
+ // Get function metadata
103
+ const meta = pikkuState('workflows', 'meta');
104
+ const workflowMeta = meta[run.workflow];
105
+ // Execute workflow function with workflow interaction
106
+ try {
107
+ const getAllServices = () => {
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, {
120
+ singletonServices: this.singletonServices,
121
+ interaction: { workflow: workflowInteraction },
122
+ getAllServices,
123
+ data: () => run.input,
124
+ });
125
+ // Workflow completed successfully
126
+ await this.updateRunStatus(runId, 'completed', result);
127
+ }
128
+ catch (error) {
129
+ // Check if it's a WorkflowAsyncException
130
+ if (error instanceof WorkflowAsyncException) {
131
+ // Normal - workflow paused for step execution
132
+ throw error;
133
+ }
134
+ // Check if it's a WorkflowCancelledException
135
+ if (error instanceof WorkflowCancelledException) {
136
+ // Workflow was cancelled - status already updated, just rethrow
137
+ throw error;
138
+ }
139
+ // Real error - mark as failed
140
+ await this.updateRunStatus(runId, 'failed', undefined, {
141
+ message: error.message,
142
+ stack: error.stack,
143
+ code: error.code,
144
+ });
145
+ throw error;
146
+ }
147
+ });
148
+ }
149
+ /**
150
+ * Execute a single workflow step (called by worker)
151
+ * Handles idempotency, RPC execution, result storage, retry logic, and orchestrator triggering
152
+ */
153
+ async executeWorkflowStep(runId, stepName, rpcName, data, rpcService) {
154
+ // Get step state
155
+ let stepState = await this.getStepState(runId, stepName);
156
+ // Idempotency - if already succeeded, resume orchestrator and return
157
+ if (stepState.status === 'succeeded') {
158
+ await this.resumeWorkflow(runId);
159
+ return;
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 {
186
+ await this.resumeWorkflow(runId);
187
+ }
188
+ catch (resumeError) {
189
+ throw resumeError;
190
+ }
191
+ }
192
+ catch (error) {
193
+ // Store error and mark failed
194
+ await this.setStepError(stepState.stepId, error);
195
+ const maxAttempts = (stepState.retries ?? 0) + 1;
196
+ const retriesExhausted = stepState.attemptCount >= maxAttempts;
197
+ if (retriesExhausted) {
198
+ // No more retries - resume orchestrator to mark workflow as failed
199
+ await this.resumeWorkflow(runId);
200
+ }
201
+ // Always throw so queue knows the job failed and can retry if needed
202
+ throw error;
203
+ }
204
+ }
205
+ /**
206
+ * Orchestrate workflow execution (called by orchestrator)
207
+ * Runs workflow job and handles async exceptions
208
+ */
209
+ async orchestrateWorkflow(runId, rpcService) {
210
+ try {
211
+ // Run workflow job (replays with caching)
212
+ await this.runWorkflowJob(runId, rpcService);
213
+ }
214
+ catch (error) {
215
+ if (error.name === 'WorkflowAsyncException' ||
216
+ error.name === 'WorkflowCancelledException') {
217
+ return;
218
+ }
219
+ await this.updateRunStatus(runId, 'failed', undefined, {
220
+ message: error.message,
221
+ stack: error.stack,
222
+ code: error.code,
223
+ });
224
+ throw error;
225
+ }
226
+ }
227
+ verifyQueueService() {
228
+ if (!this.singletonServices?.queueService) {
229
+ throw new Error('QueueService not configured. Remote workflows require a queue service.');
230
+ }
231
+ return this.singletonServices.queueService;
232
+ }
233
+ async rpcStep(runId, stepName, rpcName, data, rpcService, stepOptions) {
234
+ // Check if step already exists
235
+ let stepState;
236
+ try {
237
+ stepState = await this.getStepState(runId, stepName);
238
+ }
239
+ catch (error) {
240
+ // Step doesn't exist - create it
241
+ stepState = await this.insertStepState(runId, stepName, rpcName, data, stepOptions);
242
+ }
243
+ if (stepState.status === 'succeeded') {
244
+ // Return cached result
245
+ return stepState.result;
246
+ }
247
+ if (stepState.status === 'failed') {
248
+ // Step failed with retries exhausted - throw error to fail the workflow
249
+ const error = new Error(stepState.error?.message ||
250
+ `Step '${stepName}' failed after exhausting all retries`);
251
+ // Preserve original error properties if available
252
+ if (stepState.error) {
253
+ Object.assign(error, stepState.error);
254
+ }
255
+ throw error;
256
+ }
257
+ if (stepState.status === 'scheduled') {
258
+ // Step is already scheduled, pause workflow
259
+ throw new WorkflowAsyncException(runId, stepName);
260
+ }
261
+ // Step is pending - schedule it
262
+ await this.setStepScheduled(stepState.stepId);
263
+ // Enqueue step worker
264
+ if (this.singletonServices.queueService) {
265
+ // Map step retry options to queue job options
266
+ const retries = stepOptions?.retries ?? 0;
267
+ const retryDelay = stepOptions?.retryDelay;
268
+ await this.singletonServices.queueService.add(this.getConfig().stepWorkerQueueName, {
269
+ runId,
270
+ stepName,
271
+ rpcName,
272
+ data,
273
+ }, {
274
+ // attempts includes initial attempt, retries doesn't
275
+ attempts: retries + 1,
276
+ // Map retry delay to backoff
277
+ backoff: typeof retryDelay === 'number'
278
+ ? { type: 'fixed', delay: retryDelay }
279
+ : retryDelay === 'exponential'
280
+ ? 'exponential'
281
+ : undefined,
282
+ });
283
+ }
284
+ else {
285
+ // No queue service - execute locally
286
+ const retries = stepOptions?.retries ?? 0;
287
+ const retryDelay = stepOptions?.retryDelay;
288
+ const attemptCount = stepState.attemptCount;
289
+ try {
290
+ const result = await rpcService.rpcWithInteraction(rpcName, data, {
291
+ workflowStep: {
292
+ runId,
293
+ stepId: stepState.stepId,
294
+ attemptCount: stepState.attemptCount,
295
+ },
296
+ });
297
+ await this.setStepResult(stepState.stepId, result);
298
+ return result;
299
+ }
300
+ catch (error) {
301
+ // Record the error (marks step as failed)
302
+ await this.setStepError(stepState.stepId, error);
303
+ // Check if we should retry
304
+ if (attemptCount < retries) {
305
+ // Create a new pending retry attempt (copies metadata from failed step)
306
+ await this.createRetryAttempt(stepState.stepId, 'pending');
307
+ // Schedule orchestrator to retry after delay
308
+ await this.scheduleOrchestratorRetry(runId, retryDelay);
309
+ // Pause workflow - orchestrator will replay and pick up new attempt
310
+ throw new WorkflowAsyncException(runId, stepName);
311
+ }
312
+ // No more retries, fail the workflow
313
+ throw error;
314
+ }
315
+ }
316
+ // Pause workflow - step will callback when done
317
+ throw new WorkflowAsyncException(runId, stepName);
318
+ }
319
+ async inlineStep(runId, stepName, fn, stepOptions) {
320
+ // Check if step already exists
321
+ let stepState;
322
+ try {
323
+ stepState = await this.getStepState(runId, stepName);
324
+ }
325
+ catch (error) {
326
+ // Step doesn't exist - create it (inline, no RPC)
327
+ stepState = await this.insertStepState(runId, stepName, null, // No RPC for inline steps
328
+ null, // No data for inline steps
329
+ stepOptions);
330
+ }
331
+ if (stepState.status === 'succeeded') {
332
+ // Return cached result
333
+ return stepState.result;
334
+ }
335
+ // Execute inline function
336
+ const retries = stepOptions?.retries ?? this.getConfig().retries;
337
+ const retryDelay = stepOptions?.retryDelay ?? this.getConfig().retryDelay;
338
+ const attemptCount = stepState.attemptCount;
339
+ try {
340
+ const result = await fn();
341
+ await this.setStepResult(stepState.stepId, result);
342
+ return result;
343
+ }
344
+ catch (error) {
345
+ // Record the error (marks step as failed)
346
+ await this.setStepError(stepState.stepId, error);
347
+ // Check if we should retry
348
+ if (attemptCount < retries) {
349
+ // Create a new pending retry attempt (copies metadata from failed step)
350
+ await this.createRetryAttempt(stepState.stepId, 'pending');
351
+ // Schedule orchestrator to retry after delay
352
+ await this.scheduleOrchestratorRetry(runId, retryDelay);
353
+ // Pause workflow - orchestrator will replay and pick up new attempt
354
+ throw new WorkflowAsyncException(runId, stepName);
355
+ }
356
+ // No more retries, fail the workflow
357
+ throw error;
358
+ }
359
+ }
360
+ async sleepStep(runId, stepName, duration) {
361
+ // Check if step already exists
362
+ let stepState;
363
+ try {
364
+ stepState = await this.getStepState(runId, stepName);
365
+ }
366
+ catch (error) {
367
+ // Step doesn't exist - create it (sleep step, no RPC)
368
+ stepState = await this.insertStepState(runId, stepName, null, // No RPC for sleep steps
369
+ { duration }, // Store duration as data
370
+ undefined // No retry options for sleep
371
+ );
372
+ }
373
+ if (stepState.status === 'succeeded') {
374
+ // Sleep already completed, return immediately
375
+ return;
376
+ }
377
+ if (stepState.status === 'scheduled') {
378
+ // Sleep is already scheduled, pause workflow
379
+ throw new WorkflowAsyncException(runId, stepName);
380
+ }
381
+ // Step is pending - schedule it
382
+ await this.setStepScheduled(stepState.stepId);
383
+ // Check if we have queue service (remote mode) or inline mode
384
+ if (this.singletonServices.schedulerService) {
385
+ // Remote mode - enqueue sleep worker with delay
386
+ await this.singletonServices.schedulerService.scheduleRPC(duration, this.getConfig().sleeperRPCName, {
387
+ runId,
388
+ stepId: stepState.stepId,
389
+ });
390
+ }
391
+ else {
392
+ // Inline mode - use setTimeout with actual duration
393
+ await new Promise((resolve) => setTimeout(resolve, getDurationInMilliseconds(duration)));
394
+ await this.setStepResult(stepState.stepId, null);
395
+ return;
396
+ }
397
+ // Pause workflow - sleep will callback when done
398
+ throw new WorkflowAsyncException(runId, stepName);
399
+ }
400
+ async cancelWorkflow(runId, reason) {
401
+ // Update workflow run status to cancelled
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 = {
412
+ workflowName,
413
+ runId,
414
+ getRun: async () => (await this.getRun(runId)),
415
+ // Implement workflow.do() - RPC form
416
+ do: async (stepName, rpcNameOrFn, dataOrOptions, options) => {
417
+ this.verifyStepName(stepName);
418
+ if (typeof rpcNameOrFn === 'string') {
419
+ return await this.rpcStep(runId, stepName, rpcNameOrFn, dataOrOptions, rpcService, options);
420
+ }
421
+ else {
422
+ return await this.inlineStep(runId, stepName, rpcNameOrFn, dataOrOptions);
423
+ }
424
+ },
425
+ // Implement workflow.sleep()
426
+ sleep: async (stepName, duration) => {
427
+ this.verifyStepName(stepName);
428
+ await this.sleepStep(runId, stepName, getDurationInMilliseconds(duration));
429
+ },
430
+ // Implement workflow.cancel()
431
+ cancel: async (reason) => {
432
+ await this.cancelWorkflow(runId, reason);
433
+ },
434
+ };
435
+ return workflowInteraction;
436
+ }
437
+ verifyStepName(stepName) {
438
+ if (typeof stepName !== 'string') {
439
+ throw new WorkflowStepNameNotString(stepName);
440
+ }
441
+ }
442
+ getConfig() {
443
+ if (!this.singletonServices || !this.config) {
444
+ throw new WorkflowServiceNotInitialized();
445
+ }
446
+ return this.config;
447
+ }
448
+ }
@@ -0,0 +1,35 @@
1
+ import type { CoreWorkflow } from './workflow.types.js';
2
+ import type { CorePikkuFunctionConfig } from '../../function/functions.types.js';
3
+ import { PikkuError } from '../../errors/error-handler.js';
4
+ /**
5
+ * Exception thrown when workflow needs to pause for async step
6
+ */
7
+ export declare class WorkflowAsyncException extends Error {
8
+ readonly runId: string;
9
+ readonly stepName: string;
10
+ constructor(runId: string, stepName: string);
11
+ }
12
+ /**
13
+ * Exception thrown when workflow is cancelled
14
+ */
15
+ export declare class WorkflowCancelledException extends Error {
16
+ readonly runId: string;
17
+ readonly reason?: string | undefined;
18
+ constructor(runId: string, reason?: string | undefined);
19
+ }
20
+ /**
21
+ * Error class for workflow not found
22
+ */
23
+ export declare class WorkflowNotFoundError extends PikkuError {
24
+ constructor(name: string);
25
+ }
26
+ /**
27
+ * Error class for workflow not found
28
+ */
29
+ export declare class WorkflowRunNotFound extends PikkuError {
30
+ constructor(runId: string);
31
+ }
32
+ /**
33
+ * Register a workflow with the system
34
+ */
35
+ export declare const wireWorkflow: <PikkuFunctionConfig extends CorePikkuFunctionConfig<any, any, any> = CorePikkuFunctionConfig<any, any, any>>(workflow: CoreWorkflow<PikkuFunctionConfig>) => void;