@pikku/core 0.12.130 → 0.12.133
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 +95 -0
- package/dist/middleware-runner.d.ts +4 -0
- package/dist/middleware-runner.js +52 -16
- package/dist/services/in-memory-lease-service.d.ts +13 -0
- package/dist/services/in-memory-lease-service.js +49 -0
- package/dist/services/in-memory-workflow-service.d.ts +11 -3
- package/dist/services/in-memory-workflow-service.js +53 -23
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.js +2 -0
- package/dist/services/lease-service.d.ts +61 -0
- package/dist/services/lease-service.js +110 -0
- package/dist/services/workflow-service.d.ts +1 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/service-tests/lease-service-tests.d.ts +3 -0
- package/dist/testing/service-tests/lease-service-tests.js +138 -0
- package/dist/testing/service-tests/workflow-fencing-tests.d.ts +18 -0
- package/dist/testing/service-tests/workflow-fencing-tests.js +216 -0
- package/dist/testing/service-tests.d.ts +5 -0
- package/dist/testing/service-tests.js +8 -0
- package/dist/types/core.types.d.ts +12 -8
- package/dist/utils/hmac.d.ts +14 -10
- package/dist/utils/hmac.js +29 -25
- package/dist/wirings/http/pikku-fetch-http-response.js +13 -0
- package/dist/wirings/secret/derive-oauth2-app-secrets.js +1 -0
- package/dist/wirings/secret/secret.types.d.ts +6 -0
- package/dist/wirings/trigger/index.d.ts +2 -2
- package/dist/wirings/trigger/index.js +1 -1
- package/dist/wirings/trigger/webhook-source-runner.js +45 -1
- package/dist/wirings/trigger/webhook-source.types.d.ts +50 -0
- package/dist/wirings/trigger/webhook-source.types.js +2 -0
- package/dist/wirings/workflow/graph/graph-runner.js +33 -14
- package/dist/wirings/workflow/index.d.ts +4 -3
- package/dist/wirings/workflow/index.js +3 -2
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +57 -18
- package/dist/wirings/workflow/pikku-workflow-service.js +143 -140
- package/dist/wirings/workflow/workflow-constants.d.ts +28 -1
- package/dist/wirings/workflow/workflow-constants.js +28 -1
- package/dist/wirings/workflow/workflow-errors.d.ts +21 -0
- package/dist/wirings/workflow/workflow-errors.js +38 -0
- package/dist/wirings/workflow/workflow-queue-routing.d.ts +4 -2
- package/dist/wirings/workflow/workflow-queue-routing.js +9 -1
- package/dist/wirings/workflow/workflow-run-lease.d.ts +10 -0
- package/dist/wirings/workflow/workflow-run-lease.js +25 -0
- package/dist/wirings/workflow/workflow-run-status.d.ts +4 -0
- package/dist/wirings/workflow/workflow-run-status.js +37 -0
- package/dist/wirings/workflow/workflow-status-stream.d.ts +2 -1
- package/dist/wirings/workflow/workflow-status-stream.js +18 -1
- package/dist/wirings/workflow/workflow-step-claim.d.ts +13 -1
- package/dist/wirings/workflow/workflow-step-claim.js +29 -5
- package/dist/wirings/workflow/workflow-step-lease.d.ts +25 -0
- package/dist/wirings/workflow/workflow-step-lease.js +53 -0
- package/dist/wirings/workflow/workflow-step-retry.d.ts +14 -0
- package/dist/wirings/workflow/workflow-step-retry.js +33 -0
- package/dist/wirings/workflow/workflow-version-fallback.d.ts +11 -0
- package/dist/wirings/workflow/workflow-version-fallback.js +30 -0
- package/dist/wirings/workflow/workflow.types.d.ts +17 -0
- package/knowledge/decisions/internals/a-held-run-is-woken-later-not-retried.md +28 -0
- package/knowledge/decisions/internals/index.md +2 -1
- package/knowledge/decisions/internals/the-in-memory-workflow-service-is-inline-only-and-single-process.md +2 -2
- package/knowledge/decisions/internals/workflow-step-lock-is-held-only-to-claim-the-step.md +12 -5
- package/package.json +1 -1
- package/src/public-surface.json +18 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { wireTrigger, wireTriggerSource } from './trigger-runner.js';
|
|
2
2
|
export { wireTriggerWebhookSource, receiveWebhookSourceRequest, dispatchWebhookSourceJob, runWebhookSourceLifecycle, declaredTriggerSources, reconcileTriggerSources, enableTriggerSource, disableTriggerSource, reconcileWebhookRegistrations, teardownTriggerSources, subscribedWebhookEvents, } from './webhook-source-runner.js';
|
|
3
3
|
export type { OrphanedWebhookRegistration, WebhookRegistration, WebhookRegistrations, WebhookSourceOutcome, } from './webhook-source-runner.js';
|
|
4
|
-
export { PIKKU_INCOMING_WEBHOOK_QUEUE_NAME } from './webhook-source.types.js';
|
|
5
|
-
export type { CoreTriggerWebhookSource, TriggerEvent, WebhookCheckResult, WebhookLifecycleInput, WebhookReceiveResult, WebhookRequest, WebhookSetupResult, WebhookSourceJob, WebhookSourceMeta, WebhookSourceMethod, WebhookSourcesMeta, WebhookSourceState, WebhookTeardownInput, WebhookTeardownResult, } from './webhook-source.types.js';
|
|
4
|
+
export { PIKKU_INCOMING_WEBHOOK_QUEUE_NAME, webhookSecretCredentialName, } from './webhook-source.types.js';
|
|
5
|
+
export type { CoreTriggerWebhookSource, TriggerEvent, WebhookCheckResult, WebhookLifecycleInput, WebhookReceiveResult, WebhookRequest, WebhookSetupResult, WebhookSourceJob, WebhookSourceMeta, WebhookSourceMethod, WebhookSourcesMeta, WebhookSourceState, WebhookTeardownInput, WebhookTeardownResult, WebhookVerify, } from './webhook-source.types.js';
|
|
6
6
|
export { PikkuTriggerService } from './pikku-trigger-service.js';
|
|
7
7
|
export type { TriggerMeta, TriggerSourceMeta, CorePikkuTriggerFunction, CorePikkuTriggerFunctionConfig, CoreTrigger, } from './trigger.types.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { wireTrigger, wireTriggerSource } from './trigger-runner.js';
|
|
2
2
|
export { wireTriggerWebhookSource, receiveWebhookSourceRequest, dispatchWebhookSourceJob, runWebhookSourceLifecycle, declaredTriggerSources, reconcileTriggerSources, enableTriggerSource, disableTriggerSource, reconcileWebhookRegistrations, teardownTriggerSources, subscribedWebhookEvents, } from './webhook-source-runner.js';
|
|
3
|
-
export { PIKKU_INCOMING_WEBHOOK_QUEUE_NAME } from './webhook-source.types.js';
|
|
3
|
+
export { PIKKU_INCOMING_WEBHOOK_QUEUE_NAME, webhookSecretCredentialName, } from './webhook-source.types.js';
|
|
4
4
|
export { PikkuTriggerService } from './pikku-trigger-service.js';
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { getSingletonServices, pikkuState } from '../../pikku-state.js';
|
|
2
2
|
import { addFunction, runPikkuFunc } from '../../function/function-runner.js';
|
|
3
|
-
import { PikkuMissingMetaError } from '../../errors/errors.js';
|
|
3
|
+
import { PikkuMissingMetaError, UnauthorizedError, } from '../../errors/errors.js';
|
|
4
|
+
import { timingSafeStringEqual, verifyHmacSignature, verifyPublicKeySignature, } from '../../utils/hmac.js';
|
|
5
|
+
import { webhookSecretCredentialName } from './webhook-source.types.js';
|
|
4
6
|
const LIFECYCLE = ['receive', 'check', 'setup', 'teardown'];
|
|
5
7
|
export const wireTriggerWebhookSource = (source) => {
|
|
6
8
|
const meta = pikkuState(null, 'trigger', 'webhookSourceMeta')[source.name];
|
|
@@ -59,6 +61,44 @@ const readRequest = async (http) => {
|
|
|
59
61
|
query,
|
|
60
62
|
};
|
|
61
63
|
};
|
|
64
|
+
const signedWith = async (verify, request, secret, services) => {
|
|
65
|
+
if (typeof verify === 'function') {
|
|
66
|
+
return await verify(request, secret, services);
|
|
67
|
+
}
|
|
68
|
+
const header = (name, prefix = '') => {
|
|
69
|
+
const value = request.headers[name.toLowerCase()];
|
|
70
|
+
return value?.startsWith(prefix) ? value.slice(prefix.length) : undefined;
|
|
71
|
+
};
|
|
72
|
+
if ('hmac' in verify) {
|
|
73
|
+
const { header: name, prefix, algorithm, encoding, secretEncoding } = verify.hmac;
|
|
74
|
+
return verifyHmacSignature(secret, header(name, prefix), algorithm, request.body, encoding, secretEncoding);
|
|
75
|
+
}
|
|
76
|
+
if ('token' in verify) {
|
|
77
|
+
const token = header(verify.token.header, verify.token.prefix);
|
|
78
|
+
return !!token && timingSafeStringEqual(token, secret);
|
|
79
|
+
}
|
|
80
|
+
const { header: name, ...options } = verify.publicKey;
|
|
81
|
+
return verifyPublicKeySignature(secret, header(name), request.body, options);
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Whether the request was checked against the source's signing secret. Throws
|
|
85
|
+
* when it was checked and failed. A source without `verify` checks in its own
|
|
86
|
+
* `receive`, and a request without a body has nothing signed to check.
|
|
87
|
+
*/
|
|
88
|
+
const verifyRequest = async (source, request, services) => {
|
|
89
|
+
if (!source?.verify)
|
|
90
|
+
return true;
|
|
91
|
+
if (request.body.length === 0)
|
|
92
|
+
return false;
|
|
93
|
+
const secret = await services.credentialService?.get(source.credential ?? webhookSecretCredentialName(source.name));
|
|
94
|
+
if (typeof secret !== 'string' || !secret) {
|
|
95
|
+
throw new UnauthorizedError(`The ${source.name} webhook source has no signing secret`);
|
|
96
|
+
}
|
|
97
|
+
if (!(await signedWith(source.verify, request, secret, services))) {
|
|
98
|
+
throw new UnauthorizedError(`Invalid ${source.name} webhook signature`);
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
};
|
|
62
102
|
const validateEvents = async (source, events, logger) => {
|
|
63
103
|
const valid = [];
|
|
64
104
|
for (const event of events) {
|
|
@@ -91,6 +131,7 @@ export const receiveWebhookSourceRequest = async (sourceName, wire) => {
|
|
|
91
131
|
}
|
|
92
132
|
const source = pikkuState(null, 'trigger', 'webhookSources').get(sourceName);
|
|
93
133
|
const request = await readRequest(wire.http);
|
|
134
|
+
const verified = await verifyRequest(source, request, singletonServices);
|
|
94
135
|
const result = meta.receive
|
|
95
136
|
? await runSourceStep(singletonServices, sourceName, meta.receive, request)
|
|
96
137
|
: {
|
|
@@ -112,6 +153,9 @@ export const receiveWebhookSourceRequest = async (sourceName, wire) => {
|
|
|
112
153
|
: JSON.stringify(body);
|
|
113
154
|
return new Response(text, { status, headers });
|
|
114
155
|
}
|
|
156
|
+
if (!verified && result.events.length > 0) {
|
|
157
|
+
throw new UnauthorizedError(`The ${sourceName} webhook source received events in an unsigned request`);
|
|
158
|
+
}
|
|
115
159
|
const triggers = pikkuState(null, 'trigger', 'meta');
|
|
116
160
|
const listened = result.events.filter((event) => triggers[triggerName(sourceName, event.name)]);
|
|
117
161
|
const events = await validateEvents(source, listened, singletonServices.logger);
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
2
|
import type { CorePikkuFunctionConfig } from '../../function/functions.types.js';
|
|
3
|
+
import type { HmacAlgorithm, SecretEncoding } from '../../utils/hmac.js';
|
|
3
4
|
export declare const PIKKU_INCOMING_WEBHOOK_QUEUE_NAME = "pikku-incoming-webhooks";
|
|
5
|
+
/** `github` → `githubWebhookSecret`, `microsoft-outlook` → `microsoftOutlookWebhookSecret`. */
|
|
6
|
+
export declare const webhookSecretCredentialName: (source: string) => string;
|
|
4
7
|
/** What every trigger source produces, whichever way it arrives. */
|
|
5
8
|
export type TriggerEvent<Name extends string = string, Data = unknown> = {
|
|
6
9
|
/** The provider's event type; dispatched to the trigger named `<source>:<name>`. Empty dispatches to `<source>`. */
|
|
@@ -64,6 +67,38 @@ export type WebhookTeardownInput = {
|
|
|
64
67
|
export type WebhookTeardownResult = {
|
|
65
68
|
status: 'deleted' | 'absent';
|
|
66
69
|
};
|
|
70
|
+
/**
|
|
71
|
+
* How a source's requests are signed. The declared forms cover a signature
|
|
72
|
+
* over the raw body in one header; anything else — a timestamp in the signed
|
|
73
|
+
* payload, form fields, a URL — is a function that says whether the request is
|
|
74
|
+
* genuine, using the helpers in `@pikku/core/hmac`.
|
|
75
|
+
*/
|
|
76
|
+
export type WebhookVerify<Services = any> = {
|
|
77
|
+
hmac: {
|
|
78
|
+
header: string;
|
|
79
|
+
/** Stripped from the header before comparing, such as `sha256=`. */
|
|
80
|
+
prefix?: string;
|
|
81
|
+
algorithm: HmacAlgorithm;
|
|
82
|
+
encoding: 'hex' | 'base64';
|
|
83
|
+
/** How the stored secret is encoded. Defaults to `utf8`. */
|
|
84
|
+
secretEncoding?: SecretEncoding;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** The provider sends the shared secret itself rather than a signature. */
|
|
88
|
+
| {
|
|
89
|
+
token: {
|
|
90
|
+
header: string;
|
|
91
|
+
prefix?: string;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** The provider signs with a private key: the stored secret is its public key, as PEM. */
|
|
95
|
+
| {
|
|
96
|
+
publicKey: {
|
|
97
|
+
header: string;
|
|
98
|
+
algorithm?: string;
|
|
99
|
+
dsaEncoding?: 'der' | 'ieee-p1363';
|
|
100
|
+
};
|
|
101
|
+
} | ((request: WebhookRequest, secret: string, services: Services) => boolean | Promise<boolean>);
|
|
67
102
|
type SourceFunction<In, Out> = CorePikkuFunctionConfig<any, any> & {
|
|
68
103
|
func: (services: any, data: In, wire: any) => Promise<Out>;
|
|
69
104
|
};
|
|
@@ -76,6 +111,21 @@ export type CoreTriggerWebhookSource<Events extends Record<string, StandardSchem
|
|
|
76
111
|
route?: string;
|
|
77
112
|
/** What the source can produce. Each event's data is validated against its schema before it is queued. */
|
|
78
113
|
events?: Events;
|
|
114
|
+
/**
|
|
115
|
+
* The credential holding the signing secret. Defaults to
|
|
116
|
+
* {@link webhookSecretCredentialName}. Declaring `verify` declares this
|
|
117
|
+
* credential too, as a singleton string, so it needs no `defineCredential`.
|
|
118
|
+
*/
|
|
119
|
+
credential?: string;
|
|
120
|
+
/** What the secret is and where to find it, shown to whoever has to set it. */
|
|
121
|
+
credentialDescription?: string;
|
|
122
|
+
/**
|
|
123
|
+
* Checked before `receive` on every request with a body. A request is
|
|
124
|
+
* refused when the credential is not set or the signature does not match.
|
|
125
|
+
* A request without a body — a HEAD probe, a validation token in the query —
|
|
126
|
+
* reaches `receive` unchecked, and may be answered but dispatches nothing.
|
|
127
|
+
*/
|
|
128
|
+
verify?: WebhookVerify;
|
|
79
129
|
/** Omitted: the JSON body is one event dispatched to the trigger named `<name>`. */
|
|
80
130
|
receive?: SourceFunction<WebhookRequest, WebhookReceiveResult>;
|
|
81
131
|
check?: SourceFunction<WebhookLifecycleInput, WebhookCheckResult>;
|
|
@@ -1 +1,3 @@
|
|
|
1
1
|
export const PIKKU_INCOMING_WEBHOOK_QUEUE_NAME = 'pikku-incoming-webhooks';
|
|
2
|
+
/** `github` → `githubWebhookSecret`, `microsoft-outlook` → `microsoftOutlookWebhookSecret`. */
|
|
3
|
+
export const webhookSecretCredentialName = (source) => `${source.replace(/[-_]+([a-z0-9])/gi, (_, c) => c.toUpperCase())}WebhookSecret`;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { WorkflowAsyncException, WorkflowSuspendedException, } from '../workflow-errors.js';
|
|
2
2
|
import { DEFAULT_STEP_RETRIES } from '../workflow-constants.js';
|
|
3
|
+
import { runningStepLease } from '../workflow-step-lease.js';
|
|
3
4
|
import { pikkuState, getSingletonServices } from '../../../pikku-state.js';
|
|
4
5
|
import { RPCNotFoundError } from '../../rpc/rpc-runner.js';
|
|
5
6
|
export class ChildWorkflowStartedException extends Error {
|
|
@@ -563,11 +564,12 @@ async function createGraphResultReader(workflowService, runId, graphName, nodes,
|
|
|
563
564
|
},
|
|
564
565
|
};
|
|
565
566
|
}
|
|
567
|
+
const graphStepOptions = (nodeConfig) => ({
|
|
568
|
+
retries: nodeConfig?.retries ?? DEFAULT_STEP_RETRIES,
|
|
569
|
+
retryDelay: nodeConfig?.retryDelay,
|
|
570
|
+
});
|
|
566
571
|
async function queueGraphNode(workflowService, runId, _graphName, nodeId, rpcName, input, nodeConfig, fromStepName) {
|
|
567
|
-
const stepOptions =
|
|
568
|
-
retries: nodeConfig?.retries ?? DEFAULT_STEP_RETRIES,
|
|
569
|
-
retryDelay: nodeConfig?.retryDelay,
|
|
570
|
-
};
|
|
572
|
+
const stepOptions = graphStepOptions(nodeConfig);
|
|
571
573
|
await workflowService.insertStepState(runId, nodeId, rpcName, input, stepOptions, fromStepName);
|
|
572
574
|
await workflowService.queueStepWorker(runId, nodeId, rpcName, input, stepOptions, fromStepName);
|
|
573
575
|
}
|
|
@@ -599,6 +601,32 @@ export async function continueGraph(workflowService, runId, graphName, overrideM
|
|
|
599
601
|
const triggerInput = currentRun?.input;
|
|
600
602
|
const reader = await createGraphResultReader(workflowService, runId, graphName, nodes, triggerInput, instances);
|
|
601
603
|
const plan = planGraphTransitions(nodes, instances, branchByStep, meta.entryNodeIds ?? [], graphName, reader.fanoutWidths);
|
|
604
|
+
const resolveNodeInput = async (logical, itemIndex) => {
|
|
605
|
+
const node = nodes[logical];
|
|
606
|
+
const referencedNodeIds = extractReferencedNodeIds(node.input).filter((id) => !IGNORED_REFS.has(id));
|
|
607
|
+
const fetchedResults = await reader.read(referencedNodeIds);
|
|
608
|
+
const nodeResults = {
|
|
609
|
+
trigger: triggerInput,
|
|
610
|
+
...fetchedResults,
|
|
611
|
+
};
|
|
612
|
+
if (itemIndex !== undefined) {
|
|
613
|
+
nodeResults['$item'] = reader.fanoutItems[logical]?.[itemIndex];
|
|
614
|
+
}
|
|
615
|
+
return resolveSerializedInput(node.input, nodeResults);
|
|
616
|
+
};
|
|
617
|
+
// A node whose worker died is still `running`, so the plan counts it in
|
|
618
|
+
// flight and would wait on it forever. Its step already exists: dispatching
|
|
619
|
+
// it again is enough, and the claim makes that the next attempt.
|
|
620
|
+
for (const instance of instances) {
|
|
621
|
+
if (runningStepLease(instance) !== 'lapsed')
|
|
622
|
+
continue;
|
|
623
|
+
const logical = remapStepNamesToNodeIds([instance.stepName], nodes, graphName)[0];
|
|
624
|
+
const node = nodes[logical];
|
|
625
|
+
if (!node?.rpcName)
|
|
626
|
+
continue;
|
|
627
|
+
const itemIndex = splitFanoutInstance(instance.stepName)?.index;
|
|
628
|
+
await workflowService.queueStepWorker(runId, instance.stepName, node.rpcName, await resolveNodeInput(logical, itemIndex), graphStepOptions(node), instance.fromStepName);
|
|
629
|
+
}
|
|
602
630
|
if (plan.toFire.length === 0) {
|
|
603
631
|
if (!plan.hasInFlight && !plan.blockedWaiting) {
|
|
604
632
|
await workflowService.updateRunStatus(runId, 'completed');
|
|
@@ -609,16 +637,7 @@ export async function continueGraph(workflowService, runId, graphName, overrideM
|
|
|
609
637
|
const node = nodes[fire.logical];
|
|
610
638
|
if (!node?.rpcName)
|
|
611
639
|
continue;
|
|
612
|
-
const
|
|
613
|
-
const fetchedResults = await reader.read(referencedNodeIds);
|
|
614
|
-
const nodeResults = {
|
|
615
|
-
trigger: triggerInput,
|
|
616
|
-
...fetchedResults,
|
|
617
|
-
};
|
|
618
|
-
if (fire.itemIndex !== undefined) {
|
|
619
|
-
nodeResults['$item'] = reader.fanoutItems[fire.logical]?.[fire.itemIndex];
|
|
620
|
-
}
|
|
621
|
-
const resolvedInput = resolveSerializedInput(node.input, nodeResults);
|
|
640
|
+
const resolvedInput = await resolveNodeInput(fire.logical, fire.itemIndex);
|
|
622
641
|
await queueGraphNode(workflowService, runId, graphName, fire.instanceKey, node.rpcName, resolvedInput, node, fire.fromStepName);
|
|
623
642
|
}
|
|
624
643
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { PikkuWorkflowService } from './pikku-workflow-service.js';
|
|
2
|
-
export { WorkflowCancelledException, WorkflowSuspendedException, WorkflowDispatchException, WorkflowNotFoundError, WorkflowRunNotFoundError, WorkflowApprovalResolvedError, WorkflowStepFunctionMismatchError, } from './workflow-errors.js';
|
|
3
|
-
export { DEFAULT_STEP_RETRIES } from './workflow-constants.js';
|
|
2
|
+
export { WorkflowCancelledException, WorkflowSuspendedException, WorkflowDispatchException, WorkflowNotFoundError, WorkflowRunNotFoundError, WorkflowApprovalResolvedError, WorkflowStepFunctionMismatchError, WorkflowStepLeaseExpiredError, WorkflowStepSupersededError, } from './workflow-errors.js';
|
|
3
|
+
export { DEFAULT_STEP_RETRIES, DEFAULT_STEP_LEASE_MS, isStepLeaseLive, } from './workflow-constants.js';
|
|
4
|
+
export { leaseAttemptsExhausted } from './workflow-step-claim.js';
|
|
4
5
|
export { assertWorkflowRunOwner, WorkflowRunForbiddenError, } from './workflow-run-ownership.js';
|
|
5
6
|
export { WorkflowApprovalForbiddenError } from './workflow-approval-policy.js';
|
|
6
7
|
export { streamWorkflowRunStatus, type WorkflowStatusStreamParams, } from './workflow-status-stream.js';
|
|
@@ -14,7 +15,7 @@ export { addWorkflow } from './dsl/workflow-runner.js';
|
|
|
14
15
|
export { template, type TemplateString } from './graph/template.js';
|
|
15
16
|
export { pikkuWorkflowGraph, type PikkuWorkflowGraphConfig, type PikkuWorkflowGraphResult, } from './graph/wire-workflow-graph.js';
|
|
16
17
|
export type { WorkflowStepInput as WorkflowStepQueueInput, PikkuWorkflowOrchestratorInput, PikkuWorkflowSleeperInput, } from './workflow-queue-workers.js';
|
|
17
|
-
export type { WorkflowService, WorkflowQueueOptions, WorkflowServiceConfig, WorkflowPlannedStep, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, StepStatus, WorkflowRun, WorkflowRunStatus, StepState, WorkflowRunService, WorkflowRunMirror, CoreWorkflow, PikkuWorkflow, ContextVariable, WorkflowContext, WorkflowsMeta, WorkflowsRuntimeMeta, } from './workflow.types.js';
|
|
18
|
+
export type { WorkflowService, WorkflowQueueOptions, WorkflowServiceOptions, WorkflowServiceConfig, WorkflowPlannedStep, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, StepStatus, WorkflowRun, WorkflowRunStatus, StepState, WorkflowRunService, WorkflowRunMirror, CoreWorkflow, PikkuWorkflow, ContextVariable, WorkflowContext, WorkflowsMeta, WorkflowsRuntimeMeta, } from './workflow.types.js';
|
|
18
19
|
export type { WorkflowStepOptions, WorkflowWireDoRPC, WorkflowApprovalOptions, ApprovalOutcome, InputSource, OutputBinding, RpcStepMeta, Condition, BranchStepMeta, ParallelGroupStepMeta, FanoutStepMeta, ReturnStepMeta, InlineStepMeta, SleepStepMeta, CancelStepMeta, SuspendStepMeta, ApprovalStepMeta, SetStepMeta, SwitchCaseMeta, SwitchStepMeta, FilterStepMeta, ArrayPredicateStepMeta, WorkflowStepMeta, WorkflowStepWire, PikkuWorkflowWire, } from './workflow.types.js';
|
|
19
20
|
export { createGraph } from './graph/graph-node.js';
|
|
20
21
|
export type { GraphNodeConfig, ForEachConfig, ForEachMode, ItemFn, TemplateFn, } from './graph/workflow-graph.types.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { PikkuWorkflowService } from './pikku-workflow-service.js';
|
|
2
|
-
export { WorkflowCancelledException, WorkflowSuspendedException, WorkflowDispatchException, WorkflowNotFoundError, WorkflowRunNotFoundError, WorkflowApprovalResolvedError, WorkflowStepFunctionMismatchError, } from './workflow-errors.js';
|
|
3
|
-
export { DEFAULT_STEP_RETRIES } from './workflow-constants.js';
|
|
2
|
+
export { WorkflowCancelledException, WorkflowSuspendedException, WorkflowDispatchException, WorkflowNotFoundError, WorkflowRunNotFoundError, WorkflowApprovalResolvedError, WorkflowStepFunctionMismatchError, WorkflowStepLeaseExpiredError, WorkflowStepSupersededError, } from './workflow-errors.js';
|
|
3
|
+
export { DEFAULT_STEP_RETRIES, DEFAULT_STEP_LEASE_MS, isStepLeaseLive, } from './workflow-constants.js';
|
|
4
|
+
export { leaseAttemptsExhausted } from './workflow-step-claim.js';
|
|
4
5
|
export { assertWorkflowRunOwner, WorkflowRunForbiddenError, } from './workflow-run-ownership.js';
|
|
5
6
|
export { WorkflowApprovalForbiddenError } from './workflow-approval-policy.js';
|
|
6
7
|
export { streamWorkflowRunStatus, } from './workflow-status-stream.js';
|
|
@@ -7,6 +7,7 @@ import type { ScenarioPersonas } from '../../services/personas-service.js';
|
|
|
7
7
|
import type { PikkuRPC } from '../rpc/rpc-types.js';
|
|
8
8
|
import { type RunTimeline, type ReconstructedRunState } from './run-timeline.js';
|
|
9
9
|
import type { WorkflowRunEngine, WorkflowRunExtension } from './workflow-run-engine.types.js';
|
|
10
|
+
import type { LeaseService } from '../../services/lease-service.js';
|
|
10
11
|
export declare abstract class PikkuWorkflowService implements WorkflowService {
|
|
11
12
|
private runExtension?;
|
|
12
13
|
private runContexts;
|
|
@@ -16,14 +17,21 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
16
17
|
private exitExecution;
|
|
17
18
|
protected get logger(): import("../../services/logger.js").Logger;
|
|
18
19
|
protected mirror?: WorkflowRunMirror;
|
|
20
|
+
/**
|
|
21
|
+
* Serialises orchestration passes over a run and claims on a step. A service
|
|
22
|
+
* whose state no other process can reach — in-memory, a Durable Object —
|
|
23
|
+
* has no competitor to exclude, overrides both locks and passes none.
|
|
24
|
+
*/
|
|
25
|
+
protected readonly leaseService?: LeaseService;
|
|
19
26
|
protected readonly queueStrategy: 'per-workflow' | 'shared-groups';
|
|
20
27
|
protected readonly queueConcurrency: number;
|
|
21
28
|
protected readonly queueGroupConcurrency: number | GroupConcurrencyConfig;
|
|
22
29
|
constructor(options?: {
|
|
23
30
|
wireQueues?: boolean;
|
|
24
31
|
mirror?: WorkflowRunMirror;
|
|
32
|
+
leaseService?: LeaseService;
|
|
25
33
|
} & WorkflowQueueOptions);
|
|
26
|
-
|
|
34
|
+
protected mirrored<T>(write: () => Promise<T>, mirror: (mirror: WorkflowRunMirror, written: T) => Promise<void>): Promise<T>;
|
|
27
35
|
wireQueueWorkers(): void;
|
|
28
36
|
protected isInline(runId: string): Promise<boolean>;
|
|
29
37
|
registerInlineRun(runId: string): void;
|
|
@@ -51,18 +59,41 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
51
59
|
abstract getStepState(runId: string, stepName: string): Promise<StepState>;
|
|
52
60
|
setStepRunning(stepId: string): Promise<void>;
|
|
53
61
|
protected abstract setStepRunningImpl(stepId: string): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Push the claim on a `running` step forward, so it keeps reading as owned
|
|
64
|
+
* for another lease.
|
|
65
|
+
*
|
|
66
|
+
* Does nothing by default, which leaves a store that records no lease exactly
|
|
67
|
+
* as it was: its `running` steps are owned until they move on their own, and
|
|
68
|
+
* a dispatch that dies takes the step with it. A store that overrides this
|
|
69
|
+
* gains recovery from a lost worker, through both `claimStepForExecution` and
|
|
70
|
+
* `recoverStalledRuns`.
|
|
71
|
+
*
|
|
72
|
+
* `null` releases the lease without ending the step, for a step that is
|
|
73
|
+
* legitimately `running` with no worker on it — one parked on a child run,
|
|
74
|
+
* which the child's completion drives rather than a redispatch.
|
|
75
|
+
* `attempt` fences it to the claim that holds it.
|
|
76
|
+
*
|
|
77
|
+
* The lease runs `leaseMs` from now on the store's own clock, so workers
|
|
78
|
+
* whose clocks disagree still agree on when it lapses. Resolves `false` when
|
|
79
|
+
* the step is no longer this attempt's to renew.
|
|
80
|
+
*/
|
|
81
|
+
refreshStepLease(_stepId: string, _leaseMs: number | null, _attempt?: number): Promise<boolean>;
|
|
54
82
|
setStepScheduled(stepId: string): Promise<void>;
|
|
55
83
|
protected abstract setStepScheduledImpl(stepId: string): Promise<void>;
|
|
56
|
-
|
|
57
|
-
|
|
84
|
+
/** `attempt` fences it: a newer claim's step throws `WorkflowStepSupersededError`. */
|
|
85
|
+
setStepResult(stepId: string, result: any, attempt?: number): Promise<void>;
|
|
86
|
+
protected abstract setStepResultImpl(stepId: string, result: any, attempt?: number): Promise<void>;
|
|
58
87
|
setStepChildRunId(stepId: string, childRunId: string): Promise<void>;
|
|
59
88
|
protected abstract setStepChildRunIdImpl(stepId: string, childRunId: string): Promise<void>;
|
|
60
|
-
setStepError(stepId: string, error: Error): Promise<void>;
|
|
61
|
-
protected abstract setStepErrorImpl(stepId: string, error: Error): Promise<void>;
|
|
89
|
+
setStepError(stepId: string, error: Error, attempt?: number): Promise<void>;
|
|
90
|
+
protected abstract setStepErrorImpl(stepId: string, error: Error, attempt?: number): Promise<void>;
|
|
62
91
|
createRetryAttempt(failedStepId: string, status: 'pending' | 'running'): Promise<StepState>;
|
|
63
92
|
protected abstract createRetryAttemptImpl(failedStepId: string, status: 'pending' | 'running'): Promise<StepState>;
|
|
64
|
-
|
|
65
|
-
|
|
93
|
+
/** One orchestration pass under the run's lease; never waits (see `holdWorkflowLease`). */
|
|
94
|
+
withRunLease<T>(id: string, fn: () => Promise<T>): Promise<T>;
|
|
95
|
+
/** `fn` under one step's lease; never waits (see `holdWorkflowLease`). */
|
|
96
|
+
withStepLock<T>(runId: string, stepName: string, fn: () => Promise<T>): Promise<T>;
|
|
66
97
|
abstract close(): Promise<void>;
|
|
67
98
|
abstract getCompletedGraphState(runId: string): Promise<{
|
|
68
99
|
completedNodeIds: string[];
|
|
@@ -73,6 +104,8 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
73
104
|
stepName: string;
|
|
74
105
|
status: StepStatus;
|
|
75
106
|
fromStepName?: string;
|
|
107
|
+
/** Set by a store that records step leases; see `runningStepLease`. */
|
|
108
|
+
leaseExpiresAt?: Date;
|
|
76
109
|
}>>;
|
|
77
110
|
abstract getNodeResults(runId: string, nodeIds: string[]): Promise<Record<string, any>>;
|
|
78
111
|
setBranchTaken(stepId: string, branchKey: string): Promise<void>;
|
|
@@ -89,10 +122,15 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
89
122
|
source: string;
|
|
90
123
|
} | null>;
|
|
91
124
|
resumeWorkflow(runId: string, workflowName?: string): Promise<void>;
|
|
125
|
+
private enqueueOrchestration;
|
|
92
126
|
/**
|
|
93
127
|
* Ids of runs that are stalled: still `running`, with no step in a state that
|
|
94
|
-
* something is expected to complete (`
|
|
95
|
-
* and no step activity since `before`.
|
|
128
|
+
* something is expected to complete (`scheduled`, `suspended`, or `running`
|
|
129
|
+
* under a live lease), and no step activity since `before`.
|
|
130
|
+
*
|
|
131
|
+
* A `running` step whose lease has lapsed is the opposite of in flight — the
|
|
132
|
+
* dispatch that claimed it is gone — so it does not hold its run out of the
|
|
133
|
+
* sweep.
|
|
96
134
|
*
|
|
97
135
|
* Returns nothing by default so a store that cannot express the query keeps
|
|
98
136
|
* working unchanged; a store that overrides it gains crash recovery through
|
|
@@ -175,7 +213,6 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
175
213
|
private runWorkflowJobInner;
|
|
176
214
|
private onChildWorkflowCompleted;
|
|
177
215
|
protected onChildWorkflowFailed(childRun: WorkflowRun, error: Error): Promise<void>;
|
|
178
|
-
private runVersionMismatchFallback;
|
|
179
216
|
executeWorkflowStep(runId: string, stepName: string, rpcName: string, data: any, rpcService: PikkuRPC): Promise<void>;
|
|
180
217
|
/**
|
|
181
218
|
* Take sole ownership of a step before it runs, returning the state to run
|
|
@@ -185,18 +222,20 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
185
222
|
* believes were dropped, and a queue can redeliver a job it already handed
|
|
186
223
|
* out. This is the one place that keeps a duplicate dispatch from becoming a
|
|
187
224
|
* second execution of a side-effecting step, so it is only as strong as the
|
|
188
|
-
* exclusion it is built on
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
225
|
+
* exclusion it is built on: here, the step's lease on the app's
|
|
226
|
+
* `leaseService`. A store able to express the decision as one conditional
|
|
227
|
+
* write should override this rather than reach for a lock, which is what
|
|
228
|
+
* `@pikku/kysely` does with a status-guarded `UPDATE`.
|
|
229
|
+
*
|
|
230
|
+
* Ownership is held for `leaseMs` rather than forever. A dispatch that
|
|
231
|
+
* dies mid-step leaves the step `running` with nothing to complete it, and an
|
|
232
|
+
* unconditional rejection of `running` would then wedge it beyond the reach
|
|
233
|
+
* of both this claim and the stalled-run sweep.
|
|
192
234
|
*/
|
|
193
|
-
protected claimStepForExecution(runId: string, stepName: string, rpcName: string): Promise<StepState | null>;
|
|
235
|
+
protected claimStepForExecution(runId: string, stepName: string, rpcName: string, leaseMs: number): Promise<StepState | null>;
|
|
194
236
|
private executeWorkflowStepInner;
|
|
195
237
|
orchestrateWorkflow(runId: string, rpcService: PikkuRPC): Promise<void>;
|
|
196
|
-
private verifyQueueService;
|
|
197
238
|
private invokeStepRpc;
|
|
198
|
-
private runInlineRetryLoop;
|
|
199
|
-
private runStepCompensation;
|
|
200
239
|
private rpcStep;
|
|
201
240
|
protected inlineStep(runId: string, logicalStepName: string, fn: Function, stepOptions?: WorkflowStepOptions, data?: any, rpcName?: string | null): Promise<any>;
|
|
202
241
|
private sleepStep;
|