@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,607 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/require-await -- In-memory implementation satisfies async storage contracts synchronously. */
|
|
2
|
+
import { LogConflictError } from '@tanstack/workflow-core'
|
|
3
|
+
import type {
|
|
4
|
+
DeleteReason,
|
|
5
|
+
RunState,
|
|
6
|
+
WorkflowEvent,
|
|
7
|
+
} from '@tanstack/workflow-core'
|
|
8
|
+
import type {
|
|
9
|
+
AppendEventsArgs,
|
|
10
|
+
AppendEventsResult,
|
|
11
|
+
ClaimDueScheduleBucketsArgs,
|
|
12
|
+
ClaimDueTimersArgs,
|
|
13
|
+
ClaimRunArgs,
|
|
14
|
+
ClaimRunResult,
|
|
15
|
+
ClaimStaleRunsArgs,
|
|
16
|
+
CreateRunArgs,
|
|
17
|
+
CreateRunResult,
|
|
18
|
+
DeliverApprovalArgs,
|
|
19
|
+
DeliverApprovalResult,
|
|
20
|
+
DeliverSignalArgs,
|
|
21
|
+
DeliverSignalResult,
|
|
22
|
+
HeartbeatRunLeaseArgs,
|
|
23
|
+
LeaseOwner,
|
|
24
|
+
ListRunsArgs,
|
|
25
|
+
LoadedExecution,
|
|
26
|
+
MarkRunErroredArgs,
|
|
27
|
+
MarkRunFinishedArgs,
|
|
28
|
+
MarkRunPausedArgs,
|
|
29
|
+
MarkScheduleBucketStartedArgs,
|
|
30
|
+
ReadEventsArgs,
|
|
31
|
+
ReleaseRunLeaseArgs,
|
|
32
|
+
RunClaim,
|
|
33
|
+
RunId,
|
|
34
|
+
RunSummary,
|
|
35
|
+
RunTimeline,
|
|
36
|
+
SaveRunStateArgs,
|
|
37
|
+
ScheduleBucket,
|
|
38
|
+
ScheduleBucketId,
|
|
39
|
+
ScheduleId,
|
|
40
|
+
ScheduleTimerArgs,
|
|
41
|
+
StoredWorkflowEvent,
|
|
42
|
+
TimerWakeup,
|
|
43
|
+
UpsertScheduleArgs,
|
|
44
|
+
WorkflowExecution,
|
|
45
|
+
WorkflowExecutionStore,
|
|
46
|
+
WorkflowLease,
|
|
47
|
+
WorkflowRunStoreAdapterStore,
|
|
48
|
+
} from './types'
|
|
49
|
+
|
|
50
|
+
interface TimerRecord extends TimerWakeup {
|
|
51
|
+
lease?: WorkflowLease
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface ScheduleRecord {
|
|
55
|
+
scheduleId: ScheduleId
|
|
56
|
+
workflowId: string
|
|
57
|
+
workflowVersion?: string
|
|
58
|
+
nextFireAt?: number
|
|
59
|
+
input: unknown
|
|
60
|
+
overlapPolicy: ScheduleBucket['overlapPolicy']
|
|
61
|
+
enabled: boolean
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface ScheduleBucketRecord extends ScheduleBucket {
|
|
65
|
+
status: 'claimed' | 'started'
|
|
66
|
+
lease?: WorkflowLease
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type InMemoryWorkflowExecutionStore = WorkflowExecutionStore &
|
|
70
|
+
WorkflowRunStoreAdapterStore
|
|
71
|
+
|
|
72
|
+
export function inMemoryWorkflowExecutionStore(): InMemoryWorkflowExecutionStore {
|
|
73
|
+
const runs = new Map<RunId, WorkflowExecution>()
|
|
74
|
+
const runStates = new Map<RunId, RunState>()
|
|
75
|
+
const logs = new Map<RunId, Array<StoredWorkflowEvent>>()
|
|
76
|
+
const timers = new Map<string, TimerRecord>()
|
|
77
|
+
const signalDeliveries = new Map<string, true>()
|
|
78
|
+
const schedules = new Map<ScheduleId, ScheduleRecord>()
|
|
79
|
+
const scheduleBuckets = new Map<string, ScheduleBucketRecord>()
|
|
80
|
+
const subscribers = new Map<
|
|
81
|
+
RunId,
|
|
82
|
+
Set<(event: WorkflowEvent, index: number) => void>
|
|
83
|
+
>()
|
|
84
|
+
|
|
85
|
+
function setRun(run: WorkflowExecution) {
|
|
86
|
+
runs.set(run.runId, cloneRun(run))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getRun(runId: RunId) {
|
|
90
|
+
const run = runs.get(runId)
|
|
91
|
+
return run ? cloneRun(run) : undefined
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function updateRun(
|
|
95
|
+
runId: RunId,
|
|
96
|
+
updater: (run: WorkflowExecution) => WorkflowExecution,
|
|
97
|
+
) {
|
|
98
|
+
const existing = runs.get(runId)
|
|
99
|
+
if (!existing) return undefined
|
|
100
|
+
const next = updater(cloneRun(existing))
|
|
101
|
+
setRun(next)
|
|
102
|
+
return cloneRun(next)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
async createRun(args: CreateRunArgs): Promise<CreateRunResult> {
|
|
107
|
+
const existing = getRun(args.runId)
|
|
108
|
+
if (existing) return { kind: 'existing', run: existing }
|
|
109
|
+
|
|
110
|
+
const run: WorkflowExecution = {
|
|
111
|
+
runId: args.runId,
|
|
112
|
+
workflowId: args.workflowId,
|
|
113
|
+
workflowVersion: args.workflowVersion,
|
|
114
|
+
status: 'queued',
|
|
115
|
+
input: args.input,
|
|
116
|
+
createdAt: args.now,
|
|
117
|
+
updatedAt: args.now,
|
|
118
|
+
}
|
|
119
|
+
setRun(run)
|
|
120
|
+
return { kind: 'created', run: cloneRun(run) }
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
async loadRun(runId: RunId) {
|
|
124
|
+
return getRun(runId)
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
async loadExecution(runId: RunId): Promise<LoadedExecution | undefined> {
|
|
128
|
+
const run = getRun(runId)
|
|
129
|
+
if (!run) return undefined
|
|
130
|
+
return {
|
|
131
|
+
run,
|
|
132
|
+
events: cloneStoredEvents(logs.get(runId) ?? []),
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
async loadRunState(runId: RunId) {
|
|
137
|
+
const state = runStates.get(runId)
|
|
138
|
+
return state ? cloneRunState(state) : undefined
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
async saveRunState(args: SaveRunStateArgs) {
|
|
142
|
+
const state = cloneRunState(args.state)
|
|
143
|
+
runStates.set(state.runId, state)
|
|
144
|
+
setRun(executionFromRunState(state, runs.get(state.runId)?.lease))
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
async deleteRun(runId: RunId, _reason: DeleteReason) {
|
|
148
|
+
runs.delete(runId)
|
|
149
|
+
runStates.delete(runId)
|
|
150
|
+
logs.delete(runId)
|
|
151
|
+
subscribers.delete(runId)
|
|
152
|
+
for (const [key, timer] of timers.entries()) {
|
|
153
|
+
if (timer.runId === runId) timers.delete(key)
|
|
154
|
+
}
|
|
155
|
+
for (const key of signalDeliveries.keys()) {
|
|
156
|
+
if (key.startsWith(`${runId}:`)) signalDeliveries.delete(key)
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
async appendEvents(args: AppendEventsArgs): Promise<AppendEventsResult> {
|
|
161
|
+
const log = logs.get(args.runId) ?? []
|
|
162
|
+
if (log.length !== args.expectedNextIndex) {
|
|
163
|
+
throw new LogConflictError(
|
|
164
|
+
args.runId,
|
|
165
|
+
args.expectedNextIndex,
|
|
166
|
+
log[args.expectedNextIndex]?.event,
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const event of args.events) {
|
|
171
|
+
const stored = storeEvent(args.runId, log.length, event)
|
|
172
|
+
log.push(stored)
|
|
173
|
+
publish(subscribers, args.runId, stored.event, stored.eventIndex)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
logs.set(args.runId, log)
|
|
177
|
+
return { nextIndex: log.length }
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
async readEvents(args: ReadEventsArgs) {
|
|
181
|
+
const fromIndex = args.fromIndex ?? 0
|
|
182
|
+
return cloneStoredEvents((logs.get(args.runId) ?? []).slice(fromIndex))
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
subscribeEvents(runId, fromIndex, onEvent) {
|
|
186
|
+
const log = logs.get(runId) ?? []
|
|
187
|
+
for (let index = fromIndex; index < log.length; index++) {
|
|
188
|
+
const stored = log[index]
|
|
189
|
+
if (stored) onEvent(stored.event, stored.eventIndex)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let runSubscribers = subscribers.get(runId)
|
|
193
|
+
if (!runSubscribers) {
|
|
194
|
+
runSubscribers = new Set()
|
|
195
|
+
subscribers.set(runId, runSubscribers)
|
|
196
|
+
}
|
|
197
|
+
runSubscribers.add(onEvent)
|
|
198
|
+
|
|
199
|
+
return () => {
|
|
200
|
+
runSubscribers.delete(onEvent)
|
|
201
|
+
if (runSubscribers.size === 0) subscribers.delete(runId)
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
async claimRun(args: ClaimRunArgs): Promise<ClaimRunResult> {
|
|
206
|
+
const existing = getRun(args.runId)
|
|
207
|
+
if (!existing) return { kind: 'not-found' }
|
|
208
|
+
if (isTerminal(existing.status)) {
|
|
209
|
+
return { kind: 'not-claimable', run: existing }
|
|
210
|
+
}
|
|
211
|
+
if (!canClaim(existing.lease, args.leaseOwner, args.now)) {
|
|
212
|
+
return { kind: 'not-claimable', run: existing }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const claimed = updateRun(args.runId, (run) => ({
|
|
216
|
+
...run,
|
|
217
|
+
status: 'running',
|
|
218
|
+
lease: lease(args.leaseOwner, args.leaseMs, args.now),
|
|
219
|
+
updatedAt: args.now,
|
|
220
|
+
}))
|
|
221
|
+
return { kind: 'claimed', run: claimed! }
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
async heartbeatRunLease(args: HeartbeatRunLeaseArgs) {
|
|
225
|
+
updateRun(args.runId, (run) => {
|
|
226
|
+
if (run.lease?.owner !== args.leaseOwner) return run
|
|
227
|
+
return {
|
|
228
|
+
...run,
|
|
229
|
+
lease: lease(args.leaseOwner, args.leaseMs, args.now),
|
|
230
|
+
updatedAt: args.now,
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
async releaseRunLease(args: ReleaseRunLeaseArgs) {
|
|
236
|
+
updateRun(args.runId, (run) => {
|
|
237
|
+
if (run.lease?.owner !== args.leaseOwner) return run
|
|
238
|
+
return {
|
|
239
|
+
...run,
|
|
240
|
+
lease: undefined,
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
async markRunPaused(args: MarkRunPausedArgs) {
|
|
246
|
+
updateRun(args.runId, (run) => ({
|
|
247
|
+
...run,
|
|
248
|
+
status: 'paused',
|
|
249
|
+
waitingFor: args.waitingFor,
|
|
250
|
+
pendingApproval: args.pendingApproval,
|
|
251
|
+
wakeAt: args.wakeAt,
|
|
252
|
+
lease: undefined,
|
|
253
|
+
updatedAt: args.now,
|
|
254
|
+
}))
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
async markRunFinished(args: MarkRunFinishedArgs) {
|
|
258
|
+
updateRun(args.runId, (run) => ({
|
|
259
|
+
...run,
|
|
260
|
+
status: 'finished',
|
|
261
|
+
output: args.output,
|
|
262
|
+
waitingFor: undefined,
|
|
263
|
+
pendingApproval: undefined,
|
|
264
|
+
wakeAt: undefined,
|
|
265
|
+
lease: undefined,
|
|
266
|
+
updatedAt: args.now,
|
|
267
|
+
}))
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
async markRunErrored(args: MarkRunErroredArgs) {
|
|
271
|
+
void args.code
|
|
272
|
+
updateRun(args.runId, (run) => ({
|
|
273
|
+
...run,
|
|
274
|
+
status: 'errored',
|
|
275
|
+
error: args.error,
|
|
276
|
+
waitingFor: undefined,
|
|
277
|
+
pendingApproval: undefined,
|
|
278
|
+
wakeAt: undefined,
|
|
279
|
+
lease: undefined,
|
|
280
|
+
updatedAt: args.now,
|
|
281
|
+
}))
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
async scheduleTimer(args: ScheduleTimerArgs) {
|
|
285
|
+
timers.set(timerKey(args.runId, args.signalId), {
|
|
286
|
+
runId: args.runId,
|
|
287
|
+
workflowId: args.workflowId,
|
|
288
|
+
workflowVersion: args.workflowVersion,
|
|
289
|
+
wakeAt: args.wakeAt,
|
|
290
|
+
signalId: args.signalId,
|
|
291
|
+
})
|
|
292
|
+
updateRun(args.runId, (run) => ({
|
|
293
|
+
...run,
|
|
294
|
+
wakeAt: args.wakeAt,
|
|
295
|
+
updatedAt: args.now,
|
|
296
|
+
}))
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
async claimDueTimers(args: ClaimDueTimersArgs) {
|
|
300
|
+
const due: Array<TimerWakeup> = []
|
|
301
|
+
for (const [key, timer] of timers.entries()) {
|
|
302
|
+
if (due.length >= args.limit) break
|
|
303
|
+
if (timer.wakeAt > args.now) continue
|
|
304
|
+
if (!canClaim(timer.lease, args.leaseOwner, args.now)) continue
|
|
305
|
+
|
|
306
|
+
timers.set(key, {
|
|
307
|
+
...timer,
|
|
308
|
+
lease: lease(args.leaseOwner, args.leaseMs, args.now),
|
|
309
|
+
})
|
|
310
|
+
due.push(cloneTimerWakeup(timer))
|
|
311
|
+
}
|
|
312
|
+
return due
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
async deliverSignal<TPayload>(
|
|
316
|
+
args: DeliverSignalArgs<TPayload>,
|
|
317
|
+
): Promise<DeliverSignalResult> {
|
|
318
|
+
const run = getRun(args.runId)
|
|
319
|
+
if (!run) return { kind: 'not-found' }
|
|
320
|
+
|
|
321
|
+
const key = signalKey(args.runId, args.delivery.signalId)
|
|
322
|
+
if (signalDeliveries.has(key)) return { kind: 'duplicate', run }
|
|
323
|
+
if (run.waitingFor?.signalName !== args.delivery.name) {
|
|
324
|
+
return { kind: 'not-waiting', run }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
signalDeliveries.set(key, true)
|
|
328
|
+
timers.delete(timerKey(args.runId, args.delivery.signalId))
|
|
329
|
+
const updated = updateRun(args.runId, (current) => ({
|
|
330
|
+
...current,
|
|
331
|
+
status: 'queued',
|
|
332
|
+
waitingFor: undefined,
|
|
333
|
+
pendingApproval: undefined,
|
|
334
|
+
wakeAt: undefined,
|
|
335
|
+
updatedAt: args.now,
|
|
336
|
+
}))
|
|
337
|
+
return { kind: 'delivered', run: updated! }
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
async deliverApproval(
|
|
341
|
+
args: DeliverApprovalArgs,
|
|
342
|
+
): Promise<DeliverApprovalResult> {
|
|
343
|
+
const run = getRun(args.runId)
|
|
344
|
+
if (!run) return { kind: 'not-found' }
|
|
345
|
+
|
|
346
|
+
const key = signalKey(args.runId, `approval:${args.approval.approvalId}`)
|
|
347
|
+
if (signalDeliveries.has(key)) return { kind: 'duplicate', run }
|
|
348
|
+
if (run.pendingApproval?.approvalId !== args.approval.approvalId) {
|
|
349
|
+
return { kind: 'not-waiting', run }
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
signalDeliveries.set(key, true)
|
|
353
|
+
const updated = updateRun(args.runId, (current) => ({
|
|
354
|
+
...current,
|
|
355
|
+
status: 'queued',
|
|
356
|
+
waitingFor: undefined,
|
|
357
|
+
pendingApproval: undefined,
|
|
358
|
+
wakeAt: undefined,
|
|
359
|
+
updatedAt: args.now,
|
|
360
|
+
}))
|
|
361
|
+
return { kind: 'delivered', run: updated! }
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
async upsertSchedule(args: UpsertScheduleArgs) {
|
|
365
|
+
schedules.set(args.scheduleId, {
|
|
366
|
+
scheduleId: args.scheduleId,
|
|
367
|
+
workflowId: args.workflowId,
|
|
368
|
+
workflowVersion: args.workflowVersion,
|
|
369
|
+
nextFireAt: args.nextFireAt,
|
|
370
|
+
input: args.input,
|
|
371
|
+
overlapPolicy: args.overlapPolicy,
|
|
372
|
+
enabled: args.enabled,
|
|
373
|
+
})
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
async claimDueScheduleBuckets(args: ClaimDueScheduleBucketsArgs) {
|
|
377
|
+
const due: Array<ScheduleBucket> = []
|
|
378
|
+
for (const schedule of schedules.values()) {
|
|
379
|
+
if (due.length >= args.limit) break
|
|
380
|
+
if (!schedule.enabled || schedule.nextFireAt === undefined) continue
|
|
381
|
+
if (schedule.nextFireAt > args.now) continue
|
|
382
|
+
|
|
383
|
+
const bucketId = `${schedule.nextFireAt}` satisfies ScheduleBucketId
|
|
384
|
+
const key = scheduleBucketKey(schedule.scheduleId, bucketId)
|
|
385
|
+
const existing = scheduleBuckets.get(key)
|
|
386
|
+
if (existing?.status === 'started') continue
|
|
387
|
+
if (existing && !canClaim(existing.lease, args.leaseOwner, args.now)) {
|
|
388
|
+
continue
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const bucket: ScheduleBucketRecord = {
|
|
392
|
+
scheduleId: schedule.scheduleId,
|
|
393
|
+
bucketId,
|
|
394
|
+
workflowId: schedule.workflowId,
|
|
395
|
+
workflowVersion: schedule.workflowVersion,
|
|
396
|
+
runId: `${schedule.workflowId}:${schedule.scheduleId}:${bucketId}`,
|
|
397
|
+
fireAt: schedule.nextFireAt,
|
|
398
|
+
input: schedule.input,
|
|
399
|
+
overlapPolicy: schedule.overlapPolicy,
|
|
400
|
+
status: 'claimed',
|
|
401
|
+
lease: lease(args.leaseOwner, args.leaseMs, args.now),
|
|
402
|
+
}
|
|
403
|
+
scheduleBuckets.set(key, bucket)
|
|
404
|
+
due.push(cloneScheduleBucket(bucket))
|
|
405
|
+
}
|
|
406
|
+
return due
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
async markScheduleBucketStarted(args: MarkScheduleBucketStartedArgs) {
|
|
410
|
+
const key = scheduleBucketKey(args.scheduleId, args.bucketId)
|
|
411
|
+
const bucket = scheduleBuckets.get(key)
|
|
412
|
+
if (!bucket) return
|
|
413
|
+
scheduleBuckets.set(key, {
|
|
414
|
+
...bucket,
|
|
415
|
+
runId: args.runId,
|
|
416
|
+
status: 'started',
|
|
417
|
+
})
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
async claimStaleRuns(args: ClaimStaleRunsArgs) {
|
|
421
|
+
const claims: Array<RunClaim> = []
|
|
422
|
+
for (const run of runs.values()) {
|
|
423
|
+
if (claims.length >= args.limit) break
|
|
424
|
+
if (run.status !== 'running') continue
|
|
425
|
+
if (!run.lease || run.lease.expiresAt > args.now) continue
|
|
426
|
+
|
|
427
|
+
const nextLease = lease(args.leaseOwner, args.leaseMs, args.now)
|
|
428
|
+
const claimed = updateRun(run.runId, (current) => ({
|
|
429
|
+
...current,
|
|
430
|
+
lease: nextLease,
|
|
431
|
+
updatedAt: args.now,
|
|
432
|
+
}))
|
|
433
|
+
if (claimed) claims.push({ run: claimed, lease: cloneLease(nextLease) })
|
|
434
|
+
}
|
|
435
|
+
return claims
|
|
436
|
+
},
|
|
437
|
+
|
|
438
|
+
async listRuns(args: ListRunsArgs) {
|
|
439
|
+
const offset = args.cursor ? Number(args.cursor) : 0
|
|
440
|
+
const start = Number.isFinite(offset) && offset > 0 ? offset : 0
|
|
441
|
+
return Array.from(runs.values())
|
|
442
|
+
.filter((run) => !args.workflowId || run.workflowId === args.workflowId)
|
|
443
|
+
.filter((run) => !args.status || run.status === args.status)
|
|
444
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
445
|
+
.slice(start, start + args.limit)
|
|
446
|
+
.map(toRunSummary)
|
|
447
|
+
},
|
|
448
|
+
|
|
449
|
+
async getRunTimeline(runId: RunId): Promise<RunTimeline | undefined> {
|
|
450
|
+
const run = getRun(runId)
|
|
451
|
+
if (!run) return undefined
|
|
452
|
+
return {
|
|
453
|
+
run,
|
|
454
|
+
events: cloneStoredEvents(logs.get(runId) ?? []),
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function executionFromRunState(
|
|
461
|
+
state: RunState,
|
|
462
|
+
leaseValue?: WorkflowLease,
|
|
463
|
+
): WorkflowExecution {
|
|
464
|
+
return {
|
|
465
|
+
runId: state.runId,
|
|
466
|
+
workflowId: state.workflowId,
|
|
467
|
+
workflowVersion: state.workflowVersion,
|
|
468
|
+
status: state.status,
|
|
469
|
+
input: state.input,
|
|
470
|
+
output: state.output,
|
|
471
|
+
error: state.error,
|
|
472
|
+
waitingFor: state.waitingFor,
|
|
473
|
+
pendingApproval: state.pendingApproval,
|
|
474
|
+
wakeAt:
|
|
475
|
+
state.waitingFor?.signalName === '__timer'
|
|
476
|
+
? state.waitingFor.deadline
|
|
477
|
+
: undefined,
|
|
478
|
+
lease: leaseValue ? cloneLease(leaseValue) : undefined,
|
|
479
|
+
createdAt: state.createdAt,
|
|
480
|
+
updatedAt: state.updatedAt,
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function storeEvent(
|
|
485
|
+
runId: RunId,
|
|
486
|
+
eventIndex: number,
|
|
487
|
+
event: WorkflowEvent,
|
|
488
|
+
): StoredWorkflowEvent {
|
|
489
|
+
return {
|
|
490
|
+
runId,
|
|
491
|
+
eventIndex,
|
|
492
|
+
eventType: event.type,
|
|
493
|
+
stepId: getStepId(event),
|
|
494
|
+
event,
|
|
495
|
+
createdAt: event.ts,
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function getStepId(event: WorkflowEvent) {
|
|
500
|
+
return 'stepId' in event ? event.stepId : undefined
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function lease(owner: LeaseOwner, leaseMs: number, now: number): WorkflowLease {
|
|
504
|
+
return { owner, expiresAt: now + leaseMs }
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function canClaim(
|
|
508
|
+
existing: WorkflowLease | undefined,
|
|
509
|
+
owner: LeaseOwner,
|
|
510
|
+
now: number,
|
|
511
|
+
) {
|
|
512
|
+
return !existing || existing.owner === owner || existing.expiresAt <= now
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function isTerminal(status: WorkflowExecution['status']) {
|
|
516
|
+
return status === 'finished' || status === 'errored' || status === 'aborted'
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function timerKey(runId: RunId, signalId: string) {
|
|
520
|
+
return `${runId}:${signalId}`
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function signalKey(runId: RunId, signalId: string) {
|
|
524
|
+
return `${runId}:${signalId}`
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function scheduleBucketKey(scheduleId: ScheduleId, bucketId: ScheduleBucketId) {
|
|
528
|
+
return `${scheduleId}:${bucketId}`
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function publish(
|
|
532
|
+
subscribers: Map<RunId, Set<(event: WorkflowEvent, index: number) => void>>,
|
|
533
|
+
runId: RunId,
|
|
534
|
+
event: WorkflowEvent,
|
|
535
|
+
index: number,
|
|
536
|
+
) {
|
|
537
|
+
const runSubscribers = subscribers.get(runId)
|
|
538
|
+
if (!runSubscribers) return
|
|
539
|
+
for (const subscriber of runSubscribers) {
|
|
540
|
+
try {
|
|
541
|
+
subscriber(event, index)
|
|
542
|
+
} catch {
|
|
543
|
+
/* Subscriber errors must not break persistence. */
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function toRunSummary(run: WorkflowExecution): RunSummary {
|
|
549
|
+
return {
|
|
550
|
+
runId: run.runId,
|
|
551
|
+
workflowId: run.workflowId,
|
|
552
|
+
workflowVersion: run.workflowVersion,
|
|
553
|
+
status: run.status,
|
|
554
|
+
waitingFor: run.waitingFor,
|
|
555
|
+
pendingApproval: run.pendingApproval,
|
|
556
|
+
wakeAt: run.wakeAt,
|
|
557
|
+
createdAt: run.createdAt,
|
|
558
|
+
updatedAt: run.updatedAt,
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function cloneRun(run: WorkflowExecution): WorkflowExecution {
|
|
563
|
+
return {
|
|
564
|
+
...run,
|
|
565
|
+
waitingFor: run.waitingFor
|
|
566
|
+
? { ...run.waitingFor, meta: cloneRecord(run.waitingFor.meta) }
|
|
567
|
+
: undefined,
|
|
568
|
+
pendingApproval: run.pendingApproval
|
|
569
|
+
? { ...run.pendingApproval }
|
|
570
|
+
: undefined,
|
|
571
|
+
lease: run.lease ? cloneLease(run.lease) : undefined,
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function cloneRunState(state: RunState): RunState {
|
|
576
|
+
return {
|
|
577
|
+
...state,
|
|
578
|
+
waitingFor: state.waitingFor
|
|
579
|
+
? { ...state.waitingFor, meta: cloneRecord(state.waitingFor.meta) }
|
|
580
|
+
: undefined,
|
|
581
|
+
pendingApproval: state.pendingApproval
|
|
582
|
+
? { ...state.pendingApproval }
|
|
583
|
+
: undefined,
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function cloneStoredEvents(
|
|
588
|
+
events: ReadonlyArray<StoredWorkflowEvent>,
|
|
589
|
+
): Array<StoredWorkflowEvent> {
|
|
590
|
+
return events.map((event) => ({ ...event }))
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function cloneTimerWakeup(timer: TimerWakeup): TimerWakeup {
|
|
594
|
+
return { ...timer }
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function cloneScheduleBucket(bucket: ScheduleBucket): ScheduleBucket {
|
|
598
|
+
return { ...bucket }
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function cloneLease(leaseValue: WorkflowLease): WorkflowLease {
|
|
602
|
+
return { ...leaseValue }
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function cloneRecord(value: Record<string, unknown> | undefined) {
|
|
606
|
+
return value ? { ...value } : value
|
|
607
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export { cron, defineWorkflowRuntime, every } from './define-runtime'
|
|
2
|
+
export {
|
|
3
|
+
inMemoryWorkflowExecutionStore,
|
|
4
|
+
type InMemoryWorkflowExecutionStore,
|
|
5
|
+
} from './in-memory-store'
|
|
6
|
+
export { createRuntimeDriver } from './runtime-driver'
|
|
7
|
+
export { createRunStoreAdapter } from './run-store-adapter'
|
|
8
|
+
export { materializeWorkflowSchedules } from './schedule-materializer'
|
|
9
|
+
export type {
|
|
10
|
+
MaterializedWorkflowSchedule,
|
|
11
|
+
MaterializeWorkflowSchedulesOptions,
|
|
12
|
+
} from './schedule-materializer'
|
|
13
|
+
|
|
14
|
+
export type {
|
|
15
|
+
AppendEventsArgs,
|
|
16
|
+
AppendEventsResult,
|
|
17
|
+
ClaimDueScheduleBucketsArgs,
|
|
18
|
+
ClaimDueTimersArgs,
|
|
19
|
+
ClaimRunArgs,
|
|
20
|
+
ClaimRunResult,
|
|
21
|
+
ClaimStaleRunsArgs,
|
|
22
|
+
CreateRunArgs,
|
|
23
|
+
CreateRunResult,
|
|
24
|
+
DeliverApprovalArgs,
|
|
25
|
+
DeliverApprovalResult,
|
|
26
|
+
DeliverSignalArgs,
|
|
27
|
+
DeliverSignalResult,
|
|
28
|
+
HeartbeatRunLeaseArgs,
|
|
29
|
+
LeaseOwner,
|
|
30
|
+
ListRunsArgs,
|
|
31
|
+
LoadedExecution,
|
|
32
|
+
MarkRunErroredArgs,
|
|
33
|
+
MarkRunFinishedArgs,
|
|
34
|
+
MarkRunPausedArgs,
|
|
35
|
+
MarkScheduleBucketStartedArgs,
|
|
36
|
+
ReadEventsArgs,
|
|
37
|
+
ReleaseRunLeaseArgs,
|
|
38
|
+
RunClaim,
|
|
39
|
+
RunId,
|
|
40
|
+
RunSummary,
|
|
41
|
+
RunTimeline,
|
|
42
|
+
SaveRunStateArgs,
|
|
43
|
+
ScheduleBucket,
|
|
44
|
+
ScheduleBucketId,
|
|
45
|
+
ScheduleId,
|
|
46
|
+
ScheduleTimerArgs,
|
|
47
|
+
StoredWorkflowEvent,
|
|
48
|
+
TimerWakeup,
|
|
49
|
+
UpsertScheduleArgs,
|
|
50
|
+
WorkflowExecution,
|
|
51
|
+
WorkflowExecutionStatus,
|
|
52
|
+
WorkflowExecutionStore,
|
|
53
|
+
WorkflowId,
|
|
54
|
+
WorkflowLease,
|
|
55
|
+
WorkflowLoader,
|
|
56
|
+
WorkflowLoaderResult,
|
|
57
|
+
WorkflowOverlapPolicy,
|
|
58
|
+
WorkflowRegistration,
|
|
59
|
+
WorkflowRegistrationMap,
|
|
60
|
+
WorkflowRuntimeConfig,
|
|
61
|
+
WorkflowRuntimeDeliverApprovalArgs,
|
|
62
|
+
WorkflowRuntimeDeliverSignalArgs,
|
|
63
|
+
WorkflowRuntimeDefinition,
|
|
64
|
+
WorkflowRuntimeRunResult,
|
|
65
|
+
WorkflowRuntimeRunResultKind,
|
|
66
|
+
WorkflowRuntimeStartRunArgs,
|
|
67
|
+
WorkflowRuntimeSweepArgs,
|
|
68
|
+
WorkflowRuntimeSweepResult,
|
|
69
|
+
WorkflowRunStoreAdapter,
|
|
70
|
+
WorkflowRunStoreAdapterStore,
|
|
71
|
+
WorkflowScheduleDefinition,
|
|
72
|
+
WorkflowScheduleSpec,
|
|
73
|
+
WorkflowVersion,
|
|
74
|
+
} from './types'
|