@tanstack/workflow-runtime 0.0.1
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/README.md +22 -0
- package/dist/define-runtime.cjs +50 -0
- package/dist/define-runtime.cjs.map +1 -0
- package/dist/define-runtime.d.cts +16 -0
- package/dist/define-runtime.d.ts +16 -0
- package/dist/define-runtime.js +48 -0
- package/dist/define-runtime.js.map +1 -0
- package/dist/in-memory-store.cjs +457 -0
- package/dist/in-memory-store.cjs.map +1 -0
- package/dist/in-memory-store.d.cts +8 -0
- package/dist/in-memory-store.d.ts +8 -0
- package/dist/in-memory-store.js +457 -0
- package/dist/in-memory-store.js.map +1 -0
- package/dist/index.cjs +14 -0
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/run-store-adapter.cjs +30 -0
- package/dist/run-store-adapter.cjs.map +1 -0
- package/dist/run-store-adapter.d.cts +7 -0
- package/dist/run-store-adapter.d.ts +7 -0
- package/dist/run-store-adapter.js +29 -0
- package/dist/run-store-adapter.js.map +1 -0
- package/dist/runtime-driver.cjs +334 -0
- package/dist/runtime-driver.cjs.map +1 -0
- package/dist/runtime-driver.d.cts +12 -0
- package/dist/runtime-driver.d.ts +12 -0
- package/dist/runtime-driver.js +334 -0
- package/dist/runtime-driver.js.map +1 -0
- package/dist/schedule-materializer.cjs +156 -0
- package/dist/schedule-materializer.cjs.map +1 -0
- package/dist/schedule-materializer.d.cts +28 -0
- package/dist/schedule-materializer.d.ts +28 -0
- package/dist/schedule-materializer.js +155 -0
- package/dist/schedule-materializer.js.map +1 -0
- package/dist/types.cjs +0 -0
- package/dist/types.d.cts +375 -0
- package/dist/types.d.ts +375 -0
- package/dist/types.js +1 -0
- package/package.json +60 -0
- package/src/define-runtime.ts +46 -0
- package/src/in-memory-store.ts +607 -0
- package/src/index.ts +74 -0
- package/src/run-store-adapter.ts +49 -0
- package/src/runtime-driver.ts +536 -0
- package/src/schedule-materializer.ts +272 -0
- package/src/types.ts +462 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DeleteReason,
|
|
3
|
+
RunState,
|
|
4
|
+
WorkflowEvent,
|
|
5
|
+
} from '@tanstack/workflow-core'
|
|
6
|
+
import type {
|
|
7
|
+
WorkflowRunStoreAdapter,
|
|
8
|
+
WorkflowRunStoreAdapterStore,
|
|
9
|
+
} from './types'
|
|
10
|
+
|
|
11
|
+
export function createRunStoreAdapter(
|
|
12
|
+
store: WorkflowRunStoreAdapterStore,
|
|
13
|
+
): WorkflowRunStoreAdapter {
|
|
14
|
+
return {
|
|
15
|
+
getRunState(runId) {
|
|
16
|
+
return store.loadRunState(runId)
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
setRunState(_runId: string, state: RunState) {
|
|
20
|
+
return store.saveRunState({ state })
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
deleteRun(runId: string, reason: DeleteReason) {
|
|
24
|
+
return store.deleteRun(runId, reason)
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
async appendEvent(
|
|
28
|
+
runId: string,
|
|
29
|
+
expectedNextIndex: number,
|
|
30
|
+
event: WorkflowEvent,
|
|
31
|
+
) {
|
|
32
|
+
await store.appendEvents({
|
|
33
|
+
runId,
|
|
34
|
+
expectedNextIndex,
|
|
35
|
+
events: [event],
|
|
36
|
+
})
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
async getEvents(runId: string) {
|
|
40
|
+
const events = await store.readEvents({ runId })
|
|
41
|
+
return events.map((event) => event.event)
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
subscribe: store.subscribeEvents
|
|
45
|
+
? (runId, fromIndex, onEvent) =>
|
|
46
|
+
store.subscribeEvents!(runId, fromIndex, onEvent)
|
|
47
|
+
: undefined,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import { runWorkflow } from '@tanstack/workflow-core'
|
|
2
|
+
import { createRunStoreAdapter } from './run-store-adapter'
|
|
3
|
+
import type {
|
|
4
|
+
AnyWorkflowDefinition,
|
|
5
|
+
WorkflowEvent,
|
|
6
|
+
} from '@tanstack/workflow-core'
|
|
7
|
+
import type {
|
|
8
|
+
DeliverApprovalResult,
|
|
9
|
+
DeliverSignalResult,
|
|
10
|
+
TimerWakeup,
|
|
11
|
+
WorkflowExecution,
|
|
12
|
+
WorkflowRegistration,
|
|
13
|
+
WorkflowRuntimeConfig,
|
|
14
|
+
WorkflowRuntimeDeliverApprovalArgs,
|
|
15
|
+
WorkflowRuntimeDeliverSignalArgs,
|
|
16
|
+
WorkflowRuntimeRunResult,
|
|
17
|
+
WorkflowRuntimeRunResultKind,
|
|
18
|
+
WorkflowRuntimeStartRunArgs,
|
|
19
|
+
WorkflowRuntimeSweepArgs,
|
|
20
|
+
WorkflowRuntimeSweepResult,
|
|
21
|
+
} from './types'
|
|
22
|
+
|
|
23
|
+
const DEFAULT_LEASE_MS = 30_000
|
|
24
|
+
const DEFAULT_SWEEP_LIMIT = 25
|
|
25
|
+
|
|
26
|
+
export function createRuntimeDriver<
|
|
27
|
+
TWorkflows extends Record<string, WorkflowRegistration>,
|
|
28
|
+
>(config: WorkflowRuntimeConfig<TWorkflows>) {
|
|
29
|
+
return {
|
|
30
|
+
startRun(args: WorkflowRuntimeStartRunArgs) {
|
|
31
|
+
return startRun(config, args)
|
|
32
|
+
},
|
|
33
|
+
deliverSignal<TPayload = unknown>(
|
|
34
|
+
args: WorkflowRuntimeDeliverSignalArgs<TPayload>,
|
|
35
|
+
) {
|
|
36
|
+
return deliverSignal(config, args)
|
|
37
|
+
},
|
|
38
|
+
deliverApproval(args: WorkflowRuntimeDeliverApprovalArgs) {
|
|
39
|
+
return deliverApproval(config, args)
|
|
40
|
+
},
|
|
41
|
+
sweep(args: WorkflowRuntimeSweepArgs = {}) {
|
|
42
|
+
return sweep(config, args)
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function startRun<
|
|
48
|
+
TWorkflows extends Record<string, WorkflowRegistration>,
|
|
49
|
+
>(
|
|
50
|
+
config: WorkflowRuntimeConfig<TWorkflows>,
|
|
51
|
+
args: WorkflowRuntimeStartRunArgs,
|
|
52
|
+
): Promise<WorkflowRuntimeRunResult> {
|
|
53
|
+
const now = args.now ?? Date.now()
|
|
54
|
+
const workflow = await loadWorkflow(config, args.workflowId)
|
|
55
|
+
const workflowVersion = workflow.version
|
|
56
|
+
await config.store.createRun({
|
|
57
|
+
runId: args.runId,
|
|
58
|
+
workflowId: args.workflowId,
|
|
59
|
+
workflowVersion,
|
|
60
|
+
input: args.input,
|
|
61
|
+
now,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
return driveClaimedRun(config, {
|
|
65
|
+
workflow,
|
|
66
|
+
workflowId: args.workflowId,
|
|
67
|
+
runId: args.runId,
|
|
68
|
+
input: args.input,
|
|
69
|
+
now,
|
|
70
|
+
leaseOwner: args.leaseOwner,
|
|
71
|
+
leaseMs: args.leaseMs,
|
|
72
|
+
threadId: args.threadId,
|
|
73
|
+
includeEvents: args.includeEvents,
|
|
74
|
+
maxEvents: args.maxEvents,
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function deliverSignal<
|
|
79
|
+
TWorkflows extends Record<string, WorkflowRegistration>,
|
|
80
|
+
TPayload,
|
|
81
|
+
>(
|
|
82
|
+
config: WorkflowRuntimeConfig<TWorkflows>,
|
|
83
|
+
args: WorkflowRuntimeDeliverSignalArgs<TPayload>,
|
|
84
|
+
): Promise<WorkflowRuntimeRunResult> {
|
|
85
|
+
const now = args.now ?? Date.now()
|
|
86
|
+
const delivery = {
|
|
87
|
+
signalId: args.signalId,
|
|
88
|
+
name: args.name,
|
|
89
|
+
payload: args.payload,
|
|
90
|
+
}
|
|
91
|
+
const delivered = await config.store.deliverSignal({
|
|
92
|
+
runId: args.runId,
|
|
93
|
+
delivery,
|
|
94
|
+
now,
|
|
95
|
+
})
|
|
96
|
+
if (delivered.kind !== 'delivered') {
|
|
97
|
+
return resultFromSignalDelivery(args.runId, delivered)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const workflow = await loadWorkflow(config, delivered.run.workflowId)
|
|
101
|
+
return driveClaimedRun(config, {
|
|
102
|
+
workflow,
|
|
103
|
+
workflowId: delivered.run.workflowId,
|
|
104
|
+
runId: args.runId,
|
|
105
|
+
signalDelivery: delivery,
|
|
106
|
+
now,
|
|
107
|
+
leaseOwner: args.leaseOwner,
|
|
108
|
+
leaseMs: args.leaseMs,
|
|
109
|
+
threadId: args.threadId,
|
|
110
|
+
includeEvents: args.includeEvents,
|
|
111
|
+
maxEvents: args.maxEvents,
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function deliverApproval<
|
|
116
|
+
TWorkflows extends Record<string, WorkflowRegistration>,
|
|
117
|
+
>(
|
|
118
|
+
config: WorkflowRuntimeConfig<TWorkflows>,
|
|
119
|
+
args: WorkflowRuntimeDeliverApprovalArgs,
|
|
120
|
+
): Promise<WorkflowRuntimeRunResult> {
|
|
121
|
+
const now = args.now ?? Date.now()
|
|
122
|
+
const delivered = await config.store.deliverApproval({
|
|
123
|
+
runId: args.runId,
|
|
124
|
+
approval: args.approval,
|
|
125
|
+
now,
|
|
126
|
+
})
|
|
127
|
+
if (delivered.kind !== 'delivered') {
|
|
128
|
+
return resultFromApprovalDelivery(args.runId, delivered)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const workflow = await loadWorkflow(config, delivered.run.workflowId)
|
|
132
|
+
return driveClaimedRun(config, {
|
|
133
|
+
workflow,
|
|
134
|
+
workflowId: delivered.run.workflowId,
|
|
135
|
+
runId: args.runId,
|
|
136
|
+
approval: args.approval,
|
|
137
|
+
now,
|
|
138
|
+
leaseOwner: args.leaseOwner,
|
|
139
|
+
leaseMs: args.leaseMs,
|
|
140
|
+
threadId: args.threadId,
|
|
141
|
+
includeEvents: args.includeEvents,
|
|
142
|
+
maxEvents: args.maxEvents,
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function sweep<TWorkflows extends Record<string, WorkflowRegistration>>(
|
|
147
|
+
config: WorkflowRuntimeConfig<TWorkflows>,
|
|
148
|
+
args: WorkflowRuntimeSweepArgs,
|
|
149
|
+
): Promise<WorkflowRuntimeSweepResult> {
|
|
150
|
+
const now = args.now ?? Date.now()
|
|
151
|
+
const startedAt = Date.now()
|
|
152
|
+
const maxScheduledRuns = normalizeSweepLimit(
|
|
153
|
+
args.maxScheduledRuns ?? args.limit,
|
|
154
|
+
DEFAULT_SWEEP_LIMIT,
|
|
155
|
+
'maxScheduledRuns',
|
|
156
|
+
)
|
|
157
|
+
const maxTimers = normalizeSweepLimit(
|
|
158
|
+
args.maxTimers ?? args.limit,
|
|
159
|
+
DEFAULT_SWEEP_LIMIT,
|
|
160
|
+
'maxTimers',
|
|
161
|
+
)
|
|
162
|
+
const leaseOwner = args.leaseOwner ?? `sweep:${now}`
|
|
163
|
+
const leaseMs = args.leaseMs ?? config.defaultLeaseMs ?? DEFAULT_LEASE_MS
|
|
164
|
+
const scheduled: Array<WorkflowRuntimeRunResult> = []
|
|
165
|
+
const timers: Array<WorkflowRuntimeRunResult> = []
|
|
166
|
+
let deadlineReached = false
|
|
167
|
+
|
|
168
|
+
while (scheduled.length < maxScheduledRuns) {
|
|
169
|
+
if (isPastSweepDeadline(startedAt, args.maxDurationMs)) {
|
|
170
|
+
deadlineReached = true
|
|
171
|
+
break
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const buckets = await config.store.claimDueScheduleBuckets({
|
|
175
|
+
now,
|
|
176
|
+
limit: 1,
|
|
177
|
+
leaseOwner,
|
|
178
|
+
leaseMs,
|
|
179
|
+
})
|
|
180
|
+
const bucket = buckets[0]
|
|
181
|
+
if (!bucket) break
|
|
182
|
+
|
|
183
|
+
const result = await startRun(config, {
|
|
184
|
+
workflowId: bucket.workflowId,
|
|
185
|
+
runId: bucket.runId,
|
|
186
|
+
input: bucket.input,
|
|
187
|
+
now,
|
|
188
|
+
leaseOwner,
|
|
189
|
+
leaseMs,
|
|
190
|
+
includeEvents: args.includeEvents,
|
|
191
|
+
maxEvents: args.maxEvents,
|
|
192
|
+
})
|
|
193
|
+
if (result.kind !== 'not-claimable' && result.kind !== 'not-found') {
|
|
194
|
+
await config.store.markScheduleBucketStarted({
|
|
195
|
+
scheduleId: bucket.scheduleId,
|
|
196
|
+
bucketId: bucket.bucketId,
|
|
197
|
+
runId: bucket.runId,
|
|
198
|
+
now,
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
scheduled.push(result)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
while (timers.length < maxTimers) {
|
|
205
|
+
if (isPastSweepDeadline(startedAt, args.maxDurationMs)) {
|
|
206
|
+
deadlineReached = true
|
|
207
|
+
break
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const dueTimers = await config.store.claimDueTimers({
|
|
211
|
+
now,
|
|
212
|
+
limit: 1,
|
|
213
|
+
leaseOwner,
|
|
214
|
+
leaseMs,
|
|
215
|
+
})
|
|
216
|
+
const timer = dueTimers[0]
|
|
217
|
+
if (!timer) break
|
|
218
|
+
|
|
219
|
+
timers.push(
|
|
220
|
+
await deliverTimer(config, {
|
|
221
|
+
timer,
|
|
222
|
+
now,
|
|
223
|
+
leaseOwner,
|
|
224
|
+
leaseMs,
|
|
225
|
+
includeEvents: args.includeEvents,
|
|
226
|
+
maxEvents: args.maxEvents,
|
|
227
|
+
}),
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
scheduled,
|
|
233
|
+
timers,
|
|
234
|
+
summary: summarizeSweep(scheduled, timers),
|
|
235
|
+
deadlineReached,
|
|
236
|
+
remainingMayExist:
|
|
237
|
+
deadlineReached ||
|
|
238
|
+
scheduled.length >= maxScheduledRuns ||
|
|
239
|
+
timers.length >= maxTimers,
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function deliverTimer<
|
|
244
|
+
TWorkflows extends Record<string, WorkflowRegistration>,
|
|
245
|
+
>(
|
|
246
|
+
config: WorkflowRuntimeConfig<TWorkflows>,
|
|
247
|
+
args: {
|
|
248
|
+
timer: TimerWakeup
|
|
249
|
+
now: number
|
|
250
|
+
leaseOwner: string
|
|
251
|
+
leaseMs: number
|
|
252
|
+
includeEvents?: boolean
|
|
253
|
+
maxEvents?: number
|
|
254
|
+
},
|
|
255
|
+
) {
|
|
256
|
+
return deliverSignal(config, {
|
|
257
|
+
runId: args.timer.runId,
|
|
258
|
+
signalId: args.timer.signalId,
|
|
259
|
+
name: '__timer',
|
|
260
|
+
payload: undefined,
|
|
261
|
+
now: args.now,
|
|
262
|
+
leaseOwner: args.leaseOwner,
|
|
263
|
+
leaseMs: args.leaseMs,
|
|
264
|
+
includeEvents: args.includeEvents,
|
|
265
|
+
maxEvents: args.maxEvents,
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function driveClaimedRun<
|
|
270
|
+
TWorkflows extends Record<string, WorkflowRegistration>,
|
|
271
|
+
>(
|
|
272
|
+
config: WorkflowRuntimeConfig<TWorkflows>,
|
|
273
|
+
args: {
|
|
274
|
+
workflow: AnyWorkflowDefinition
|
|
275
|
+
workflowId: string
|
|
276
|
+
runId: string
|
|
277
|
+
input?: unknown
|
|
278
|
+
signalDelivery?: Parameters<typeof runWorkflow>[0]['signalDelivery']
|
|
279
|
+
approval?: Parameters<typeof runWorkflow>[0]['approval']
|
|
280
|
+
now: number
|
|
281
|
+
leaseOwner?: string
|
|
282
|
+
leaseMs?: number
|
|
283
|
+
threadId?: string
|
|
284
|
+
includeEvents?: boolean
|
|
285
|
+
maxEvents?: number
|
|
286
|
+
},
|
|
287
|
+
): Promise<WorkflowRuntimeRunResult> {
|
|
288
|
+
const leaseOwner = args.leaseOwner ?? `runtime:${args.runId}`
|
|
289
|
+
const leaseMs = args.leaseMs ?? config.defaultLeaseMs ?? DEFAULT_LEASE_MS
|
|
290
|
+
const claim = await config.store.claimRun({
|
|
291
|
+
runId: args.runId,
|
|
292
|
+
leaseOwner,
|
|
293
|
+
leaseMs,
|
|
294
|
+
now: args.now,
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
if (claim.kind === 'not-found') {
|
|
298
|
+
return {
|
|
299
|
+
kind: 'not-found',
|
|
300
|
+
runId: args.runId,
|
|
301
|
+
workflowId: args.workflowId,
|
|
302
|
+
eventCount: 0,
|
|
303
|
+
events: [],
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (claim.kind === 'not-claimable') {
|
|
307
|
+
return {
|
|
308
|
+
kind: 'not-claimable',
|
|
309
|
+
runId: args.runId,
|
|
310
|
+
workflowId: args.workflowId,
|
|
311
|
+
run: claim.run,
|
|
312
|
+
eventCount: 0,
|
|
313
|
+
events: [],
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const runStore = createRunStoreAdapter(config.store)
|
|
318
|
+
const collected = await collectWorkflowEvents(
|
|
319
|
+
runWorkflow({
|
|
320
|
+
workflow: args.workflow,
|
|
321
|
+
runStore,
|
|
322
|
+
runId: args.runId,
|
|
323
|
+
input: args.input,
|
|
324
|
+
signalDelivery: args.signalDelivery,
|
|
325
|
+
approval: args.approval,
|
|
326
|
+
threadId: args.threadId,
|
|
327
|
+
}),
|
|
328
|
+
{
|
|
329
|
+
includeEvents: args.includeEvents ?? true,
|
|
330
|
+
maxEvents: args.maxEvents,
|
|
331
|
+
},
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
await syncTimerFromRunState(config, args.runId, args.workflowId, args.now)
|
|
335
|
+
await config.store.releaseRunLease({ runId: args.runId, leaseOwner })
|
|
336
|
+
|
|
337
|
+
const run = await config.store.loadRun(args.runId)
|
|
338
|
+
return {
|
|
339
|
+
kind: classifyRun(run, collected.eventCount),
|
|
340
|
+
runId: args.runId,
|
|
341
|
+
workflowId: args.workflowId,
|
|
342
|
+
run,
|
|
343
|
+
events: collected.events,
|
|
344
|
+
eventCount: collected.eventCount,
|
|
345
|
+
eventsTruncated: collected.eventsTruncated || undefined,
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function syncTimerFromRunState<
|
|
350
|
+
TWorkflows extends Record<string, WorkflowRegistration>,
|
|
351
|
+
>(
|
|
352
|
+
config: WorkflowRuntimeConfig<TWorkflows>,
|
|
353
|
+
runId: string,
|
|
354
|
+
workflowId: string,
|
|
355
|
+
now: number,
|
|
356
|
+
) {
|
|
357
|
+
const state = await config.store.loadRunState(runId)
|
|
358
|
+
const deadline = state?.waitingFor?.deadline
|
|
359
|
+
if (state?.waitingFor?.signalName !== '__timer' || deadline === undefined) {
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
await config.store.scheduleTimer({
|
|
364
|
+
runId,
|
|
365
|
+
workflowId,
|
|
366
|
+
workflowVersion: state.workflowVersion,
|
|
367
|
+
wakeAt: deadline,
|
|
368
|
+
signalId: `timer:${runId}:${deadline}`,
|
|
369
|
+
now,
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function loadWorkflow<
|
|
374
|
+
TWorkflows extends Record<string, WorkflowRegistration>,
|
|
375
|
+
>(
|
|
376
|
+
config: WorkflowRuntimeConfig<TWorkflows>,
|
|
377
|
+
workflowId: string,
|
|
378
|
+
): Promise<AnyWorkflowDefinition> {
|
|
379
|
+
const registration = config.workflows[workflowId]
|
|
380
|
+
if (!registration) {
|
|
381
|
+
throw new Error(`Workflow "${workflowId}" is not registered.`)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const workflow = normalizeWorkflowLoaderResult(await registration.load())
|
|
385
|
+
const previousVersions = []
|
|
386
|
+
for (const loadPrevious of Object.values(
|
|
387
|
+
registration.previousVersions ?? {},
|
|
388
|
+
)) {
|
|
389
|
+
previousVersions.push(normalizeWorkflowLoaderResult(await loadPrevious()))
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (registration.version || previousVersions.length > 0) {
|
|
393
|
+
return {
|
|
394
|
+
...workflow,
|
|
395
|
+
version: registration.version ?? workflow.version,
|
|
396
|
+
previousVersions: [
|
|
397
|
+
...(workflow.previousVersions ?? []),
|
|
398
|
+
...previousVersions,
|
|
399
|
+
],
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return workflow
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function normalizeWorkflowLoaderResult(
|
|
407
|
+
result: Awaited<ReturnType<WorkflowRegistration['load']>>,
|
|
408
|
+
): AnyWorkflowDefinition {
|
|
409
|
+
if ('__kind' in result) return result
|
|
410
|
+
if ('default' in result) return result.default
|
|
411
|
+
return result.workflow
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function resultFromSignalDelivery(
|
|
415
|
+
runId: string,
|
|
416
|
+
result: Exclude<DeliverSignalResult, { kind: 'delivered' }>,
|
|
417
|
+
): WorkflowRuntimeRunResult {
|
|
418
|
+
return {
|
|
419
|
+
kind: result.kind,
|
|
420
|
+
runId,
|
|
421
|
+
run: 'run' in result ? result.run : undefined,
|
|
422
|
+
workflowId: 'run' in result ? result.run.workflowId : undefined,
|
|
423
|
+
events: [],
|
|
424
|
+
eventCount: 0,
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function resultFromApprovalDelivery(
|
|
429
|
+
runId: string,
|
|
430
|
+
result: Exclude<DeliverApprovalResult, { kind: 'delivered' }>,
|
|
431
|
+
): WorkflowRuntimeRunResult {
|
|
432
|
+
return {
|
|
433
|
+
kind: result.kind,
|
|
434
|
+
runId,
|
|
435
|
+
run: 'run' in result ? result.run : undefined,
|
|
436
|
+
workflowId: 'run' in result ? result.run.workflowId : undefined,
|
|
437
|
+
events: [],
|
|
438
|
+
eventCount: 0,
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function classifyRun(
|
|
443
|
+
run: WorkflowExecution | undefined,
|
|
444
|
+
eventCount: number,
|
|
445
|
+
): WorkflowRuntimeRunResult['kind'] {
|
|
446
|
+
if (run?.status === 'finished') return 'completed'
|
|
447
|
+
if (run?.status === 'paused') return 'paused'
|
|
448
|
+
if (run?.status === 'errored' || run?.status === 'aborted') return 'errored'
|
|
449
|
+
if (run?.status === 'running' || run?.status === 'queued') return 'running'
|
|
450
|
+
return eventCount > 0 ? 'running' : 'not-found'
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function normalizeSweepLimit(
|
|
454
|
+
value: number | undefined,
|
|
455
|
+
fallback: number,
|
|
456
|
+
label: string,
|
|
457
|
+
) {
|
|
458
|
+
const limit = value ?? fallback
|
|
459
|
+
if (!Number.isInteger(limit) || limit < 0) {
|
|
460
|
+
throw new Error(`Workflow sweep ${label} must be a non-negative integer.`)
|
|
461
|
+
}
|
|
462
|
+
return limit
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function isPastSweepDeadline(
|
|
466
|
+
startedAt: number,
|
|
467
|
+
maxDurationMs: number | undefined,
|
|
468
|
+
) {
|
|
469
|
+
return maxDurationMs !== undefined && Date.now() - startedAt >= maxDurationMs
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function summarizeSweep(
|
|
473
|
+
scheduled: ReadonlyArray<WorkflowRuntimeRunResult>,
|
|
474
|
+
timers: ReadonlyArray<WorkflowRuntimeRunResult>,
|
|
475
|
+
): WorkflowRuntimeSweepResult['summary'] {
|
|
476
|
+
return {
|
|
477
|
+
scheduled: countRunKinds(scheduled),
|
|
478
|
+
timers: countRunKinds(timers),
|
|
479
|
+
eventCount: sumEventCounts(scheduled) + sumEventCounts(timers),
|
|
480
|
+
returnedEventCount:
|
|
481
|
+
sumReturnedEventCounts(scheduled) + sumReturnedEventCounts(timers),
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function countRunKinds(runs: ReadonlyArray<WorkflowRuntimeRunResult>) {
|
|
486
|
+
const counts: Partial<Record<WorkflowRuntimeRunResultKind, number>> = {}
|
|
487
|
+
for (const run of runs) {
|
|
488
|
+
counts[run.kind] = (counts[run.kind] ?? 0) + 1
|
|
489
|
+
}
|
|
490
|
+
return counts
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function sumEventCounts(runs: ReadonlyArray<WorkflowRuntimeRunResult>) {
|
|
494
|
+
return runs.reduce((sum, run) => sum + run.eventCount, 0)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function sumReturnedEventCounts(runs: ReadonlyArray<WorkflowRuntimeRunResult>) {
|
|
498
|
+
return runs.reduce((sum, run) => sum + run.events.length, 0)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function collectWorkflowEvents(
|
|
502
|
+
iterable: AsyncIterable<WorkflowEvent>,
|
|
503
|
+
options: {
|
|
504
|
+
includeEvents: boolean
|
|
505
|
+
maxEvents?: number
|
|
506
|
+
},
|
|
507
|
+
) {
|
|
508
|
+
if (
|
|
509
|
+
options.maxEvents !== undefined &&
|
|
510
|
+
(!Number.isInteger(options.maxEvents) || options.maxEvents < 0)
|
|
511
|
+
) {
|
|
512
|
+
throw new Error(
|
|
513
|
+
'Workflow event collection maxEvents must be a non-negative integer.',
|
|
514
|
+
)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const events: Array<WorkflowEvent> = []
|
|
518
|
+
let eventCount = 0
|
|
519
|
+
let eventsTruncated = false
|
|
520
|
+
|
|
521
|
+
for await (const event of iterable) {
|
|
522
|
+
eventCount++
|
|
523
|
+
if (!options.includeEvents) continue
|
|
524
|
+
if (options.maxEvents === undefined || events.length < options.maxEvents) {
|
|
525
|
+
events.push(event)
|
|
526
|
+
} else {
|
|
527
|
+
eventsTruncated = true
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
events,
|
|
533
|
+
eventCount,
|
|
534
|
+
eventsTruncated,
|
|
535
|
+
}
|
|
536
|
+
}
|