@pikku/core 0.12.67 → 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 +397 -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/in-memory-workflow-service.d.ts +2 -2
- package/dist/services/in-memory-workflow-service.js +2 -2
- 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/ai-agent/ai-agent-prepare.d.ts +18 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +26 -4
- package/dist/wirings/cli/command-parser.js +11 -1
- package/dist/wirings/queue/index.d.ts +1 -1
- package/dist/wirings/queue/queue.types.d.ts +30 -0
- 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 +118 -12
- package/dist/wirings/workflow/pikku-workflow-service.js +166 -153
- 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 +119 -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/in-memory-workflow-service.test.ts +50 -1
- package/src/services/in-memory-workflow-service.ts +3 -2
- 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/ai-agent/ai-agent-prepare.test.ts +29 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +38 -4
- package/src/wirings/cli/command-parser.test.ts +60 -0
- package/src/wirings/cli/command-parser.ts +12 -1
- package/src/wirings/queue/index.ts +2 -0
- package/src/wirings/queue/queue.types.ts +32 -0
- 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 +74 -0
- package/src/wirings/workflow/pikku-scenario-service.ts +682 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +126 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +306 -228
- 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 +137 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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,
|
|
@@ -60,11 +59,9 @@ import type {
|
|
|
60
59
|
WorkflowRunWire,
|
|
61
60
|
WorkflowStatus,
|
|
62
61
|
WorkflowVersionStatus,
|
|
62
|
+
WorkflowQueueOptions,
|
|
63
63
|
WorkflowServiceConfig,
|
|
64
64
|
WorkflowStepOptions,
|
|
65
|
-
WorkflowExpectEventuallyOptions,
|
|
66
|
-
WorkflowExpectErrorOptions,
|
|
67
|
-
WorkflowExpectServiceOptions,
|
|
68
65
|
} from './workflow.types.js'
|
|
69
66
|
import {
|
|
70
67
|
continueGraph,
|
|
@@ -88,7 +85,12 @@ import {
|
|
|
88
85
|
type RunTimeline,
|
|
89
86
|
type ReconstructedRunState,
|
|
90
87
|
} from './run-timeline.js'
|
|
91
|
-
import type {
|
|
88
|
+
import type {
|
|
89
|
+
GroupConcurrencyConfig,
|
|
90
|
+
JobGroup,
|
|
91
|
+
JobOptions,
|
|
92
|
+
PikkuWorkerConfig,
|
|
93
|
+
} from '../queue/queue.types.js'
|
|
92
94
|
|
|
93
95
|
/**
|
|
94
96
|
* Default number of retries for a workflow step when none is specified. The
|
|
@@ -240,6 +242,101 @@ const WORKFLOW_END_STATES: ReadonlySet<string> = new Set([
|
|
|
240
242
|
'suspended',
|
|
241
243
|
])
|
|
242
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
|
+
|
|
243
340
|
/**
|
|
244
341
|
* Abstract workflow state service
|
|
245
342
|
* Implementations provide pluggable storage backends (SQLite, PostgreSQL, etc.)
|
|
@@ -247,9 +344,7 @@ const WORKFLOW_END_STATES: ReadonlySet<string> = new Set([
|
|
|
247
344
|
*/
|
|
248
345
|
export abstract class PikkuWorkflowService implements WorkflowService {
|
|
249
346
|
private inlineRuns = new Set<string>()
|
|
250
|
-
|
|
251
|
-
// process-local by nature, so they ride this map, never the persisted wire.
|
|
252
|
-
private runActors = new Map<string, ScenarioActors>()
|
|
347
|
+
private runExtension?: WorkflowRunExtension
|
|
253
348
|
|
|
254
349
|
protected get logger() {
|
|
255
350
|
return getSingletonServices()?.logger
|
|
@@ -257,11 +352,21 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
257
352
|
|
|
258
353
|
protected mirror?: WorkflowRunMirror
|
|
259
354
|
|
|
355
|
+
protected readonly queueStrategy: 'per-workflow' | 'shared-groups'
|
|
356
|
+
protected readonly queueConcurrency: number
|
|
357
|
+
protected readonly queueGroupConcurrency: number | GroupConcurrencyConfig
|
|
358
|
+
|
|
260
359
|
constructor(
|
|
261
|
-
options: {
|
|
360
|
+
options: {
|
|
361
|
+
wireQueues?: boolean
|
|
362
|
+
mirror?: WorkflowRunMirror
|
|
363
|
+
} & WorkflowQueueOptions = {}
|
|
262
364
|
) {
|
|
263
365
|
const wireQueues = options.wireQueues ?? true
|
|
264
366
|
this.mirror = options.mirror
|
|
367
|
+
this.queueStrategy = options.queueStrategy ?? 'per-workflow'
|
|
368
|
+
this.queueConcurrency = options.queueConcurrency ?? 20
|
|
369
|
+
this.queueGroupConcurrency = options.queueGroupConcurrency ?? 2
|
|
265
370
|
if (wireQueues) {
|
|
266
371
|
this.wireQueueWorkers()
|
|
267
372
|
}
|
|
@@ -306,29 +411,44 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
306
411
|
const registerWorkflowFunc = (
|
|
307
412
|
funcId: string,
|
|
308
413
|
func: { func: unknown },
|
|
309
|
-
queueName: string
|
|
414
|
+
queueName: string,
|
|
415
|
+
config?: PikkuWorkerConfig
|
|
310
416
|
) => {
|
|
311
417
|
if (functions.has(funcId)) return
|
|
312
418
|
addFunction(funcId, func as never)
|
|
313
419
|
if (!queueMeta[queueName]) {
|
|
314
420
|
queueMeta[queueName] = { pikkuFuncId: funcId, name: queueName }
|
|
315
421
|
}
|
|
316
|
-
wireQueueWorker({ name: queueName, func } as never)
|
|
422
|
+
wireQueueWorker({ name: queueName, func, config } as never)
|
|
317
423
|
if (!functionsMeta[funcId]) {
|
|
318
424
|
functionsMeta[funcId] = mkMeta(funcId)
|
|
319
425
|
}
|
|
320
426
|
}
|
|
321
427
|
|
|
428
|
+
// Under 'shared-groups' every workflow runs through these two queues and is
|
|
429
|
+
// kept from hogging them by the per-group cap, so the per-workflow queues
|
|
430
|
+
// below are left unconsumed — one set of pollers for the whole system
|
|
431
|
+
// instead of one per workflow.
|
|
432
|
+
const sharedGroups = this.queueStrategy === 'shared-groups'
|
|
433
|
+
const sharedQueueConfig: PikkuWorkerConfig | undefined = sharedGroups
|
|
434
|
+
? {
|
|
435
|
+
batchSize: this.queueConcurrency,
|
|
436
|
+
groupConcurrency: this.queueGroupConcurrency,
|
|
437
|
+
}
|
|
438
|
+
: undefined
|
|
439
|
+
|
|
322
440
|
// Register shared queue workers for monolith deployments
|
|
323
441
|
registerWorkflowFunc(
|
|
324
442
|
'pikkuWorkflowOrchestrator',
|
|
325
443
|
{ func: pikkuWorkflowOrchestratorFunc },
|
|
326
|
-
'pikku-workflow-orchestrator'
|
|
444
|
+
'pikku-workflow-orchestrator',
|
|
445
|
+
sharedQueueConfig
|
|
327
446
|
)
|
|
328
447
|
registerWorkflowFunc(
|
|
329
448
|
'pikkuWorkflowStepWorker',
|
|
330
449
|
{ func: pikkuWorkflowWorkerFunc },
|
|
331
|
-
'pikku-workflow-step-worker'
|
|
450
|
+
'pikku-workflow-step-worker',
|
|
451
|
+
sharedQueueConfig
|
|
332
452
|
)
|
|
333
453
|
|
|
334
454
|
// Register per-workflow queue workers (root + addon packages)
|
|
@@ -351,18 +471,41 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
351
471
|
}
|
|
352
472
|
}
|
|
353
473
|
|
|
354
|
-
|
|
474
|
+
if (!sharedGroups) {
|
|
475
|
+
registerQueueWorkers(pikkuState(null, 'queue', 'meta'))
|
|
355
476
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
477
|
+
const addons = pikkuState(null, 'addons', 'packages')
|
|
478
|
+
if (addons) {
|
|
479
|
+
for (const [, addon] of addons) {
|
|
480
|
+
const addonQueueMeta = pikkuState(addon.package, 'queue', 'meta')
|
|
481
|
+
if (addonQueueMeta) {
|
|
482
|
+
registerQueueWorkers(addonQueueMeta)
|
|
483
|
+
}
|
|
362
484
|
}
|
|
363
485
|
}
|
|
364
486
|
}
|
|
365
487
|
|
|
488
|
+
// Workflows exist but no per-workflow orchestrator queue was registered:
|
|
489
|
+
// the generated queue meta never reached the runtime (most often the
|
|
490
|
+
// bootstrap doesn't import the queue-workers meta, so `queue.meta` is
|
|
491
|
+
// empty). Everything still "works" — dispatch silently falls back to the
|
|
492
|
+
// single shared orchestrator queue — but the isolation is gone: one slow
|
|
493
|
+
// workflow step head-of-line-blocks every other workflow behind it. That
|
|
494
|
+
// is invisible until a queue starves, so say so loudly at wiring time.
|
|
495
|
+
const workflowCount = Object.keys(
|
|
496
|
+
pikkuState(null, 'workflows', 'meta') ?? {}
|
|
497
|
+
).length
|
|
498
|
+
const perWorkflowQueues = Object.keys(queueMeta).filter((name) =>
|
|
499
|
+
name.startsWith('wf-orchestrator-')
|
|
500
|
+
).length
|
|
501
|
+
if (workflowCount > 0 && perWorkflowQueues === 0) {
|
|
502
|
+
this.logger?.warn?.(
|
|
503
|
+
`[pikku] ${workflowCount} workflow(s) registered but no per-workflow orchestrator queues were found in queue meta. ` +
|
|
504
|
+
`All workflows will share a single orchestrator queue, where one slow step blocks every other workflow behind it. ` +
|
|
505
|
+
`Check that the generated bootstrap imports the queue-workers meta (pikku-queue-workers-wirings-meta.gen.js).`
|
|
506
|
+
)
|
|
507
|
+
}
|
|
508
|
+
|
|
366
509
|
if (!functions.has('pikkuWorkflowSleeper')) {
|
|
367
510
|
addFunction('pikkuWorkflowSleeper', {
|
|
368
511
|
func: pikkuWorkflowSleeperFunc,
|
|
@@ -577,7 +720,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
577
720
|
* Creates pending step in both workflow_step and workflow_step_history
|
|
578
721
|
* @param runId - Run ID
|
|
579
722
|
* @param stepName - Step cache key
|
|
580
|
-
* @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.
|
|
581
727
|
* @param data - Step input data
|
|
582
728
|
* @param stepOptions - Step options (retries, retryDelay)
|
|
583
729
|
* @returns Step state with generated stepId
|
|
@@ -927,7 +1073,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
927
1073
|
await queueService.add(
|
|
928
1074
|
this.getOrchestratorQueueName(workflowName),
|
|
929
1075
|
{ runId },
|
|
930
|
-
|
|
1076
|
+
{
|
|
1077
|
+
...this.resolveStepJobOptions(),
|
|
1078
|
+
group: this.getJobGroup(workflowName),
|
|
1079
|
+
}
|
|
931
1080
|
)
|
|
932
1081
|
}
|
|
933
1082
|
|
|
@@ -970,7 +1119,12 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
970
1119
|
JSON.parse(
|
|
971
1120
|
JSON.stringify({ runId, stepName, rpcName, data, fromStepName })
|
|
972
1121
|
),
|
|
973
|
-
|
|
1122
|
+
{
|
|
1123
|
+
...this.resolveStepJobOptions(stepOptions),
|
|
1124
|
+
// Group by step function, mirroring how per-step queues split them —
|
|
1125
|
+
// one slow step function can't monopolise the shared step worker.
|
|
1126
|
+
group: this.getJobGroup(rpcName),
|
|
1127
|
+
}
|
|
974
1128
|
)
|
|
975
1129
|
}
|
|
976
1130
|
|
|
@@ -1005,7 +1159,12 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1005
1159
|
await queueService.add(
|
|
1006
1160
|
this.getOrchestratorQueueName(workflowName),
|
|
1007
1161
|
{ runId },
|
|
1008
|
-
|
|
1162
|
+
{
|
|
1163
|
+
...(retryDelay
|
|
1164
|
+
? { delay: getDurationInMilliseconds(retryDelay) }
|
|
1165
|
+
: undefined),
|
|
1166
|
+
group: this.getJobGroup(workflowName),
|
|
1167
|
+
}
|
|
1009
1168
|
)
|
|
1010
1169
|
}
|
|
1011
1170
|
|
|
@@ -1054,7 +1213,10 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1054
1213
|
JSON.parse(
|
|
1055
1214
|
JSON.stringify({ runId, stepName, rpcName, data, fromStepName })
|
|
1056
1215
|
),
|
|
1057
|
-
|
|
1216
|
+
{
|
|
1217
|
+
...this.resolveStepJobOptions(stepOptions),
|
|
1218
|
+
group: this.getJobGroup(rpcName),
|
|
1219
|
+
}
|
|
1058
1220
|
)
|
|
1059
1221
|
} catch (cause) {
|
|
1060
1222
|
// The queue is down/unreachable — NOT a step failure. Surface it as a
|
|
@@ -1092,37 +1254,28 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1092
1254
|
return true
|
|
1093
1255
|
}
|
|
1094
1256
|
|
|
1095
|
-
/**
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
const
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
)
|
|
1109
|
-
return undefined
|
|
1110
|
-
}
|
|
1111
|
-
const actorsConfig = await metaService.getScenarioActorsMeta()
|
|
1112
|
-
if (!actorsConfig || Object.keys(actorsConfig).length === 0) {
|
|
1113
|
-
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),
|
|
1114
1271
|
}
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
actors: actorsConfig,
|
|
1123
|
-
signInPath,
|
|
1124
|
-
rpcPath,
|
|
1125
|
-
})
|
|
1272
|
+
const extension = create(engine)
|
|
1273
|
+
this.runExtension = extension
|
|
1274
|
+
return extension
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
public getRunExtension(): WorkflowRunExtension | undefined {
|
|
1278
|
+
return this.runExtension
|
|
1126
1279
|
}
|
|
1127
1280
|
|
|
1128
1281
|
/**
|
|
@@ -1130,13 +1283,21 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1130
1283
|
* Automatically detects workflow type (DSL or graph) from meta and executes accordingly
|
|
1131
1284
|
* @param options.inline - If true, execute workflow directly without queue service
|
|
1132
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.
|
|
1133
1289
|
*/
|
|
1134
1290
|
public async startWorkflow<I>(
|
|
1135
1291
|
name: string,
|
|
1136
1292
|
input: I,
|
|
1137
1293
|
wire: WorkflowRunWire,
|
|
1138
1294
|
rpcService: any,
|
|
1139
|
-
options?: {
|
|
1295
|
+
options?: {
|
|
1296
|
+
inline?: boolean
|
|
1297
|
+
startNode?: string
|
|
1298
|
+
actors?: ScenarioActors
|
|
1299
|
+
onRunCreated?: (runId: string) => void
|
|
1300
|
+
}
|
|
1140
1301
|
): Promise<{ runId: string }> {
|
|
1141
1302
|
// Resolve workflow from static meta (root or addon namespace), then dynamic DB
|
|
1142
1303
|
const resolved = resolveWorkflowMeta(name)
|
|
@@ -1200,14 +1361,9 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1200
1361
|
}
|
|
1201
1362
|
)
|
|
1202
1363
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
? await this.resolveScenarioActors()
|
|
1207
|
-
: undefined)
|
|
1208
|
-
if (actors) {
|
|
1209
|
-
this.runActors.set(runId, actors)
|
|
1210
|
-
}
|
|
1364
|
+
options?.onRunCreated?.(runId)
|
|
1365
|
+
|
|
1366
|
+
await this.runExtension?.attachRunContext(runId, workflowMeta, options)
|
|
1211
1367
|
|
|
1212
1368
|
if (shouldInline) {
|
|
1213
1369
|
this.inlineRuns.add(runId)
|
|
@@ -1244,7 +1400,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1244
1400
|
}
|
|
1245
1401
|
} finally {
|
|
1246
1402
|
this.inlineRuns.delete(runId)
|
|
1247
|
-
this.
|
|
1403
|
+
this.runExtension?.detachRunContext(runId)
|
|
1248
1404
|
}
|
|
1249
1405
|
} else {
|
|
1250
1406
|
await this.resumeWorkflow(runId)
|
|
@@ -1415,15 +1571,33 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1415
1571
|
workflowWire.pikkuUserId = run.wire?.pikkuUserId
|
|
1416
1572
|
const wire: PikkuWire = {
|
|
1417
1573
|
workflow: workflowWire,
|
|
1418
|
-
scenario:
|
|
1419
|
-
workflowMeta?.source === 'scenario' ? workflowWire : undefined,
|
|
1420
1574
|
pikkuUserId: run.wire?.pikkuUserId,
|
|
1421
1575
|
session: rpcService?.wire?.session,
|
|
1422
1576
|
rpc: rpcService?.wire?.rpc,
|
|
1423
|
-
// User-flow actors registered for this run (see startWorkflow options)
|
|
1424
|
-
actors: this.runActors.get(runId),
|
|
1425
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
|
|
1426
1598
|
try {
|
|
1599
|
+
await this.runExtension?.onBeforeRunFunc(lifecycle)
|
|
1600
|
+
|
|
1427
1601
|
const result = await runPikkuFunc(
|
|
1428
1602
|
'workflow',
|
|
1429
1603
|
workflowMeta.name,
|
|
@@ -1440,11 +1614,15 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1440
1614
|
await this.updateRunStatus(runId, 'completed', result)
|
|
1441
1615
|
await this.onChildWorkflowCompleted(run, result)
|
|
1442
1616
|
} catch (error: any) {
|
|
1617
|
+
failure = error
|
|
1618
|
+
|
|
1443
1619
|
if (error instanceof WorkflowAsyncException) {
|
|
1620
|
+
outcome = 'interrupted'
|
|
1444
1621
|
throw error
|
|
1445
1622
|
}
|
|
1446
1623
|
|
|
1447
1624
|
if (error instanceof WorkflowCancelledException) {
|
|
1625
|
+
outcome = 'failed'
|
|
1448
1626
|
await this.updateRunStatus(runId, 'cancelled', undefined, {
|
|
1449
1627
|
message: error.message || 'Workflow cancelled',
|
|
1450
1628
|
stack: '',
|
|
@@ -1455,6 +1633,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1455
1633
|
}
|
|
1456
1634
|
|
|
1457
1635
|
if (error instanceof WorkflowSuspendedException) {
|
|
1636
|
+
outcome = 'interrupted'
|
|
1458
1637
|
await this.updateRunStatus(runId, 'suspended', undefined, {
|
|
1459
1638
|
message: error.message || 'Workflow suspended',
|
|
1460
1639
|
stack: '',
|
|
@@ -1463,6 +1642,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1463
1642
|
throw error
|
|
1464
1643
|
}
|
|
1465
1644
|
|
|
1645
|
+
outcome = 'failed'
|
|
1466
1646
|
await this.updateRunStatus(runId, 'failed', undefined, {
|
|
1467
1647
|
message: error.message,
|
|
1468
1648
|
stack: error.stack,
|
|
@@ -1471,6 +1651,8 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1471
1651
|
await this.onChildWorkflowFailed(run, error)
|
|
1472
1652
|
|
|
1473
1653
|
throw error
|
|
1654
|
+
} finally {
|
|
1655
|
+
await this.runExtension?.onAfterRunFunc(lifecycle, outcome, failure)
|
|
1474
1656
|
}
|
|
1475
1657
|
})
|
|
1476
1658
|
}
|
|
@@ -1489,7 +1671,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1489
1671
|
await this.resumeWorkflow(parentRunId)
|
|
1490
1672
|
}
|
|
1491
1673
|
|
|
1492
|
-
|
|
1674
|
+
protected async onChildWorkflowFailed(
|
|
1493
1675
|
childRun: WorkflowRun,
|
|
1494
1676
|
error: Error
|
|
1495
1677
|
): Promise<void> {
|
|
@@ -2018,11 +2200,25 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2018
2200
|
)
|
|
2019
2201
|
}
|
|
2020
2202
|
|
|
2021
|
-
|
|
2203
|
+
protected async inlineStep(
|
|
2022
2204
|
runId: string,
|
|
2023
2205
|
logicalStepName: string,
|
|
2024
2206
|
fn: Function,
|
|
2025
|
-
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
|
|
2026
2222
|
): Promise<any> {
|
|
2027
2223
|
const fromStepName = this.lastStepName(runId)
|
|
2028
2224
|
const stepName = this.nextStepKey(runId, logicalStepName)
|
|
@@ -2031,12 +2227,12 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2031
2227
|
try {
|
|
2032
2228
|
stepState = await this.getStepState(runId, stepName)
|
|
2033
2229
|
} catch {
|
|
2034
|
-
// Step doesn't exist - create it (inline,
|
|
2230
|
+
// Step doesn't exist - create it (inline, so never dispatched)
|
|
2035
2231
|
stepState = await this.insertStepState(
|
|
2036
2232
|
runId,
|
|
2037
2233
|
stepName,
|
|
2038
|
-
|
|
2039
|
-
|
|
2234
|
+
rpcName,
|
|
2235
|
+
data,
|
|
2040
2236
|
stepOptions,
|
|
2041
2237
|
fromStepName
|
|
2042
2238
|
)
|
|
@@ -2225,7 +2421,11 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2225
2421
|
await queueService.add(
|
|
2226
2422
|
this.getOrchestratorQueueName(run.workflow),
|
|
2227
2423
|
{ runId },
|
|
2228
|
-
{
|
|
2424
|
+
{
|
|
2425
|
+
...this.resolveStepJobOptions(),
|
|
2426
|
+
delay,
|
|
2427
|
+
group: this.getJobGroup(run.workflow),
|
|
2428
|
+
}
|
|
2229
2429
|
)
|
|
2230
2430
|
} catch (error) {
|
|
2231
2431
|
this.logger?.warn(
|
|
@@ -2420,8 +2620,8 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2420
2620
|
runId: string,
|
|
2421
2621
|
rpcService: any,
|
|
2422
2622
|
addonNamespace?: string | null
|
|
2423
|
-
):
|
|
2424
|
-
const workflowWire:
|
|
2623
|
+
): PikkuWorkflowWire {
|
|
2624
|
+
const workflowWire: PikkuWorkflowWire = {
|
|
2425
2625
|
name,
|
|
2426
2626
|
runId,
|
|
2427
2627
|
getRun: async () => (await this.getRun(runId)) as WorkflowRun,
|
|
@@ -2457,146 +2657,6 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2457
2657
|
}
|
|
2458
2658
|
},
|
|
2459
2659
|
|
|
2460
|
-
// Durable polling step: invoke an RPC (as an actor when options.as is
|
|
2461
|
-
// set) until the predicate passes or `within` elapses. The whole poll is
|
|
2462
|
-
// ONE recorded step, so replay returns the cached outcome.
|
|
2463
|
-
expectEventually: async (
|
|
2464
|
-
stepName: string,
|
|
2465
|
-
rpcName: string,
|
|
2466
|
-
data: any,
|
|
2467
|
-
predicate: (output: any) => boolean,
|
|
2468
|
-
options?: WorkflowExpectEventuallyOptions
|
|
2469
|
-
) => {
|
|
2470
|
-
this.verifyStepName(stepName)
|
|
2471
|
-
const resolvedRpcName =
|
|
2472
|
-
addonNamespace && !rpcName.includes(':')
|
|
2473
|
-
? `${addonNamespace}:${rpcName}`
|
|
2474
|
-
: rpcName
|
|
2475
|
-
const within = getDurationInMilliseconds(options?.within ?? '30s')
|
|
2476
|
-
const interval = getDurationInMilliseconds(options?.interval ?? '1s')
|
|
2477
|
-
return await this.inlineStep(
|
|
2478
|
-
runId,
|
|
2479
|
-
stepName,
|
|
2480
|
-
async () => {
|
|
2481
|
-
const deadline = Date.now() + within
|
|
2482
|
-
let last: any
|
|
2483
|
-
while (true) {
|
|
2484
|
-
last = options?.actor
|
|
2485
|
-
? await options.actor.invoke(resolvedRpcName, data)
|
|
2486
|
-
: await rpcService.rpcWithWire(resolvedRpcName, data, {})
|
|
2487
|
-
if (predicate(last)) return last
|
|
2488
|
-
if (Date.now() + interval > deadline) {
|
|
2489
|
-
throw new Error(
|
|
2490
|
-
`[workflow] expectEventually '${stepName}' ('${resolvedRpcName}'` +
|
|
2491
|
-
`${options?.actor ? ` as '${options.actor.name}'` : ''}) did not pass within ${within}ms; ` +
|
|
2492
|
-
`last result: ${JSON.stringify(last)?.slice(0, 300)}`
|
|
2493
|
-
)
|
|
2494
|
-
}
|
|
2495
|
-
await new Promise((resolve) => setTimeout(resolve, interval))
|
|
2496
|
-
}
|
|
2497
|
-
},
|
|
2498
|
-
options
|
|
2499
|
-
)
|
|
2500
|
-
},
|
|
2501
|
-
|
|
2502
|
-
expectError: async (
|
|
2503
|
-
stepName: string,
|
|
2504
|
-
rpcName: string,
|
|
2505
|
-
data: any,
|
|
2506
|
-
options?: WorkflowExpectErrorOptions
|
|
2507
|
-
) => {
|
|
2508
|
-
this.verifyStepName(stepName)
|
|
2509
|
-
const resolvedRpcName =
|
|
2510
|
-
addonNamespace && !rpcName.includes(':')
|
|
2511
|
-
? `${addonNamespace}:${rpcName}`
|
|
2512
|
-
: rpcName
|
|
2513
|
-
return await this.inlineStep(
|
|
2514
|
-
runId,
|
|
2515
|
-
stepName,
|
|
2516
|
-
async () => {
|
|
2517
|
-
let result: any
|
|
2518
|
-
try {
|
|
2519
|
-
result = options?.actor
|
|
2520
|
-
? await options.actor.invoke(resolvedRpcName, data)
|
|
2521
|
-
: await rpcService.rpcWithWire(resolvedRpcName, data, {})
|
|
2522
|
-
} catch (e: any) {
|
|
2523
|
-
const message = e?.message ?? String(e)
|
|
2524
|
-
if (options?.matches) {
|
|
2525
|
-
const matched =
|
|
2526
|
-
typeof options.matches === 'string'
|
|
2527
|
-
? message.includes(options.matches)
|
|
2528
|
-
: options.matches.test(message)
|
|
2529
|
-
if (!matched) {
|
|
2530
|
-
throw new Error(
|
|
2531
|
-
`[workflow] expectError '${stepName}' ('${resolvedRpcName}') threw, but the message did not match ${options.matches}: ${message}`
|
|
2532
|
-
)
|
|
2533
|
-
}
|
|
2534
|
-
}
|
|
2535
|
-
return message
|
|
2536
|
-
}
|
|
2537
|
-
throw new Error(
|
|
2538
|
-
`[workflow] expectError '${stepName}' ('${resolvedRpcName}') expected an error but the call succeeded: ${JSON.stringify(result)?.slice(0, 300)}`
|
|
2539
|
-
)
|
|
2540
|
-
},
|
|
2541
|
-
options
|
|
2542
|
-
)
|
|
2543
|
-
},
|
|
2544
|
-
|
|
2545
|
-
expectService: async (
|
|
2546
|
-
stepName: string,
|
|
2547
|
-
serviceMethod: string,
|
|
2548
|
-
options?: WorkflowExpectServiceOptions
|
|
2549
|
-
) => {
|
|
2550
|
-
this.verifyStepName(stepName)
|
|
2551
|
-
const [service, method] = serviceMethod.split('.')
|
|
2552
|
-
if (!service || !method) {
|
|
2553
|
-
throw new Error(
|
|
2554
|
-
`[workflow] expectService '${stepName}' needs 'service.method', got '${serviceMethod}'`
|
|
2555
|
-
)
|
|
2556
|
-
}
|
|
2557
|
-
await this.inlineStep(
|
|
2558
|
-
runId,
|
|
2559
|
-
stepName,
|
|
2560
|
-
async () => {
|
|
2561
|
-
const rpcName = 'pikkuScenarioGetStubCalls'
|
|
2562
|
-
const calls: Array<{
|
|
2563
|
-
service: string
|
|
2564
|
-
method: string
|
|
2565
|
-
args: unknown[]
|
|
2566
|
-
}> = options?.actor
|
|
2567
|
-
? await options.actor.invoke(rpcName, { service })
|
|
2568
|
-
: await rpcService.rpcWithWire(rpcName, { service }, {})
|
|
2569
|
-
const matching = (calls ?? []).filter(
|
|
2570
|
-
(c) =>
|
|
2571
|
-
c.service === service &&
|
|
2572
|
-
c.method === method &&
|
|
2573
|
-
(options?.calledWith === undefined ||
|
|
2574
|
-
JSON.stringify(c.args?.[0]) ===
|
|
2575
|
-
JSON.stringify(options.calledWith))
|
|
2576
|
-
)
|
|
2577
|
-
const expected = options?.times
|
|
2578
|
-
const ok =
|
|
2579
|
-
expected === undefined
|
|
2580
|
-
? matching.length > 0
|
|
2581
|
-
: matching.length === expected
|
|
2582
|
-
if (!ok) {
|
|
2583
|
-
const seen =
|
|
2584
|
-
(calls ?? [])
|
|
2585
|
-
.map(
|
|
2586
|
-
(c) =>
|
|
2587
|
-
`${c.service}.${c.method}(${JSON.stringify(c.args?.[0])?.slice(0, 120) ?? ''})`
|
|
2588
|
-
)
|
|
2589
|
-
.join('\n ') || '(none)'
|
|
2590
|
-
throw new Error(
|
|
2591
|
-
`[workflow] expectService '${stepName}' expected ${expected ?? 'at least one'} call(s) to '${serviceMethod}'` +
|
|
2592
|
-
`${options?.calledWith !== undefined ? ` with ${JSON.stringify(options.calledWith)}` : ''}, found ${matching.length}. Recorded:\n ${seen}`
|
|
2593
|
-
)
|
|
2594
|
-
}
|
|
2595
|
-
},
|
|
2596
|
-
options
|
|
2597
|
-
)
|
|
2598
|
-
},
|
|
2599
|
-
|
|
2600
2660
|
// Implement workflow.sleep()
|
|
2601
2661
|
sleep: async (stepName: string, duration: string | number) => {
|
|
2602
2662
|
this.verifyStepName(stepName)
|
|
@@ -2615,16 +2675,18 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2615
2675
|
approval: (async (reason: string, options: WorkflowApprovalOptions) => {
|
|
2616
2676
|
this.verifyStepName(reason)
|
|
2617
2677
|
return await this.approvalStep(runId, reason, options)
|
|
2618
|
-
}) as
|
|
2619
|
-
|
|
2620
|
-
runScheduledTask: async (taskName: string) => {
|
|
2621
|
-
await runScheduledTask({ name: taskName })
|
|
2622
|
-
},
|
|
2678
|
+
}) as PikkuWorkflowWire['approval'],
|
|
2623
2679
|
}
|
|
2680
|
+
this.runExtension?.decorateWorkflowWire(workflowWire, {
|
|
2681
|
+
name,
|
|
2682
|
+
runId,
|
|
2683
|
+
rpcService,
|
|
2684
|
+
addonNamespace,
|
|
2685
|
+
})
|
|
2624
2686
|
return workflowWire
|
|
2625
2687
|
}
|
|
2626
2688
|
|
|
2627
|
-
|
|
2689
|
+
protected verifyStepName(stepName: string) {
|
|
2628
2690
|
if (typeof stepName !== 'string') {
|
|
2629
2691
|
throw new WorkflowStepNameNotString(stepName)
|
|
2630
2692
|
}
|
|
@@ -2655,7 +2717,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2655
2717
|
* queues — but it produces to them — so registrations would miss them.
|
|
2656
2718
|
*/
|
|
2657
2719
|
protected getOrchestratorQueueName(workflowName?: string): string {
|
|
2658
|
-
if (workflowName) {
|
|
2720
|
+
if (workflowName && this.queueStrategy !== 'shared-groups') {
|
|
2659
2721
|
const perWorkflow = `wf-orchestrator-${toKebab(workflowName)}`
|
|
2660
2722
|
const meta = pikkuState(null, 'queue', 'meta')
|
|
2661
2723
|
if (meta[perWorkflow]) {
|
|
@@ -2666,7 +2728,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2666
2728
|
}
|
|
2667
2729
|
|
|
2668
2730
|
protected getStepWorkerQueueName(rpcName?: string): string {
|
|
2669
|
-
if (rpcName) {
|
|
2731
|
+
if (rpcName && this.queueStrategy !== 'shared-groups') {
|
|
2670
2732
|
const perStep = `wf-step-${toKebab(rpcName)}`
|
|
2671
2733
|
const meta = pikkuState(null, 'queue', 'meta')
|
|
2672
2734
|
if (meta[perStep]) {
|
|
@@ -2675,4 +2737,20 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
2675
2737
|
}
|
|
2676
2738
|
return this.getConfig().stepWorkerQueueName
|
|
2677
2739
|
}
|
|
2740
|
+
|
|
2741
|
+
/**
|
|
2742
|
+
* Fairness key for a job on a shared queue. Under `'per-workflow'` the queue
|
|
2743
|
+
* name already isolates workflows, so no group is needed — returning one
|
|
2744
|
+
* anyway would cap a workflow inside its own dedicated queue.
|
|
2745
|
+
*
|
|
2746
|
+
* The tier repeats the id so a workflow can be given its own limit purely
|
|
2747
|
+
* from config, with no per-workflow wiring; an unmatched tier falls back to
|
|
2748
|
+
* the default limit.
|
|
2749
|
+
*/
|
|
2750
|
+
protected getJobGroup(id?: string): JobGroup | undefined {
|
|
2751
|
+
if (!id || this.queueStrategy !== 'shared-groups') {
|
|
2752
|
+
return undefined
|
|
2753
|
+
}
|
|
2754
|
+
return { id, tier: id }
|
|
2755
|
+
}
|
|
2678
2756
|
}
|