@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.
Files changed (47) hide show
  1. package/README.md +22 -0
  2. package/dist/define-runtime.cjs +50 -0
  3. package/dist/define-runtime.cjs.map +1 -0
  4. package/dist/define-runtime.d.cts +16 -0
  5. package/dist/define-runtime.d.ts +16 -0
  6. package/dist/define-runtime.js +48 -0
  7. package/dist/define-runtime.js.map +1 -0
  8. package/dist/in-memory-store.cjs +457 -0
  9. package/dist/in-memory-store.cjs.map +1 -0
  10. package/dist/in-memory-store.d.cts +8 -0
  11. package/dist/in-memory-store.d.ts +8 -0
  12. package/dist/in-memory-store.js +457 -0
  13. package/dist/in-memory-store.js.map +1 -0
  14. package/dist/index.cjs +14 -0
  15. package/dist/index.d.cts +7 -0
  16. package/dist/index.d.ts +7 -0
  17. package/dist/index.js +7 -0
  18. package/dist/run-store-adapter.cjs +30 -0
  19. package/dist/run-store-adapter.cjs.map +1 -0
  20. package/dist/run-store-adapter.d.cts +7 -0
  21. package/dist/run-store-adapter.d.ts +7 -0
  22. package/dist/run-store-adapter.js +29 -0
  23. package/dist/run-store-adapter.js.map +1 -0
  24. package/dist/runtime-driver.cjs +334 -0
  25. package/dist/runtime-driver.cjs.map +1 -0
  26. package/dist/runtime-driver.d.cts +12 -0
  27. package/dist/runtime-driver.d.ts +12 -0
  28. package/dist/runtime-driver.js +334 -0
  29. package/dist/runtime-driver.js.map +1 -0
  30. package/dist/schedule-materializer.cjs +156 -0
  31. package/dist/schedule-materializer.cjs.map +1 -0
  32. package/dist/schedule-materializer.d.cts +28 -0
  33. package/dist/schedule-materializer.d.ts +28 -0
  34. package/dist/schedule-materializer.js +155 -0
  35. package/dist/schedule-materializer.js.map +1 -0
  36. package/dist/types.cjs +0 -0
  37. package/dist/types.d.cts +375 -0
  38. package/dist/types.d.ts +375 -0
  39. package/dist/types.js +1 -0
  40. package/package.json +60 -0
  41. package/src/define-runtime.ts +46 -0
  42. package/src/in-memory-store.ts +607 -0
  43. package/src/index.ts +74 -0
  44. package/src/run-store-adapter.ts +49 -0
  45. package/src/runtime-driver.ts +536 -0
  46. package/src/schedule-materializer.ts +272 -0
  47. package/src/types.ts +462 -0
@@ -0,0 +1,334 @@
1
+ import { createRunStoreAdapter } from "./run-store-adapter.js";
2
+ import { runWorkflow } from "@tanstack/workflow-core";
3
+
4
+ //#region src/runtime-driver.ts
5
+ const DEFAULT_LEASE_MS = 3e4;
6
+ const DEFAULT_SWEEP_LIMIT = 25;
7
+ function createRuntimeDriver(config) {
8
+ return {
9
+ startRun(args) {
10
+ return startRun(config, args);
11
+ },
12
+ deliverSignal(args) {
13
+ return deliverSignal(config, args);
14
+ },
15
+ deliverApproval(args) {
16
+ return deliverApproval(config, args);
17
+ },
18
+ sweep(args = {}) {
19
+ return sweep(config, args);
20
+ }
21
+ };
22
+ }
23
+ async function startRun(config, args) {
24
+ const now = args.now ?? Date.now();
25
+ const workflow = await loadWorkflow(config, args.workflowId);
26
+ const workflowVersion = workflow.version;
27
+ await config.store.createRun({
28
+ runId: args.runId,
29
+ workflowId: args.workflowId,
30
+ workflowVersion,
31
+ input: args.input,
32
+ now
33
+ });
34
+ return driveClaimedRun(config, {
35
+ workflow,
36
+ workflowId: args.workflowId,
37
+ runId: args.runId,
38
+ input: args.input,
39
+ now,
40
+ leaseOwner: args.leaseOwner,
41
+ leaseMs: args.leaseMs,
42
+ threadId: args.threadId,
43
+ includeEvents: args.includeEvents,
44
+ maxEvents: args.maxEvents
45
+ });
46
+ }
47
+ async function deliverSignal(config, args) {
48
+ const now = args.now ?? Date.now();
49
+ const delivery = {
50
+ signalId: args.signalId,
51
+ name: args.name,
52
+ payload: args.payload
53
+ };
54
+ const delivered = await config.store.deliverSignal({
55
+ runId: args.runId,
56
+ delivery,
57
+ now
58
+ });
59
+ if (delivered.kind !== "delivered") return resultFromSignalDelivery(args.runId, delivered);
60
+ return driveClaimedRun(config, {
61
+ workflow: await loadWorkflow(config, delivered.run.workflowId),
62
+ workflowId: delivered.run.workflowId,
63
+ runId: args.runId,
64
+ signalDelivery: delivery,
65
+ now,
66
+ leaseOwner: args.leaseOwner,
67
+ leaseMs: args.leaseMs,
68
+ threadId: args.threadId,
69
+ includeEvents: args.includeEvents,
70
+ maxEvents: args.maxEvents
71
+ });
72
+ }
73
+ async function deliverApproval(config, args) {
74
+ const now = args.now ?? Date.now();
75
+ const delivered = await config.store.deliverApproval({
76
+ runId: args.runId,
77
+ approval: args.approval,
78
+ now
79
+ });
80
+ if (delivered.kind !== "delivered") return resultFromApprovalDelivery(args.runId, delivered);
81
+ return driveClaimedRun(config, {
82
+ workflow: await loadWorkflow(config, delivered.run.workflowId),
83
+ workflowId: delivered.run.workflowId,
84
+ runId: args.runId,
85
+ approval: args.approval,
86
+ now,
87
+ leaseOwner: args.leaseOwner,
88
+ leaseMs: args.leaseMs,
89
+ threadId: args.threadId,
90
+ includeEvents: args.includeEvents,
91
+ maxEvents: args.maxEvents
92
+ });
93
+ }
94
+ async function sweep(config, args) {
95
+ const now = args.now ?? Date.now();
96
+ const startedAt = Date.now();
97
+ const maxScheduledRuns = normalizeSweepLimit(args.maxScheduledRuns ?? args.limit, DEFAULT_SWEEP_LIMIT, "maxScheduledRuns");
98
+ const maxTimers = normalizeSweepLimit(args.maxTimers ?? args.limit, DEFAULT_SWEEP_LIMIT, "maxTimers");
99
+ const leaseOwner = args.leaseOwner ?? `sweep:${now}`;
100
+ const leaseMs = args.leaseMs ?? config.defaultLeaseMs ?? DEFAULT_LEASE_MS;
101
+ const scheduled = [];
102
+ const timers = [];
103
+ let deadlineReached = false;
104
+ while (scheduled.length < maxScheduledRuns) {
105
+ if (isPastSweepDeadline(startedAt, args.maxDurationMs)) {
106
+ deadlineReached = true;
107
+ break;
108
+ }
109
+ const bucket = (await config.store.claimDueScheduleBuckets({
110
+ now,
111
+ limit: 1,
112
+ leaseOwner,
113
+ leaseMs
114
+ }))[0];
115
+ if (!bucket) break;
116
+ const result = await startRun(config, {
117
+ workflowId: bucket.workflowId,
118
+ runId: bucket.runId,
119
+ input: bucket.input,
120
+ now,
121
+ leaseOwner,
122
+ leaseMs,
123
+ includeEvents: args.includeEvents,
124
+ maxEvents: args.maxEvents
125
+ });
126
+ if (result.kind !== "not-claimable" && result.kind !== "not-found") await config.store.markScheduleBucketStarted({
127
+ scheduleId: bucket.scheduleId,
128
+ bucketId: bucket.bucketId,
129
+ runId: bucket.runId,
130
+ now
131
+ });
132
+ scheduled.push(result);
133
+ }
134
+ while (timers.length < maxTimers) {
135
+ if (isPastSweepDeadline(startedAt, args.maxDurationMs)) {
136
+ deadlineReached = true;
137
+ break;
138
+ }
139
+ const timer = (await config.store.claimDueTimers({
140
+ now,
141
+ limit: 1,
142
+ leaseOwner,
143
+ leaseMs
144
+ }))[0];
145
+ if (!timer) break;
146
+ timers.push(await deliverTimer(config, {
147
+ timer,
148
+ now,
149
+ leaseOwner,
150
+ leaseMs,
151
+ includeEvents: args.includeEvents,
152
+ maxEvents: args.maxEvents
153
+ }));
154
+ }
155
+ return {
156
+ scheduled,
157
+ timers,
158
+ summary: summarizeSweep(scheduled, timers),
159
+ deadlineReached,
160
+ remainingMayExist: deadlineReached || scheduled.length >= maxScheduledRuns || timers.length >= maxTimers
161
+ };
162
+ }
163
+ async function deliverTimer(config, args) {
164
+ return deliverSignal(config, {
165
+ runId: args.timer.runId,
166
+ signalId: args.timer.signalId,
167
+ name: "__timer",
168
+ payload: void 0,
169
+ now: args.now,
170
+ leaseOwner: args.leaseOwner,
171
+ leaseMs: args.leaseMs,
172
+ includeEvents: args.includeEvents,
173
+ maxEvents: args.maxEvents
174
+ });
175
+ }
176
+ async function driveClaimedRun(config, args) {
177
+ const leaseOwner = args.leaseOwner ?? `runtime:${args.runId}`;
178
+ const leaseMs = args.leaseMs ?? config.defaultLeaseMs ?? DEFAULT_LEASE_MS;
179
+ const claim = await config.store.claimRun({
180
+ runId: args.runId,
181
+ leaseOwner,
182
+ leaseMs,
183
+ now: args.now
184
+ });
185
+ if (claim.kind === "not-found") return {
186
+ kind: "not-found",
187
+ runId: args.runId,
188
+ workflowId: args.workflowId,
189
+ eventCount: 0,
190
+ events: []
191
+ };
192
+ if (claim.kind === "not-claimable") return {
193
+ kind: "not-claimable",
194
+ runId: args.runId,
195
+ workflowId: args.workflowId,
196
+ run: claim.run,
197
+ eventCount: 0,
198
+ events: []
199
+ };
200
+ const runStore = createRunStoreAdapter(config.store);
201
+ const collected = await collectWorkflowEvents(runWorkflow({
202
+ workflow: args.workflow,
203
+ runStore,
204
+ runId: args.runId,
205
+ input: args.input,
206
+ signalDelivery: args.signalDelivery,
207
+ approval: args.approval,
208
+ threadId: args.threadId
209
+ }), {
210
+ includeEvents: args.includeEvents ?? true,
211
+ maxEvents: args.maxEvents
212
+ });
213
+ await syncTimerFromRunState(config, args.runId, args.workflowId, args.now);
214
+ await config.store.releaseRunLease({
215
+ runId: args.runId,
216
+ leaseOwner
217
+ });
218
+ const run = await config.store.loadRun(args.runId);
219
+ return {
220
+ kind: classifyRun(run, collected.eventCount),
221
+ runId: args.runId,
222
+ workflowId: args.workflowId,
223
+ run,
224
+ events: collected.events,
225
+ eventCount: collected.eventCount,
226
+ eventsTruncated: collected.eventsTruncated || void 0
227
+ };
228
+ }
229
+ async function syncTimerFromRunState(config, runId, workflowId, now) {
230
+ const state = await config.store.loadRunState(runId);
231
+ const deadline = state?.waitingFor?.deadline;
232
+ if (state?.waitingFor?.signalName !== "__timer" || deadline === void 0) return;
233
+ await config.store.scheduleTimer({
234
+ runId,
235
+ workflowId,
236
+ workflowVersion: state.workflowVersion,
237
+ wakeAt: deadline,
238
+ signalId: `timer:${runId}:${deadline}`,
239
+ now
240
+ });
241
+ }
242
+ async function loadWorkflow(config, workflowId) {
243
+ const registration = config.workflows[workflowId];
244
+ if (!registration) throw new Error(`Workflow "${workflowId}" is not registered.`);
245
+ const workflow = normalizeWorkflowLoaderResult(await registration.load());
246
+ const previousVersions = [];
247
+ for (const loadPrevious of Object.values(registration.previousVersions ?? {})) previousVersions.push(normalizeWorkflowLoaderResult(await loadPrevious()));
248
+ if (registration.version || previousVersions.length > 0) return {
249
+ ...workflow,
250
+ version: registration.version ?? workflow.version,
251
+ previousVersions: [...workflow.previousVersions ?? [], ...previousVersions]
252
+ };
253
+ return workflow;
254
+ }
255
+ function normalizeWorkflowLoaderResult(result) {
256
+ if ("__kind" in result) return result;
257
+ if ("default" in result) return result.default;
258
+ return result.workflow;
259
+ }
260
+ function resultFromSignalDelivery(runId, result) {
261
+ return {
262
+ kind: result.kind,
263
+ runId,
264
+ run: "run" in result ? result.run : void 0,
265
+ workflowId: "run" in result ? result.run.workflowId : void 0,
266
+ events: [],
267
+ eventCount: 0
268
+ };
269
+ }
270
+ function resultFromApprovalDelivery(runId, result) {
271
+ return {
272
+ kind: result.kind,
273
+ runId,
274
+ run: "run" in result ? result.run : void 0,
275
+ workflowId: "run" in result ? result.run.workflowId : void 0,
276
+ events: [],
277
+ eventCount: 0
278
+ };
279
+ }
280
+ function classifyRun(run, eventCount) {
281
+ if (run?.status === "finished") return "completed";
282
+ if (run?.status === "paused") return "paused";
283
+ if (run?.status === "errored" || run?.status === "aborted") return "errored";
284
+ if (run?.status === "running" || run?.status === "queued") return "running";
285
+ return eventCount > 0 ? "running" : "not-found";
286
+ }
287
+ function normalizeSweepLimit(value, fallback, label) {
288
+ const limit = value ?? fallback;
289
+ if (!Number.isInteger(limit) || limit < 0) throw new Error(`Workflow sweep ${label} must be a non-negative integer.`);
290
+ return limit;
291
+ }
292
+ function isPastSweepDeadline(startedAt, maxDurationMs) {
293
+ return maxDurationMs !== void 0 && Date.now() - startedAt >= maxDurationMs;
294
+ }
295
+ function summarizeSweep(scheduled, timers) {
296
+ return {
297
+ scheduled: countRunKinds(scheduled),
298
+ timers: countRunKinds(timers),
299
+ eventCount: sumEventCounts(scheduled) + sumEventCounts(timers),
300
+ returnedEventCount: sumReturnedEventCounts(scheduled) + sumReturnedEventCounts(timers)
301
+ };
302
+ }
303
+ function countRunKinds(runs) {
304
+ const counts = {};
305
+ for (const run of runs) counts[run.kind] = (counts[run.kind] ?? 0) + 1;
306
+ return counts;
307
+ }
308
+ function sumEventCounts(runs) {
309
+ return runs.reduce((sum, run) => sum + run.eventCount, 0);
310
+ }
311
+ function sumReturnedEventCounts(runs) {
312
+ return runs.reduce((sum, run) => sum + run.events.length, 0);
313
+ }
314
+ async function collectWorkflowEvents(iterable, options) {
315
+ if (options.maxEvents !== void 0 && (!Number.isInteger(options.maxEvents) || options.maxEvents < 0)) throw new Error("Workflow event collection maxEvents must be a non-negative integer.");
316
+ const events = [];
317
+ let eventCount = 0;
318
+ let eventsTruncated = false;
319
+ for await (const event of iterable) {
320
+ eventCount++;
321
+ if (!options.includeEvents) continue;
322
+ if (options.maxEvents === void 0 || events.length < options.maxEvents) events.push(event);
323
+ else eventsTruncated = true;
324
+ }
325
+ return {
326
+ events,
327
+ eventCount,
328
+ eventsTruncated
329
+ };
330
+ }
331
+
332
+ //#endregion
333
+ export { createRuntimeDriver };
334
+ //# sourceMappingURL=runtime-driver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-driver.js","names":[],"sources":["../src/runtime-driver.ts"],"sourcesContent":["import { runWorkflow } from '@tanstack/workflow-core'\nimport { createRunStoreAdapter } from './run-store-adapter'\nimport type {\n AnyWorkflowDefinition,\n WorkflowEvent,\n} from '@tanstack/workflow-core'\nimport type {\n DeliverApprovalResult,\n DeliverSignalResult,\n TimerWakeup,\n WorkflowExecution,\n WorkflowRegistration,\n WorkflowRuntimeConfig,\n WorkflowRuntimeDeliverApprovalArgs,\n WorkflowRuntimeDeliverSignalArgs,\n WorkflowRuntimeRunResult,\n WorkflowRuntimeRunResultKind,\n WorkflowRuntimeStartRunArgs,\n WorkflowRuntimeSweepArgs,\n WorkflowRuntimeSweepResult,\n} from './types'\n\nconst DEFAULT_LEASE_MS = 30_000\nconst DEFAULT_SWEEP_LIMIT = 25\n\nexport function createRuntimeDriver<\n TWorkflows extends Record<string, WorkflowRegistration>,\n>(config: WorkflowRuntimeConfig<TWorkflows>) {\n return {\n startRun(args: WorkflowRuntimeStartRunArgs) {\n return startRun(config, args)\n },\n deliverSignal<TPayload = unknown>(\n args: WorkflowRuntimeDeliverSignalArgs<TPayload>,\n ) {\n return deliverSignal(config, args)\n },\n deliverApproval(args: WorkflowRuntimeDeliverApprovalArgs) {\n return deliverApproval(config, args)\n },\n sweep(args: WorkflowRuntimeSweepArgs = {}) {\n return sweep(config, args)\n },\n }\n}\n\nasync function startRun<\n TWorkflows extends Record<string, WorkflowRegistration>,\n>(\n config: WorkflowRuntimeConfig<TWorkflows>,\n args: WorkflowRuntimeStartRunArgs,\n): Promise<WorkflowRuntimeRunResult> {\n const now = args.now ?? Date.now()\n const workflow = await loadWorkflow(config, args.workflowId)\n const workflowVersion = workflow.version\n await config.store.createRun({\n runId: args.runId,\n workflowId: args.workflowId,\n workflowVersion,\n input: args.input,\n now,\n })\n\n return driveClaimedRun(config, {\n workflow,\n workflowId: args.workflowId,\n runId: args.runId,\n input: args.input,\n now,\n leaseOwner: args.leaseOwner,\n leaseMs: args.leaseMs,\n threadId: args.threadId,\n includeEvents: args.includeEvents,\n maxEvents: args.maxEvents,\n })\n}\n\nasync function deliverSignal<\n TWorkflows extends Record<string, WorkflowRegistration>,\n TPayload,\n>(\n config: WorkflowRuntimeConfig<TWorkflows>,\n args: WorkflowRuntimeDeliverSignalArgs<TPayload>,\n): Promise<WorkflowRuntimeRunResult> {\n const now = args.now ?? Date.now()\n const delivery = {\n signalId: args.signalId,\n name: args.name,\n payload: args.payload,\n }\n const delivered = await config.store.deliverSignal({\n runId: args.runId,\n delivery,\n now,\n })\n if (delivered.kind !== 'delivered') {\n return resultFromSignalDelivery(args.runId, delivered)\n }\n\n const workflow = await loadWorkflow(config, delivered.run.workflowId)\n return driveClaimedRun(config, {\n workflow,\n workflowId: delivered.run.workflowId,\n runId: args.runId,\n signalDelivery: delivery,\n now,\n leaseOwner: args.leaseOwner,\n leaseMs: args.leaseMs,\n threadId: args.threadId,\n includeEvents: args.includeEvents,\n maxEvents: args.maxEvents,\n })\n}\n\nasync function deliverApproval<\n TWorkflows extends Record<string, WorkflowRegistration>,\n>(\n config: WorkflowRuntimeConfig<TWorkflows>,\n args: WorkflowRuntimeDeliverApprovalArgs,\n): Promise<WorkflowRuntimeRunResult> {\n const now = args.now ?? Date.now()\n const delivered = await config.store.deliverApproval({\n runId: args.runId,\n approval: args.approval,\n now,\n })\n if (delivered.kind !== 'delivered') {\n return resultFromApprovalDelivery(args.runId, delivered)\n }\n\n const workflow = await loadWorkflow(config, delivered.run.workflowId)\n return driveClaimedRun(config, {\n workflow,\n workflowId: delivered.run.workflowId,\n runId: args.runId,\n approval: args.approval,\n now,\n leaseOwner: args.leaseOwner,\n leaseMs: args.leaseMs,\n threadId: args.threadId,\n includeEvents: args.includeEvents,\n maxEvents: args.maxEvents,\n })\n}\n\nasync function sweep<TWorkflows extends Record<string, WorkflowRegistration>>(\n config: WorkflowRuntimeConfig<TWorkflows>,\n args: WorkflowRuntimeSweepArgs,\n): Promise<WorkflowRuntimeSweepResult> {\n const now = args.now ?? Date.now()\n const startedAt = Date.now()\n const maxScheduledRuns = normalizeSweepLimit(\n args.maxScheduledRuns ?? args.limit,\n DEFAULT_SWEEP_LIMIT,\n 'maxScheduledRuns',\n )\n const maxTimers = normalizeSweepLimit(\n args.maxTimers ?? args.limit,\n DEFAULT_SWEEP_LIMIT,\n 'maxTimers',\n )\n const leaseOwner = args.leaseOwner ?? `sweep:${now}`\n const leaseMs = args.leaseMs ?? config.defaultLeaseMs ?? DEFAULT_LEASE_MS\n const scheduled: Array<WorkflowRuntimeRunResult> = []\n const timers: Array<WorkflowRuntimeRunResult> = []\n let deadlineReached = false\n\n while (scheduled.length < maxScheduledRuns) {\n if (isPastSweepDeadline(startedAt, args.maxDurationMs)) {\n deadlineReached = true\n break\n }\n\n const buckets = await config.store.claimDueScheduleBuckets({\n now,\n limit: 1,\n leaseOwner,\n leaseMs,\n })\n const bucket = buckets[0]\n if (!bucket) break\n\n const result = await startRun(config, {\n workflowId: bucket.workflowId,\n runId: bucket.runId,\n input: bucket.input,\n now,\n leaseOwner,\n leaseMs,\n includeEvents: args.includeEvents,\n maxEvents: args.maxEvents,\n })\n if (result.kind !== 'not-claimable' && result.kind !== 'not-found') {\n await config.store.markScheduleBucketStarted({\n scheduleId: bucket.scheduleId,\n bucketId: bucket.bucketId,\n runId: bucket.runId,\n now,\n })\n }\n scheduled.push(result)\n }\n\n while (timers.length < maxTimers) {\n if (isPastSweepDeadline(startedAt, args.maxDurationMs)) {\n deadlineReached = true\n break\n }\n\n const dueTimers = await config.store.claimDueTimers({\n now,\n limit: 1,\n leaseOwner,\n leaseMs,\n })\n const timer = dueTimers[0]\n if (!timer) break\n\n timers.push(\n await deliverTimer(config, {\n timer,\n now,\n leaseOwner,\n leaseMs,\n includeEvents: args.includeEvents,\n maxEvents: args.maxEvents,\n }),\n )\n }\n\n return {\n scheduled,\n timers,\n summary: summarizeSweep(scheduled, timers),\n deadlineReached,\n remainingMayExist:\n deadlineReached ||\n scheduled.length >= maxScheduledRuns ||\n timers.length >= maxTimers,\n }\n}\n\nasync function deliverTimer<\n TWorkflows extends Record<string, WorkflowRegistration>,\n>(\n config: WorkflowRuntimeConfig<TWorkflows>,\n args: {\n timer: TimerWakeup\n now: number\n leaseOwner: string\n leaseMs: number\n includeEvents?: boolean\n maxEvents?: number\n },\n) {\n return deliverSignal(config, {\n runId: args.timer.runId,\n signalId: args.timer.signalId,\n name: '__timer',\n payload: undefined,\n now: args.now,\n leaseOwner: args.leaseOwner,\n leaseMs: args.leaseMs,\n includeEvents: args.includeEvents,\n maxEvents: args.maxEvents,\n })\n}\n\nasync function driveClaimedRun<\n TWorkflows extends Record<string, WorkflowRegistration>,\n>(\n config: WorkflowRuntimeConfig<TWorkflows>,\n args: {\n workflow: AnyWorkflowDefinition\n workflowId: string\n runId: string\n input?: unknown\n signalDelivery?: Parameters<typeof runWorkflow>[0]['signalDelivery']\n approval?: Parameters<typeof runWorkflow>[0]['approval']\n now: number\n leaseOwner?: string\n leaseMs?: number\n threadId?: string\n includeEvents?: boolean\n maxEvents?: number\n },\n): Promise<WorkflowRuntimeRunResult> {\n const leaseOwner = args.leaseOwner ?? `runtime:${args.runId}`\n const leaseMs = args.leaseMs ?? config.defaultLeaseMs ?? DEFAULT_LEASE_MS\n const claim = await config.store.claimRun({\n runId: args.runId,\n leaseOwner,\n leaseMs,\n now: args.now,\n })\n\n if (claim.kind === 'not-found') {\n return {\n kind: 'not-found',\n runId: args.runId,\n workflowId: args.workflowId,\n eventCount: 0,\n events: [],\n }\n }\n if (claim.kind === 'not-claimable') {\n return {\n kind: 'not-claimable',\n runId: args.runId,\n workflowId: args.workflowId,\n run: claim.run,\n eventCount: 0,\n events: [],\n }\n }\n\n const runStore = createRunStoreAdapter(config.store)\n const collected = await collectWorkflowEvents(\n runWorkflow({\n workflow: args.workflow,\n runStore,\n runId: args.runId,\n input: args.input,\n signalDelivery: args.signalDelivery,\n approval: args.approval,\n threadId: args.threadId,\n }),\n {\n includeEvents: args.includeEvents ?? true,\n maxEvents: args.maxEvents,\n },\n )\n\n await syncTimerFromRunState(config, args.runId, args.workflowId, args.now)\n await config.store.releaseRunLease({ runId: args.runId, leaseOwner })\n\n const run = await config.store.loadRun(args.runId)\n return {\n kind: classifyRun(run, collected.eventCount),\n runId: args.runId,\n workflowId: args.workflowId,\n run,\n events: collected.events,\n eventCount: collected.eventCount,\n eventsTruncated: collected.eventsTruncated || undefined,\n }\n}\n\nasync function syncTimerFromRunState<\n TWorkflows extends Record<string, WorkflowRegistration>,\n>(\n config: WorkflowRuntimeConfig<TWorkflows>,\n runId: string,\n workflowId: string,\n now: number,\n) {\n const state = await config.store.loadRunState(runId)\n const deadline = state?.waitingFor?.deadline\n if (state?.waitingFor?.signalName !== '__timer' || deadline === undefined) {\n return\n }\n\n await config.store.scheduleTimer({\n runId,\n workflowId,\n workflowVersion: state.workflowVersion,\n wakeAt: deadline,\n signalId: `timer:${runId}:${deadline}`,\n now,\n })\n}\n\nasync function loadWorkflow<\n TWorkflows extends Record<string, WorkflowRegistration>,\n>(\n config: WorkflowRuntimeConfig<TWorkflows>,\n workflowId: string,\n): Promise<AnyWorkflowDefinition> {\n const registration = config.workflows[workflowId]\n if (!registration) {\n throw new Error(`Workflow \"${workflowId}\" is not registered.`)\n }\n\n const workflow = normalizeWorkflowLoaderResult(await registration.load())\n const previousVersions = []\n for (const loadPrevious of Object.values(\n registration.previousVersions ?? {},\n )) {\n previousVersions.push(normalizeWorkflowLoaderResult(await loadPrevious()))\n }\n\n if (registration.version || previousVersions.length > 0) {\n return {\n ...workflow,\n version: registration.version ?? workflow.version,\n previousVersions: [\n ...(workflow.previousVersions ?? []),\n ...previousVersions,\n ],\n }\n }\n\n return workflow\n}\n\nfunction normalizeWorkflowLoaderResult(\n result: Awaited<ReturnType<WorkflowRegistration['load']>>,\n): AnyWorkflowDefinition {\n if ('__kind' in result) return result\n if ('default' in result) return result.default\n return result.workflow\n}\n\nfunction resultFromSignalDelivery(\n runId: string,\n result: Exclude<DeliverSignalResult, { kind: 'delivered' }>,\n): WorkflowRuntimeRunResult {\n return {\n kind: result.kind,\n runId,\n run: 'run' in result ? result.run : undefined,\n workflowId: 'run' in result ? result.run.workflowId : undefined,\n events: [],\n eventCount: 0,\n }\n}\n\nfunction resultFromApprovalDelivery(\n runId: string,\n result: Exclude<DeliverApprovalResult, { kind: 'delivered' }>,\n): WorkflowRuntimeRunResult {\n return {\n kind: result.kind,\n runId,\n run: 'run' in result ? result.run : undefined,\n workflowId: 'run' in result ? result.run.workflowId : undefined,\n events: [],\n eventCount: 0,\n }\n}\n\nfunction classifyRun(\n run: WorkflowExecution | undefined,\n eventCount: number,\n): WorkflowRuntimeRunResult['kind'] {\n if (run?.status === 'finished') return 'completed'\n if (run?.status === 'paused') return 'paused'\n if (run?.status === 'errored' || run?.status === 'aborted') return 'errored'\n if (run?.status === 'running' || run?.status === 'queued') return 'running'\n return eventCount > 0 ? 'running' : 'not-found'\n}\n\nfunction normalizeSweepLimit(\n value: number | undefined,\n fallback: number,\n label: string,\n) {\n const limit = value ?? fallback\n if (!Number.isInteger(limit) || limit < 0) {\n throw new Error(`Workflow sweep ${label} must be a non-negative integer.`)\n }\n return limit\n}\n\nfunction isPastSweepDeadline(\n startedAt: number,\n maxDurationMs: number | undefined,\n) {\n return maxDurationMs !== undefined && Date.now() - startedAt >= maxDurationMs\n}\n\nfunction summarizeSweep(\n scheduled: ReadonlyArray<WorkflowRuntimeRunResult>,\n timers: ReadonlyArray<WorkflowRuntimeRunResult>,\n): WorkflowRuntimeSweepResult['summary'] {\n return {\n scheduled: countRunKinds(scheduled),\n timers: countRunKinds(timers),\n eventCount: sumEventCounts(scheduled) + sumEventCounts(timers),\n returnedEventCount:\n sumReturnedEventCounts(scheduled) + sumReturnedEventCounts(timers),\n }\n}\n\nfunction countRunKinds(runs: ReadonlyArray<WorkflowRuntimeRunResult>) {\n const counts: Partial<Record<WorkflowRuntimeRunResultKind, number>> = {}\n for (const run of runs) {\n counts[run.kind] = (counts[run.kind] ?? 0) + 1\n }\n return counts\n}\n\nfunction sumEventCounts(runs: ReadonlyArray<WorkflowRuntimeRunResult>) {\n return runs.reduce((sum, run) => sum + run.eventCount, 0)\n}\n\nfunction sumReturnedEventCounts(runs: ReadonlyArray<WorkflowRuntimeRunResult>) {\n return runs.reduce((sum, run) => sum + run.events.length, 0)\n}\n\nasync function collectWorkflowEvents(\n iterable: AsyncIterable<WorkflowEvent>,\n options: {\n includeEvents: boolean\n maxEvents?: number\n },\n) {\n if (\n options.maxEvents !== undefined &&\n (!Number.isInteger(options.maxEvents) || options.maxEvents < 0)\n ) {\n throw new Error(\n 'Workflow event collection maxEvents must be a non-negative integer.',\n )\n }\n\n const events: Array<WorkflowEvent> = []\n let eventCount = 0\n let eventsTruncated = false\n\n for await (const event of iterable) {\n eventCount++\n if (!options.includeEvents) continue\n if (options.maxEvents === undefined || events.length < options.maxEvents) {\n events.push(event)\n } else {\n eventsTruncated = true\n }\n }\n\n return {\n events,\n eventCount,\n eventsTruncated,\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,156 @@
1
+
2
+ //#region src/schedule-materializer.ts
3
+ const DEFAULT_CRON_LOOKBACK_MS = 768 * 60 * 60 * 1e3;
4
+ async function materializeWorkflowSchedules(runtime, options = {}) {
5
+ const now = options.now ?? Date.now();
6
+ const cronLookbackMs = options.cronLookbackMs ?? DEFAULT_CRON_LOOKBACK_MS;
7
+ const materialized = [];
8
+ if (!Number.isFinite(cronLookbackMs) || cronLookbackMs < 0) throw new Error("Workflow cron lookback must be a non-negative number.");
9
+ for (const [workflowId, registration] of Object.entries(runtime.workflows)) {
10
+ const schedules = registration.schedules ?? [];
11
+ for (let index = 0; index < schedules.length; index++) {
12
+ const definition = schedules[index];
13
+ const scheduleId = getScheduleId(workflowId, definition, index);
14
+ if (definition.enabled === false) {
15
+ await runtime.store.upsertSchedule({
16
+ scheduleId,
17
+ workflowId,
18
+ workflowVersion: registration.version,
19
+ schedule: definition.schedule,
20
+ overlapPolicy: definition.overlapPolicy ?? "skip",
21
+ input: void 0,
22
+ nextFireAt: void 0,
23
+ enabled: false,
24
+ now
25
+ });
26
+ materialized.push({
27
+ kind: "disabled",
28
+ workflowId,
29
+ scheduleId,
30
+ schedule: definition.schedule
31
+ });
32
+ continue;
33
+ }
34
+ const fireAt = getDueFireAt(definition.schedule, now, cronLookbackMs);
35
+ if (fireAt === void 0) {
36
+ materialized.push({
37
+ kind: "not-due",
38
+ workflowId,
39
+ scheduleId,
40
+ schedule: definition.schedule
41
+ });
42
+ continue;
43
+ }
44
+ await runtime.store.upsertSchedule({
45
+ scheduleId,
46
+ workflowId,
47
+ workflowVersion: registration.version,
48
+ schedule: definition.schedule,
49
+ overlapPolicy: definition.overlapPolicy ?? "skip",
50
+ input: await resolveScheduleInput(definition.input),
51
+ nextFireAt: fireAt,
52
+ enabled: true,
53
+ now
54
+ });
55
+ materialized.push({
56
+ kind: "materialized",
57
+ workflowId,
58
+ scheduleId,
59
+ fireAt,
60
+ schedule: definition.schedule
61
+ });
62
+ }
63
+ }
64
+ return materialized;
65
+ }
66
+ function getScheduleId(workflowId, definition, index) {
67
+ return definition.id ?? `${workflowId}:${index}`;
68
+ }
69
+ async function resolveScheduleInput(input) {
70
+ return typeof input === "function" ? await input() : input;
71
+ }
72
+ function getDueFireAt(schedule, now, cronLookbackMs) {
73
+ if (schedule.kind === "interval") {
74
+ if (!Number.isFinite(schedule.everyMs) || schedule.everyMs <= 0) throw new Error("Interval workflow schedules must use a positive everyMs.");
75
+ return Math.floor(now / schedule.everyMs) * schedule.everyMs;
76
+ }
77
+ return getPreviousCronFireAt(schedule, now, cronLookbackMs);
78
+ }
79
+ function getPreviousCronFireAt(schedule, now, lookbackMs) {
80
+ if (schedule.timezone && schedule.timezone !== "UTC") throw new Error(`Workflow cron schedules are materialized in UTC. Received timezone "${schedule.timezone}".`);
81
+ const cron = parseCronExpression(schedule.expression);
82
+ const start = floorToMinute(now);
83
+ const end = start - lookbackMs;
84
+ for (let timestamp = start; timestamp >= end; timestamp -= 6e4) if (matchesCron(cron, new Date(timestamp))) return timestamp;
85
+ }
86
+ function parseCronExpression(expression) {
87
+ const fields = expression.trim().split(/\s+/);
88
+ if (fields.length !== 5) throw new Error(`Workflow cron schedules must use five fields. Received "${expression}".`);
89
+ return {
90
+ minute: parseCronField(fields[0], 0, 59),
91
+ hour: parseCronField(fields[1], 0, 23),
92
+ dayOfMonth: parseCronField(fields[2], 1, 31),
93
+ month: parseCronField(fields[3], 1, 12),
94
+ dayOfWeek: parseCronField(fields[4], 0, 7, normalizeDayOfWeek)
95
+ };
96
+ }
97
+ function parseCronField(field, min, max, normalize = (value) => value) {
98
+ const values = /* @__PURE__ */ new Set();
99
+ const parts = field.split(",");
100
+ for (const part of parts) {
101
+ const [rangePart, stepPart] = part.split("/");
102
+ const step = stepPart === void 0 ? 1 : Number(stepPart);
103
+ if (!Number.isInteger(step) || step <= 0) throw new Error(`Invalid cron step "${part}".`);
104
+ const range = parseCronRange(rangePart, min, max);
105
+ for (let value = range.start; value <= range.end; value += step) values.add(normalize(value));
106
+ }
107
+ return {
108
+ wildcard: field === "*",
109
+ values
110
+ };
111
+ }
112
+ function parseCronRange(range, min, max) {
113
+ if (range === "*") return {
114
+ start: min,
115
+ end: max
116
+ };
117
+ const bounds = range.split("-");
118
+ if (bounds.length === 1) {
119
+ const value = parseCronNumber(bounds[0], min, max);
120
+ return {
121
+ start: value,
122
+ end: value
123
+ };
124
+ }
125
+ if (bounds.length === 2) {
126
+ const start = parseCronNumber(bounds[0], min, max);
127
+ const end = parseCronNumber(bounds[1], min, max);
128
+ if (end < start) throw new Error(`Invalid cron range "${range}".`);
129
+ return {
130
+ start,
131
+ end
132
+ };
133
+ }
134
+ throw new Error(`Invalid cron range "${range}".`);
135
+ }
136
+ function parseCronNumber(value, min, max) {
137
+ const parsed = Number(value);
138
+ if (!Number.isInteger(parsed) || parsed < min || parsed > max) throw new Error(`Invalid cron value "${value}".`);
139
+ return parsed;
140
+ }
141
+ function normalizeDayOfWeek(value) {
142
+ return value === 7 ? 0 : value;
143
+ }
144
+ function matchesCron(cron, date) {
145
+ const dayOfMonthMatches = cron.dayOfMonth.values.has(date.getUTCDate());
146
+ const dayOfWeekMatches = cron.dayOfWeek.values.has(date.getUTCDay());
147
+ const dayMatches = !cron.dayOfMonth.wildcard && !cron.dayOfWeek.wildcard ? dayOfMonthMatches || dayOfWeekMatches : dayOfMonthMatches && dayOfWeekMatches;
148
+ return cron.minute.values.has(date.getUTCMinutes()) && cron.hour.values.has(date.getUTCHours()) && dayMatches && cron.month.values.has(date.getUTCMonth() + 1);
149
+ }
150
+ function floorToMinute(timestamp) {
151
+ return Math.floor(timestamp / 6e4) * 6e4;
152
+ }
153
+
154
+ //#endregion
155
+ exports.materializeWorkflowSchedules = materializeWorkflowSchedules;
156
+ //# sourceMappingURL=schedule-materializer.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule-materializer.cjs","names":[],"sources":["../src/schedule-materializer.ts"],"sourcesContent":["import type {\n ScheduleId,\n WorkflowRegistrationMap,\n WorkflowRuntimeDefinition,\n WorkflowScheduleDefinition,\n WorkflowScheduleSpec,\n} from './types'\n\nconst DEFAULT_CRON_LOOKBACK_MS = 32 * 24 * 60 * 60 * 1000\n\nexport interface MaterializeWorkflowSchedulesOptions {\n now?: number\n cronLookbackMs?: number\n}\n\nexport type MaterializedWorkflowSchedule =\n | {\n kind: 'materialized'\n workflowId: string\n scheduleId: ScheduleId\n fireAt: number\n schedule: WorkflowScheduleSpec\n }\n | {\n kind: 'disabled'\n workflowId: string\n scheduleId: ScheduleId\n schedule: WorkflowScheduleSpec\n }\n | {\n kind: 'not-due'\n workflowId: string\n scheduleId: ScheduleId\n schedule: WorkflowScheduleSpec\n }\n\nexport async function materializeWorkflowSchedules<\n TWorkflows extends WorkflowRegistrationMap,\n>(\n runtime: WorkflowRuntimeDefinition<TWorkflows>,\n options: MaterializeWorkflowSchedulesOptions = {},\n): Promise<Array<MaterializedWorkflowSchedule>> {\n const now = options.now ?? Date.now()\n const cronLookbackMs = options.cronLookbackMs ?? DEFAULT_CRON_LOOKBACK_MS\n const materialized: Array<MaterializedWorkflowSchedule> = []\n\n if (!Number.isFinite(cronLookbackMs) || cronLookbackMs < 0) {\n throw new Error('Workflow cron lookback must be a non-negative number.')\n }\n\n for (const [workflowId, registration] of Object.entries(runtime.workflows)) {\n const schedules = registration.schedules ?? []\n for (let index = 0; index < schedules.length; index++) {\n const definition = schedules[index]!\n const scheduleId = getScheduleId(workflowId, definition, index)\n\n if (definition.enabled === false) {\n await runtime.store.upsertSchedule({\n scheduleId,\n workflowId,\n workflowVersion: registration.version,\n schedule: definition.schedule,\n overlapPolicy: definition.overlapPolicy ?? 'skip',\n input: undefined,\n nextFireAt: undefined,\n enabled: false,\n now,\n })\n materialized.push({\n kind: 'disabled',\n workflowId,\n scheduleId,\n schedule: definition.schedule,\n })\n continue\n }\n\n const fireAt = getDueFireAt(definition.schedule, now, cronLookbackMs)\n if (fireAt === undefined) {\n materialized.push({\n kind: 'not-due',\n workflowId,\n scheduleId,\n schedule: definition.schedule,\n })\n continue\n }\n\n await runtime.store.upsertSchedule({\n scheduleId,\n workflowId,\n workflowVersion: registration.version,\n schedule: definition.schedule,\n overlapPolicy: definition.overlapPolicy ?? 'skip',\n input: await resolveScheduleInput(definition.input),\n nextFireAt: fireAt,\n enabled: true,\n now,\n })\n materialized.push({\n kind: 'materialized',\n workflowId,\n scheduleId,\n fireAt,\n schedule: definition.schedule,\n })\n }\n }\n\n return materialized\n}\n\nfunction getScheduleId(\n workflowId: string,\n definition: WorkflowScheduleDefinition,\n index: number,\n): ScheduleId {\n return definition.id ?? `${workflowId}:${index}`\n}\n\nasync function resolveScheduleInput(\n input: WorkflowScheduleDefinition['input'],\n) {\n return typeof input === 'function' ? await input() : input\n}\n\nfunction getDueFireAt(\n schedule: WorkflowScheduleSpec,\n now: number,\n cronLookbackMs: number,\n) {\n if (schedule.kind === 'interval') {\n if (!Number.isFinite(schedule.everyMs) || schedule.everyMs <= 0) {\n throw new Error(\n 'Interval workflow schedules must use a positive everyMs.',\n )\n }\n return Math.floor(now / schedule.everyMs) * schedule.everyMs\n }\n\n return getPreviousCronFireAt(schedule, now, cronLookbackMs)\n}\n\nfunction getPreviousCronFireAt(\n schedule: Extract<WorkflowScheduleSpec, { kind: 'cron' }>,\n now: number,\n lookbackMs: number,\n) {\n if (schedule.timezone && schedule.timezone !== 'UTC') {\n throw new Error(\n `Workflow cron schedules are materialized in UTC. Received timezone \"${schedule.timezone}\".`,\n )\n }\n\n const cron = parseCronExpression(schedule.expression)\n const start = floorToMinute(now)\n const end = start - lookbackMs\n\n for (let timestamp = start; timestamp >= end; timestamp -= 60_000) {\n if (matchesCron(cron, new Date(timestamp))) return timestamp\n }\n\n return undefined\n}\n\ninterface ParsedCronExpression {\n minute: ParsedCronField\n hour: ParsedCronField\n dayOfMonth: ParsedCronField\n month: ParsedCronField\n dayOfWeek: ParsedCronField\n}\n\ninterface ParsedCronField {\n wildcard: boolean\n values: ReadonlySet<number>\n}\n\nfunction parseCronExpression(expression: string): ParsedCronExpression {\n const fields = expression.trim().split(/\\s+/)\n if (fields.length !== 5) {\n throw new Error(\n `Workflow cron schedules must use five fields. Received \"${expression}\".`,\n )\n }\n\n return {\n minute: parseCronField(fields[0]!, 0, 59),\n hour: parseCronField(fields[1]!, 0, 23),\n dayOfMonth: parseCronField(fields[2]!, 1, 31),\n month: parseCronField(fields[3]!, 1, 12),\n dayOfWeek: parseCronField(fields[4]!, 0, 7, normalizeDayOfWeek),\n }\n}\n\nfunction parseCronField(\n field: string,\n min: number,\n max: number,\n normalize: (value: number) => number = (value) => value,\n): ParsedCronField {\n const values = new Set<number>()\n const parts = field.split(',')\n\n for (const part of parts) {\n const [rangePart, stepPart] = part.split('/')\n const step = stepPart === undefined ? 1 : Number(stepPart)\n if (!Number.isInteger(step) || step <= 0) {\n throw new Error(`Invalid cron step \"${part}\".`)\n }\n\n const range = parseCronRange(rangePart!, min, max)\n for (let value = range.start; value <= range.end; value += step) {\n values.add(normalize(value))\n }\n }\n\n return {\n wildcard: field === '*',\n values,\n }\n}\n\nfunction parseCronRange(range: string, min: number, max: number) {\n if (range === '*') return { start: min, end: max }\n\n const bounds = range.split('-')\n if (bounds.length === 1) {\n const value = parseCronNumber(bounds[0]!, min, max)\n return { start: value, end: value }\n }\n if (bounds.length === 2) {\n const start = parseCronNumber(bounds[0]!, min, max)\n const end = parseCronNumber(bounds[1]!, min, max)\n if (end < start) throw new Error(`Invalid cron range \"${range}\".`)\n return { start, end }\n }\n\n throw new Error(`Invalid cron range \"${range}\".`)\n}\n\nfunction parseCronNumber(value: string, min: number, max: number) {\n const parsed = Number(value)\n if (!Number.isInteger(parsed) || parsed < min || parsed > max) {\n throw new Error(`Invalid cron value \"${value}\".`)\n }\n return parsed\n}\n\nfunction normalizeDayOfWeek(value: number) {\n return value === 7 ? 0 : value\n}\n\nfunction matchesCron(cron: ParsedCronExpression, date: Date) {\n const dayOfMonthMatches = cron.dayOfMonth.values.has(date.getUTCDate())\n const dayOfWeekMatches = cron.dayOfWeek.values.has(date.getUTCDay())\n const dayMatches =\n !cron.dayOfMonth.wildcard && !cron.dayOfWeek.wildcard\n ? dayOfMonthMatches || dayOfWeekMatches\n : dayOfMonthMatches && dayOfWeekMatches\n\n return (\n cron.minute.values.has(date.getUTCMinutes()) &&\n cron.hour.values.has(date.getUTCHours()) &&\n dayMatches &&\n cron.month.values.has(date.getUTCMonth() + 1)\n )\n}\n\nfunction floorToMinute(timestamp: number) {\n return Math.floor(timestamp / 60_000) * 60_000\n}\n"],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import { ScheduleId, WorkflowRegistrationMap, WorkflowRuntimeDefinition, WorkflowScheduleSpec } from "./types.cjs";
2
+
3
+ //#region src/schedule-materializer.d.ts
4
+ interface MaterializeWorkflowSchedulesOptions {
5
+ now?: number;
6
+ cronLookbackMs?: number;
7
+ }
8
+ type MaterializedWorkflowSchedule = {
9
+ kind: 'materialized';
10
+ workflowId: string;
11
+ scheduleId: ScheduleId;
12
+ fireAt: number;
13
+ schedule: WorkflowScheduleSpec;
14
+ } | {
15
+ kind: 'disabled';
16
+ workflowId: string;
17
+ scheduleId: ScheduleId;
18
+ schedule: WorkflowScheduleSpec;
19
+ } | {
20
+ kind: 'not-due';
21
+ workflowId: string;
22
+ scheduleId: ScheduleId;
23
+ schedule: WorkflowScheduleSpec;
24
+ };
25
+ declare function materializeWorkflowSchedules<TWorkflows extends WorkflowRegistrationMap>(runtime: WorkflowRuntimeDefinition<TWorkflows>, options?: MaterializeWorkflowSchedulesOptions): Promise<Array<MaterializedWorkflowSchedule>>;
26
+ //#endregion
27
+ export { MaterializeWorkflowSchedulesOptions, MaterializedWorkflowSchedule, materializeWorkflowSchedules };
28
+ //# sourceMappingURL=schedule-materializer.d.cts.map
@@ -0,0 +1,28 @@
1
+ import { ScheduleId, WorkflowRegistrationMap, WorkflowRuntimeDefinition, WorkflowScheduleSpec } from "./types.js";
2
+
3
+ //#region src/schedule-materializer.d.ts
4
+ interface MaterializeWorkflowSchedulesOptions {
5
+ now?: number;
6
+ cronLookbackMs?: number;
7
+ }
8
+ type MaterializedWorkflowSchedule = {
9
+ kind: 'materialized';
10
+ workflowId: string;
11
+ scheduleId: ScheduleId;
12
+ fireAt: number;
13
+ schedule: WorkflowScheduleSpec;
14
+ } | {
15
+ kind: 'disabled';
16
+ workflowId: string;
17
+ scheduleId: ScheduleId;
18
+ schedule: WorkflowScheduleSpec;
19
+ } | {
20
+ kind: 'not-due';
21
+ workflowId: string;
22
+ scheduleId: ScheduleId;
23
+ schedule: WorkflowScheduleSpec;
24
+ };
25
+ declare function materializeWorkflowSchedules<TWorkflows extends WorkflowRegistrationMap>(runtime: WorkflowRuntimeDefinition<TWorkflows>, options?: MaterializeWorkflowSchedulesOptions): Promise<Array<MaterializedWorkflowSchedule>>;
26
+ //#endregion
27
+ export { MaterializeWorkflowSchedulesOptions, MaterializedWorkflowSchedule, materializeWorkflowSchedules };
28
+ //# sourceMappingURL=schedule-materializer.d.ts.map