@voyantjs/workflows 0.6.7 → 0.6.9
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/dist/auth/index.d.ts +26 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +137 -0
- package/dist/conditions.d.ts +29 -0
- package/dist/conditions.d.ts.map +1 -0
- package/dist/conditions.js +5 -0
- package/dist/handler/index.d.ts +104 -0
- package/dist/handler/index.d.ts.map +1 -0
- package/dist/handler/index.js +238 -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 +187 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +7 -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 +102 -0
- package/dist/runtime/ctx.d.ts.map +1 -0
- package/dist/runtime/ctx.js +607 -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 +159 -0
- package/dist/runtime/executor.d.ts.map +1 -0
- package/dist/runtime/executor.js +225 -0
- package/dist/runtime/journal.d.ts +55 -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 +595 -0
- package/dist/trigger.d.ts +122 -0
- package/dist/trigger.d.ts.map +1 -0
- package/dist/trigger.js +23 -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 +212 -0
- package/dist/workflow.d.ts.map +1 -0
- package/dist/workflow.js +46 -0
- package/package.json +30 -30
- package/src/auth/index.ts +46 -52
- package/src/conditions.ts +13 -13
- package/src/handler/index.ts +110 -106
- package/src/index.ts +7 -7
- package/src/protocol/index.ts +137 -71
- package/src/rate-limit/index.ts +77 -78
- package/src/runtime/ctx.ts +354 -342
- package/src/runtime/determinism.ts +27 -27
- package/src/runtime/errors.ts +17 -17
- package/src/runtime/executor.ts +179 -172
- package/src/runtime/journal.ts +25 -25
- package/src/testing/index.ts +268 -202
- package/src/trigger.ts +64 -71
- package/src/types.ts +16 -18
- package/src/workflow.ts +154 -152
package/src/runtime/ctx.ts
CHANGED
|
@@ -3,15 +3,14 @@
|
|
|
3
3
|
// The executor owns the waitpoint-pending queue and the callbacks
|
|
4
4
|
// into the orchestrator; ctx is a thin shell that delegates.
|
|
5
5
|
|
|
6
|
-
import type {
|
|
7
|
-
|
|
8
|
-
EnvironmentName,
|
|
9
|
-
RetryPolicy,
|
|
10
|
-
RunTrigger,
|
|
11
|
-
WaitpointKind,
|
|
12
|
-
} from "../types.js";
|
|
6
|
+
import type { SerializedError } from "../protocol/index.js"
|
|
7
|
+
import type { Duration, RetryPolicy, WaitpointKind } from "../types.js"
|
|
13
8
|
import type {
|
|
14
9
|
EnvironmentContext,
|
|
10
|
+
GroupApi,
|
|
11
|
+
GroupScope,
|
|
12
|
+
InvokeApi,
|
|
13
|
+
InvokeOptions,
|
|
15
14
|
MetadataApi,
|
|
16
15
|
MetadataValue,
|
|
17
16
|
ParallelApi,
|
|
@@ -21,34 +20,24 @@ import type {
|
|
|
21
20
|
StepFn,
|
|
22
21
|
StepOptions,
|
|
23
22
|
StreamApi,
|
|
23
|
+
TokenWait,
|
|
24
|
+
Waitable,
|
|
24
25
|
WaitForEventApi,
|
|
25
26
|
WaitForSignalApi,
|
|
26
27
|
WaitForTokenApi,
|
|
27
|
-
Waitable,
|
|
28
28
|
WorkflowContext,
|
|
29
29
|
WorkflowHandle,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
TokenWait,
|
|
33
|
-
GroupApi,
|
|
34
|
-
GroupScope,
|
|
35
|
-
} from "../workflow.js";
|
|
36
|
-
import type { JournalSlice, StepJournalEntry, WaitpointResolutionEntry } from "./journal.js";
|
|
37
|
-
import type { SerializedError } from "../protocol/index.js";
|
|
30
|
+
} from "../workflow.js"
|
|
31
|
+
import { advanceClockTo, type ClockState, createRandomUUID, now } from "./determinism.js"
|
|
38
32
|
import {
|
|
39
|
-
WaitpointPendingSignal,
|
|
40
|
-
RunCancelledSignal,
|
|
41
33
|
CompensateRequestedSignal,
|
|
42
|
-
isWaitpointPending,
|
|
43
|
-
isRunCancelled,
|
|
44
34
|
isCompensateRequested,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
} from "./determinism.js";
|
|
35
|
+
isRunCancelled,
|
|
36
|
+
isWaitpointPending,
|
|
37
|
+
RunCancelledSignal,
|
|
38
|
+
WaitpointPendingSignal,
|
|
39
|
+
} from "./errors.js"
|
|
40
|
+
import type { JournalSlice, StepJournalEntry, WaitpointResolutionEntry } from "./journal.js"
|
|
52
41
|
|
|
53
42
|
/**
|
|
54
43
|
* Callbacks the executor provides for operations that must reach the
|
|
@@ -57,13 +46,13 @@ import {
|
|
|
57
46
|
export interface RuntimeCallbacks {
|
|
58
47
|
/** Run a new step and journal the result. Called only for steps not already in the journal. */
|
|
59
48
|
runStep(args: {
|
|
60
|
-
stepId: string
|
|
61
|
-
attempt: number
|
|
62
|
-
input: unknown
|
|
63
|
-
options: StepOptions<unknown
|
|
64
|
-
fn: StepFn<unknown
|
|
65
|
-
stepCtx: StepContext
|
|
66
|
-
}): Promise<StepJournalEntry
|
|
49
|
+
stepId: string
|
|
50
|
+
attempt: number
|
|
51
|
+
input: unknown
|
|
52
|
+
options: StepOptions<unknown>
|
|
53
|
+
fn: StepFn<unknown>
|
|
54
|
+
stepCtx: StepContext
|
|
55
|
+
}): Promise<StepJournalEntry>
|
|
67
56
|
|
|
68
57
|
/**
|
|
69
58
|
* Called when a step completes successfully and had a `compensate`
|
|
@@ -72,13 +61,13 @@ export interface RuntimeCallbacks {
|
|
|
72
61
|
* is invoked.
|
|
73
62
|
*/
|
|
74
63
|
recordCompensable(args: {
|
|
75
|
-
stepId: string
|
|
76
|
-
output: unknown
|
|
77
|
-
compensate: (output: unknown) => Promise<void
|
|
78
|
-
}): void
|
|
64
|
+
stepId: string
|
|
65
|
+
output: unknown
|
|
66
|
+
compensate: (output: unknown) => Promise<void>
|
|
67
|
+
}): void
|
|
79
68
|
|
|
80
69
|
/** Current length of the compensable list. Used by `ctx.group` checkpoints. */
|
|
81
|
-
compensableLength(): number
|
|
70
|
+
compensableLength(): number
|
|
82
71
|
|
|
83
72
|
/**
|
|
84
73
|
* Remove and return compensables added since `fromIndex`. Used by
|
|
@@ -86,10 +75,10 @@ export interface RuntimeCallbacks {
|
|
|
86
75
|
* compensables.
|
|
87
76
|
*/
|
|
88
77
|
spliceCompensable(fromIndex: number): Array<{
|
|
89
|
-
stepId: string
|
|
90
|
-
output: unknown
|
|
91
|
-
compensate: (output: unknown) => Promise<void
|
|
92
|
-
}
|
|
78
|
+
stepId: string
|
|
79
|
+
output: unknown
|
|
80
|
+
compensate: (output: unknown) => Promise<void>
|
|
81
|
+
}>
|
|
93
82
|
|
|
94
83
|
/**
|
|
95
84
|
* Called by `ctx.stream()` for each chunk produced by the source.
|
|
@@ -97,65 +86,65 @@ export interface RuntimeCallbacks {
|
|
|
97
86
|
* journals the chunk; in tests the harness collects chunks.
|
|
98
87
|
*/
|
|
99
88
|
pushStreamChunk(args: {
|
|
100
|
-
streamId: string
|
|
101
|
-
seq: number
|
|
102
|
-
encoding: "text" | "json" | "base64"
|
|
103
|
-
chunk: unknown
|
|
104
|
-
final: boolean
|
|
105
|
-
}): void
|
|
89
|
+
streamId: string
|
|
90
|
+
seq: number
|
|
91
|
+
encoding: "text" | "json" | "base64"
|
|
92
|
+
chunk: unknown
|
|
93
|
+
final: boolean
|
|
94
|
+
}): void
|
|
106
95
|
|
|
107
96
|
/** Register a new waitpoint; execution will yield after this returns. */
|
|
108
97
|
registerWaitpoint(args: {
|
|
109
|
-
clientWaitpointId: string
|
|
110
|
-
kind: WaitpointKind
|
|
111
|
-
meta: Record<string, unknown
|
|
112
|
-
timeoutMs?: number
|
|
113
|
-
}): void
|
|
98
|
+
clientWaitpointId: string
|
|
99
|
+
kind: WaitpointKind
|
|
100
|
+
meta: Record<string, unknown>
|
|
101
|
+
timeoutMs?: number
|
|
102
|
+
}): void
|
|
114
103
|
|
|
115
104
|
/** Push a metadata mutation; flushed on waitpoint yield and run completion. */
|
|
116
105
|
pushMetadata(op: {
|
|
117
|
-
op: "set" | "increment" | "append" | "remove"
|
|
118
|
-
key: string
|
|
119
|
-
value?: unknown
|
|
120
|
-
target?: "self" | "parent" | "root"
|
|
121
|
-
}): void
|
|
106
|
+
op: "set" | "increment" | "append" | "remove"
|
|
107
|
+
key: string
|
|
108
|
+
value?: unknown
|
|
109
|
+
target?: "self" | "parent" | "root"
|
|
110
|
+
}): void
|
|
122
111
|
|
|
123
112
|
/** Increment invocation counter when the body resumes after eviction. */
|
|
124
|
-
readonly invocationCount: number
|
|
113
|
+
readonly invocationCount: number
|
|
125
114
|
|
|
126
115
|
/** Cancellation signal exposed as `ctx.signal`. */
|
|
127
|
-
readonly abortSignal: AbortSignal
|
|
116
|
+
readonly abortSignal: AbortSignal
|
|
128
117
|
}
|
|
129
118
|
|
|
130
119
|
export interface RuntimeEnvironment {
|
|
131
|
-
readonly run: RunContext
|
|
132
|
-
readonly workflow: { id: string; version: string }
|
|
133
|
-
readonly environment: EnvironmentContext
|
|
134
|
-
readonly project: { id: string; slug: string }
|
|
135
|
-
readonly organization: { id: string; slug: string }
|
|
120
|
+
readonly run: RunContext
|
|
121
|
+
readonly workflow: { id: string; version: string }
|
|
122
|
+
readonly environment: EnvironmentContext
|
|
123
|
+
readonly project: { id: string; slug: string }
|
|
124
|
+
readonly organization: { id: string; slug: string }
|
|
136
125
|
}
|
|
137
126
|
|
|
138
127
|
export interface CtxBuildArgs {
|
|
139
|
-
env: RuntimeEnvironment
|
|
140
|
-
journal: JournalSlice
|
|
141
|
-
callbacks: RuntimeCallbacks
|
|
142
|
-
clock: ClockState
|
|
143
|
-
random: () => number
|
|
128
|
+
env: RuntimeEnvironment
|
|
129
|
+
journal: JournalSlice
|
|
130
|
+
callbacks: RuntimeCallbacks
|
|
131
|
+
clock: ClockState
|
|
132
|
+
random: () => number
|
|
144
133
|
/** Mutated as ctx.setRetry is called; each step option inherits. */
|
|
145
|
-
retryOverride: { current: RetryPolicy | undefined }
|
|
134
|
+
retryOverride: { current: RetryPolicy | undefined }
|
|
146
135
|
}
|
|
147
136
|
|
|
148
137
|
export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
|
|
149
|
-
const { env, journal, callbacks, clock, random, retryOverride } = args
|
|
138
|
+
const { env, journal, callbacks, clock, random, retryOverride } = args
|
|
150
139
|
|
|
151
140
|
// Per-ctx client-id counter. Reset on each ctx (= each invocation),
|
|
152
141
|
// which means ids are stable relative to body execution order.
|
|
153
|
-
let clientIdSeq = 0
|
|
154
|
-
const nextClientId = (): number => ++clientIdSeq
|
|
142
|
+
let clientIdSeq = 0
|
|
143
|
+
const nextClientId = (): number => ++clientIdSeq
|
|
155
144
|
|
|
156
145
|
function checkCancel(): void {
|
|
157
146
|
if (callbacks.abortSignal.aborted) {
|
|
158
|
-
throw new RunCancelledSignal()
|
|
147
|
+
throw new RunCancelledSignal()
|
|
159
148
|
}
|
|
160
149
|
}
|
|
161
150
|
|
|
@@ -166,14 +155,15 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
|
|
|
166
155
|
optsOrFn: StepOptions<unknown> | StepFn<unknown>,
|
|
167
156
|
maybeFn?: StepFn<unknown>,
|
|
168
157
|
) => {
|
|
169
|
-
checkCancel()
|
|
170
|
-
const opts: StepOptions<unknown> = typeof optsOrFn === "function" ? {} : optsOrFn
|
|
171
|
-
const fn: StepFn<unknown> =
|
|
158
|
+
checkCancel()
|
|
159
|
+
const opts: StepOptions<unknown> = typeof optsOrFn === "function" ? {} : optsOrFn
|
|
160
|
+
const fn: StepFn<unknown> =
|
|
161
|
+
typeof optsOrFn === "function" ? optsOrFn : (maybeFn as StepFn<unknown>)
|
|
172
162
|
|
|
173
163
|
// Journal hit? Return cached.
|
|
174
|
-
const cached = journal.stepResults[id]
|
|
164
|
+
const cached = journal.stepResults[id]
|
|
175
165
|
if (cached) {
|
|
176
|
-
advanceClockTo(clock, cached.finishedAt)
|
|
166
|
+
advanceClockTo(clock, cached.finishedAt)
|
|
177
167
|
if (cached.status === "ok") {
|
|
178
168
|
// Re-register compensable on replay so compensations are available
|
|
179
169
|
// if this invocation ends up rolling back.
|
|
@@ -182,54 +172,54 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
|
|
|
182
172
|
stepId: id,
|
|
183
173
|
output: cached.output,
|
|
184
174
|
compensate: opts.compensate as (output: unknown) => Promise<void>,
|
|
185
|
-
})
|
|
175
|
+
})
|
|
186
176
|
}
|
|
187
|
-
return cached.output
|
|
177
|
+
return cached.output
|
|
188
178
|
}
|
|
189
179
|
// Journaled error rethrows on replay so catch blocks behave consistently.
|
|
190
|
-
const e = new Error(cached.error?.message ?? "step failed")
|
|
191
|
-
(e as { code?: string }).code = cached.error?.code
|
|
192
|
-
throw e
|
|
180
|
+
const e = new Error(cached.error?.message ?? "step failed")
|
|
181
|
+
;(e as { code?: string }).code = cached.error?.code
|
|
182
|
+
throw e
|
|
193
183
|
}
|
|
194
184
|
|
|
195
185
|
// Execute a new step via the callback, with the retry loop.
|
|
196
186
|
const mergedOpts: StepOptions<unknown> = {
|
|
197
187
|
...opts,
|
|
198
188
|
retry: opts.retry ?? retryOverride.current,
|
|
199
|
-
}
|
|
200
|
-
const policy = normalizeRetry(mergedOpts.retry)
|
|
201
|
-
let attempt = 0
|
|
202
|
-
let lastEntry: StepJournalEntry | undefined
|
|
189
|
+
}
|
|
190
|
+
const policy = normalizeRetry(mergedOpts.retry)
|
|
191
|
+
let attempt = 0
|
|
192
|
+
let lastEntry: StepJournalEntry | undefined
|
|
203
193
|
|
|
204
194
|
// Per-step timeout: compose the run-level abort signal with a
|
|
205
195
|
// per-call AbortSignal.timeout so cooperative step bodies (fetch,
|
|
206
196
|
// setTimeout wrappers, custom AbortSignal observers) stop early
|
|
207
197
|
// on timeout. Hard enforcement for uncooperative bodies is done
|
|
208
198
|
// below by racing the wrapped fn against a timeout rejection.
|
|
209
|
-
const timeoutMs = mergedOpts.timeout !== undefined ? toMs(mergedOpts.timeout) : undefined
|
|
199
|
+
const timeoutMs = mergedOpts.timeout !== undefined ? toMs(mergedOpts.timeout) : undefined
|
|
210
200
|
const fnWithTimeout: StepFn<unknown> =
|
|
211
201
|
timeoutMs !== undefined
|
|
212
202
|
? async (stepCtx) => {
|
|
213
|
-
let timer: ReturnType<typeof setTimeout> | undefined
|
|
203
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
214
204
|
try {
|
|
215
205
|
return await Promise.race([
|
|
216
206
|
fn(stepCtx),
|
|
217
207
|
new Promise<never>((_, reject) => {
|
|
218
208
|
timer = setTimeout(() => {
|
|
219
|
-
const e = new Error(`step "${id}" timed out after ${timeoutMs}ms`)
|
|
220
|
-
(e as Error & { code?: string }).code = "TIMEOUT"
|
|
221
|
-
reject(e)
|
|
222
|
-
}, timeoutMs)
|
|
209
|
+
const e = new Error(`step "${id}" timed out after ${timeoutMs}ms`)
|
|
210
|
+
;(e as Error & { code?: string }).code = "TIMEOUT"
|
|
211
|
+
reject(e)
|
|
212
|
+
}, timeoutMs)
|
|
223
213
|
}),
|
|
224
|
-
])
|
|
214
|
+
])
|
|
225
215
|
} finally {
|
|
226
|
-
if (timer !== undefined) clearTimeout(timer)
|
|
216
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
227
217
|
}
|
|
228
218
|
}
|
|
229
|
-
: fn
|
|
219
|
+
: fn
|
|
230
220
|
|
|
231
221
|
while (attempt < policy.max) {
|
|
232
|
-
attempt += 1
|
|
222
|
+
attempt += 1
|
|
233
223
|
const stepCtx: StepContext = {
|
|
234
224
|
signal:
|
|
235
225
|
timeoutMs !== undefined
|
|
@@ -237,9 +227,13 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
|
|
|
237
227
|
: callbacks.abortSignal,
|
|
238
228
|
attempt,
|
|
239
229
|
log: (level, msg, data) => {
|
|
240
|
-
console[level === "error" ? "error" : level === "warn" ? "warn" : "log"](
|
|
230
|
+
console[level === "error" ? "error" : level === "warn" ? "warn" : "log"](
|
|
231
|
+
`[${id}]`,
|
|
232
|
+
msg,
|
|
233
|
+
data ?? "",
|
|
234
|
+
)
|
|
241
235
|
},
|
|
242
|
-
}
|
|
236
|
+
}
|
|
243
237
|
const entry = await callbacks.runStep({
|
|
244
238
|
stepId: id,
|
|
245
239
|
attempt,
|
|
@@ -247,42 +241,42 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
|
|
|
247
241
|
options: mergedOpts,
|
|
248
242
|
fn: fnWithTimeout,
|
|
249
243
|
stepCtx,
|
|
250
|
-
})
|
|
251
|
-
lastEntry = entry
|
|
244
|
+
})
|
|
245
|
+
lastEntry = entry
|
|
252
246
|
|
|
253
247
|
if (entry.status === "ok") {
|
|
254
|
-
journal.stepResults[id] = entry
|
|
255
|
-
advanceClockTo(clock, entry.finishedAt)
|
|
248
|
+
journal.stepResults[id] = entry
|
|
249
|
+
advanceClockTo(clock, entry.finishedAt)
|
|
256
250
|
if (opts.compensate) {
|
|
257
251
|
callbacks.recordCompensable({
|
|
258
252
|
stepId: id,
|
|
259
253
|
output: entry.output,
|
|
260
254
|
compensate: opts.compensate as (output: unknown) => Promise<void>,
|
|
261
|
-
})
|
|
255
|
+
})
|
|
262
256
|
}
|
|
263
|
-
return entry.output
|
|
257
|
+
return entry.output
|
|
264
258
|
}
|
|
265
259
|
|
|
266
260
|
// Failed attempt. Check if we should stop retrying.
|
|
267
|
-
if (entry.error?.code === "FATAL") break
|
|
268
|
-
if (attempt >= policy.max) break
|
|
261
|
+
if (entry.error?.code === "FATAL") break
|
|
262
|
+
if (attempt >= policy.max) break
|
|
269
263
|
|
|
270
264
|
// In production the step handler returns { retryAfter } to the DO
|
|
271
265
|
// which sets an alarm; here the spike/test harness continues
|
|
272
266
|
// immediately. retryAfter from RetryableError wins over the policy
|
|
273
267
|
// backoff when set.
|
|
274
|
-
const retryAfter = readRetryAfter(entry.error)
|
|
275
|
-
await maybeDelay(retryAfter ?? backoffDelay(policy, attempt))
|
|
268
|
+
const retryAfter = readRetryAfter(entry.error)
|
|
269
|
+
await maybeDelay(retryAfter ?? backoffDelay(policy, attempt))
|
|
276
270
|
}
|
|
277
271
|
|
|
278
272
|
// Retries exhausted (or never retried).
|
|
279
|
-
const finalEntry = lastEntry
|
|
280
|
-
journal.stepResults[id] = finalEntry
|
|
281
|
-
advanceClockTo(clock, finalEntry.finishedAt)
|
|
282
|
-
const e = new Error(finalEntry.error?.message ?? "step failed")
|
|
283
|
-
(e as { code?: string }).code = finalEntry.error?.code
|
|
284
|
-
throw e
|
|
285
|
-
}) as StepApi
|
|
273
|
+
const finalEntry = lastEntry!
|
|
274
|
+
journal.stepResults[id] = finalEntry
|
|
275
|
+
advanceClockTo(clock, finalEntry.finishedAt)
|
|
276
|
+
const e = new Error(finalEntry.error?.message ?? "step failed")
|
|
277
|
+
;(e as { code?: string }).code = finalEntry.error?.code
|
|
278
|
+
throw e
|
|
279
|
+
}) as StepApi
|
|
286
280
|
|
|
287
281
|
// ---- waits ----
|
|
288
282
|
|
|
@@ -292,25 +286,25 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
|
|
|
292
286
|
meta: Record<string, unknown>,
|
|
293
287
|
timeoutMs?: number,
|
|
294
288
|
): never {
|
|
295
|
-
callbacks.registerWaitpoint({ clientWaitpointId, kind, meta, timeoutMs })
|
|
296
|
-
throw new WaitpointPendingSignal(clientWaitpointId)
|
|
289
|
+
callbacks.registerWaitpoint({ clientWaitpointId, kind, meta, timeoutMs })
|
|
290
|
+
throw new WaitpointPendingSignal(clientWaitpointId)
|
|
297
291
|
}
|
|
298
292
|
|
|
299
293
|
function lookupWaitpoint(id: string): WaitpointResolutionEntry | undefined {
|
|
300
|
-
return journal.waitpointsResolved[id]
|
|
294
|
+
return journal.waitpointsResolved[id]
|
|
301
295
|
}
|
|
302
296
|
|
|
303
297
|
const sleep = async (duration: Duration): Promise<void> => {
|
|
304
|
-
checkCancel()
|
|
305
|
-
const id = `sleep:${nextClientId()}
|
|
306
|
-
const resolved = lookupWaitpoint(id)
|
|
298
|
+
checkCancel()
|
|
299
|
+
const id = `sleep:${nextClientId()}`
|
|
300
|
+
const resolved = lookupWaitpoint(id)
|
|
307
301
|
if (resolved) {
|
|
308
|
-
advanceClockTo(clock, resolved.resolvedAt)
|
|
309
|
-
return
|
|
302
|
+
advanceClockTo(clock, resolved.resolvedAt)
|
|
303
|
+
return
|
|
310
304
|
}
|
|
311
|
-
const ms = toMs(duration)
|
|
312
|
-
yieldWaitpoint(id, "DATETIME", { durationMs: ms }, ms)
|
|
313
|
-
}
|
|
305
|
+
const ms = toMs(duration)
|
|
306
|
+
yieldWaitpoint(id, "DATETIME", { durationMs: ms }, ms)
|
|
307
|
+
}
|
|
314
308
|
|
|
315
309
|
function makeWaitable<T>(
|
|
316
310
|
kind: WaitpointKind,
|
|
@@ -322,69 +316,70 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
|
|
|
322
316
|
): Waitable<T> {
|
|
323
317
|
// --- thenable: single first-match-wins resolution ---
|
|
324
318
|
const resolve = (): T | null => {
|
|
325
|
-
const resolved = lookupWaitpoint(clientWaitpointId)
|
|
319
|
+
const resolved = lookupWaitpoint(clientWaitpointId)
|
|
326
320
|
if (!resolved) {
|
|
327
|
-
yieldWaitpoint(clientWaitpointId, kind, meta, timeoutMs)
|
|
321
|
+
yieldWaitpoint(clientWaitpointId, kind, meta, timeoutMs)
|
|
328
322
|
}
|
|
329
|
-
advanceClockTo(clock, resolved.resolvedAt)
|
|
323
|
+
advanceClockTo(clock, resolved.resolvedAt)
|
|
330
324
|
if (resolved.payload === undefined && onTimeout === "throw") {
|
|
331
|
-
throw new Error(`waitpoint ${clientWaitpointId} timed out`)
|
|
325
|
+
throw new Error(`waitpoint ${clientWaitpointId} timed out`)
|
|
332
326
|
}
|
|
333
|
-
return (resolved.payload ?? null) as T | null
|
|
334
|
-
}
|
|
327
|
+
return (resolved.payload ?? null) as T | null
|
|
328
|
+
}
|
|
335
329
|
|
|
336
330
|
// --- iterable: fresh waitpoint per .next() call ---
|
|
337
331
|
function makeIterator(): AsyncIterableIterator<T> {
|
|
338
|
-
let closed = false
|
|
332
|
+
let closed = false
|
|
339
333
|
return {
|
|
340
334
|
async next(): Promise<IteratorResult<T>> {
|
|
341
|
-
if (closed) return { value: undefined as unknown as T, done: true }
|
|
342
|
-
checkCancel()
|
|
343
|
-
const iterId = `${iterIdPrefix}:iter:${nextClientId()}
|
|
344
|
-
const resolvedIter = lookupWaitpoint(iterId)
|
|
335
|
+
if (closed) return { value: undefined as unknown as T, done: true }
|
|
336
|
+
checkCancel()
|
|
337
|
+
const iterId = `${iterIdPrefix}:iter:${nextClientId()}`
|
|
338
|
+
const resolvedIter = lookupWaitpoint(iterId)
|
|
345
339
|
if (!resolvedIter) {
|
|
346
|
-
yieldWaitpoint(iterId, kind, { ...meta, iter: true }, timeoutMs)
|
|
340
|
+
yieldWaitpoint(iterId, kind, { ...meta, iter: true }, timeoutMs)
|
|
347
341
|
}
|
|
348
|
-
advanceClockTo(clock, resolvedIter.resolvedAt)
|
|
342
|
+
advanceClockTo(clock, resolvedIter.resolvedAt)
|
|
349
343
|
// End-of-stream marker. Harness / orchestrator writes this to
|
|
350
344
|
// tell the iterator the source has no more events.
|
|
351
|
-
const payload = resolvedIter.payload as unknown
|
|
345
|
+
const payload = resolvedIter.payload as unknown
|
|
352
346
|
if (isStreamEnd(payload)) {
|
|
353
|
-
closed = true
|
|
354
|
-
return { value: undefined as unknown as T, done: true }
|
|
347
|
+
closed = true
|
|
348
|
+
return { value: undefined as unknown as T, done: true }
|
|
355
349
|
}
|
|
356
350
|
if (payload === undefined && onTimeout === "throw") {
|
|
357
|
-
throw new Error(`waitpoint ${iterId} timed out`)
|
|
351
|
+
throw new Error(`waitpoint ${iterId} timed out`)
|
|
358
352
|
}
|
|
359
|
-
return { value: payload as T, done: false }
|
|
353
|
+
return { value: payload as T, done: false }
|
|
360
354
|
},
|
|
361
355
|
async return(): Promise<IteratorResult<T>> {
|
|
362
|
-
closed = true
|
|
363
|
-
return { value: undefined as unknown as T, done: true }
|
|
356
|
+
closed = true
|
|
357
|
+
return { value: undefined as unknown as T, done: true }
|
|
364
358
|
},
|
|
365
359
|
[Symbol.asyncIterator]() {
|
|
366
|
-
return this
|
|
360
|
+
return this
|
|
367
361
|
},
|
|
368
|
-
}
|
|
362
|
+
}
|
|
369
363
|
}
|
|
370
364
|
|
|
371
365
|
const thenable: Waitable<T> = {
|
|
366
|
+
// biome-ignore lint/suspicious/noThenProperty: Waitable intentionally implements PromiseLike for `await`.
|
|
372
367
|
then(onFulfilled, onRejected) {
|
|
373
368
|
try {
|
|
374
|
-
const r = resolve()
|
|
375
|
-
return Promise.resolve(r).then(onFulfilled, onRejected)
|
|
369
|
+
const r = resolve()
|
|
370
|
+
return Promise.resolve(r).then(onFulfilled, onRejected)
|
|
376
371
|
} catch (e) {
|
|
377
|
-
return Promise.reject(e).then(onFulfilled, onRejected)
|
|
372
|
+
return Promise.reject(e).then(onFulfilled, onRejected)
|
|
378
373
|
}
|
|
379
374
|
},
|
|
380
375
|
[Symbol.asyncIterator]() {
|
|
381
|
-
return makeIterator()
|
|
376
|
+
return makeIterator()
|
|
382
377
|
},
|
|
383
378
|
close() {
|
|
384
379
|
// no-op; `return()` on the iterator handles early break.
|
|
385
380
|
},
|
|
386
|
-
}
|
|
387
|
-
return thenable
|
|
381
|
+
}
|
|
382
|
+
return thenable
|
|
388
383
|
}
|
|
389
384
|
|
|
390
385
|
function isStreamEnd(payload: unknown): boolean {
|
|
@@ -392,13 +387,16 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
|
|
|
392
387
|
typeof payload === "object" &&
|
|
393
388
|
payload !== null &&
|
|
394
389
|
(payload as { __voyantStreamEnd?: boolean }).__voyantStreamEnd === true
|
|
395
|
-
)
|
|
390
|
+
)
|
|
396
391
|
}
|
|
397
392
|
|
|
398
|
-
const waitForEvent: WaitForEventApi = ((
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
393
|
+
const waitForEvent: WaitForEventApi = ((
|
|
394
|
+
eventType: string,
|
|
395
|
+
opts?: { timeout?: Duration; onTimeout?: "null" | "throw" },
|
|
396
|
+
) => {
|
|
397
|
+
checkCancel()
|
|
398
|
+
const thenableId = `event:${eventType}:${nextClientId()}`
|
|
399
|
+
const iterPrefix = `event:${eventType}`
|
|
402
400
|
return makeWaitable(
|
|
403
401
|
"EVENT",
|
|
404
402
|
thenableId,
|
|
@@ -406,13 +404,16 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
|
|
|
406
404
|
{ eventType },
|
|
407
405
|
opts?.timeout ? toMs(opts.timeout) : undefined,
|
|
408
406
|
opts?.onTimeout,
|
|
409
|
-
)
|
|
410
|
-
}) as WaitForEventApi
|
|
407
|
+
)
|
|
408
|
+
}) as WaitForEventApi
|
|
411
409
|
|
|
412
|
-
const waitForSignal: WaitForSignalApi = ((
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
410
|
+
const waitForSignal: WaitForSignalApi = ((
|
|
411
|
+
name: string,
|
|
412
|
+
opts?: { timeout?: Duration; onTimeout?: "null" | "throw" },
|
|
413
|
+
) => {
|
|
414
|
+
checkCancel()
|
|
415
|
+
const thenableId = `signal:${name}:${nextClientId()}`
|
|
416
|
+
const iterPrefix = `signal:${name}`
|
|
416
417
|
return makeWaitable(
|
|
417
418
|
"SIGNAL",
|
|
418
419
|
thenableId,
|
|
@@ -420,39 +421,39 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
|
|
|
420
421
|
{ signalName: name },
|
|
421
422
|
opts?.timeout ? toMs(opts.timeout) : undefined,
|
|
422
423
|
opts?.onTimeout,
|
|
423
|
-
)
|
|
424
|
-
}) as WaitForSignalApi
|
|
424
|
+
)
|
|
425
|
+
}) as WaitForSignalApi
|
|
425
426
|
|
|
426
427
|
const waitForToken: WaitForTokenApi = (async (opts?: {
|
|
427
|
-
tokenId?: string
|
|
428
|
-
timeout?: Duration
|
|
429
|
-
onTimeout?: "null" | "throw"
|
|
428
|
+
tokenId?: string
|
|
429
|
+
timeout?: Duration
|
|
430
|
+
onTimeout?: "null" | "throw"
|
|
430
431
|
}) => {
|
|
431
|
-
checkCancel()
|
|
432
|
+
checkCancel()
|
|
432
433
|
// Allocate a stable id per call. User-supplied `tokenId` is kept
|
|
433
434
|
// verbatim so external systems can reference the same value.
|
|
434
|
-
const tokenId = opts?.tokenId ?? `tok_${nextClientId()}
|
|
435
|
-
const waitpointId = `token:${tokenId}
|
|
436
|
-
const timeoutMs = opts?.timeout ? toMs(opts.timeout) : undefined
|
|
437
|
-
const onTimeout = opts?.onTimeout ?? "null"
|
|
435
|
+
const tokenId = opts?.tokenId ?? `tok_${nextClientId()}`
|
|
436
|
+
const waitpointId = `token:${tokenId}`
|
|
437
|
+
const timeoutMs = opts?.timeout ? toMs(opts.timeout) : undefined
|
|
438
|
+
const onTimeout = opts?.onTimeout ?? "null"
|
|
438
439
|
|
|
439
440
|
return {
|
|
440
441
|
tokenId,
|
|
441
442
|
url: `/__voyant/tokens/${tokenId}`,
|
|
442
443
|
wait: async (): Promise<unknown> => {
|
|
443
|
-
checkCancel()
|
|
444
|
-
const resolved = lookupWaitpoint(waitpointId)
|
|
444
|
+
checkCancel()
|
|
445
|
+
const resolved = lookupWaitpoint(waitpointId)
|
|
445
446
|
if (resolved) {
|
|
446
|
-
advanceClockTo(clock, resolved.resolvedAt)
|
|
447
|
+
advanceClockTo(clock, resolved.resolvedAt)
|
|
447
448
|
if (resolved.payload === undefined && onTimeout === "throw") {
|
|
448
|
-
throw new Error(`token ${tokenId} timed out`)
|
|
449
|
+
throw new Error(`token ${tokenId} timed out`)
|
|
449
450
|
}
|
|
450
|
-
return resolved.payload ?? null
|
|
451
|
+
return resolved.payload ?? null
|
|
451
452
|
}
|
|
452
|
-
yieldWaitpoint(waitpointId, "MANUAL", { tokenId }, timeoutMs)
|
|
453
|
+
yieldWaitpoint(waitpointId, "MANUAL", { tokenId }, timeoutMs)
|
|
453
454
|
},
|
|
454
|
-
} as TokenWait<unknown
|
|
455
|
-
}) as WaitForTokenApi
|
|
455
|
+
} as TokenWait<unknown>
|
|
456
|
+
}) as WaitForTokenApi
|
|
456
457
|
|
|
457
458
|
// ---- invoke / parallel ----
|
|
458
459
|
|
|
@@ -461,17 +462,17 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
|
|
|
461
462
|
input: TIn,
|
|
462
463
|
opts?: InvokeOptions,
|
|
463
464
|
): Promise<TOut> => {
|
|
464
|
-
checkCancel()
|
|
465
|
-
const id = `invoke:${wf.id}:${nextClientId()}
|
|
466
|
-
const resolved = journal.waitpointsResolved[id]
|
|
465
|
+
checkCancel()
|
|
466
|
+
const id = `invoke:${wf.id}:${nextClientId()}`
|
|
467
|
+
const resolved = journal.waitpointsResolved[id]
|
|
467
468
|
if (resolved) {
|
|
468
|
-
advanceClockTo(clock, resolved.resolvedAt)
|
|
469
|
+
advanceClockTo(clock, resolved.resolvedAt)
|
|
469
470
|
if (resolved.error) {
|
|
470
|
-
const e = new Error(resolved.error.message)
|
|
471
|
-
(e as { code?: string }).code = resolved.error.code
|
|
472
|
-
throw e
|
|
471
|
+
const e = new Error(resolved.error.message)
|
|
472
|
+
;(e as { code?: string }).code = resolved.error.code
|
|
473
|
+
throw e
|
|
473
474
|
}
|
|
474
|
-
return resolved.payload as TOut
|
|
475
|
+
return resolved.payload as TOut
|
|
475
476
|
}
|
|
476
477
|
yieldWaitpoint(id, "RUN", {
|
|
477
478
|
childWorkflowId: wf.id,
|
|
@@ -480,133 +481,131 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
|
|
|
480
481
|
tags: opts?.tags ?? [],
|
|
481
482
|
lockToVersion: opts?.lockToVersion,
|
|
482
483
|
idempotencyKey: opts?.idempotencyKey,
|
|
483
|
-
})
|
|
484
|
-
}) as InvokeApi
|
|
484
|
+
})
|
|
485
|
+
}) as InvokeApi
|
|
485
486
|
|
|
486
487
|
const parallel: ParallelApi = async <T, R>(
|
|
487
488
|
items: readonly T[],
|
|
488
489
|
fn: (item: T, index: number) => Promise<R>,
|
|
489
490
|
opts?: { concurrency?: number; settle?: boolean },
|
|
490
491
|
): Promise<R[]> => {
|
|
491
|
-
checkCancel()
|
|
492
|
-
const total = items.length
|
|
493
|
-
if (total === 0) return []
|
|
494
|
-
const concurrency = Math.max(1, opts?.concurrency ?? total)
|
|
495
|
-
const settle = opts?.settle ?? false
|
|
492
|
+
checkCancel()
|
|
493
|
+
const total = items.length
|
|
494
|
+
if (total === 0) return []
|
|
495
|
+
const concurrency = Math.max(1, opts?.concurrency ?? total)
|
|
496
|
+
const settle = opts?.settle ?? false
|
|
496
497
|
|
|
497
|
-
const results: R[] = new Array(total)
|
|
498
|
-
const errors: { index: number; error: unknown }[] = []
|
|
499
|
-
let cursor = 0
|
|
500
|
-
let aborted = false
|
|
498
|
+
const results: R[] = new Array(total)
|
|
499
|
+
const errors: { index: number; error: unknown }[] = []
|
|
500
|
+
let cursor = 0
|
|
501
|
+
let aborted = false
|
|
501
502
|
|
|
502
503
|
async function worker(): Promise<void> {
|
|
503
504
|
while (!aborted) {
|
|
504
|
-
const i = cursor
|
|
505
|
-
if (i >= total) return
|
|
505
|
+
const i = cursor++
|
|
506
|
+
if (i >= total) return
|
|
506
507
|
try {
|
|
507
|
-
results[i] = await fn(items[i]!, i)
|
|
508
|
+
results[i] = await fn(items[i]!, i)
|
|
508
509
|
} catch (err) {
|
|
509
510
|
if (settle) {
|
|
510
|
-
errors.push({ index: i, error: err })
|
|
511
|
+
errors.push({ index: i, error: err })
|
|
511
512
|
} else {
|
|
512
|
-
aborted = true
|
|
513
|
-
throw err
|
|
513
|
+
aborted = true
|
|
514
|
+
throw err
|
|
514
515
|
}
|
|
515
516
|
}
|
|
516
517
|
}
|
|
517
518
|
}
|
|
518
519
|
|
|
519
|
-
const workerCount = Math.min(concurrency, total)
|
|
520
|
-
const workers = Array.from({ length: workerCount }, () => worker())
|
|
520
|
+
const workerCount = Math.min(concurrency, total)
|
|
521
|
+
const workers = Array.from({ length: workerCount }, () => worker())
|
|
521
522
|
|
|
522
523
|
if (settle) {
|
|
523
|
-
await Promise.all(workers)
|
|
524
|
+
await Promise.all(workers)
|
|
524
525
|
if (errors.length > 0) {
|
|
525
526
|
// Attach details so callers can inspect which items failed.
|
|
526
527
|
const agg = new AggregateError(
|
|
527
528
|
errors.map((e) => (e.error instanceof Error ? e.error : new Error(String(e.error)))),
|
|
528
529
|
`ctx.parallel: ${errors.length}/${total} iteration${errors.length === 1 ? "" : "s"} failed`,
|
|
529
|
-
)
|
|
530
|
-
(agg as { failedIndices?: number[] }).failedIndices = errors.map((e) => e.index)
|
|
531
|
-
throw agg
|
|
530
|
+
)
|
|
531
|
+
;(agg as { failedIndices?: number[] }).failedIndices = errors.map((e) => e.index)
|
|
532
|
+
throw agg
|
|
532
533
|
}
|
|
533
|
-
return results
|
|
534
|
+
return results
|
|
534
535
|
}
|
|
535
536
|
|
|
536
|
-
await Promise.all(workers)
|
|
537
|
-
return results
|
|
538
|
-
}
|
|
537
|
+
await Promise.all(workers)
|
|
538
|
+
return results
|
|
539
|
+
}
|
|
539
540
|
|
|
540
541
|
// ---- streams ----
|
|
541
542
|
|
|
542
|
-
const activeStreamIds = new Set<string>()
|
|
543
|
+
const activeStreamIds = new Set<string>()
|
|
543
544
|
|
|
544
545
|
async function consumeStream(
|
|
545
546
|
streamId: string,
|
|
546
547
|
source: AsyncIterable<unknown>,
|
|
547
548
|
encoding: "text" | "json" | "base64",
|
|
548
549
|
): Promise<void> {
|
|
549
|
-
checkCancel()
|
|
550
|
+
checkCancel()
|
|
550
551
|
if (activeStreamIds.has(streamId)) {
|
|
551
|
-
throw new Error(`ctx.stream: duplicate streamId "${streamId}" within the same run`)
|
|
552
|
+
throw new Error(`ctx.stream: duplicate streamId "${streamId}" within the same run`)
|
|
552
553
|
}
|
|
553
|
-
activeStreamIds.add(streamId)
|
|
554
|
+
activeStreamIds.add(streamId)
|
|
554
555
|
// Replay skip: the prior invocation already drained this source
|
|
555
556
|
// and the orchestrator has the chunks. Re-iterating would double
|
|
556
557
|
// any side effects (LLM calls, billable APIs, file reads).
|
|
557
558
|
if (journal.streamsCompleted[streamId]) {
|
|
558
|
-
return
|
|
559
|
+
return
|
|
559
560
|
}
|
|
560
|
-
let seq = 0
|
|
561
|
-
const iter = source[Symbol.asyncIterator]()
|
|
561
|
+
let seq = 0
|
|
562
|
+
const iter = source[Symbol.asyncIterator]()
|
|
562
563
|
try {
|
|
563
564
|
while (true) {
|
|
564
|
-
checkCancel()
|
|
565
|
-
const { value, done } = await iter.next()
|
|
565
|
+
checkCancel()
|
|
566
|
+
const { value, done } = await iter.next()
|
|
566
567
|
if (done) {
|
|
567
|
-
callbacks.pushStreamChunk({ streamId, seq: seq + 1, encoding, chunk: null, final: true })
|
|
568
|
-
journal.streamsCompleted[streamId] = { chunkCount: seq + 1 }
|
|
569
|
-
return
|
|
568
|
+
callbacks.pushStreamChunk({ streamId, seq: seq + 1, encoding, chunk: null, final: true })
|
|
569
|
+
journal.streamsCompleted[streamId] = { chunkCount: seq + 1 }
|
|
570
|
+
return
|
|
570
571
|
}
|
|
571
|
-
seq += 1
|
|
572
|
-
const chunk = normalizeChunk(value, encoding)
|
|
573
|
-
callbacks.pushStreamChunk({ streamId, seq, encoding, chunk, final: false })
|
|
572
|
+
seq += 1
|
|
573
|
+
const chunk = normalizeChunk(value, encoding)
|
|
574
|
+
callbacks.pushStreamChunk({ streamId, seq, encoding, chunk, final: false })
|
|
574
575
|
}
|
|
575
576
|
} catch (err) {
|
|
576
577
|
// Emit a final frame so consumers know the stream closed, then
|
|
577
578
|
// propagate so the workflow body's error handling kicks in. No
|
|
578
579
|
// journal entry — a failed stream should re-iterate on replay
|
|
579
580
|
// (so the error surfaces deterministically).
|
|
580
|
-
callbacks.pushStreamChunk({ streamId, seq: seq + 1, encoding, chunk: null, final: true })
|
|
581
|
-
throw err
|
|
581
|
+
callbacks.pushStreamChunk({ streamId, seq: seq + 1, encoding, chunk: null, final: true })
|
|
582
|
+
throw err
|
|
582
583
|
}
|
|
583
584
|
}
|
|
584
585
|
|
|
585
586
|
const streamImpl = async (
|
|
586
587
|
streamId: string,
|
|
587
|
-
sourceOrFn:
|
|
588
|
-
| AsyncIterable<unknown>
|
|
589
|
-
| (() => AsyncGenerator<unknown>),
|
|
588
|
+
sourceOrFn: AsyncIterable<unknown> | (() => AsyncGenerator<unknown>),
|
|
590
589
|
): Promise<void> => {
|
|
591
590
|
const source =
|
|
592
591
|
typeof sourceOrFn === "function"
|
|
593
592
|
? (sourceOrFn as () => AsyncGenerator<unknown>)()
|
|
594
|
-
: sourceOrFn
|
|
595
|
-
await consumeStream(streamId, source, inferEncoding(source))
|
|
596
|
-
}
|
|
593
|
+
: sourceOrFn
|
|
594
|
+
await consumeStream(streamId, source, inferEncoding(source))
|
|
595
|
+
}
|
|
597
596
|
|
|
598
597
|
// Typed shape variants. Each forwards to consumeStream with a fixed encoding.
|
|
599
|
-
(streamImpl as unknown as { text: StreamApi["text"] }).text = async (id, source) => {
|
|
600
|
-
await consumeStream(id, source, "text")
|
|
601
|
-
}
|
|
602
|
-
(streamImpl as unknown as { json: StreamApi["json"] }).json = async (id, source) => {
|
|
603
|
-
await consumeStream(id, source, "json")
|
|
604
|
-
}
|
|
605
|
-
(streamImpl as unknown as { bytes: StreamApi["bytes"] }).bytes = async (id, source) => {
|
|
606
|
-
await consumeStream(id, source, "base64")
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
const stream = streamImpl as unknown as StreamApi
|
|
598
|
+
;(streamImpl as unknown as { text: StreamApi["text"] }).text = async (id, source) => {
|
|
599
|
+
await consumeStream(id, source, "text")
|
|
600
|
+
}
|
|
601
|
+
;(streamImpl as unknown as { json: StreamApi["json"] }).json = async (id, source) => {
|
|
602
|
+
await consumeStream(id, source, "json")
|
|
603
|
+
}
|
|
604
|
+
;(streamImpl as unknown as { bytes: StreamApi["bytes"] }).bytes = async (id, source) => {
|
|
605
|
+
await consumeStream(id, source, "base64")
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const stream = streamImpl as unknown as StreamApi
|
|
610
609
|
|
|
611
610
|
// ---- groups ----
|
|
612
611
|
|
|
@@ -621,11 +620,11 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
|
|
|
621
620
|
// outer list — they'll still be rolled back if the enclosing workflow
|
|
622
621
|
// later throws.
|
|
623
622
|
const runScopedCompensations = async (fromIndex: number): Promise<void> => {
|
|
624
|
-
const scopeEntries = callbacks.spliceCompensable(fromIndex)
|
|
623
|
+
const scopeEntries = callbacks.spliceCompensable(fromIndex)
|
|
625
624
|
for (let i = scopeEntries.length - 1; i >= 0; i--) {
|
|
626
|
-
const c = scopeEntries[i]
|
|
625
|
+
const c = scopeEntries[i]!
|
|
627
626
|
try {
|
|
628
|
-
await c.compensate(c.output)
|
|
627
|
+
await c.compensate(c.output)
|
|
629
628
|
} catch {
|
|
630
629
|
// One bad compensation in a scope does not abort the others.
|
|
631
630
|
// Errors here don't surface to the executor — the outer rollback
|
|
@@ -633,54 +632,58 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
|
|
|
633
632
|
// unwind.
|
|
634
633
|
}
|
|
635
634
|
}
|
|
636
|
-
}
|
|
635
|
+
}
|
|
637
636
|
|
|
638
637
|
const group: GroupApi = async <T>(
|
|
639
638
|
_name: string,
|
|
640
639
|
fn: (scope: GroupScope) => Promise<T>,
|
|
641
640
|
): Promise<T> => {
|
|
642
|
-
checkCancel()
|
|
643
|
-
const checkpointStart = callbacks.compensableLength()
|
|
641
|
+
checkCancel()
|
|
642
|
+
const checkpointStart = callbacks.compensableLength()
|
|
644
643
|
try {
|
|
645
644
|
return await fn({
|
|
646
645
|
step,
|
|
647
646
|
compensate: async (): Promise<never> => {
|
|
648
|
-
await runScopedCompensations(checkpointStart)
|
|
649
|
-
throw new CompensateRequestedSignal()
|
|
647
|
+
await runScopedCompensations(checkpointStart)
|
|
648
|
+
throw new CompensateRequestedSignal()
|
|
650
649
|
},
|
|
651
|
-
})
|
|
650
|
+
})
|
|
652
651
|
} catch (err) {
|
|
653
652
|
// Only run scoped compensations for real user errors — internal
|
|
654
653
|
// signals (waitpoint yield, cancellation, compensate-requested)
|
|
655
654
|
// are re-thrown unchanged so the executor can route them.
|
|
656
|
-
if (
|
|
657
|
-
|
|
658
|
-
!isRunCancelled(err) &&
|
|
659
|
-
!isCompensateRequested(err)
|
|
660
|
-
) {
|
|
661
|
-
await runScopedCompensations(checkpointStart);
|
|
655
|
+
if (!isWaitpointPending(err) && !isRunCancelled(err) && !isCompensateRequested(err)) {
|
|
656
|
+
await runScopedCompensations(checkpointStart)
|
|
662
657
|
}
|
|
663
|
-
throw err
|
|
658
|
+
throw err
|
|
664
659
|
}
|
|
665
|
-
}
|
|
660
|
+
}
|
|
666
661
|
|
|
667
662
|
// ---- metadata ----
|
|
668
663
|
|
|
669
664
|
const metadata: MetadataApi = {
|
|
670
|
-
set(key, value) {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
665
|
+
set(key, value) {
|
|
666
|
+
callbacks.pushMetadata({ op: "set", key, value })
|
|
667
|
+
},
|
|
668
|
+
increment(key, by = 1) {
|
|
669
|
+
callbacks.pushMetadata({ op: "increment", key, value: by })
|
|
670
|
+
},
|
|
671
|
+
append(key, value) {
|
|
672
|
+
callbacks.pushMetadata({ op: "append", key, value })
|
|
673
|
+
},
|
|
674
|
+
remove(key) {
|
|
675
|
+
callbacks.pushMetadata({ op: "remove", key })
|
|
676
|
+
},
|
|
674
677
|
// Mutations are pushed immediately via `callbacks.pushMetadata`
|
|
675
678
|
// and collected on the response envelope; no explicit flush is
|
|
676
679
|
// needed.
|
|
677
680
|
flush: async () => {},
|
|
678
|
-
}
|
|
681
|
+
}
|
|
679
682
|
|
|
680
683
|
// ---- retry override ----
|
|
681
684
|
|
|
682
685
|
function setRetry(policy: RetryPolicy): void {
|
|
683
|
-
retryOverride.current = policy
|
|
686
|
+
retryOverride.current = policy
|
|
684
687
|
}
|
|
685
688
|
|
|
686
689
|
return {
|
|
@@ -706,10 +709,10 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
|
|
|
706
709
|
randomUUID: createRandomUUID(random),
|
|
707
710
|
setRetry,
|
|
708
711
|
compensate: async (): Promise<never> => {
|
|
709
|
-
checkCancel()
|
|
710
|
-
throw new CompensateRequestedSignal()
|
|
712
|
+
checkCancel()
|
|
713
|
+
throw new CompensateRequestedSignal()
|
|
711
714
|
},
|
|
712
|
-
} satisfies WorkflowContext<unknown
|
|
715
|
+
} satisfies WorkflowContext<unknown>
|
|
713
716
|
}
|
|
714
717
|
|
|
715
718
|
// ---- helpers ----
|
|
@@ -717,92 +720,94 @@ export function buildCtx(args: CtxBuildArgs): WorkflowContext<unknown> {
|
|
|
717
720
|
function inferEncoding(source: unknown): "text" | "json" | "base64" {
|
|
718
721
|
// Default to json for the generic ctx.stream(id, generator) call. The
|
|
719
722
|
// typed variants (text/json/bytes) override this.
|
|
720
|
-
void source
|
|
721
|
-
return "json"
|
|
723
|
+
void source
|
|
724
|
+
return "json"
|
|
722
725
|
}
|
|
723
726
|
|
|
724
727
|
function normalizeChunk(value: unknown, encoding: "text" | "json" | "base64"): unknown {
|
|
725
728
|
if (encoding === "text") {
|
|
726
|
-
return typeof value === "string" ? value : String(value)
|
|
729
|
+
return typeof value === "string" ? value : String(value)
|
|
727
730
|
}
|
|
728
731
|
if (encoding === "base64") {
|
|
729
732
|
if (value instanceof Uint8Array) {
|
|
730
|
-
return toBase64(value)
|
|
733
|
+
return toBase64(value)
|
|
731
734
|
}
|
|
732
|
-
throw new Error("ctx.stream.bytes: expected Uint8Array chunks")
|
|
735
|
+
throw new Error("ctx.stream.bytes: expected Uint8Array chunks")
|
|
733
736
|
}
|
|
734
|
-
return value
|
|
737
|
+
return value // json — pass through
|
|
735
738
|
}
|
|
736
739
|
|
|
737
740
|
function toBase64(bytes: Uint8Array): string {
|
|
738
741
|
// Node + modern runtimes provide Buffer or btoa. Use Buffer when
|
|
739
742
|
// available for efficiency; fall back to manual encode for isolates.
|
|
740
743
|
const g = globalThis as unknown as {
|
|
741
|
-
Buffer?: { from(b: Uint8Array): { toString(enc: "base64"): string } }
|
|
742
|
-
btoa?: (s: string) => string
|
|
743
|
-
}
|
|
744
|
-
if (g.Buffer) return g.Buffer.from(bytes).toString("base64")
|
|
744
|
+
Buffer?: { from(b: Uint8Array): { toString(enc: "base64"): string } }
|
|
745
|
+
btoa?: (s: string) => string
|
|
746
|
+
}
|
|
747
|
+
if (g.Buffer) return g.Buffer.from(bytes).toString("base64")
|
|
745
748
|
if (g.btoa) {
|
|
746
|
-
let s = ""
|
|
747
|
-
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]!)
|
|
748
|
-
return g.btoa(s)
|
|
749
|
+
let s = ""
|
|
750
|
+
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]!)
|
|
751
|
+
return g.btoa(s)
|
|
749
752
|
}
|
|
750
753
|
// Manual fallback (rare).
|
|
751
|
-
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
752
|
-
let out = ""
|
|
753
|
-
let i = 0
|
|
754
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
755
|
+
let out = ""
|
|
756
|
+
let i = 0
|
|
754
757
|
while (i < bytes.length) {
|
|
755
|
-
const b1 = bytes[i++]
|
|
756
|
-
const b2 = i < bytes.length ? bytes[i++]! : 0
|
|
757
|
-
const b3 = i < bytes.length ? bytes[i++]! : 0
|
|
758
|
-
out += chars[b1 >> 2]
|
|
759
|
-
out += chars[((b1 & 3) << 4) | (b2 >> 4)]
|
|
760
|
-
out += i - 1 > bytes.length ? "=" : chars[((b2 & 15) << 2) | (b3 >> 6)]
|
|
761
|
-
out += i > bytes.length ? "=" : chars[b3 & 63]
|
|
758
|
+
const b1 = bytes[i++]!
|
|
759
|
+
const b2 = i < bytes.length ? bytes[i++]! : 0
|
|
760
|
+
const b3 = i < bytes.length ? bytes[i++]! : 0
|
|
761
|
+
out += chars[b1 >> 2]!
|
|
762
|
+
out += chars[((b1 & 3) << 4) | (b2 >> 4)]!
|
|
763
|
+
out += i - 1 > bytes.length ? "=" : chars[((b2 & 15) << 2) | (b3 >> 6)]!
|
|
764
|
+
out += i > bytes.length ? "=" : chars[b3 & 63]!
|
|
762
765
|
}
|
|
763
|
-
return out
|
|
766
|
+
return out
|
|
764
767
|
}
|
|
765
768
|
|
|
766
769
|
interface ResolvedRetryPolicy {
|
|
767
|
-
max: number
|
|
768
|
-
backoff: "exponential" | "linear" | "fixed"
|
|
769
|
-
initial: number
|
|
770
|
-
maxDelay: number
|
|
770
|
+
max: number
|
|
771
|
+
backoff: "exponential" | "linear" | "fixed"
|
|
772
|
+
initial: number // ms
|
|
773
|
+
maxDelay: number // ms
|
|
771
774
|
}
|
|
772
775
|
|
|
773
|
-
function normalizeRetry(
|
|
774
|
-
input
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
const max = input.max ?? 3;
|
|
778
|
-
const policy = input as RetryPolicy;
|
|
776
|
+
function normalizeRetry(input: RetryPolicy | { max: 0 } | undefined): ResolvedRetryPolicy {
|
|
777
|
+
if (!input) return { max: 1, backoff: "exponential", initial: 1000, maxDelay: 60_000 }
|
|
778
|
+
const max = input.max ?? 3
|
|
779
|
+
const policy = input as RetryPolicy
|
|
779
780
|
return {
|
|
780
781
|
max: Math.max(1, max),
|
|
781
782
|
backoff: policy.backoff ?? "exponential",
|
|
782
783
|
initial: policy.initial !== undefined ? toMs(policy.initial) : 1000,
|
|
783
784
|
maxDelay: policy.maxDelay !== undefined ? toMs(policy.maxDelay) : 60_000,
|
|
784
|
-
}
|
|
785
|
+
}
|
|
785
786
|
}
|
|
786
787
|
|
|
787
788
|
function backoffDelay(policy: ResolvedRetryPolicy, attempt: number): number {
|
|
788
789
|
// `attempt` is 1-indexed; delay applies *before* the next attempt.
|
|
789
|
-
if (policy.backoff === "fixed") return Math.min(policy.initial, policy.maxDelay)
|
|
790
|
-
if (policy.backoff === "linear") return Math.min(policy.initial * attempt, policy.maxDelay)
|
|
790
|
+
if (policy.backoff === "fixed") return Math.min(policy.initial, policy.maxDelay)
|
|
791
|
+
if (policy.backoff === "linear") return Math.min(policy.initial * attempt, policy.maxDelay)
|
|
791
792
|
// exponential
|
|
792
|
-
return Math.min(policy.initial *
|
|
793
|
+
return Math.min(policy.initial * 2 ** (attempt - 1), policy.maxDelay)
|
|
793
794
|
}
|
|
794
795
|
|
|
795
796
|
function readRetryAfter(err: SerializedError | undefined): number | undefined {
|
|
796
|
-
if (!err) return undefined
|
|
797
|
-
if (err.code !== "RETRYABLE") return undefined
|
|
798
|
-
const raw = (err.data as { retryAfter?: unknown } | undefined)?.retryAfter
|
|
799
|
-
if (raw === undefined) return undefined
|
|
800
|
-
if (typeof raw === "number") return raw
|
|
801
|
-
if (raw instanceof Date) return raw.getTime() - Date.now()
|
|
797
|
+
if (!err) return undefined
|
|
798
|
+
if (err.code !== "RETRYABLE") return undefined
|
|
799
|
+
const raw = (err.data as { retryAfter?: unknown } | undefined)?.retryAfter
|
|
800
|
+
if (raw === undefined) return undefined
|
|
801
|
+
if (typeof raw === "number") return raw
|
|
802
|
+
if (raw instanceof Date) return raw.getTime() - Date.now()
|
|
802
803
|
if (typeof raw === "string") {
|
|
803
|
-
try {
|
|
804
|
+
try {
|
|
805
|
+
return toMs(raw as Duration)
|
|
806
|
+
} catch {
|
|
807
|
+
return undefined
|
|
808
|
+
}
|
|
804
809
|
}
|
|
805
|
-
return undefined
|
|
810
|
+
return undefined
|
|
806
811
|
}
|
|
807
812
|
|
|
808
813
|
/**
|
|
@@ -812,27 +817,34 @@ function readRetryAfter(err: SerializedError | undefined): number | undefined {
|
|
|
812
817
|
* through `setTimeout(0)` at most) so the suite stays fast.
|
|
813
818
|
*/
|
|
814
819
|
async function maybeDelay(ms: number): Promise<void> {
|
|
815
|
-
if (ms <= 0) return
|
|
820
|
+
if (ms <= 0) return
|
|
816
821
|
// Cap at 10ms in-process regardless of declared delay. Test harness
|
|
817
822
|
// doesn't model real time; production replaces this with a DO alarm.
|
|
818
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(ms, 10)))
|
|
823
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(ms, 10)))
|
|
819
824
|
}
|
|
820
825
|
|
|
821
826
|
function toMs(d: Duration): number {
|
|
822
|
-
if (typeof d === "number") return d
|
|
823
|
-
const m = /^(\d+)(ms|s|m|h|d|w)$/.exec(d)
|
|
824
|
-
if (!m) throw new Error(`invalid duration: ${String(d)}`)
|
|
825
|
-
const n = Number(m[1])
|
|
827
|
+
if (typeof d === "number") return d
|
|
828
|
+
const m = /^(\d+)(ms|s|m|h|d|w)$/.exec(d)
|
|
829
|
+
if (!m) throw new Error(`invalid duration: ${String(d)}`)
|
|
830
|
+
const n = Number(m[1])
|
|
826
831
|
switch (m[2]) {
|
|
827
|
-
case "ms":
|
|
828
|
-
|
|
829
|
-
case "
|
|
830
|
-
|
|
831
|
-
case "
|
|
832
|
-
|
|
833
|
-
|
|
832
|
+
case "ms":
|
|
833
|
+
return n
|
|
834
|
+
case "s":
|
|
835
|
+
return n * 1000
|
|
836
|
+
case "m":
|
|
837
|
+
return n * 60_000
|
|
838
|
+
case "h":
|
|
839
|
+
return n * 3_600_000
|
|
840
|
+
case "d":
|
|
841
|
+
return n * 86_400_000
|
|
842
|
+
case "w":
|
|
843
|
+
return n * 604_800_000
|
|
844
|
+
default:
|
|
845
|
+
throw new Error(`invalid duration unit: ${m[2]}`)
|
|
834
846
|
}
|
|
835
847
|
}
|
|
836
848
|
|
|
837
849
|
// Re-exports used by the executor for metadata type checking.
|
|
838
|
-
export type { MetadataValue }
|
|
850
|
+
export type { MetadataValue }
|