@pikku/core 0.12.69 → 0.12.70
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 +334 -0
- package/README.md +34 -2
- package/dist/function/functions.types.d.ts +27 -0
- package/dist/index.d.ts +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/internal.js +1 -1
- package/dist/pikku-state.js +1 -0
- package/dist/services/http-scenario-actors.d.ts +12 -4
- package/dist/services/http-scenario-actors.js +47 -45
- package/dist/services/index.d.ts +2 -1
- package/dist/services/index.js +1 -0
- package/dist/services/meta-service.d.ts +5 -1
- package/dist/services/meta-service.js +44 -18
- package/dist/services/scenario-actors-service.d.ts +108 -2
- package/dist/services/scenario-actors-service.js +40 -1
- package/dist/types/core.types.d.ts +21 -3
- package/dist/types/state.types.d.ts +3 -1
- package/dist/wirings/actor-flow/actor-flow.types.d.ts +1 -1
- package/dist/wirings/actor-flow/index.d.ts +1 -1
- package/dist/wirings/actor-flow/run-conversation.d.ts +10 -10
- package/dist/wirings/actor-flow/run-conversation.js +27 -27
- package/dist/wirings/cli/command-parser.js +11 -1
- package/dist/wirings/rpc/rpc-runner.js +1 -1
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +52 -3
- package/dist/wirings/workflow/feature.d.ts +28 -0
- package/dist/wirings/workflow/feature.js +57 -0
- package/dist/wirings/workflow/index.d.ts +13 -2
- package/dist/wirings/workflow/index.js +15 -0
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +121 -0
- package/dist/wirings/workflow/pikku-scenario-service.js +419 -0
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +103 -10
- package/dist/wirings/workflow/pikku-workflow-service.js +80 -135
- package/dist/wirings/workflow/scenario-cookie-jar.d.ts +29 -0
- package/dist/wirings/workflow/scenario-cookie-jar.js +51 -0
- package/dist/wirings/workflow/scenario-poll.d.ts +20 -0
- package/dist/wirings/workflow/scenario-poll.js +25 -0
- package/dist/wirings/workflow/scenario-prose.d.ts +38 -0
- package/dist/wirings/workflow/scenario-prose.js +45 -0
- package/dist/wirings/workflow/scenario-step-guards.d.ts +16 -0
- package/dist/wirings/workflow/scenario-step-guards.js +29 -0
- package/dist/wirings/workflow/scenario-step.types.d.ts +148 -0
- package/dist/wirings/workflow/scenario-step.types.js +1 -0
- package/dist/wirings/workflow/workflow.types.d.ts +81 -2
- package/package.json +3 -1
- package/src/function/functions.types.ts +32 -0
- package/src/index.ts +1 -0
- package/src/internal.ts +5 -1
- package/src/pikku-state.ts +1 -0
- package/src/services/http-scenario-actors.test.ts +85 -1
- package/src/services/http-scenario-actors.ts +65 -51
- package/src/services/index.ts +5 -0
- package/src/services/meta-service.test.ts +79 -0
- package/src/services/meta-service.ts +61 -26
- package/src/services/scenario-actors-service.ts +157 -2
- package/src/types/core.types.ts +27 -2
- package/src/types/state.types.ts +3 -0
- package/src/wirings/actor-flow/actor-flow.types.ts +1 -1
- package/src/wirings/actor-flow/index.ts +1 -1
- package/src/wirings/actor-flow/run-conversation.test.ts +12 -6
- package/src/wirings/actor-flow/run-conversation.ts +36 -41
- package/src/wirings/cli/command-parser.test.ts +60 -0
- package/src/wirings/cli/command-parser.ts +12 -1
- package/src/wirings/rpc/rpc-runner.test.ts +28 -5
- package/src/wirings/rpc/rpc-runner.ts +1 -1
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +86 -2
- package/src/wirings/workflow/feature.test.ts +131 -0
- package/src/wirings/workflow/feature.ts +78 -0
- package/src/wirings/workflow/index.ts +73 -0
- package/src/wirings/workflow/pikku-scenario-service.ts +682 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +55 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +196 -208
- package/src/wirings/workflow/scenario-cookie-jar.test.ts +108 -0
- package/src/wirings/workflow/scenario-cookie-jar.ts +65 -0
- package/src/wirings/workflow/scenario-hooks.test.ts +212 -0
- package/src/wirings/workflow/scenario-poll.test.ts +66 -0
- package/src/wirings/workflow/scenario-poll.ts +36 -0
- package/src/wirings/workflow/scenario-prose.test.ts +152 -0
- package/src/wirings/workflow/scenario-prose.ts +79 -0
- package/src/wirings/workflow/scenario-service.test.ts +155 -0
- package/src/wirings/workflow/scenario-step-guards.ts +43 -0
- package/src/wirings/workflow/scenario-step.test.ts +441 -8
- package/src/wirings/workflow/scenario-step.types.ts +157 -0
- package/src/wirings/workflow/workflow.types.ts +98 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -875,3 +875,58 @@ describe('pikku-workflow-service approval', () => {
|
|
|
875
875
|
cleanup()
|
|
876
876
|
})
|
|
877
877
|
})
|
|
878
|
+
|
|
879
|
+
describe('pikku-workflow-service run identity on failure', () => {
|
|
880
|
+
// A failing inline run throws before startWorkflow can return `{ runId }`,
|
|
881
|
+
// so a caller that wants to read the run back — its steps, which one failed,
|
|
882
|
+
// what it was called with — has nothing to read it by. `onRunCreated` fires
|
|
883
|
+
// as soon as the run exists, which is the only moment guaranteed to happen
|
|
884
|
+
// whether the run goes on to pass, fail, or suspend.
|
|
885
|
+
const runFailing = async (name: string) => {
|
|
886
|
+
resetPikkuState()
|
|
887
|
+
const ws = new InMemoryWorkflowService()
|
|
888
|
+
pikkuState(null, 'package', 'singletonServices', {
|
|
889
|
+
logger: { error() {}, info() {}, warn() {}, debug() {} },
|
|
890
|
+
} as any)
|
|
891
|
+
|
|
892
|
+
const metaState = pikkuState(null, 'workflows', 'meta')
|
|
893
|
+
metaState[name] = {
|
|
894
|
+
name,
|
|
895
|
+
pikkuFuncId: name,
|
|
896
|
+
source: 'scenario',
|
|
897
|
+
graphHash: `${name}-hash`,
|
|
898
|
+
} as any
|
|
899
|
+
pikkuState(null, 'function', 'meta')[name] = {
|
|
900
|
+
name,
|
|
901
|
+
sessionless: true,
|
|
902
|
+
permissions: [],
|
|
903
|
+
} as any
|
|
904
|
+
|
|
905
|
+
addWorkflow(name, {
|
|
906
|
+
func: async () => {
|
|
907
|
+
throw new Error('the scenario blew up')
|
|
908
|
+
},
|
|
909
|
+
} as any)
|
|
910
|
+
|
|
911
|
+
const created: string[] = []
|
|
912
|
+
let error: any
|
|
913
|
+
try {
|
|
914
|
+
await ws.startWorkflow(name, {}, {} as any, {}, {
|
|
915
|
+
onRunCreated: (runId: string) => created.push(runId),
|
|
916
|
+
} as any)
|
|
917
|
+
} catch (e: any) {
|
|
918
|
+
error = e
|
|
919
|
+
}
|
|
920
|
+
return { ws, created, error }
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
test('reports the run id of a run that fails inline', async () => {
|
|
924
|
+
const { ws, created, error } = await runFailing('onRunCreatedFailing')
|
|
925
|
+
|
|
926
|
+
assert.equal(error?.message, 'the scenario blew up')
|
|
927
|
+
assert.equal(created.length, 1, 'the run existed, so its id must be known')
|
|
928
|
+
const run = await ws.getRun(created[0]!)
|
|
929
|
+
assert.equal(run?.status, 'failed')
|
|
930
|
+
assert.equal(run?.error?.message, 'the scenario blew up')
|
|
931
|
+
})
|
|
932
|
+
})
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
pikkuState,
|
|
12
12
|
} from '../../pikku-state.js'
|
|
13
13
|
import { getDurationInMilliseconds } from '../../time-utils.js'
|
|
14
|
-
import { createHttpScenarioActors } from '../../services/http-scenario-actors.js'
|
|
15
14
|
|
|
16
15
|
const resolveWorkflowMeta = (
|
|
17
16
|
name: string
|
|
@@ -46,10 +45,10 @@ const toKebab = (s: string) =>
|
|
|
46
45
|
s.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
|
|
47
46
|
import type { PikkuWire, SerializedError } from '../../types/core.types.js'
|
|
48
47
|
import type { QueueService } from '../queue/queue.types.js'
|
|
49
|
-
import { runScheduledTask } from '../scheduler/scheduler-runner.js'
|
|
50
48
|
import type {
|
|
51
49
|
ApprovalOutcome,
|
|
52
|
-
|
|
50
|
+
CoreWorkflow,
|
|
51
|
+
PikkuWorkflowWire,
|
|
53
52
|
StepState,
|
|
54
53
|
StepStatus,
|
|
55
54
|
WorkflowApprovalOptions,
|
|
@@ -63,9 +62,6 @@ import type {
|
|
|
63
62
|
WorkflowQueueOptions,
|
|
64
63
|
WorkflowServiceConfig,
|
|
65
64
|
WorkflowStepOptions,
|
|
66
|
-
WorkflowExpectEventuallyOptions,
|
|
67
|
-
WorkflowExpectErrorOptions,
|
|
68
|
-
WorkflowExpectServiceOptions,
|
|
69
65
|
} from './workflow.types.js'
|
|
70
66
|
import {
|
|
71
67
|
continueGraph,
|
|
@@ -246,6 +242,101 @@ const WORKFLOW_END_STATES: ReadonlySet<string> = new Set([
|
|
|
246
242
|
'suspended',
|
|
247
243
|
])
|
|
248
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Everything an extension needs to run something of its own around a run's
|
|
247
|
+
* function: the registration it was resolved from, the run itself (so `input`
|
|
248
|
+
* is the same object the function is called with), and the wire it is given.
|
|
249
|
+
*/
|
|
250
|
+
export interface RunLifecycleContext {
|
|
251
|
+
runId: string
|
|
252
|
+
run: WorkflowRun
|
|
253
|
+
workflowMeta: any
|
|
254
|
+
workflow: CoreWorkflow
|
|
255
|
+
wire: PikkuWire
|
|
256
|
+
packageName: string | null
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The slice of the run engine an extension is allowed to drive, handed to it at
|
|
261
|
+
* construction. It exists so that recording a durable step stays available to
|
|
262
|
+
* an extension without `inlineStep` and friends becoming public API on every
|
|
263
|
+
* workflow service a production app instantiates.
|
|
264
|
+
*/
|
|
265
|
+
export interface WorkflowRunEngine {
|
|
266
|
+
inlineStep(
|
|
267
|
+
runId: string,
|
|
268
|
+
logicalStepName: string,
|
|
269
|
+
fn: Function,
|
|
270
|
+
stepOptions?: WorkflowStepOptions,
|
|
271
|
+
data?: any,
|
|
272
|
+
funcName?: string
|
|
273
|
+
): Promise<any>
|
|
274
|
+
updateRunStatus(
|
|
275
|
+
runId: string,
|
|
276
|
+
status: WorkflowStatus,
|
|
277
|
+
output?: any,
|
|
278
|
+
error?: SerializedError
|
|
279
|
+
): Promise<void>
|
|
280
|
+
onChildWorkflowFailed(run: WorkflowRun, error: unknown): Promise<void>
|
|
281
|
+
verifyStepName(stepName: unknown): void
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* A capability layered onto a run without being a workflow service itself.
|
|
286
|
+
*
|
|
287
|
+
* The engine names nothing about what an extension is for: a bundler drops an
|
|
288
|
+
* unused *module* but never an unused class member, so the alternative — a
|
|
289
|
+
* subclass carrying the capability — puts it in every server built on Pikku
|
|
290
|
+
* whether or not the app ever uses it. Scenarios are the one implementation
|
|
291
|
+
* today (`PikkuScenarioService` in `@pikku/core/scenario`).
|
|
292
|
+
*/
|
|
293
|
+
export interface WorkflowRunExtension {
|
|
294
|
+
/** Per-run state resolved once the run has an id. */
|
|
295
|
+
attachRunContext(
|
|
296
|
+
runId: string,
|
|
297
|
+
workflowMeta: any,
|
|
298
|
+
options?: Record<string, any>
|
|
299
|
+
): Promise<void>
|
|
300
|
+
/** Release whatever `attachRunContext` stored. */
|
|
301
|
+
detachRunContext(runId: string): void
|
|
302
|
+
/** Contribute extra members to the wire a run's function is invoked with. */
|
|
303
|
+
decorateRunWire(
|
|
304
|
+
wire: PikkuWire,
|
|
305
|
+
context: {
|
|
306
|
+
runId: string
|
|
307
|
+
workflowMeta: any
|
|
308
|
+
workflowWire: PikkuWorkflowWire
|
|
309
|
+
}
|
|
310
|
+
): void
|
|
311
|
+
/** Contribute extra members to the workflow wire itself, in place. */
|
|
312
|
+
decorateWorkflowWire(
|
|
313
|
+
workflowWire: PikkuWorkflowWire,
|
|
314
|
+
context: {
|
|
315
|
+
name: string
|
|
316
|
+
runId: string
|
|
317
|
+
rpcService: any
|
|
318
|
+
addonNamespace?: string | null
|
|
319
|
+
}
|
|
320
|
+
): void
|
|
321
|
+
/**
|
|
322
|
+
* Called immediately before the run's function, inside the run lock. Throwing
|
|
323
|
+
* skips the function and fails the run.
|
|
324
|
+
*/
|
|
325
|
+
onBeforeRunFunc(context: RunLifecycleContext): Promise<void>
|
|
326
|
+
/**
|
|
327
|
+
* Called once the run reaches a terminal state, in a `finally`, so it runs
|
|
328
|
+
* whether the function passed or threw. `interrupted` means the run suspended
|
|
329
|
+
* or went async and is still mid-flight. Throwing here propagates, replacing
|
|
330
|
+
* a pending failure, so an extension that must not mask one handles that
|
|
331
|
+
* itself.
|
|
332
|
+
*/
|
|
333
|
+
onAfterRunFunc(
|
|
334
|
+
context: RunLifecycleContext,
|
|
335
|
+
outcome: 'completed' | 'failed' | 'interrupted',
|
|
336
|
+
failure: unknown
|
|
337
|
+
): Promise<void>
|
|
338
|
+
}
|
|
339
|
+
|
|
249
340
|
/**
|
|
250
341
|
* Abstract workflow state service
|
|
251
342
|
* Implementations provide pluggable storage backends (SQLite, PostgreSQL, etc.)
|
|
@@ -253,9 +344,7 @@ const WORKFLOW_END_STATES: ReadonlySet<string> = new Set([
|
|
|
253
344
|
*/
|
|
254
345
|
export abstract class PikkuWorkflowService implements WorkflowService {
|
|
255
346
|
private inlineRuns = new Set<string>()
|
|
256
|
-
|
|
257
|
-
// process-local by nature, so they ride this map, never the persisted wire.
|
|
258
|
-
private runActors = new Map<string, ScenarioActors>()
|
|
347
|
+
private runExtension?: WorkflowRunExtension
|
|
259
348
|
|
|
260
349
|
protected get logger() {
|
|
261
350
|
return getSingletonServices()?.logger
|
|
@@ -631,7 +720,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
631
720
|
* Creates pending step in both workflow_step and workflow_step_history
|
|
632
721
|
* @param runId - Run ID
|
|
633
722
|
* @param stepName - Step cache key
|
|
634
|
-
* @param rpcName - RPC
|
|
723
|
+
* @param rpcName - The name this step was dispatched by: an RPC for a
|
|
724
|
+
* `workflow.do` step, a step function for a scenario step, null for a
|
|
725
|
+
* closure. Nothing dispatches off this value — it is recorded so a reader
|
|
726
|
+
* can join a step back to the function that ran it.
|
|
635
727
|
* @param data - Step input data
|
|
636
728
|
* @param stepOptions - Step options (retries, retryDelay)
|
|
637
729
|
* @returns Step state with generated stepId
|
|
@@ -1162,37 +1254,28 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1162
1254
|
return true
|
|
1163
1255
|
}
|
|
1164
1256
|
|
|
1165
|
-
/**
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
const
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
)
|
|
1179
|
-
return undefined
|
|
1180
|
-
}
|
|
1181
|
-
const actorsConfig = await metaService.getScenarioActorsMeta()
|
|
1182
|
-
if (!actorsConfig || Object.keys(actorsConfig).length === 0) {
|
|
1183
|
-
return undefined
|
|
1257
|
+
/**
|
|
1258
|
+
* Install the one extension a run may have, built from a handle onto the run
|
|
1259
|
+
* engine so that `inlineStep` and friends stay protected rather than becoming
|
|
1260
|
+
* public API. Returns the extension, so the caller keeps a typed reference to
|
|
1261
|
+
* whatever it just built.
|
|
1262
|
+
*/
|
|
1263
|
+
public setRunExtension<T extends WorkflowRunExtension>(
|
|
1264
|
+
create: (engine: WorkflowRunEngine) => T
|
|
1265
|
+
): T {
|
|
1266
|
+
const engine: WorkflowRunEngine = {
|
|
1267
|
+
inlineStep: this.inlineStep.bind(this),
|
|
1268
|
+
updateRunStatus: this.updateRunStatus.bind(this),
|
|
1269
|
+
onChildWorkflowFailed: this.onChildWorkflowFailed.bind(this),
|
|
1270
|
+
verifyStepName: this.verifyStepName.bind(this),
|
|
1184
1271
|
}
|
|
1185
|
-
const
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
actors: actorsConfig,
|
|
1193
|
-
signInPath,
|
|
1194
|
-
rpcPath,
|
|
1195
|
-
})
|
|
1272
|
+
const extension = create(engine)
|
|
1273
|
+
this.runExtension = extension
|
|
1274
|
+
return extension
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
public getRunExtension(): WorkflowRunExtension | undefined {
|
|
1278
|
+
return this.runExtension
|
|
1196
1279
|
}
|
|
1197
1280
|
|
|
1198
1281
|
/**
|
|
@@ -1200,13 +1283,21 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1200
1283
|
* Automatically detects workflow type (DSL or graph) from meta and executes accordingly
|
|
1201
1284
|
* @param options.inline - If true, execute workflow directly without queue service
|
|
1202
1285
|
* @param options.startNode - Starting node ID for graph workflows (from wire config)
|
|
1286
|
+
* @param options.onRunCreated - Called with the run id the moment the run exists.
|
|
1287
|
+
* An inline run that fails throws instead of returning, so this is the only
|
|
1288
|
+
* way a caller can still read that run back — its steps, and which one failed.
|
|
1203
1289
|
*/
|
|
1204
1290
|
public async startWorkflow<I>(
|
|
1205
1291
|
name: string,
|
|
1206
1292
|
input: I,
|
|
1207
1293
|
wire: WorkflowRunWire,
|
|
1208
1294
|
rpcService: any,
|
|
1209
|
-
options?: {
|
|
1295
|
+
options?: {
|
|
1296
|
+
inline?: boolean
|
|
1297
|
+
startNode?: string
|
|
1298
|
+
actors?: ScenarioActors
|
|
1299
|
+
onRunCreated?: (runId: string) => void
|
|
1300
|
+
}
|
|
1210
1301
|
): Promise<{ runId: string }> {
|
|
1211
1302
|
// Resolve workflow from static meta (root or addon namespace), then dynamic DB
|
|
1212
1303
|
const resolved = resolveWorkflowMeta(name)
|
|
@@ -1270,14 +1361,9 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1270
1361
|
}
|
|
1271
1362
|
)
|
|
1272
1363
|
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
? await this.resolveScenarioActors()
|
|
1277
|
-
: undefined)
|
|
1278
|
-
if (actors) {
|
|
1279
|
-
this.runActors.set(runId, actors)
|
|
1280
|
-
}
|
|
1364
|
+
options?.onRunCreated?.(runId)
|
|
1365
|
+
|
|
1366
|
+
await this.runExtension?.attachRunContext(runId, workflowMeta, options)
|
|
1281
1367
|
|
|
1282
1368
|
if (shouldInline) {
|
|
1283
1369
|
this.inlineRuns.add(runId)
|
|
@@ -1314,7 +1400,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1314
1400
|
}
|
|
1315
1401
|
} finally {
|
|
1316
1402
|
this.inlineRuns.delete(runId)
|
|
1317
|
-
this.
|
|
1403
|
+
this.runExtension?.detachRunContext(runId)
|
|
1318
1404
|
}
|
|
1319
1405
|
} else {
|
|
1320
1406
|
await this.resumeWorkflow(runId)
|
|
@@ -1485,15 +1571,33 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1485
1571
|
workflowWire.pikkuUserId = run.wire?.pikkuUserId
|
|
1486
1572
|
const wire: PikkuWire = {
|
|
1487
1573
|
workflow: workflowWire,
|
|
1488
|
-
scenario:
|
|
1489
|
-
workflowMeta?.source === 'scenario' ? workflowWire : undefined,
|
|
1490
1574
|
pikkuUserId: run.wire?.pikkuUserId,
|
|
1491
1575
|
session: rpcService?.wire?.session,
|
|
1492
1576
|
rpc: rpcService?.wire?.rpc,
|
|
1493
|
-
// User-flow actors registered for this run (see startWorkflow options)
|
|
1494
|
-
actors: this.runActors.get(runId),
|
|
1495
1577
|
}
|
|
1578
|
+
this.runExtension?.decorateRunWire(wire, {
|
|
1579
|
+
runId,
|
|
1580
|
+
workflowMeta,
|
|
1581
|
+
workflowWire,
|
|
1582
|
+
})
|
|
1583
|
+
|
|
1584
|
+
const lifecycle: RunLifecycleContext = {
|
|
1585
|
+
runId,
|
|
1586
|
+
run,
|
|
1587
|
+
workflowMeta,
|
|
1588
|
+
workflow,
|
|
1589
|
+
wire,
|
|
1590
|
+
packageName: pkgName,
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// `interrupted` means the run has not reached a terminal state — it is
|
|
1594
|
+
// suspended or waiting — so teardown would run while the run is still
|
|
1595
|
+
// mid-flight.
|
|
1596
|
+
let outcome: 'completed' | 'failed' | 'interrupted' = 'completed'
|
|
1597
|
+
let failure: any
|
|
1496
1598
|
try {
|
|
1599
|
+
await this.runExtension?.onBeforeRunFunc(lifecycle)
|
|
1600
|
+
|
|
1497
1601
|
const result = await runPikkuFunc(
|
|
1498
1602
|
'workflow',
|
|
1499
1603
|
workflowMeta.name,
|
|
@@ -1510,11 +1614,15 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1510
1614
|
await this.updateRunStatus(runId, 'completed', result)
|
|
1511
1615
|
await this.onChildWorkflowCompleted(run, result)
|
|
1512
1616
|
} catch (error: any) {
|
|
1617
|
+
failure = error
|
|
1618
|
+
|
|
1513
1619
|
if (error instanceof WorkflowAsyncException) {
|
|
1620
|
+
outcome = 'interrupted'
|
|
1514
1621
|
throw error
|
|
1515
1622
|
}
|
|
1516
1623
|
|
|
1517
1624
|
if (error instanceof WorkflowCancelledException) {
|
|
1625
|
+
outcome = 'failed'
|
|
1518
1626
|
await this.updateRunStatus(runId, 'cancelled', undefined, {
|
|
1519
1627
|
message: error.message || 'Workflow cancelled',
|
|
1520
1628
|
stack: '',
|
|
@@ -1525,6 +1633,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1525
1633
|
}
|
|
1526
1634
|
|
|
1527
1635
|
if (error instanceof WorkflowSuspendedException) {
|
|
1636
|
+
outcome = 'interrupted'
|
|
1528
1637
|
await this.updateRunStatus(runId, 'suspended', undefined, {
|
|
1529
1638
|
message: error.message || 'Workflow suspended',
|
|
1530
1639
|
stack: '',
|
|
@@ -1533,6 +1642,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1533
1642
|
throw error
|
|
1534
1643
|
}
|
|
1535
1644
|
|
|
1645
|
+
outcome = 'failed'
|
|
1536
1646
|
await this.updateRunStatus(runId, 'failed', undefined, {
|
|
1537
1647
|
message: error.message,
|
|
1538
1648
|
stack: error.stack,
|
|
@@ -1541,6 +1651,8 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1541
1651
|
await this.onChildWorkflowFailed(run, error)
|
|
1542
1652
|
|
|
1543
1653
|
throw error
|
|
1654
|
+
} finally {
|
|
1655
|
+
await this.runExtension?.onAfterRunFunc(lifecycle, outcome, failure)
|
|
1544
1656
|
}
|
|
1545
1657
|
})
|
|
1546
1658
|
}
|
|
@@ -1559,7 +1671,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1559
1671
|
await this.resumeWorkflow(parentRunId)
|
|
1560
1672
|
}
|
|
1561
1673
|
|
|
1562
|
-
|
|
1674
|
+
protected async onChildWorkflowFailed(
|
|
1563
1675
|
childRun: WorkflowRun,
|
|
1564
1676
|
error: Error
|
|
1565
1677
|
): Promise<void> {
|
|
@@ -2088,11 +2200,25 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2088
2200
|
)
|
|
2089
2201
|
}
|
|
2090
2202
|
|
|
2091
|
-
|
|
2203
|
+
protected async inlineStep(
|
|
2092
2204
|
runId: string,
|
|
2093
2205
|
logicalStepName: string,
|
|
2094
2206
|
fn: Function,
|
|
2095
|
-
stepOptions?: WorkflowStepOptions
|
|
2207
|
+
stepOptions?: WorkflowStepOptions,
|
|
2208
|
+
/**
|
|
2209
|
+
* The input this step was called with, recorded on the run so a reporter can
|
|
2210
|
+
* name the values under test. A closure step has none; a scenario step does.
|
|
2211
|
+
*/
|
|
2212
|
+
data: any = null,
|
|
2213
|
+
/**
|
|
2214
|
+
* The name this step was dispatched by, for the kinds of inline step that
|
|
2215
|
+
* have one. A closure step has no name; a scenario step is a step RPC, so
|
|
2216
|
+
* it records the step function that ran — which is the only way to join a
|
|
2217
|
+
* step back to its declaration when its durable name was built at runtime
|
|
2218
|
+
* (a step called in a loop reaches the run as `sees @pikku/addon-todos`,
|
|
2219
|
+
* declared as `sees ${packageName}`).
|
|
2220
|
+
*/
|
|
2221
|
+
rpcName: string | null = null
|
|
2096
2222
|
): Promise<any> {
|
|
2097
2223
|
const fromStepName = this.lastStepName(runId)
|
|
2098
2224
|
const stepName = this.nextStepKey(runId, logicalStepName)
|
|
@@ -2101,12 +2227,12 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2101
2227
|
try {
|
|
2102
2228
|
stepState = await this.getStepState(runId, stepName)
|
|
2103
2229
|
} catch {
|
|
2104
|
-
// Step doesn't exist - create it (inline,
|
|
2230
|
+
// Step doesn't exist - create it (inline, so never dispatched)
|
|
2105
2231
|
stepState = await this.insertStepState(
|
|
2106
2232
|
runId,
|
|
2107
2233
|
stepName,
|
|
2108
|
-
|
|
2109
|
-
|
|
2234
|
+
rpcName,
|
|
2235
|
+
data,
|
|
2110
2236
|
stepOptions,
|
|
2111
2237
|
fromStepName
|
|
2112
2238
|
)
|
|
@@ -2494,8 +2620,8 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2494
2620
|
runId: string,
|
|
2495
2621
|
rpcService: any,
|
|
2496
2622
|
addonNamespace?: string | null
|
|
2497
|
-
):
|
|
2498
|
-
const workflowWire:
|
|
2623
|
+
): PikkuWorkflowWire {
|
|
2624
|
+
const workflowWire: PikkuWorkflowWire = {
|
|
2499
2625
|
name,
|
|
2500
2626
|
runId,
|
|
2501
2627
|
getRun: async () => (await this.getRun(runId)) as WorkflowRun,
|
|
@@ -2531,146 +2657,6 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2531
2657
|
}
|
|
2532
2658
|
},
|
|
2533
2659
|
|
|
2534
|
-
// Durable polling step: invoke an RPC (as an actor when options.as is
|
|
2535
|
-
// set) until the predicate passes or `within` elapses. The whole poll is
|
|
2536
|
-
// ONE recorded step, so replay returns the cached outcome.
|
|
2537
|
-
expectEventually: async (
|
|
2538
|
-
stepName: string,
|
|
2539
|
-
rpcName: string,
|
|
2540
|
-
data: any,
|
|
2541
|
-
predicate: (output: any) => boolean,
|
|
2542
|
-
options?: WorkflowExpectEventuallyOptions
|
|
2543
|
-
) => {
|
|
2544
|
-
this.verifyStepName(stepName)
|
|
2545
|
-
const resolvedRpcName =
|
|
2546
|
-
addonNamespace && !rpcName.includes(':')
|
|
2547
|
-
? `${addonNamespace}:${rpcName}`
|
|
2548
|
-
: rpcName
|
|
2549
|
-
const within = getDurationInMilliseconds(options?.within ?? '30s')
|
|
2550
|
-
const interval = getDurationInMilliseconds(options?.interval ?? '1s')
|
|
2551
|
-
return await this.inlineStep(
|
|
2552
|
-
runId,
|
|
2553
|
-
stepName,
|
|
2554
|
-
async () => {
|
|
2555
|
-
const deadline = Date.now() + within
|
|
2556
|
-
let last: any
|
|
2557
|
-
while (true) {
|
|
2558
|
-
last = options?.actor
|
|
2559
|
-
? await options.actor.invoke(resolvedRpcName, data)
|
|
2560
|
-
: await rpcService.rpcWithWire(resolvedRpcName, data, {})
|
|
2561
|
-
if (predicate(last)) return last
|
|
2562
|
-
if (Date.now() + interval > deadline) {
|
|
2563
|
-
throw new Error(
|
|
2564
|
-
`[workflow] expectEventually '${stepName}' ('${resolvedRpcName}'` +
|
|
2565
|
-
`${options?.actor ? ` as '${options.actor.name}'` : ''}) did not pass within ${within}ms; ` +
|
|
2566
|
-
`last result: ${JSON.stringify(last)?.slice(0, 300)}`
|
|
2567
|
-
)
|
|
2568
|
-
}
|
|
2569
|
-
await new Promise((resolve) => setTimeout(resolve, interval))
|
|
2570
|
-
}
|
|
2571
|
-
},
|
|
2572
|
-
options
|
|
2573
|
-
)
|
|
2574
|
-
},
|
|
2575
|
-
|
|
2576
|
-
expectError: async (
|
|
2577
|
-
stepName: string,
|
|
2578
|
-
rpcName: string,
|
|
2579
|
-
data: any,
|
|
2580
|
-
options?: WorkflowExpectErrorOptions
|
|
2581
|
-
) => {
|
|
2582
|
-
this.verifyStepName(stepName)
|
|
2583
|
-
const resolvedRpcName =
|
|
2584
|
-
addonNamespace && !rpcName.includes(':')
|
|
2585
|
-
? `${addonNamespace}:${rpcName}`
|
|
2586
|
-
: rpcName
|
|
2587
|
-
return await this.inlineStep(
|
|
2588
|
-
runId,
|
|
2589
|
-
stepName,
|
|
2590
|
-
async () => {
|
|
2591
|
-
let result: any
|
|
2592
|
-
try {
|
|
2593
|
-
result = options?.actor
|
|
2594
|
-
? await options.actor.invoke(resolvedRpcName, data)
|
|
2595
|
-
: await rpcService.rpcWithWire(resolvedRpcName, data, {})
|
|
2596
|
-
} catch (e: any) {
|
|
2597
|
-
const message = e?.message ?? String(e)
|
|
2598
|
-
if (options?.matches) {
|
|
2599
|
-
const matched =
|
|
2600
|
-
typeof options.matches === 'string'
|
|
2601
|
-
? message.includes(options.matches)
|
|
2602
|
-
: options.matches.test(message)
|
|
2603
|
-
if (!matched) {
|
|
2604
|
-
throw new Error(
|
|
2605
|
-
`[workflow] expectError '${stepName}' ('${resolvedRpcName}') threw, but the message did not match ${options.matches}: ${message}`
|
|
2606
|
-
)
|
|
2607
|
-
}
|
|
2608
|
-
}
|
|
2609
|
-
return message
|
|
2610
|
-
}
|
|
2611
|
-
throw new Error(
|
|
2612
|
-
`[workflow] expectError '${stepName}' ('${resolvedRpcName}') expected an error but the call succeeded: ${JSON.stringify(result)?.slice(0, 300)}`
|
|
2613
|
-
)
|
|
2614
|
-
},
|
|
2615
|
-
options
|
|
2616
|
-
)
|
|
2617
|
-
},
|
|
2618
|
-
|
|
2619
|
-
expectService: async (
|
|
2620
|
-
stepName: string,
|
|
2621
|
-
serviceMethod: string,
|
|
2622
|
-
options?: WorkflowExpectServiceOptions
|
|
2623
|
-
) => {
|
|
2624
|
-
this.verifyStepName(stepName)
|
|
2625
|
-
const [service, method] = serviceMethod.split('.')
|
|
2626
|
-
if (!service || !method) {
|
|
2627
|
-
throw new Error(
|
|
2628
|
-
`[workflow] expectService '${stepName}' needs 'service.method', got '${serviceMethod}'`
|
|
2629
|
-
)
|
|
2630
|
-
}
|
|
2631
|
-
await this.inlineStep(
|
|
2632
|
-
runId,
|
|
2633
|
-
stepName,
|
|
2634
|
-
async () => {
|
|
2635
|
-
const rpcName = 'pikkuScenarioGetStubCalls'
|
|
2636
|
-
const calls: Array<{
|
|
2637
|
-
service: string
|
|
2638
|
-
method: string
|
|
2639
|
-
args: unknown[]
|
|
2640
|
-
}> = options?.actor
|
|
2641
|
-
? await options.actor.invoke(rpcName, { service })
|
|
2642
|
-
: await rpcService.rpcWithWire(rpcName, { service }, {})
|
|
2643
|
-
const matching = (calls ?? []).filter(
|
|
2644
|
-
(c) =>
|
|
2645
|
-
c.service === service &&
|
|
2646
|
-
c.method === method &&
|
|
2647
|
-
(options?.calledWith === undefined ||
|
|
2648
|
-
JSON.stringify(c.args?.[0]) ===
|
|
2649
|
-
JSON.stringify(options.calledWith))
|
|
2650
|
-
)
|
|
2651
|
-
const expected = options?.times
|
|
2652
|
-
const ok =
|
|
2653
|
-
expected === undefined
|
|
2654
|
-
? matching.length > 0
|
|
2655
|
-
: matching.length === expected
|
|
2656
|
-
if (!ok) {
|
|
2657
|
-
const seen =
|
|
2658
|
-
(calls ?? [])
|
|
2659
|
-
.map(
|
|
2660
|
-
(c) =>
|
|
2661
|
-
`${c.service}.${c.method}(${JSON.stringify(c.args?.[0])?.slice(0, 120) ?? ''})`
|
|
2662
|
-
)
|
|
2663
|
-
.join('\n ') || '(none)'
|
|
2664
|
-
throw new Error(
|
|
2665
|
-
`[workflow] expectService '${stepName}' expected ${expected ?? 'at least one'} call(s) to '${serviceMethod}'` +
|
|
2666
|
-
`${options?.calledWith !== undefined ? ` with ${JSON.stringify(options.calledWith)}` : ''}, found ${matching.length}. Recorded:\n ${seen}`
|
|
2667
|
-
)
|
|
2668
|
-
}
|
|
2669
|
-
},
|
|
2670
|
-
options
|
|
2671
|
-
)
|
|
2672
|
-
},
|
|
2673
|
-
|
|
2674
2660
|
// Implement workflow.sleep()
|
|
2675
2661
|
sleep: async (stepName: string, duration: string | number) => {
|
|
2676
2662
|
this.verifyStepName(stepName)
|
|
@@ -2689,16 +2675,18 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2689
2675
|
approval: (async (reason: string, options: WorkflowApprovalOptions) => {
|
|
2690
2676
|
this.verifyStepName(reason)
|
|
2691
2677
|
return await this.approvalStep(runId, reason, options)
|
|
2692
|
-
}) as
|
|
2693
|
-
|
|
2694
|
-
runScheduledTask: async (taskName: string) => {
|
|
2695
|
-
await runScheduledTask({ name: taskName })
|
|
2696
|
-
},
|
|
2678
|
+
}) as PikkuWorkflowWire['approval'],
|
|
2697
2679
|
}
|
|
2680
|
+
this.runExtension?.decorateWorkflowWire(workflowWire, {
|
|
2681
|
+
name,
|
|
2682
|
+
runId,
|
|
2683
|
+
rpcService,
|
|
2684
|
+
addonNamespace,
|
|
2685
|
+
})
|
|
2698
2686
|
return workflowWire
|
|
2699
2687
|
}
|
|
2700
2688
|
|
|
2701
|
-
|
|
2689
|
+
protected verifyStepName(stepName: string) {
|
|
2702
2690
|
if (typeof stepName !== 'string') {
|
|
2703
2691
|
throw new WorkflowStepNameNotString(stepName)
|
|
2704
2692
|
}
|