@voyant-travel/workflows 0.107.10
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/LICENSE +201 -0
- package/NOTICE +52 -0
- package/README.md +79 -0
- package/dist/auth/index.d.ts +125 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +352 -0
- package/dist/bindings.d.ts +119 -0
- package/dist/bindings.d.ts.map +1 -0
- package/dist/bindings.js +19 -0
- package/dist/client.d.ts +135 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +305 -0
- package/dist/conditions.d.ts +29 -0
- package/dist/conditions.d.ts.map +1 -0
- package/dist/conditions.js +5 -0
- package/dist/config.d.ts +93 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +7 -0
- package/dist/driver.d.ts +237 -0
- package/dist/driver.d.ts.map +1 -0
- package/dist/driver.js +53 -0
- package/dist/errors.d.ts +58 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +76 -0
- package/dist/events/compile.d.ts +34 -0
- package/dist/events/compile.d.ts.map +1 -0
- package/dist/events/compile.js +204 -0
- package/dist/events/index.d.ts +8 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +11 -0
- package/dist/events/input-mapper.d.ts +24 -0
- package/dist/events/input-mapper.d.ts.map +1 -0
- package/dist/events/input-mapper.js +169 -0
- package/dist/events/manifest-builder.d.ts +42 -0
- package/dist/events/manifest-builder.d.ts.map +1 -0
- package/dist/events/manifest-builder.js +313 -0
- package/dist/events/payload-hash.d.ts +46 -0
- package/dist/events/payload-hash.d.ts.map +1 -0
- package/dist/events/payload-hash.js +98 -0
- package/dist/events/predicate.d.ts +77 -0
- package/dist/events/predicate.d.ts.map +1 -0
- package/dist/events/predicate.js +347 -0
- package/dist/events/registry.d.ts +37 -0
- package/dist/events/registry.d.ts.map +1 -0
- package/dist/events/registry.js +47 -0
- package/dist/handler/index.d.ts +114 -0
- package/dist/handler/index.d.ts.map +1 -0
- package/dist/handler/index.js +267 -0
- package/dist/handler/resume.d.ts +41 -0
- package/dist/handler/resume.d.ts.map +1 -0
- package/dist/handler/resume.js +44 -0
- package/dist/http-ingest.d.ts +54 -0
- package/dist/http-ingest.d.ts.map +1 -0
- package/dist/http-ingest.js +214 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/protocol/index.d.ts +345 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +110 -0
- package/dist/rate-limit/index.d.ts +40 -0
- package/dist/rate-limit/index.d.ts.map +1 -0
- package/dist/rate-limit/index.js +139 -0
- package/dist/runtime/ctx.d.ts +111 -0
- package/dist/runtime/ctx.d.ts.map +1 -0
- package/dist/runtime/ctx.js +624 -0
- package/dist/runtime/determinism.d.ts +19 -0
- package/dist/runtime/determinism.d.ts.map +1 -0
- package/dist/runtime/determinism.js +61 -0
- package/dist/runtime/errors.d.ts +21 -0
- package/dist/runtime/errors.d.ts.map +1 -0
- package/dist/runtime/errors.js +45 -0
- package/dist/runtime/executor.d.ts +166 -0
- package/dist/runtime/executor.d.ts.map +1 -0
- package/dist/runtime/executor.js +226 -0
- package/dist/runtime/journal.d.ts +56 -0
- package/dist/runtime/journal.d.ts.map +1 -0
- package/dist/runtime/journal.js +28 -0
- package/dist/testing/index.d.ts +117 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +599 -0
- package/dist/trigger.d.ts +37 -0
- package/dist/trigger.d.ts.map +1 -0
- package/dist/trigger.js +11 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/workflow.d.ts +222 -0
- package/dist/workflow.d.ts.map +1 -0
- package/dist/workflow.js +55 -0
- package/package.json +120 -0
- package/src/auth/index.ts +398 -0
- package/src/bindings.ts +135 -0
- package/src/client.ts +498 -0
- package/src/conditions.ts +43 -0
- package/src/config.ts +114 -0
- package/src/driver.ts +277 -0
- package/src/errors.ts +109 -0
- package/src/events/compile.ts +268 -0
- package/src/events/index.ts +42 -0
- package/src/events/input-mapper.ts +201 -0
- package/src/events/manifest-builder.ts +372 -0
- package/src/events/payload-hash.ts +110 -0
- package/src/events/predicate.ts +390 -0
- package/src/events/registry.ts +86 -0
- package/src/handler/index.ts +413 -0
- package/src/handler/resume.ts +100 -0
- package/src/http-ingest.ts +299 -0
- package/src/index.ts +18 -0
- package/src/protocol/index.ts +483 -0
- package/src/rate-limit/index.ts +181 -0
- package/src/runtime/ctx.ts +876 -0
- package/src/runtime/determinism.ts +75 -0
- package/src/runtime/errors.ts +58 -0
- package/src/runtime/executor.ts +442 -0
- package/src/runtime/journal.ts +80 -0
- package/src/testing/index.ts +796 -0
- package/src/trigger.ts +63 -0
- package/src/types.ts +80 -0
- package/src/workflow.ts +328 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
// @voyant-travel/workflows/handler
|
|
2
|
+
//
|
|
3
|
+
// The tenant side of the runtime protocol (see docs/runtime-protocol.md §2).
|
|
4
|
+
// The orchestrator invokes `POST /__voyant/workflow-step`; the tenant
|
|
5
|
+
// Worker responds by running the workflow body once (one invocation of
|
|
6
|
+
// the executor) and returning the executor response as JSON.
|
|
7
|
+
//
|
|
8
|
+
// export default { fetch: createStepHandler() }
|
|
9
|
+
//
|
|
10
|
+
// is enough to make a tenant Worker protocol-conformant. Auth is
|
|
11
|
+
// optional at the SDK level: in production, wire the HMAC verifier
|
|
12
|
+
// bundled by `voyant build`; for local dev, leave it unset.
|
|
13
|
+
//
|
|
14
|
+
// The executor's native response shape is returned verbatim — the wire
|
|
15
|
+
// document calls for compensated/compensation_failed to be folded into
|
|
16
|
+
// "failed" for the first draft, but since the draft is not yet locked
|
|
17
|
+
// and the executor-shape already round-trips losslessly, we keep the
|
|
18
|
+
// full discriminated union here. The orchestrator adapter can collapse.
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
type ExecuteWorkflowStepRequest,
|
|
22
|
+
type ExecuteWorkflowStepResponse,
|
|
23
|
+
executeWorkflowStep,
|
|
24
|
+
type StepRunner,
|
|
25
|
+
} from "../runtime/executor.js"
|
|
26
|
+
|
|
27
|
+
export type { StepJournalEntry } from "../runtime/journal.js"
|
|
28
|
+
export type { ExecuteWorkflowStepRequest, ExecuteWorkflowStepResponse, StepRunner }
|
|
29
|
+
export { executeWorkflowStep }
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
PROTOCOL_VERSION,
|
|
33
|
+
type ProtocolVersion,
|
|
34
|
+
type WorkflowActivationMetadata,
|
|
35
|
+
} from "../protocol/index.js"
|
|
36
|
+
import type { RateLimiter } from "../rate-limit/index.js"
|
|
37
|
+
import type { RuntimeEnvironment } from "../runtime/ctx.js"
|
|
38
|
+
import type { JournalSlice, StepJournalEntry } from "../runtime/journal.js"
|
|
39
|
+
import type { RunTrigger } from "../types.js"
|
|
40
|
+
import { getWorkflow } from "../workflow.js"
|
|
41
|
+
|
|
42
|
+
export interface StepHandlerDeps {
|
|
43
|
+
/**
|
|
44
|
+
* Optional. Called before parsing the body. Should throw / reject
|
|
45
|
+
* if the request is not from a trusted orchestrator. In production
|
|
46
|
+
* this verifies the `X-Voyant-Dispatch-Auth` HMAC against a public
|
|
47
|
+
* key embedded by `voyant build`.
|
|
48
|
+
*/
|
|
49
|
+
verifyRequest?: (req: Request) => void | Promise<void>
|
|
50
|
+
/** Injectable clock. Defaults to Date.now. */
|
|
51
|
+
now?: () => number
|
|
52
|
+
/** Optional structured logger. */
|
|
53
|
+
logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void
|
|
54
|
+
/**
|
|
55
|
+
* Rate limiter shared across step invocations. Required when any
|
|
56
|
+
* registered workflow declares `options.rateLimit` on a step; see
|
|
57
|
+
* `createInMemoryRateLimiter` in `@voyant-travel/workflows/rate-limit` for
|
|
58
|
+
* the reference impl. One instance per Worker process is the
|
|
59
|
+
* intended cardinality — state is kept in the limiter's closure.
|
|
60
|
+
*/
|
|
61
|
+
rateLimiter?: RateLimiter
|
|
62
|
+
/**
|
|
63
|
+
* Runner for steps declared with `options.runtime === "node"`.
|
|
64
|
+
* Leave unset for handlers that only run edge steps; any node step
|
|
65
|
+
* will then fail with `NODE_RUNTIME_UNAVAILABLE`.
|
|
66
|
+
*
|
|
67
|
+
* Typical impl dispatches to a separate sandboxed context:
|
|
68
|
+
* - Local dev: an in-process passthrough (same Node process).
|
|
69
|
+
* - CF production: a Cloudflare Container binding, via
|
|
70
|
+
* `createCfContainerStepRunner` from `@voyant-travel/workflows-orchestrator-cloudflare`.
|
|
71
|
+
*
|
|
72
|
+
* This is bring-your-own because the right dispatch shape depends on
|
|
73
|
+
* the target runtime; the executor only cares that a runner exists.
|
|
74
|
+
*/
|
|
75
|
+
nodeStepRunner?: StepRunner
|
|
76
|
+
/**
|
|
77
|
+
* Read-only service resolver, surfaced to step bodies as `ctx.services`.
|
|
78
|
+
* The framework's `createApp()` wires this from its `ModuleContainer`;
|
|
79
|
+
* raw orchestrator harnesses (tests, ad-hoc scripts) typically leave
|
|
80
|
+
* it unset, in which case `ctx.services.resolve(...)` throws with a
|
|
81
|
+
* clear message. See architecture doc §11.
|
|
82
|
+
*/
|
|
83
|
+
services?: import("../driver.js").ServiceResolver
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The HTTP request body the orchestrator sends. */
|
|
87
|
+
export interface WorkflowStepRequest {
|
|
88
|
+
protocolVersion: ProtocolVersion
|
|
89
|
+
runId: string
|
|
90
|
+
workflowId: string
|
|
91
|
+
workflowVersion: string
|
|
92
|
+
invocationCount: number
|
|
93
|
+
input: unknown
|
|
94
|
+
journal: JournalSlice
|
|
95
|
+
environment: "production" | "preview" | "development"
|
|
96
|
+
deadline: number
|
|
97
|
+
tenantMeta: {
|
|
98
|
+
tenantId: string
|
|
99
|
+
projectId: string
|
|
100
|
+
organizationId: string
|
|
101
|
+
projectSlug?: string
|
|
102
|
+
organizationSlug?: string
|
|
103
|
+
}
|
|
104
|
+
runMeta: {
|
|
105
|
+
number: number
|
|
106
|
+
attempt: number
|
|
107
|
+
triggeredBy: RunTrigger
|
|
108
|
+
tags: string[]
|
|
109
|
+
startedAt: number
|
|
110
|
+
}
|
|
111
|
+
activation?: WorkflowActivationMetadata
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The JSON response body the tenant returns. */
|
|
115
|
+
export type WorkflowStepResponse = ExecuteWorkflowStepResponse
|
|
116
|
+
|
|
117
|
+
/** Error-response envelope used for HTTP 4xx/5xx. */
|
|
118
|
+
export interface StepHandlerError {
|
|
119
|
+
error: string
|
|
120
|
+
message: string
|
|
121
|
+
details?: unknown
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export {
|
|
125
|
+
type BuildResumeStepRequestInput,
|
|
126
|
+
type BuildResumeStepRequestResult,
|
|
127
|
+
buildResumeStepRequest,
|
|
128
|
+
} from "./resume.js"
|
|
129
|
+
|
|
130
|
+
/** Build an HTTP fetch-style handler. */
|
|
131
|
+
export function createStepHandler(deps: StepHandlerDeps = {}): (req: Request) => Promise<Response> {
|
|
132
|
+
return async (req) => {
|
|
133
|
+
if (req.method !== "POST") {
|
|
134
|
+
return jsonResponse(405, errorBody("method_not_allowed", "POST required"))
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
if (deps.verifyRequest) await deps.verifyRequest(req)
|
|
138
|
+
} catch (err) {
|
|
139
|
+
deps.logger?.("warn", "step handler: auth rejected", {
|
|
140
|
+
error: err instanceof Error ? err.message : String(err),
|
|
141
|
+
})
|
|
142
|
+
return jsonResponse(401, errorBody("unauthorized", errMessage(err)))
|
|
143
|
+
}
|
|
144
|
+
let raw: unknown
|
|
145
|
+
try {
|
|
146
|
+
raw = await req.json()
|
|
147
|
+
} catch (err) {
|
|
148
|
+
return jsonResponse(400, errorBody("invalid_json", errMessage(err)))
|
|
149
|
+
}
|
|
150
|
+
// The incoming Request carries its own AbortSignal; threading it
|
|
151
|
+
// through lets `ctx.signal` observe client-side aborts (orchestrator
|
|
152
|
+
// cancellations, closed fetches, etc.) during step execution.
|
|
153
|
+
const out = await runStepInner(raw, deps, { signal: req.signal })
|
|
154
|
+
return jsonResponse(out.status, out.body)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Per-invocation options available to callers of the transport-free entry point. */
|
|
159
|
+
export interface StepRequestOptions {
|
|
160
|
+
/** AbortSignal forwarded to `ctx.signal` inside the step body. */
|
|
161
|
+
signal?: AbortSignal
|
|
162
|
+
/**
|
|
163
|
+
* Fires synchronously from `ctx.stream.*` as each chunk is produced.
|
|
164
|
+
* Used by orchestrators that want to broadcast chunks live
|
|
165
|
+
* (dashboards, queues) before the invocation returns.
|
|
166
|
+
*/
|
|
167
|
+
onStreamChunk?: (chunk: import("../runtime/executor.js").StreamChunk) => void
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Transport-free entry point. Callers that already parsed the body
|
|
172
|
+
* (e.g. local orchestrator in-memory, tests) invoke this directly.
|
|
173
|
+
* Returns either the step response or an error envelope with the HTTP
|
|
174
|
+
* status the caller should use.
|
|
175
|
+
*/
|
|
176
|
+
export async function handleStepRequest(
|
|
177
|
+
raw: unknown,
|
|
178
|
+
deps: StepHandlerDeps = {},
|
|
179
|
+
opts: StepRequestOptions = {},
|
|
180
|
+
): Promise<
|
|
181
|
+
{ status: number; body: WorkflowStepResponse } | { status: number; body: StepHandlerError }
|
|
182
|
+
> {
|
|
183
|
+
return runStepInner(raw, deps, opts)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function runStepInner(
|
|
187
|
+
raw: unknown,
|
|
188
|
+
deps: StepHandlerDeps,
|
|
189
|
+
opts: StepRequestOptions = {},
|
|
190
|
+
): Promise<
|
|
191
|
+
{ status: number; body: WorkflowStepResponse } | { status: number; body: StepHandlerError }
|
|
192
|
+
> {
|
|
193
|
+
const parsed = parseRequest(raw)
|
|
194
|
+
if (!parsed.ok) return { status: 400, body: errorBody("invalid_request", parsed.message) }
|
|
195
|
+
|
|
196
|
+
const reqBody = parsed.value
|
|
197
|
+
if (reqBody.protocolVersion !== PROTOCOL_VERSION) {
|
|
198
|
+
return {
|
|
199
|
+
status: 426,
|
|
200
|
+
body: errorBody(
|
|
201
|
+
"protocol_version_mismatch",
|
|
202
|
+
`tenant supports protocol ${PROTOCOL_VERSION}, got ${String(reqBody.protocolVersion)}`,
|
|
203
|
+
),
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const def = getWorkflow(reqBody.workflowId)
|
|
208
|
+
if (!def) {
|
|
209
|
+
return {
|
|
210
|
+
status: 404,
|
|
211
|
+
body: errorBody(
|
|
212
|
+
"workflow_not_found",
|
|
213
|
+
`workflow "${reqBody.workflowId}" is not registered in this bundle`,
|
|
214
|
+
),
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const now = deps.now ?? (() => Date.now())
|
|
219
|
+
const stepRunner = createInProcessStepRunner(now)
|
|
220
|
+
|
|
221
|
+
const runtimeEnv: RuntimeEnvironment = {
|
|
222
|
+
run: {
|
|
223
|
+
id: reqBody.runId,
|
|
224
|
+
number: reqBody.runMeta.number,
|
|
225
|
+
attempt: reqBody.runMeta.attempt,
|
|
226
|
+
triggeredBy: reqBody.runMeta.triggeredBy,
|
|
227
|
+
tags: reqBody.runMeta.tags,
|
|
228
|
+
startedAt: reqBody.runMeta.startedAt,
|
|
229
|
+
},
|
|
230
|
+
workflow: { id: reqBody.workflowId, version: reqBody.workflowVersion },
|
|
231
|
+
environment: { name: reqBody.environment },
|
|
232
|
+
project: {
|
|
233
|
+
id: reqBody.tenantMeta.projectId,
|
|
234
|
+
slug: reqBody.tenantMeta.projectSlug ?? reqBody.tenantMeta.projectId,
|
|
235
|
+
},
|
|
236
|
+
organization: {
|
|
237
|
+
id: reqBody.tenantMeta.organizationId,
|
|
238
|
+
slug: reqBody.tenantMeta.organizationSlug ?? reqBody.tenantMeta.organizationId,
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const response = await executeWorkflowStep(def, {
|
|
244
|
+
runId: reqBody.runId,
|
|
245
|
+
workflowId: reqBody.workflowId,
|
|
246
|
+
workflowVersion: reqBody.workflowVersion,
|
|
247
|
+
input: reqBody.input,
|
|
248
|
+
journal: reqBody.journal,
|
|
249
|
+
invocationCount: reqBody.invocationCount,
|
|
250
|
+
environment: runtimeEnv,
|
|
251
|
+
triggeredBy: reqBody.runMeta.triggeredBy,
|
|
252
|
+
runStartedAt: reqBody.runMeta.startedAt,
|
|
253
|
+
tags: reqBody.runMeta.tags,
|
|
254
|
+
stepRunner,
|
|
255
|
+
nodeStepRunner: deps.nodeStepRunner,
|
|
256
|
+
rateLimiter: deps.rateLimiter,
|
|
257
|
+
services: deps.services,
|
|
258
|
+
now,
|
|
259
|
+
abortSignal: opts.signal,
|
|
260
|
+
onStreamChunk: opts.onStreamChunk,
|
|
261
|
+
})
|
|
262
|
+
return { status: 200, body: response }
|
|
263
|
+
} catch (err) {
|
|
264
|
+
deps.logger?.("error", "step handler: executor threw", {
|
|
265
|
+
error: err instanceof Error ? err.message : String(err),
|
|
266
|
+
})
|
|
267
|
+
return {
|
|
268
|
+
status: 500,
|
|
269
|
+
body: errorBody("executor_error", errMessage(err)),
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Build a step runner that executes the step body in the same
|
|
276
|
+
* process. Suitable for `runtime: "edge"`. Container-runtime steps
|
|
277
|
+
* will swap this for a dispatching runner that POSTs to a pod.
|
|
278
|
+
*/
|
|
279
|
+
function createInProcessStepRunner(now: () => number): StepRunner {
|
|
280
|
+
return async ({ stepId: _stepId, attempt, fn, stepCtx }): Promise<StepJournalEntry> => {
|
|
281
|
+
const startedAt = now()
|
|
282
|
+
try {
|
|
283
|
+
const output = await fn(stepCtx)
|
|
284
|
+
return {
|
|
285
|
+
attempt,
|
|
286
|
+
status: "ok",
|
|
287
|
+
output,
|
|
288
|
+
startedAt,
|
|
289
|
+
finishedAt: now(),
|
|
290
|
+
}
|
|
291
|
+
} catch (err) {
|
|
292
|
+
const e = err as Error
|
|
293
|
+
const code =
|
|
294
|
+
typeof (err as { code?: unknown }).code === "string"
|
|
295
|
+
? (err as { code: string }).code
|
|
296
|
+
: "UNKNOWN"
|
|
297
|
+
const retryAfter = (err as { retryAfter?: unknown }).retryAfter
|
|
298
|
+
return {
|
|
299
|
+
attempt,
|
|
300
|
+
status: "err",
|
|
301
|
+
error: {
|
|
302
|
+
category: "USER_ERROR",
|
|
303
|
+
code,
|
|
304
|
+
message: e?.message ?? String(err),
|
|
305
|
+
name: e?.name,
|
|
306
|
+
stack: e?.stack,
|
|
307
|
+
data: retryAfter !== undefined ? { retryAfter } : undefined,
|
|
308
|
+
},
|
|
309
|
+
startedAt,
|
|
310
|
+
finishedAt: now(),
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ---- Parsing ----
|
|
317
|
+
|
|
318
|
+
function parseRequest(
|
|
319
|
+
raw: unknown,
|
|
320
|
+
): { ok: true; value: WorkflowStepRequest } | { ok: false; message: string } {
|
|
321
|
+
if (raw === null || typeof raw !== "object") {
|
|
322
|
+
return { ok: false, message: "body must be a JSON object" }
|
|
323
|
+
}
|
|
324
|
+
const r = raw as Record<string, unknown>
|
|
325
|
+
const required: (keyof WorkflowStepRequest)[] = [
|
|
326
|
+
"protocolVersion",
|
|
327
|
+
"runId",
|
|
328
|
+
"workflowId",
|
|
329
|
+
"workflowVersion",
|
|
330
|
+
"invocationCount",
|
|
331
|
+
"journal",
|
|
332
|
+
"environment",
|
|
333
|
+
"deadline",
|
|
334
|
+
"tenantMeta",
|
|
335
|
+
"runMeta",
|
|
336
|
+
]
|
|
337
|
+
for (const k of required) {
|
|
338
|
+
if (!(k in r)) return { ok: false, message: `missing required field "${k}"` }
|
|
339
|
+
}
|
|
340
|
+
if (typeof r.protocolVersion !== "number") {
|
|
341
|
+
return { ok: false, message: "`protocolVersion` must be a number" }
|
|
342
|
+
}
|
|
343
|
+
if (typeof r.runId !== "string" || r.runId.length === 0) {
|
|
344
|
+
return { ok: false, message: "`runId` must be a non-empty string" }
|
|
345
|
+
}
|
|
346
|
+
if (typeof r.workflowId !== "string" || r.workflowId.length === 0) {
|
|
347
|
+
return { ok: false, message: "`workflowId` must be a non-empty string" }
|
|
348
|
+
}
|
|
349
|
+
if (typeof r.workflowVersion !== "string" || r.workflowVersion.length === 0) {
|
|
350
|
+
return { ok: false, message: "`workflowVersion` must be a non-empty string" }
|
|
351
|
+
}
|
|
352
|
+
if (typeof r.invocationCount !== "number" || r.invocationCount < 1) {
|
|
353
|
+
return { ok: false, message: "`invocationCount` must be >= 1" }
|
|
354
|
+
}
|
|
355
|
+
if (typeof r.deadline !== "number") {
|
|
356
|
+
return { ok: false, message: "`deadline` must be a number" }
|
|
357
|
+
}
|
|
358
|
+
if (!r.journal || typeof r.journal !== "object") {
|
|
359
|
+
return { ok: false, message: "`journal` must be an object" }
|
|
360
|
+
}
|
|
361
|
+
const env = r.environment
|
|
362
|
+
if (env !== "production" && env !== "preview" && env !== "development") {
|
|
363
|
+
return { ok: false, message: "`environment` must be production | preview | development" }
|
|
364
|
+
}
|
|
365
|
+
if (!r.tenantMeta || typeof r.tenantMeta !== "object") {
|
|
366
|
+
return { ok: false, message: "`tenantMeta` must be an object" }
|
|
367
|
+
}
|
|
368
|
+
if (!r.runMeta || typeof r.runMeta !== "object") {
|
|
369
|
+
return { ok: false, message: "`runMeta` must be an object" }
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
ok: true,
|
|
373
|
+
value: {
|
|
374
|
+
protocolVersion: r.protocolVersion as ProtocolVersion,
|
|
375
|
+
runId: r.runId,
|
|
376
|
+
workflowId: r.workflowId,
|
|
377
|
+
workflowVersion: r.workflowVersion,
|
|
378
|
+
invocationCount: r.invocationCount,
|
|
379
|
+
input: r.input,
|
|
380
|
+
journal: r.journal as JournalSlice,
|
|
381
|
+
environment: env,
|
|
382
|
+
deadline: r.deadline,
|
|
383
|
+
tenantMeta: r.tenantMeta as WorkflowStepRequest["tenantMeta"],
|
|
384
|
+
runMeta: r.runMeta as WorkflowStepRequest["runMeta"],
|
|
385
|
+
activation: isObjectRecord(r.activation)
|
|
386
|
+
? (r.activation as WorkflowActivationMetadata)
|
|
387
|
+
: undefined,
|
|
388
|
+
},
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ---- Helpers ----
|
|
393
|
+
|
|
394
|
+
function jsonResponse(status: number, body: unknown): Response {
|
|
395
|
+
return new Response(JSON.stringify(body), {
|
|
396
|
+
status,
|
|
397
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function errorBody(error: string, message: string, details?: unknown): StepHandlerError {
|
|
402
|
+
const out: StepHandlerError = { error, message }
|
|
403
|
+
if (details !== undefined) out.details = details
|
|
404
|
+
return out
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function errMessage(err: unknown): string {
|
|
408
|
+
return err instanceof Error ? err.message : String(err)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
412
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
413
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyWorkflowResumeToJournal,
|
|
3
|
+
PROTOCOL_VERSION,
|
|
4
|
+
type ProtocolVersion,
|
|
5
|
+
type WorkflowActivationFreshness,
|
|
6
|
+
type WorkflowBundleReference,
|
|
7
|
+
type WorkflowJournalReference,
|
|
8
|
+
type WorkflowPayloadReference,
|
|
9
|
+
type WorkflowWaitpointResumeTarget,
|
|
10
|
+
type WorkflowWaitpointSnapshot,
|
|
11
|
+
} from "../protocol/index.js"
|
|
12
|
+
import type { JournalSlice } from "../runtime/journal.js"
|
|
13
|
+
import type { WorkflowStepRequest } from "./index.js"
|
|
14
|
+
|
|
15
|
+
export interface BuildResumeStepRequestInput {
|
|
16
|
+
runId: string
|
|
17
|
+
workflowId: string
|
|
18
|
+
workflowVersion: string
|
|
19
|
+
input: unknown
|
|
20
|
+
journal: JournalSlice
|
|
21
|
+
pendingWaitpoints: readonly WorkflowWaitpointResumeTarget[]
|
|
22
|
+
waitpointId?: string
|
|
23
|
+
waitpointKey?: string
|
|
24
|
+
parkedAt?: number
|
|
25
|
+
resumePayload?: unknown
|
|
26
|
+
resumePayloadRef?: WorkflowPayloadReference
|
|
27
|
+
resolvedAt?: number
|
|
28
|
+
matchedEventId?: string
|
|
29
|
+
source?: "live" | "inbox" | "replay"
|
|
30
|
+
protocolVersion?: ProtocolVersion
|
|
31
|
+
invocationCount: number
|
|
32
|
+
environment: "production" | "preview" | "development"
|
|
33
|
+
deadline: number
|
|
34
|
+
tenantMeta: WorkflowStepRequest["tenantMeta"]
|
|
35
|
+
runMeta: WorkflowStepRequest["runMeta"]
|
|
36
|
+
workflowReleaseId?: string
|
|
37
|
+
releaseId?: string
|
|
38
|
+
bundle?: WorkflowBundleReference
|
|
39
|
+
journalRef?: WorkflowJournalReference
|
|
40
|
+
freshness?: WorkflowActivationFreshness
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type BuildResumeStepRequestResult =
|
|
44
|
+
| {
|
|
45
|
+
ok: true
|
|
46
|
+
request: WorkflowStepRequest
|
|
47
|
+
waitpoint: WorkflowWaitpointSnapshot
|
|
48
|
+
}
|
|
49
|
+
| {
|
|
50
|
+
ok: false
|
|
51
|
+
code: "missing_waitpoint_selector" | "waitpoint_not_found"
|
|
52
|
+
message: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function buildResumeStepRequest(
|
|
56
|
+
input: BuildResumeStepRequestInput,
|
|
57
|
+
): BuildResumeStepRequestResult {
|
|
58
|
+
const applied = applyWorkflowResumeToJournal({
|
|
59
|
+
journal: input.journal,
|
|
60
|
+
waitpoints: input.pendingWaitpoints,
|
|
61
|
+
waitpointId: input.waitpointId,
|
|
62
|
+
waitpointKey: input.waitpointKey,
|
|
63
|
+
parkedAt: input.parkedAt,
|
|
64
|
+
payload: input.resumePayload,
|
|
65
|
+
payloadRef: input.resumePayloadRef,
|
|
66
|
+
resolvedAt: input.resolvedAt,
|
|
67
|
+
matchedEventId: input.matchedEventId,
|
|
68
|
+
source: input.source,
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
if (!applied.ok) return applied
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
ok: true,
|
|
75
|
+
waitpoint: applied.waitpoint,
|
|
76
|
+
request: {
|
|
77
|
+
protocolVersion: input.protocolVersion ?? PROTOCOL_VERSION,
|
|
78
|
+
runId: input.runId,
|
|
79
|
+
workflowId: input.workflowId,
|
|
80
|
+
workflowVersion: input.workflowVersion,
|
|
81
|
+
invocationCount: input.invocationCount,
|
|
82
|
+
input: input.input,
|
|
83
|
+
journal: applied.journal,
|
|
84
|
+
environment: input.environment,
|
|
85
|
+
deadline: input.deadline,
|
|
86
|
+
tenantMeta: input.tenantMeta,
|
|
87
|
+
runMeta: input.runMeta,
|
|
88
|
+
activation: {
|
|
89
|
+
kind: "resume",
|
|
90
|
+
workflowReleaseId: input.workflowReleaseId,
|
|
91
|
+
releaseId: input.releaseId,
|
|
92
|
+
bundle: input.bundle,
|
|
93
|
+
journalRef: input.journalRef,
|
|
94
|
+
waitpoint: applied.waitpoint,
|
|
95
|
+
resumePayloadRef: input.resumePayloadRef,
|
|
96
|
+
freshness: input.freshness,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
}
|