@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
package/src/client.ts
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
// Client-safe workflow API for app/server code that triggers or forwards
|
|
2
|
+
// workflow activity to a managed Voyant Cloud runtime.
|
|
3
|
+
|
|
4
|
+
import type { IngestEventArgs, IngestEventResponse, WorkflowDriver } from "./driver.js"
|
|
5
|
+
import type { WorkflowManifest } from "./protocol/index.js"
|
|
6
|
+
import type { Duration, EnvironmentName, RunStatus } from "./types.js"
|
|
7
|
+
import type { WorkflowHandle } from "./workflow.js"
|
|
8
|
+
|
|
9
|
+
export interface WorkflowsClient {
|
|
10
|
+
trigger<TIn, TOut>(
|
|
11
|
+
workflow: WorkflowHandle<TIn, TOut> | string,
|
|
12
|
+
input: TIn,
|
|
13
|
+
opts?: TriggerOptions,
|
|
14
|
+
): Promise<Run<TOut>>
|
|
15
|
+
|
|
16
|
+
signal(runId: string, name: string, payload: unknown, opts?: { nonce?: string }): Promise<void>
|
|
17
|
+
completeToken(tokenId: string, payload: unknown): Promise<void>
|
|
18
|
+
|
|
19
|
+
cancel(runId: string, opts?: { compensate?: boolean; reason?: string }): Promise<void>
|
|
20
|
+
retry(runId: string, opts: { mode: "re-trigger" | "resume" }): Promise<Run>
|
|
21
|
+
replay(runId: string, opts?: { fromStepId?: string; input?: unknown }): Promise<Run>
|
|
22
|
+
|
|
23
|
+
get(runId: string): Promise<RunDetail>
|
|
24
|
+
list(opts?: ListRunsOptions): Promise<{ runs: RunSummary[]; nextCursor?: string }>
|
|
25
|
+
|
|
26
|
+
mintAccessToken(opts: MintAccessTokenOptions): Promise<PublicAccessToken>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TriggerOptions {
|
|
30
|
+
idempotencyKey?: string
|
|
31
|
+
delay?: Duration | Date
|
|
32
|
+
debounce?: { key: string; delay: Duration; mode?: "leading" | "trailing" }
|
|
33
|
+
ttl?: Duration
|
|
34
|
+
tags?: string[]
|
|
35
|
+
priority?: number
|
|
36
|
+
concurrencyKey?: string
|
|
37
|
+
lockToVersion?: string
|
|
38
|
+
environment?: EnvironmentName
|
|
39
|
+
issuePublicAccessToken?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface Run<TOut = unknown> {
|
|
43
|
+
id: string
|
|
44
|
+
workflowId: string
|
|
45
|
+
status: RunStatus
|
|
46
|
+
startedAt: number
|
|
47
|
+
accessToken?: string
|
|
48
|
+
/** Phantom; used only for TypeScript inference. */
|
|
49
|
+
readonly __output?: TOut
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface RunSummary {
|
|
53
|
+
id: string
|
|
54
|
+
workflowId: string
|
|
55
|
+
status: RunStatus
|
|
56
|
+
startedAt: number
|
|
57
|
+
completedAt?: number
|
|
58
|
+
tags: string[]
|
|
59
|
+
environment: EnvironmentName
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface RunDetail<TOut = unknown> extends RunSummary {
|
|
63
|
+
version: string
|
|
64
|
+
input: unknown
|
|
65
|
+
output?: TOut
|
|
66
|
+
error?: unknown
|
|
67
|
+
durationMs?: number
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ListRunsOptions {
|
|
71
|
+
workflowId?: string
|
|
72
|
+
status?: RunStatus | RunStatus[]
|
|
73
|
+
environment?: EnvironmentName
|
|
74
|
+
tag?: string
|
|
75
|
+
since?: Date | number
|
|
76
|
+
until?: Date | number
|
|
77
|
+
cursor?: string
|
|
78
|
+
limit?: number
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface MintAccessTokenOptions {
|
|
82
|
+
target:
|
|
83
|
+
| { kind: "run"; runId: string }
|
|
84
|
+
| { kind: "workflow"; workflowId: string }
|
|
85
|
+
| { kind: "tag"; tag: string }
|
|
86
|
+
scope: ("read" | "trigger" | "cancel")[]
|
|
87
|
+
ttl?: Duration
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface PublicAccessToken {
|
|
91
|
+
token: string
|
|
92
|
+
exp: number
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface CloudWorkflowsClientEnv {
|
|
96
|
+
VOYANT_CLOUD_WORKFLOWS_URL?: string
|
|
97
|
+
VOYANT_CLOUD_WORKFLOW_TRIGGER_TOKEN?: string
|
|
98
|
+
VOYANT_CLOUD_APP_SLUG?: string
|
|
99
|
+
VOYANT_CLOUD_ENVIRONMENT?: string
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface CloudWorkflowsClientOptions {
|
|
103
|
+
baseUrl?: string
|
|
104
|
+
triggerToken?: string
|
|
105
|
+
appSlug?: string
|
|
106
|
+
environment?: EnvironmentName
|
|
107
|
+
fetch?: typeof fetch
|
|
108
|
+
env?: CloudWorkflowsClientEnv
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface CloudWorkflowDriverOptions extends CloudWorkflowsClientOptions {
|
|
112
|
+
/**
|
|
113
|
+
* Managed Cloud deployments should leave this disabled. Workflow releases
|
|
114
|
+
* are created by the deployment/control-plane path, not by the app runtime.
|
|
115
|
+
* The enabled mode exists only for explicitly wired adapters that own their
|
|
116
|
+
* release-registration boundary.
|
|
117
|
+
*/
|
|
118
|
+
manifestRegistration?: "disabled" | "enabled"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const DEFAULT_ENVIRONMENT: EnvironmentName = "production"
|
|
122
|
+
|
|
123
|
+
let configuredClient: WorkflowsClient | undefined
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Install the process-local `workflows` client implementation used by the
|
|
127
|
+
* root `@voyant-travel/workflows` singleton. Apps may call this during boot, or
|
|
128
|
+
* rely on deployment-injected Voyant Cloud environment variables.
|
|
129
|
+
*/
|
|
130
|
+
export function configureWorkflowsClient(client: WorkflowsClient): void {
|
|
131
|
+
configuredClient = client
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function getConfiguredWorkflowsClient(): WorkflowsClient | undefined {
|
|
135
|
+
return configuredClient
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const workflows: WorkflowsClient = new Proxy({} as WorkflowsClient, {
|
|
139
|
+
get(_, method: keyof WorkflowsClient & string) {
|
|
140
|
+
return (...args: unknown[]) => {
|
|
141
|
+
const client = configuredClient ?? tryCreateCloudClientFromEnv()
|
|
142
|
+
if (!client) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`@voyant-travel/workflows: workflows.${method}() requires a configured workflows client. ` +
|
|
145
|
+
`Use configureWorkflowsClient(createCloudWorkflowsClient(...)) or provide the ` +
|
|
146
|
+
`VOYANT_CLOUD_WORKFLOWS_URL, VOYANT_CLOUD_WORKFLOW_TRIGGER_TOKEN, ` +
|
|
147
|
+
`VOYANT_CLOUD_APP_SLUG, and VOYANT_CLOUD_ENVIRONMENT deployment variables.`,
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
const fn = client[method]
|
|
151
|
+
if (typeof fn !== "function") {
|
|
152
|
+
throw new Error(`@voyant-travel/workflows: workflows.${method} is not implemented`)
|
|
153
|
+
}
|
|
154
|
+
return (fn as (...inner: unknown[]) => unknown)(...args)
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
export function createCloudWorkflowsClient(
|
|
160
|
+
options: CloudWorkflowsClientOptions = {},
|
|
161
|
+
): WorkflowsClient {
|
|
162
|
+
const config = resolveCloudConfig(options)
|
|
163
|
+
const httpFetch = options.fetch ?? globalThis.fetch
|
|
164
|
+
if (typeof httpFetch !== "function") {
|
|
165
|
+
throw new Error("@voyant-travel/workflows/client: global fetch is unavailable")
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
async trigger<TIn, TOut>(
|
|
170
|
+
workflow: WorkflowHandle<TIn, TOut> | string,
|
|
171
|
+
input: TIn,
|
|
172
|
+
opts?: TriggerOptions,
|
|
173
|
+
): Promise<Run<TOut>> {
|
|
174
|
+
const workflowId = workflowIdOf(workflow)
|
|
175
|
+
const environment = opts?.environment ?? config.environment
|
|
176
|
+
const res = await cloudFetch(httpFetch, config, {
|
|
177
|
+
method: "POST",
|
|
178
|
+
path: `/apps/${encodeURIComponent(config.appSlug)}/${environment}/workflows/${encodeURIComponent(
|
|
179
|
+
workflowId,
|
|
180
|
+
)}/runs`,
|
|
181
|
+
body: {
|
|
182
|
+
input,
|
|
183
|
+
options: serializeTriggerOptions(opts),
|
|
184
|
+
},
|
|
185
|
+
idempotencyKey: opts?.idempotencyKey,
|
|
186
|
+
})
|
|
187
|
+
return normalizeRun<TOut>(await readJson(res), workflowId)
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
signal() {
|
|
191
|
+
return unsupported("signal")
|
|
192
|
+
},
|
|
193
|
+
completeToken() {
|
|
194
|
+
return unsupported("completeToken")
|
|
195
|
+
},
|
|
196
|
+
cancel() {
|
|
197
|
+
return unsupported("cancel")
|
|
198
|
+
},
|
|
199
|
+
retry() {
|
|
200
|
+
return unsupported("retry")
|
|
201
|
+
},
|
|
202
|
+
replay() {
|
|
203
|
+
return unsupported("replay")
|
|
204
|
+
},
|
|
205
|
+
get() {
|
|
206
|
+
return unsupported("get")
|
|
207
|
+
},
|
|
208
|
+
list() {
|
|
209
|
+
return unsupported("list")
|
|
210
|
+
},
|
|
211
|
+
mintAccessToken() {
|
|
212
|
+
return unsupported("mintAccessToken")
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Cloud-mode driver for framework event forwarding. It does not execute runs
|
|
219
|
+
* locally; it forwards triggers/events/manifests to the hosted runtime using
|
|
220
|
+
* the same deployment-scoped credentials as the client.
|
|
221
|
+
*/
|
|
222
|
+
export function createCloudWorkflowDriver(
|
|
223
|
+
options: CloudWorkflowDriverOptions = {},
|
|
224
|
+
): WorkflowDriver {
|
|
225
|
+
const client = createCloudWorkflowsClient(options)
|
|
226
|
+
const config = resolveCloudConfig(options)
|
|
227
|
+
const httpFetch = options.fetch ?? globalThis.fetch
|
|
228
|
+
const manifestRegistration = options.manifestRegistration ?? "disabled"
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
async registerManifest(args): Promise<{ versionId: string }> {
|
|
232
|
+
if (manifestRegistration === "disabled") {
|
|
233
|
+
return { versionId: args.manifest.versionId }
|
|
234
|
+
}
|
|
235
|
+
const res = await cloudFetch(httpFetch, config, {
|
|
236
|
+
method: "POST",
|
|
237
|
+
path: `/apps/${encodeURIComponent(config.appSlug)}/${args.environment}/workflow-releases`,
|
|
238
|
+
body: { manifest: args.manifest },
|
|
239
|
+
})
|
|
240
|
+
const body = await readJson(res)
|
|
241
|
+
return {
|
|
242
|
+
versionId:
|
|
243
|
+
readString(body, ["versionId"]) ??
|
|
244
|
+
readString(body, ["data", "versionId"]) ??
|
|
245
|
+
args.manifest.versionId,
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
trigger(workflow, input, opts) {
|
|
249
|
+
return client.trigger(workflow, input, opts)
|
|
250
|
+
},
|
|
251
|
+
async ingestEvent(args: IngestEventArgs): Promise<IngestEventResponse> {
|
|
252
|
+
const eventId =
|
|
253
|
+
typeof args.envelope.metadata?.eventId === "string"
|
|
254
|
+
? args.envelope.metadata.eventId
|
|
255
|
+
: args.idempotencyKey
|
|
256
|
+
const res = await cloudFetch(httpFetch, config, {
|
|
257
|
+
method: "POST",
|
|
258
|
+
path: `/apps/${encodeURIComponent(config.appSlug)}/${args.environment}/events`,
|
|
259
|
+
body: {
|
|
260
|
+
envelope: args.envelope,
|
|
261
|
+
idempotencyKey: args.idempotencyKey,
|
|
262
|
+
},
|
|
263
|
+
idempotencyKey: eventId,
|
|
264
|
+
})
|
|
265
|
+
return (await readJson(res)) as IngestEventResponse
|
|
266
|
+
},
|
|
267
|
+
async getManifest(args): Promise<WorkflowManifest | null> {
|
|
268
|
+
const res = await cloudFetch(httpFetch, config, {
|
|
269
|
+
method: "GET",
|
|
270
|
+
path: `/apps/${encodeURIComponent(config.appSlug)}/${args.environment}/workflow-releases/current`,
|
|
271
|
+
allowStatuses: [404],
|
|
272
|
+
})
|
|
273
|
+
if (res.status === 404) return null
|
|
274
|
+
const body = await readJson(res)
|
|
275
|
+
return (readRecord(body, ["manifest"]) ??
|
|
276
|
+
readRecord(body, ["data", "manifest"]) ??
|
|
277
|
+
body) as WorkflowManifest
|
|
278
|
+
},
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
interface ResolvedCloudConfig {
|
|
283
|
+
baseUrl: string
|
|
284
|
+
triggerToken: string
|
|
285
|
+
appSlug: string
|
|
286
|
+
environment: EnvironmentName
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
interface CloudFetchArgs {
|
|
290
|
+
method: "GET" | "POST"
|
|
291
|
+
path: string
|
|
292
|
+
body?: unknown
|
|
293
|
+
idempotencyKey?: string
|
|
294
|
+
allowStatuses?: number[]
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function tryCreateCloudClientFromEnv(): WorkflowsClient | undefined {
|
|
298
|
+
const env = defaultEnv()
|
|
299
|
+
if (
|
|
300
|
+
!env.VOYANT_CLOUD_WORKFLOWS_URL ||
|
|
301
|
+
!env.VOYANT_CLOUD_WORKFLOW_TRIGGER_TOKEN ||
|
|
302
|
+
!env.VOYANT_CLOUD_APP_SLUG
|
|
303
|
+
) {
|
|
304
|
+
return undefined
|
|
305
|
+
}
|
|
306
|
+
const client = createCloudWorkflowsClient({ env })
|
|
307
|
+
configuredClient = client
|
|
308
|
+
return client
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function resolveCloudConfig(options: CloudWorkflowsClientOptions): ResolvedCloudConfig {
|
|
312
|
+
const env = { ...defaultEnv(), ...options.env }
|
|
313
|
+
const baseUrl = options.baseUrl ?? env.VOYANT_CLOUD_WORKFLOWS_URL
|
|
314
|
+
const triggerToken = options.triggerToken ?? env.VOYANT_CLOUD_WORKFLOW_TRIGGER_TOKEN
|
|
315
|
+
const appSlug = options.appSlug ?? env.VOYANT_CLOUD_APP_SLUG
|
|
316
|
+
const environment = parseEnvironment(
|
|
317
|
+
options.environment ?? env.VOYANT_CLOUD_ENVIRONMENT ?? DEFAULT_ENVIRONMENT,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
const missing = [
|
|
321
|
+
["baseUrl", baseUrl],
|
|
322
|
+
["triggerToken", triggerToken],
|
|
323
|
+
["appSlug", appSlug],
|
|
324
|
+
]
|
|
325
|
+
.filter(([, value]) => !value)
|
|
326
|
+
.map(([name]) => name)
|
|
327
|
+
if (missing.length > 0) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
`@voyant-travel/workflows/client: missing Cloud workflow configuration: ${missing.join(", ")}`,
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
baseUrl: stripTrailingSlash(requireConfigValue(baseUrl, "baseUrl")),
|
|
335
|
+
triggerToken: requireConfigValue(triggerToken, "triggerToken"),
|
|
336
|
+
appSlug: requireConfigValue(appSlug, "appSlug"),
|
|
337
|
+
environment,
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function requireConfigValue(value: string | undefined, name: string): string {
|
|
342
|
+
if (!value) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
`@voyant-travel/workflows/client: missing Cloud workflow configuration: ${name}`,
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
return value
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function cloudFetch(
|
|
351
|
+
httpFetch: typeof fetch,
|
|
352
|
+
config: ResolvedCloudConfig,
|
|
353
|
+
args: CloudFetchArgs,
|
|
354
|
+
): Promise<Response> {
|
|
355
|
+
const headers = new Headers({
|
|
356
|
+
accept: "application/json",
|
|
357
|
+
authorization: `Bearer ${config.triggerToken}`,
|
|
358
|
+
})
|
|
359
|
+
if (args.body !== undefined) headers.set("content-type", "application/json")
|
|
360
|
+
if (args.idempotencyKey) headers.set("idempotency-key", args.idempotencyKey)
|
|
361
|
+
|
|
362
|
+
const res = await httpFetch(`${config.baseUrl}/cloud/v1${args.path}`, {
|
|
363
|
+
method: args.method,
|
|
364
|
+
headers,
|
|
365
|
+
body: args.body === undefined ? undefined : JSON.stringify(args.body),
|
|
366
|
+
})
|
|
367
|
+
if (!res.ok && !args.allowStatuses?.includes(res.status)) {
|
|
368
|
+
throw new Error(
|
|
369
|
+
`Voyant Cloud workflows request failed: ${res.status} ${res.statusText} ${await res.text()}`,
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
return res
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function readJson(res: Response): Promise<unknown> {
|
|
376
|
+
const text = await res.text()
|
|
377
|
+
if (text.length === 0) return {}
|
|
378
|
+
return JSON.parse(text) as unknown
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function normalizeRun<TOut>(body: unknown, fallbackWorkflowId: string): Run<TOut> {
|
|
382
|
+
const source =
|
|
383
|
+
readRecord(body, ["data", "run"]) ??
|
|
384
|
+
readRecord(body, ["run"]) ??
|
|
385
|
+
readRecord(body, ["data"]) ??
|
|
386
|
+
asRecord(body)
|
|
387
|
+
const id = readString(source, ["id"]) ?? readString(source, ["runId"])
|
|
388
|
+
if (!id) {
|
|
389
|
+
throw new Error("Voyant Cloud workflows response did not include a run id")
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
id,
|
|
393
|
+
workflowId:
|
|
394
|
+
readString(source, ["workflowId"]) ??
|
|
395
|
+
readString(source, ["workflow", "id"]) ??
|
|
396
|
+
fallbackWorkflowId,
|
|
397
|
+
status: parseRunStatus(readString(source, ["status"]) ?? "pending"),
|
|
398
|
+
startedAt: readNumber(source, ["startedAt"]) ?? Date.now(),
|
|
399
|
+
accessToken: readString(source, ["accessToken"]),
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function serializeTriggerOptions(opts: TriggerOptions | undefined): Record<string, unknown> {
|
|
404
|
+
if (!opts) return {}
|
|
405
|
+
const out: Record<string, unknown> = {}
|
|
406
|
+
for (const [key, value] of Object.entries(opts)) {
|
|
407
|
+
if (value === undefined || key === "environment") continue
|
|
408
|
+
out[key] = value instanceof Date ? value.toISOString() : value
|
|
409
|
+
}
|
|
410
|
+
return out
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function workflowIdOf(workflow: WorkflowHandle<unknown, unknown> | string): string {
|
|
414
|
+
return typeof workflow === "string" ? workflow : workflow.id
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function unsupported(method: string): never {
|
|
418
|
+
throw new Error(
|
|
419
|
+
`@voyant-travel/workflows/client: workflows.${method}() is not supported by the ` +
|
|
420
|
+
`managed Cloud trigger client yet.`,
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function defaultEnv(): CloudWorkflowsClientEnv {
|
|
425
|
+
const processEnv = (
|
|
426
|
+
globalThis as typeof globalThis & {
|
|
427
|
+
process?: { env?: CloudWorkflowsClientEnv }
|
|
428
|
+
}
|
|
429
|
+
).process?.env
|
|
430
|
+
return processEnv ?? {}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function parseEnvironment(value: string): EnvironmentName {
|
|
434
|
+
if (value === "production" || value === "preview" || value === "development") return value
|
|
435
|
+
throw new Error(
|
|
436
|
+
`@voyant-travel/workflows/client: invalid environment "${value}"; expected production, preview, or development`,
|
|
437
|
+
)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function parseRunStatus(value: string): RunStatus {
|
|
441
|
+
const normalized = value.toLowerCase()
|
|
442
|
+
if (
|
|
443
|
+
normalized === "pending" ||
|
|
444
|
+
normalized === "queued" ||
|
|
445
|
+
normalized === "running" ||
|
|
446
|
+
normalized === "waiting" ||
|
|
447
|
+
normalized === "completed" ||
|
|
448
|
+
normalized === "failed" ||
|
|
449
|
+
normalized === "cancelled" ||
|
|
450
|
+
normalized === "cancelled_by_dev_reload" ||
|
|
451
|
+
normalized === "cancelled_by_version_sunset" ||
|
|
452
|
+
normalized === "compensated" ||
|
|
453
|
+
normalized === "compensation_failed" ||
|
|
454
|
+
normalized === "timed_out"
|
|
455
|
+
) {
|
|
456
|
+
return normalized
|
|
457
|
+
}
|
|
458
|
+
return "pending"
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function stripTrailingSlash(value: string): string {
|
|
462
|
+
return value.replace(/\/+$/, "")
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
466
|
+
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : {}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function readRecord(value: unknown, path: string[]): Record<string, unknown> | undefined {
|
|
470
|
+
const current = readAtPath(value, path)
|
|
471
|
+
return typeof current === "object" && current !== null
|
|
472
|
+
? (current as Record<string, unknown>)
|
|
473
|
+
: undefined
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function readString(value: unknown, path: string[]): string | undefined {
|
|
477
|
+
const current = readAtPath(value, path)
|
|
478
|
+
return typeof current === "string" && current.length > 0 ? current : undefined
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function readNumber(value: unknown, path: string[]): number | undefined {
|
|
482
|
+
const current = readAtPath(value, path)
|
|
483
|
+
if (typeof current === "number") return current
|
|
484
|
+
if (typeof current === "string" && current.length > 0) {
|
|
485
|
+
const parsed = Number(current)
|
|
486
|
+
if (Number.isFinite(parsed)) return parsed
|
|
487
|
+
}
|
|
488
|
+
return undefined
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function readAtPath(value: unknown, path: string[]): unknown {
|
|
492
|
+
let current: unknown = value
|
|
493
|
+
for (const part of path) {
|
|
494
|
+
if (typeof current !== "object" || current === null) return undefined
|
|
495
|
+
current = (current as Record<string, unknown>)[part]
|
|
496
|
+
}
|
|
497
|
+
return current
|
|
498
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Condition types used in step options (waitFor / cancelIf / skipIf)
|
|
2
|
+
// and in event-filter match expressions.
|
|
3
|
+
// Authoritative contract in docs/sdk-surface.md §4.
|
|
4
|
+
|
|
5
|
+
import type { Duration, RunStatus } from "./types.js"
|
|
6
|
+
|
|
7
|
+
export interface EventCondition {
|
|
8
|
+
event: string
|
|
9
|
+
match?: Record<string, unknown> | ((payload: unknown) => boolean)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SignalCondition {
|
|
13
|
+
signal: string
|
|
14
|
+
match?: Record<string, unknown> | ((payload: unknown) => boolean)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TimeCondition {
|
|
18
|
+
after?: Duration | Date
|
|
19
|
+
before?: Duration | Date
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RunStatusCondition {
|
|
23
|
+
run: { id: string; status: RunStatus[] }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface OrCondition {
|
|
27
|
+
or: Condition[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AndCondition {
|
|
31
|
+
and: Condition[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type Condition =
|
|
35
|
+
| EventCondition
|
|
36
|
+
| SignalCondition
|
|
37
|
+
| TimeCondition
|
|
38
|
+
| RunStatusCondition
|
|
39
|
+
| OrCondition
|
|
40
|
+
| AndCondition
|
|
41
|
+
|
|
42
|
+
export const or = (...conditions: Condition[]): OrCondition => ({ or: conditions })
|
|
43
|
+
export const and = (...conditions: Condition[]): AndCondition => ({ and: conditions })
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// @voyant-travel/workflows/config
|
|
2
|
+
//
|
|
3
|
+
// Types and `defineConfig` helper for `voyant.config.ts`.
|
|
4
|
+
// Contract defined in docs/sdk-surface.md §10 and docs/design.md §5.4.3.
|
|
5
|
+
|
|
6
|
+
export type Duration = number | `${number}${"ms" | "s" | "m" | "h" | "d" | "w"}`
|
|
7
|
+
|
|
8
|
+
/** Cloudflare Container instance types — see `@voyant-travel/workflows` for the size table. */
|
|
9
|
+
export type MachineType =
|
|
10
|
+
| "lite"
|
|
11
|
+
| "basic"
|
|
12
|
+
| "standard-1"
|
|
13
|
+
| "standard-2"
|
|
14
|
+
| "standard-3"
|
|
15
|
+
| "standard-4"
|
|
16
|
+
| (string & {})
|
|
17
|
+
|
|
18
|
+
export type EnvironmentName = "production" | "preview" | "development"
|
|
19
|
+
|
|
20
|
+
export type MeterKey =
|
|
21
|
+
| "edgeCpuMs"
|
|
22
|
+
| "containerSeconds"
|
|
23
|
+
| "warmSlotHours"
|
|
24
|
+
| "runCount"
|
|
25
|
+
| "activeScheduleHours"
|
|
26
|
+
| "payloadStorageGbHour"
|
|
27
|
+
| "retentionRunMonths"
|
|
28
|
+
|
|
29
|
+
export type BindingDeclaration =
|
|
30
|
+
| { type: "d1"; name: string }
|
|
31
|
+
| { type: "r2"; name: string }
|
|
32
|
+
| { type: "kv"; name: string }
|
|
33
|
+
| { type: "queue"; name: string }
|
|
34
|
+
|
|
35
|
+
export interface EnvironmentConfig {
|
|
36
|
+
customDomain?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface RetryPolicy {
|
|
40
|
+
max?: number
|
|
41
|
+
backoff?: "exponential" | "linear" | "fixed"
|
|
42
|
+
initial?: Duration
|
|
43
|
+
maxDelay?: Duration
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface BuildExtension {
|
|
47
|
+
name: string
|
|
48
|
+
/** Open-ended hook surface; each extension defines its own contract. */
|
|
49
|
+
[key: string]: unknown
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface Instrumentation {
|
|
53
|
+
name: string
|
|
54
|
+
[key: string]: unknown
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface WorkflowsConfig {
|
|
58
|
+
dirs?: string[]
|
|
59
|
+
defaults?: {
|
|
60
|
+
retries?: RetryPolicy
|
|
61
|
+
timeout?: Duration
|
|
62
|
+
timezone?: string
|
|
63
|
+
machine?: MachineType
|
|
64
|
+
concurrency?: { strategy?: "queue" | "cancel-in-progress" | "cancel-newest" | "round-robin" }
|
|
65
|
+
}
|
|
66
|
+
containerPool?: {
|
|
67
|
+
defaultMachine?: MachineType
|
|
68
|
+
maxConcurrency?: number
|
|
69
|
+
warmPoolSize?: Partial<Record<EnvironmentName, number>>
|
|
70
|
+
evictionTtl?: Duration
|
|
71
|
+
perPodConcurrency?: number
|
|
72
|
+
scaleOutPolicy?: "none" | "onDemand"
|
|
73
|
+
preWarmStrategy?: "onFirstRequest" | "onDeploy"
|
|
74
|
+
}
|
|
75
|
+
build?: {
|
|
76
|
+
extensions?: BuildExtension[]
|
|
77
|
+
defineEnv?: Record<string, string>
|
|
78
|
+
}
|
|
79
|
+
instrumentations?: Instrumentation[]
|
|
80
|
+
dev?: {
|
|
81
|
+
port?: number
|
|
82
|
+
preservation?: "smart" | "always" | "never"
|
|
83
|
+
simulateEnvironment?: EnvironmentName
|
|
84
|
+
}
|
|
85
|
+
billing?: {
|
|
86
|
+
caps?: {
|
|
87
|
+
monthly?: Partial<Record<MeterKey, number>>
|
|
88
|
+
action?: "hard-stop" | "soft-notify"
|
|
89
|
+
perEnvironment?: Partial<
|
|
90
|
+
Record<EnvironmentName, { monthly?: Partial<Record<MeterKey, number>> }>
|
|
91
|
+
>
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
versioning?: {
|
|
95
|
+
retireAfter?: Duration
|
|
96
|
+
onSunset?: "cancel" | "migrate" | "extend"
|
|
97
|
+
sunsetNoticePeriod?: Duration
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface VoyantConfig {
|
|
102
|
+
projectId: string
|
|
103
|
+
entry: {
|
|
104
|
+
worker: string
|
|
105
|
+
container?: string
|
|
106
|
+
}
|
|
107
|
+
environments: Record<EnvironmentName, EnvironmentConfig>
|
|
108
|
+
bindings: Record<string, BindingDeclaration>
|
|
109
|
+
workflows?: WorkflowsConfig
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function defineConfig(config: VoyantConfig): VoyantConfig {
|
|
113
|
+
return config
|
|
114
|
+
}
|