@pikku/core 0.10.2 → 0.11.1

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