@pikku/core 0.11.0 → 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.
- package/CHANGELOG.md +11 -1
- package/dist/errors/errors.d.ts +1 -1
- package/dist/errors/errors.js +3 -3
- package/dist/function/function-runner.d.ts +6 -7
- package/dist/function/function-runner.js +35 -16
- package/dist/function/functions.types.d.ts +22 -30
- package/dist/function/functions.types.js +9 -6
- package/dist/handle-error.d.ts +1 -1
- package/dist/handle-error.js +1 -1
- package/dist/middleware/auth-apikey.js +3 -3
- package/dist/middleware/auth-bearer.js +3 -3
- package/dist/middleware/auth-cookie.js +5 -5
- package/dist/middleware/timeout.js +3 -2
- package/dist/middleware-runner.d.ts +3 -6
- package/dist/middleware-runner.js +11 -12
- package/dist/permissions.d.ts +6 -6
- package/dist/permissions.js +15 -16
- package/dist/services/user-session-service.d.ts +7 -3
- package/dist/services/user-session-service.js +8 -1
- package/dist/types/core.types.d.ts +29 -38
- package/dist/types/core.types.js +3 -14
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +3 -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 +6 -9
- package/dist/wirings/channel/channel-runner.d.ts +2 -2
- 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/serverless/serverless-channel-runner.d.ts +1 -1
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +41 -46
- package/dist/wirings/cli/cli-runner.d.ts +5 -5
- package/dist/wirings/cli/cli-runner.js +10 -28
- package/dist/wirings/cli/cli.types.d.ts +15 -11
- package/dist/wirings/http/http-runner.d.ts +6 -6
- package/dist/wirings/http/http-runner.js +25 -39
- package/dist/wirings/http/http.types.d.ts +5 -10
- package/dist/wirings/mcp/mcp-runner.d.ts +2 -2
- package/dist/wirings/mcp/mcp-runner.js +12 -19
- 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 +7 -17
- package/dist/wirings/queue/queue.types.d.ts +4 -11
- package/dist/wirings/rpc/rpc-runner.d.ts +10 -8
- package/dist/wirings/rpc/rpc-runner.js +36 -47
- package/dist/wirings/scheduler/scheduler-runner.d.ts +3 -3
- package/dist/wirings/scheduler/scheduler-runner.js +13 -24
- package/dist/wirings/scheduler/scheduler.types.d.ts +3 -8
- package/dist/wirings/workflow/index.d.ts +11 -2
- package/dist/wirings/workflow/index.js +10 -2
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +12 -4
- package/dist/wirings/workflow/pikku-workflow-service.js +61 -79
- package/dist/wirings/workflow/workflow-runner.d.ts +3 -4
- package/dist/wirings/workflow/workflow-runner.js +11 -14
- package/dist/wirings/workflow/workflow.types.d.ts +114 -29
- package/package.json +2 -2
- package/src/errors/errors.ts +3 -3
- package/src/factory-functions.test.ts +9 -36
- package/src/function/function-runner.test.ts +57 -68
- package/src/function/function-runner.ts +51 -28
- package/src/function/function-runner.ts.bak +188 -0
- package/src/function/functions.types.ts +68 -51
- package/src/handle-error.ts +1 -1
- 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 +13 -16
- package/src/permissions.test.ts +41 -45
- package/src/permissions.ts +21 -21
- package/src/services/user-session-service.ts +14 -4
- package/src/types/core.types.ts +67 -55
- package/src/utils.ts +4 -4
- package/src/wirings/channel/channel-common.ts +4 -7
- package/src/wirings/channel/channel-handler.ts +16 -22
- package/src/wirings/channel/channel-runner.ts +2 -2
- package/src/wirings/channel/channel.types.ts +14 -11
- package/src/wirings/channel/local/local-channel-runner.test.ts +5 -3
- package/src/wirings/channel/local/local-channel-runner.ts +23 -38
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +51 -77
- package/src/wirings/cli/cli-runner.test.ts +7 -7
- package/src/wirings/cli/cli-runner.ts +24 -62
- package/src/wirings/cli/cli.types.ts +22 -16
- package/src/wirings/http/http-runner.test.ts +8 -8
- package/src/wirings/http/http-runner.ts +29 -56
- package/src/wirings/http/http.types.ts +5 -14
- package/src/wirings/mcp/mcp-runner.ts +23 -48
- 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 +10 -32
- package/src/wirings/queue/queue.types.ts +4 -11
- package/src/wirings/rpc/rpc-runner.ts +41 -55
- package/src/wirings/scheduler/scheduler-runner.test.ts +627 -0
- package/src/wirings/scheduler/scheduler-runner.ts +24 -50
- package/src/wirings/scheduler/scheduler.types.ts +3 -9
- package/src/wirings/workflow/index.ts +24 -3
- package/src/wirings/workflow/pikku-workflow-service.ts +83 -93
- package/src/wirings/workflow/workflow-runner.ts +12 -25
- package/src/wirings/workflow/workflow.types.ts +144 -54
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CoreConfig, CoreSingletonServices,
|
|
1
|
+
import { CoreConfig, CoreSingletonServices, CreateWireServices, SerializedError } from '../../types/core.types.js';
|
|
2
2
|
import type { StepState, WorkflowRun, WorkflowStatus, WorkflowService, WorkflowStepOptions } from './workflow.types.js';
|
|
3
3
|
export declare class WorkflowServiceNotInitialized extends Error {
|
|
4
4
|
}
|
|
@@ -13,9 +13,9 @@ export declare class WorkflowStepNameNotString extends Error {
|
|
|
13
13
|
export declare abstract class PikkuWorkflowService implements WorkflowService {
|
|
14
14
|
private config;
|
|
15
15
|
private singletonServices;
|
|
16
|
-
private
|
|
16
|
+
private createWireServices;
|
|
17
17
|
constructor();
|
|
18
|
-
setServices(singletonServices: CoreSingletonServices,
|
|
18
|
+
setServices(singletonServices: CoreSingletonServices, createWireServices: CreateWireServices, { workflow }: CoreConfig): void;
|
|
19
19
|
/**
|
|
20
20
|
* Create a new workflow run
|
|
21
21
|
* @param name - Workflow name
|
|
@@ -103,6 +103,14 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
103
103
|
* @returns Function result
|
|
104
104
|
*/
|
|
105
105
|
abstract withRunLock<T>(id: string, fn: () => Promise<T>): Promise<T>;
|
|
106
|
+
/**
|
|
107
|
+
* Execute function within a step lock to prevent concurrent step execution
|
|
108
|
+
* @param runId - Run ID
|
|
109
|
+
* @param stepName - Step name
|
|
110
|
+
* @param fn - Function to execute
|
|
111
|
+
* @returns Function result
|
|
112
|
+
*/
|
|
113
|
+
abstract withStepLock<T>(runId: string, stepName: string, fn: () => Promise<T>): Promise<T>;
|
|
106
114
|
/**
|
|
107
115
|
* Close any open connections
|
|
108
116
|
*/
|
|
@@ -146,7 +154,7 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
146
154
|
private inlineStep;
|
|
147
155
|
private sleepStep;
|
|
148
156
|
private cancelWorkflow;
|
|
149
|
-
private
|
|
157
|
+
private createWorkflowWire;
|
|
150
158
|
private verifyStepName;
|
|
151
159
|
private getConfig;
|
|
152
160
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
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 { PikkuWiringTypes, } from '../../types/core.types.js';
|
|
5
4
|
import { WorkflowAsyncException, WorkflowCancelledException, WorkflowNotFoundError, WorkflowRunNotFound, } from './workflow-runner.js';
|
|
6
5
|
export class WorkflowServiceNotInitialized extends Error {
|
|
7
6
|
}
|
|
@@ -18,11 +17,11 @@ export class WorkflowStepNameNotString extends Error {
|
|
|
18
17
|
export class PikkuWorkflowService {
|
|
19
18
|
config;
|
|
20
19
|
singletonServices;
|
|
21
|
-
|
|
20
|
+
createWireServices;
|
|
22
21
|
constructor() { }
|
|
23
|
-
setServices(singletonServices,
|
|
22
|
+
setServices(singletonServices, createWireServices, { workflow }) {
|
|
24
23
|
this.singletonServices = singletonServices;
|
|
25
|
-
this.
|
|
24
|
+
this.createWireServices = createWireServices;
|
|
26
25
|
this.config = {
|
|
27
26
|
retries: workflow?.retries ?? 0,
|
|
28
27
|
retryDelay: workflow?.retryDelay ?? 0,
|
|
@@ -84,49 +83,30 @@ export class PikkuWorkflowService {
|
|
|
84
83
|
return { runId };
|
|
85
84
|
}
|
|
86
85
|
async runWorkflowJob(runId, rpcService) {
|
|
87
|
-
// Get the run
|
|
88
86
|
const run = await this.getRun(runId);
|
|
89
87
|
if (!run) {
|
|
90
88
|
throw new WorkflowRunNotFound(runId);
|
|
91
89
|
}
|
|
92
|
-
// Get workflow registration
|
|
93
90
|
const registrations = pikkuState('workflows', 'registrations');
|
|
94
91
|
const workflow = registrations.get(run.workflow);
|
|
95
92
|
if (!workflow) {
|
|
96
93
|
throw new WorkflowNotFoundError(run.workflow);
|
|
97
94
|
}
|
|
98
|
-
// Use lock to prevent concurrent execution
|
|
99
95
|
await this.withRunLock(runId, async () => {
|
|
100
|
-
// Create workflow interaction object
|
|
101
|
-
const workflowInteraction = this.createWorkflowInteraction(run.workflow, runId, rpcService);
|
|
102
|
-
// Get function metadata
|
|
103
96
|
const meta = pikkuState('workflows', 'meta');
|
|
104
97
|
const workflowMeta = meta[run.workflow];
|
|
105
|
-
|
|
98
|
+
const workflowWire = this.createWorkflowWire(run.workflow, runId, rpcService);
|
|
99
|
+
const wire = { workflow: workflowWire };
|
|
106
100
|
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, {
|
|
101
|
+
const result = await runPikkuFunc('workflow', workflowMeta.workflowName, workflowMeta.pikkuFuncName, {
|
|
120
102
|
singletonServices: this.singletonServices,
|
|
121
|
-
|
|
122
|
-
|
|
103
|
+
wire,
|
|
104
|
+
createWireServices: this.createWireServices,
|
|
123
105
|
data: () => run.input,
|
|
124
106
|
});
|
|
125
|
-
// Workflow completed successfully
|
|
126
107
|
await this.updateRunStatus(runId, 'completed', result);
|
|
127
108
|
}
|
|
128
109
|
catch (error) {
|
|
129
|
-
// Check if it's a WorkflowAsyncException
|
|
130
110
|
if (error instanceof WorkflowAsyncException) {
|
|
131
111
|
// Normal - workflow paused for step execution
|
|
132
112
|
throw error;
|
|
@@ -136,7 +116,6 @@ export class PikkuWorkflowService {
|
|
|
136
116
|
// Workflow was cancelled - status already updated, just rethrow
|
|
137
117
|
throw error;
|
|
138
118
|
}
|
|
139
|
-
// Real error - mark as failed
|
|
140
119
|
await this.updateRunStatus(runId, 'failed', undefined, {
|
|
141
120
|
message: error.message,
|
|
142
121
|
stack: error.stack,
|
|
@@ -151,56 +130,59 @@ export class PikkuWorkflowService {
|
|
|
151
130
|
* Handles idempotency, RPC execution, result storage, retry logic, and orchestrator triggering
|
|
152
131
|
*/
|
|
153
132
|
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 {
|
|
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') {
|
|
186
139
|
await this.resumeWorkflow(runId);
|
|
140
|
+
return;
|
|
187
141
|
}
|
|
188
|
-
|
|
189
|
-
|
|
142
|
+
// Log warning if already running (race condition)
|
|
143
|
+
if (stepState.status === 'running') {
|
|
144
|
+
return;
|
|
190
145
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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);
|
|
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');
|
|
200
149
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
+
});
|
|
204
186
|
}
|
|
205
187
|
/**
|
|
206
188
|
* Orchestrate workflow execution (called by orchestrator)
|
|
@@ -287,7 +269,7 @@ export class PikkuWorkflowService {
|
|
|
287
269
|
const retryDelay = stepOptions?.retryDelay;
|
|
288
270
|
const attemptCount = stepState.attemptCount;
|
|
289
271
|
try {
|
|
290
|
-
const result = await rpcService.
|
|
272
|
+
const result = await rpcService.rpcWithWire(rpcName, data, {
|
|
291
273
|
workflowStep: {
|
|
292
274
|
runId,
|
|
293
275
|
stepId: stepState.stepId,
|
|
@@ -407,8 +389,8 @@ export class PikkuWorkflowService {
|
|
|
407
389
|
// Throw cancellation exception to stop workflow execution
|
|
408
390
|
throw new WorkflowCancelledException(runId, reason);
|
|
409
391
|
}
|
|
410
|
-
|
|
411
|
-
const
|
|
392
|
+
createWorkflowWire(workflowName, runId, rpcService) {
|
|
393
|
+
const workflowWire = {
|
|
412
394
|
workflowName,
|
|
413
395
|
runId,
|
|
414
396
|
getRun: async () => (await this.getRun(runId)),
|
|
@@ -432,7 +414,7 @@ export class PikkuWorkflowService {
|
|
|
432
414
|
await this.cancelWorkflow(runId, reason);
|
|
433
415
|
},
|
|
434
416
|
};
|
|
435
|
-
return
|
|
417
|
+
return workflowWire;
|
|
436
418
|
}
|
|
437
419
|
verifyStepName(stepName) {
|
|
438
420
|
if (typeof stepName !== 'string') {
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { CoreWorkflow } from './workflow.types.js';
|
|
2
|
-
import type { CorePikkuFunctionConfig } from '../../function/functions.types.js';
|
|
3
1
|
import { PikkuError } from '../../errors/error-handler.js';
|
|
4
2
|
/**
|
|
5
3
|
* Exception thrown when workflow needs to pause for async step
|
|
@@ -30,6 +28,7 @@ export declare class WorkflowRunNotFound extends PikkuError {
|
|
|
30
28
|
constructor(runId: string);
|
|
31
29
|
}
|
|
32
30
|
/**
|
|
33
|
-
*
|
|
31
|
+
* Add a workflow to the system
|
|
32
|
+
* This is called by the generated workflow wirings
|
|
34
33
|
*/
|
|
35
|
-
export declare const
|
|
34
|
+
export declare const addWorkflow: (workflowName: string, workflowFunc: any) => void;
|
|
@@ -44,25 +44,22 @@ export class WorkflowRunNotFound extends PikkuError {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
47
|
+
* Add a workflow to the system
|
|
48
|
+
* This is called by the generated workflow wirings
|
|
48
49
|
*/
|
|
49
|
-
export const
|
|
50
|
+
export const addWorkflow = (workflowName, workflowFunc) => {
|
|
50
51
|
// Get workflow metadata from inspector
|
|
51
52
|
const meta = pikkuState('workflows', 'meta');
|
|
52
|
-
const workflowMeta = meta[
|
|
53
|
+
const workflowMeta = meta[workflowName];
|
|
53
54
|
if (!workflowMeta) {
|
|
54
|
-
throw new Error(`Workflow metadata not found for '${
|
|
55
|
+
throw new Error(`Workflow metadata not found for '${workflowName}'. Make sure to run the CLI to generate metadata.`);
|
|
55
56
|
}
|
|
56
|
-
// Register the function with pikku
|
|
57
|
-
addFunction(workflowMeta.pikkuFuncName, {
|
|
58
|
-
func: workflow.func.func,
|
|
59
|
-
auth: workflow.func.auth,
|
|
60
|
-
permissions: workflow.func.permissions,
|
|
61
|
-
middleware: workflow.func.middleware,
|
|
62
|
-
tags: workflow.func.tags,
|
|
63
|
-
docs: workflow.func.docs,
|
|
64
|
-
});
|
|
65
57
|
// Store workflow definition in state
|
|
66
58
|
const registrations = pikkuState('workflows', 'registrations');
|
|
67
|
-
registrations.set(
|
|
59
|
+
registrations.set(workflowName, {
|
|
60
|
+
name: workflowName,
|
|
61
|
+
func: workflowFunc,
|
|
62
|
+
});
|
|
63
|
+
// Register the function with pikku
|
|
64
|
+
addFunction(workflowMeta.pikkuFuncName, workflowFunc);
|
|
68
65
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SerializedError, CoreSingletonServices, CreateWireServices, CoreConfig, CommonWireMeta } from '../../types/core.types.js';
|
|
2
2
|
import { CorePikkuFunctionConfig } from '../../function/functions.types.js';
|
|
3
3
|
export interface WorkflowServiceConfig {
|
|
4
4
|
retries: number;
|
|
@@ -73,8 +73,6 @@ export interface StepState {
|
|
|
73
73
|
export type CoreWorkflow<PikkuFunctionConfig extends CorePikkuFunctionConfig<any, any, any> = CorePikkuFunctionConfig<any, any, any>> = {
|
|
74
74
|
/** Unique workflow name */
|
|
75
75
|
name: string;
|
|
76
|
-
/** Description of the workflow */
|
|
77
|
-
description?: string;
|
|
78
76
|
/** The workflow function */
|
|
79
77
|
func: PikkuFunctionConfig;
|
|
80
78
|
/** Middleware chain for this workflow */
|
|
@@ -83,8 +81,6 @@ export type CoreWorkflow<PikkuFunctionConfig extends CorePikkuFunctionConfig<any
|
|
|
83
81
|
permissions?: PikkuFunctionConfig['permissions'];
|
|
84
82
|
/** Tags for organization and filtering */
|
|
85
83
|
tags?: string[];
|
|
86
|
-
/** Documentation metadata */
|
|
87
|
-
docs?: PikkuDocs;
|
|
88
84
|
};
|
|
89
85
|
/**
|
|
90
86
|
* Workflow step options
|
|
@@ -100,30 +96,108 @@ export interface WorkflowStepOptions {
|
|
|
100
96
|
/**
|
|
101
97
|
* Type signature for workflow.do() RPC form - used by inspector
|
|
102
98
|
*/
|
|
103
|
-
export type
|
|
99
|
+
export type WorkflowWireDoRPC = <TOutput = any, TInput = any>(stepName: string, rpcName: string, data: TInput, options?: WorkflowStepOptions) => Promise<TOutput>;
|
|
104
100
|
/**
|
|
105
101
|
* Type signature for workflow.do() inline form - used by inspector
|
|
106
102
|
*/
|
|
107
|
-
export type
|
|
103
|
+
export type WorkflowWireDoInline = <T>(stepName: string, fn: () => Promise<T> | T, options?: WorkflowStepOptions) => Promise<T>;
|
|
108
104
|
/**
|
|
109
105
|
* Type signature for workflow.sleep() - used by inspector
|
|
110
106
|
*/
|
|
111
|
-
export type
|
|
107
|
+
export type WorkflowWireSleep = (stepName: string, duration: string) => Promise<void>;
|
|
112
108
|
/**
|
|
113
|
-
*
|
|
109
|
+
* Input source for step arguments in simple workflows
|
|
110
|
+
*/
|
|
111
|
+
export type InputSource = {
|
|
112
|
+
from: 'input';
|
|
113
|
+
path: string;
|
|
114
|
+
} | {
|
|
115
|
+
from: 'outputVar';
|
|
116
|
+
name: string;
|
|
117
|
+
path?: string;
|
|
118
|
+
} | {
|
|
119
|
+
from: 'item';
|
|
120
|
+
path: string;
|
|
121
|
+
} | {
|
|
122
|
+
from: 'literal';
|
|
123
|
+
value: unknown;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Output binding for return statements in simple workflows
|
|
127
|
+
*/
|
|
128
|
+
export interface OutputBinding {
|
|
129
|
+
from: 'outputVar' | 'input';
|
|
130
|
+
name?: string;
|
|
131
|
+
path?: string;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* RPC step metadata (base form)
|
|
114
135
|
*/
|
|
115
|
-
export
|
|
136
|
+
export interface RpcStepMeta {
|
|
116
137
|
/** RPC form - generates queue worker */
|
|
117
138
|
type: 'rpc';
|
|
118
139
|
/** Cache key (stepName from workflow.do) */
|
|
119
140
|
stepName: string;
|
|
120
141
|
/** RPC to invoke */
|
|
121
142
|
rpcName: string;
|
|
122
|
-
/**
|
|
123
|
-
|
|
143
|
+
/** Output variable name (if assigned) */
|
|
144
|
+
outputVar?: string;
|
|
145
|
+
/** Input source mappings */
|
|
146
|
+
inputs?: Record<string, InputSource>;
|
|
124
147
|
/** Step options */
|
|
125
148
|
options?: WorkflowStepOptions;
|
|
126
|
-
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Branch step metadata (if/else control flow)
|
|
152
|
+
*/
|
|
153
|
+
export interface BranchStepMeta {
|
|
154
|
+
type: 'branch';
|
|
155
|
+
/** Condition expression (as source string) */
|
|
156
|
+
condition: string;
|
|
157
|
+
/** Branch paths */
|
|
158
|
+
branches: {
|
|
159
|
+
then: WorkflowStepMeta[];
|
|
160
|
+
else?: WorkflowStepMeta[];
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Parallel group step metadata (Promise.all with multiple steps)
|
|
165
|
+
*/
|
|
166
|
+
export interface ParallelGroupStepMeta {
|
|
167
|
+
type: 'parallel';
|
|
168
|
+
/** Child steps to execute in parallel */
|
|
169
|
+
children: RpcStepMeta[];
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Fanout step metadata (parallel or sequential iteration)
|
|
173
|
+
*/
|
|
174
|
+
export interface FanoutStepMeta {
|
|
175
|
+
type: 'fanout';
|
|
176
|
+
/** Step name for this fanout */
|
|
177
|
+
stepName: string;
|
|
178
|
+
/** Source array variable name */
|
|
179
|
+
sourceVar: string;
|
|
180
|
+
/** Iterator variable name */
|
|
181
|
+
itemVar: string;
|
|
182
|
+
/** Execution mode */
|
|
183
|
+
mode: 'parallel' | 'sequential';
|
|
184
|
+
/** Child step to execute per iteration */
|
|
185
|
+
child: RpcStepMeta;
|
|
186
|
+
/** Time between iterations (sequential mode only) */
|
|
187
|
+
timeBetween?: string;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Return step metadata (workflow output)
|
|
191
|
+
*/
|
|
192
|
+
export interface ReturnStepMeta {
|
|
193
|
+
type: 'return';
|
|
194
|
+
/** Output bindings */
|
|
195
|
+
outputs: Record<string, OutputBinding>;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Inline step metadata (legacy support)
|
|
199
|
+
*/
|
|
200
|
+
export interface InlineStepMeta {
|
|
127
201
|
/** Inline form - local execution */
|
|
128
202
|
type: 'inline';
|
|
129
203
|
/** Cache key (stepName from workflow.do) */
|
|
@@ -132,19 +206,34 @@ export type WorkflowStepMeta = {
|
|
|
132
206
|
description?: string;
|
|
133
207
|
/** Step options */
|
|
134
208
|
options?: WorkflowStepOptions;
|
|
135
|
-
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Sleep step metadata
|
|
212
|
+
*/
|
|
213
|
+
export interface SleepStepMeta {
|
|
136
214
|
/** Sleep step */
|
|
137
215
|
type: 'sleep';
|
|
138
216
|
/** Cache key (stepName from workflow.sleep) */
|
|
139
217
|
stepName: string;
|
|
140
218
|
/** Sleep duration */
|
|
141
219
|
duration: string | number;
|
|
142
|
-
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Cancel step metadata
|
|
223
|
+
*/
|
|
224
|
+
export interface CancelStepMeta {
|
|
225
|
+
/** Cancel step */
|
|
226
|
+
type: 'cancel';
|
|
227
|
+
}
|
|
143
228
|
/**
|
|
144
|
-
* Workflow step
|
|
229
|
+
* Workflow step metadata (extracted by inspector)
|
|
230
|
+
*/
|
|
231
|
+
export type WorkflowStepMeta = RpcStepMeta | BranchStepMeta | ParallelGroupStepMeta | FanoutStepMeta | ReturnStepMeta | InlineStepMeta | SleepStepMeta | CancelStepMeta;
|
|
232
|
+
/**
|
|
233
|
+
* Workflow step wire context for RPC functions
|
|
145
234
|
* Provides step-level metadata including retry attempt tracking
|
|
146
235
|
*/
|
|
147
|
-
export interface
|
|
236
|
+
export interface WorkflowStepWire {
|
|
148
237
|
/** The workflow run ID */
|
|
149
238
|
runId: string;
|
|
150
239
|
/** The unique step ID */
|
|
@@ -153,10 +242,10 @@ export interface WorkflowStepInteraction {
|
|
|
153
242
|
attemptCount: number;
|
|
154
243
|
}
|
|
155
244
|
/**
|
|
156
|
-
* Workflow
|
|
245
|
+
* Workflow wire object for middleware
|
|
157
246
|
* Provides workflow-specific capabilities to function execution
|
|
158
247
|
*/
|
|
159
|
-
export interface
|
|
248
|
+
export interface PikkuWorkflowWire {
|
|
160
249
|
/** The workflow name */
|
|
161
250
|
workflowName: string;
|
|
162
251
|
/** The current run ID */
|
|
@@ -164,9 +253,9 @@ export interface PikkuWorkflowInteraction {
|
|
|
164
253
|
/** Get the current workflow run */
|
|
165
254
|
getRun: () => Promise<WorkflowRun>;
|
|
166
255
|
/** Execute a workflow step (overloaded - RPC or inline form) */
|
|
167
|
-
do:
|
|
256
|
+
do: WorkflowWireDoRPC & WorkflowWireDoInline;
|
|
168
257
|
/** Sleep for a duration */
|
|
169
|
-
sleep:
|
|
258
|
+
sleep: WorkflowWireSleep;
|
|
170
259
|
/** Cancel the current workflow run */
|
|
171
260
|
cancel: (reason?: string) => Promise<void>;
|
|
172
261
|
}
|
|
@@ -186,15 +275,11 @@ export interface PikkuWorkflow {
|
|
|
186
275
|
/**
|
|
187
276
|
* Workflows metadata for inspector/CLI
|
|
188
277
|
*/
|
|
189
|
-
export type WorkflowsMeta = Record<string, {
|
|
190
|
-
pikkuFuncName: string;
|
|
278
|
+
export type WorkflowsMeta = Record<string, CommonWireMeta & {
|
|
191
279
|
workflowName: string;
|
|
192
|
-
description?: string;
|
|
193
|
-
session?: undefined;
|
|
194
|
-
docs?: PikkuDocs;
|
|
195
|
-
tags?: string[];
|
|
196
|
-
middleware?: MiddlewareMetadata[];
|
|
197
280
|
steps: WorkflowStepMeta[];
|
|
281
|
+
/** Whether this workflow conforms to simple workflow DSL */
|
|
282
|
+
simple?: boolean;
|
|
198
283
|
}>;
|
|
199
284
|
/**
|
|
200
285
|
* Interface for workflow orchestration
|
|
@@ -210,7 +295,7 @@ export interface WorkflowService {
|
|
|
210
295
|
withRunLock<T>(id: string, fn: () => Promise<T>): Promise<T>;
|
|
211
296
|
close(): Promise<void>;
|
|
212
297
|
resumeWorkflow(runId: string): Promise<void>;
|
|
213
|
-
setServices(singletonServices: CoreSingletonServices,
|
|
298
|
+
setServices(singletonServices: CoreSingletonServices, createWireServices: CreateWireServices, config: CoreConfig): void;
|
|
214
299
|
startWorkflow<I>(name: string, input: I, rpcService: any): Promise<{
|
|
215
300
|
runId: string;
|
|
216
301
|
}>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/core",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"author": "yasser.fadl@gmail.com",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"picoquery": "^2.5.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@types/node": "^24.
|
|
45
|
+
"@types/node": "^24.10.1",
|
|
46
46
|
"tsx": "^4.20.6",
|
|
47
47
|
"typescript": "^5.9"
|
|
48
48
|
},
|
package/src/errors/errors.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { addError, PikkuError } from './error-handler.js'
|
|
5
5
|
|
|
6
|
-
export class
|
|
7
|
-
addError(
|
|
6
|
+
export class InvalidMiddlewareWireError extends PikkuError {}
|
|
7
|
+
addError(InvalidMiddlewareWireError, {
|
|
8
8
|
status: 500,
|
|
9
|
-
message: 'The middleware
|
|
9
|
+
message: 'The middleware wire is invalid for the current wiring type.',
|
|
10
10
|
})
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -7,44 +7,22 @@ import {
|
|
|
7
7
|
pikkuMiddlewareFactory,
|
|
8
8
|
} from './index.js'
|
|
9
9
|
|
|
10
|
-
test('pikkuPermission factory function', async () => {
|
|
11
|
-
const permission = pikkuPermission(async ({ logger }, data, session) => {
|
|
12
|
-
return true
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
assert.strictEqual(typeof permission, 'function')
|
|
16
|
-
assert.strictEqual(permission.length, 3) // services, data, session
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
test('pikkuMiddleware factory function', async () => {
|
|
20
|
-
const middleware = pikkuMiddleware(async ({ logger }, interactions, next) => {
|
|
21
|
-
await next()
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
assert.strictEqual(typeof middleware, 'function')
|
|
25
|
-
assert.strictEqual(middleware.length, 3) // services, interactions, next
|
|
26
|
-
})
|
|
27
|
-
|
|
28
10
|
test('pikkuPermission returns the same function', async () => {
|
|
29
|
-
const originalFn = async (
|
|
11
|
+
const originalFn = async () => true
|
|
30
12
|
const wrappedFn = pikkuPermission(originalFn)
|
|
31
|
-
|
|
32
13
|
assert.strictEqual(wrappedFn, originalFn)
|
|
33
14
|
})
|
|
34
15
|
|
|
35
16
|
test('pikkuMiddleware returns the same function', async () => {
|
|
36
|
-
const originalFn = async ({ logger },
|
|
37
|
-
await next()
|
|
38
|
-
}
|
|
17
|
+
const originalFn = async ({ logger }, wires, next) => next
|
|
39
18
|
const wrappedFn = pikkuMiddleware(originalFn)
|
|
40
|
-
|
|
41
19
|
assert.strictEqual(wrappedFn, originalFn)
|
|
42
20
|
})
|
|
43
21
|
|
|
44
22
|
test('pikkuPermissionFactory creates a factory function', async () => {
|
|
45
23
|
const factory = pikkuPermissionFactory<{ role: string }>(({ role }) => {
|
|
46
|
-
return pikkuPermission(async ({ logger }, data,
|
|
47
|
-
return
|
|
24
|
+
return pikkuPermission(async ({ logger }, data, { initialSession }) => {
|
|
25
|
+
return (initialSession as any)?.role === role
|
|
48
26
|
})
|
|
49
27
|
})
|
|
50
28
|
|
|
@@ -59,7 +37,7 @@ test('pikkuPermissionFactory creates a factory function', async () => {
|
|
|
59
37
|
|
|
60
38
|
test('pikkuMiddlewareFactory creates a factory function', async () => {
|
|
61
39
|
const factory = pikkuMiddlewareFactory<{ message: string }>(({ message }) => {
|
|
62
|
-
return pikkuMiddleware(async ({ logger },
|
|
40
|
+
return pikkuMiddleware(async ({ logger }, wires, next) => {
|
|
63
41
|
logger.info(message)
|
|
64
42
|
await next()
|
|
65
43
|
})
|
|
@@ -71,26 +49,21 @@ test('pikkuMiddlewareFactory creates a factory function', async () => {
|
|
|
71
49
|
// Call the factory to get middleware
|
|
72
50
|
const middleware = factory({ message: 'test' })
|
|
73
51
|
assert.strictEqual(typeof middleware, 'function')
|
|
74
|
-
assert.strictEqual(middleware.length, 3) // services,
|
|
52
|
+
assert.strictEqual(middleware.length, 3) // services, wires, next
|
|
75
53
|
})
|
|
76
54
|
|
|
77
55
|
test('pikkuPermissionFactory returns the same factory', async () => {
|
|
78
|
-
const originalFactory = (
|
|
79
|
-
|
|
80
|
-
return session?.role === role
|
|
81
|
-
})
|
|
82
|
-
const wrappedFactory = pikkuPermissionFactory(originalFactory)
|
|
83
|
-
|
|
56
|
+
const originalFactory = () => pikkuPermission(async () => true)
|
|
57
|
+
const wrappedFactory = pikkuPermissionFactory(originalFactory as any)
|
|
84
58
|
assert.strictEqual(wrappedFactory, originalFactory)
|
|
85
59
|
})
|
|
86
60
|
|
|
87
61
|
test('pikkuMiddlewareFactory returns the same factory', async () => {
|
|
88
62
|
const originalFactory = ({ message }) =>
|
|
89
|
-
pikkuMiddleware(async ({ logger },
|
|
63
|
+
pikkuMiddleware(async ({ logger }, _wires, next) => {
|
|
90
64
|
logger.info(message)
|
|
91
65
|
await next()
|
|
92
66
|
})
|
|
93
67
|
const wrappedFactory = pikkuMiddlewareFactory(originalFactory)
|
|
94
|
-
|
|
95
68
|
assert.strictEqual(wrappedFactory, originalFactory)
|
|
96
69
|
})
|