ai-workflows 2.0.2 → 2.1.3

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 (98) hide show
  1. package/.turbo/turbo-build.log +4 -5
  2. package/.turbo/turbo-test.log +169 -0
  3. package/CHANGELOG.md +29 -0
  4. package/LICENSE +21 -0
  5. package/README.md +303 -184
  6. package/dist/barrier.d.ts +153 -0
  7. package/dist/barrier.d.ts.map +1 -0
  8. package/dist/barrier.js +339 -0
  9. package/dist/barrier.js.map +1 -0
  10. package/dist/cascade-context.d.ts +149 -0
  11. package/dist/cascade-context.d.ts.map +1 -0
  12. package/dist/cascade-context.js +324 -0
  13. package/dist/cascade-context.js.map +1 -0
  14. package/dist/cascade-executor.d.ts +196 -0
  15. package/dist/cascade-executor.d.ts.map +1 -0
  16. package/dist/cascade-executor.js +384 -0
  17. package/dist/cascade-executor.js.map +1 -0
  18. package/dist/context.d.ts.map +1 -1
  19. package/dist/context.js +4 -1
  20. package/dist/context.js.map +1 -1
  21. package/dist/dependency-graph.d.ts +157 -0
  22. package/dist/dependency-graph.d.ts.map +1 -0
  23. package/dist/dependency-graph.js +382 -0
  24. package/dist/dependency-graph.js.map +1 -0
  25. package/dist/every.d.ts +31 -2
  26. package/dist/every.d.ts.map +1 -1
  27. package/dist/every.js +63 -32
  28. package/dist/every.js.map +1 -1
  29. package/dist/graph/index.d.ts +8 -0
  30. package/dist/graph/index.d.ts.map +1 -0
  31. package/dist/graph/index.js +8 -0
  32. package/dist/graph/index.js.map +1 -0
  33. package/dist/graph/topological-sort.d.ts +121 -0
  34. package/dist/graph/topological-sort.d.ts.map +1 -0
  35. package/dist/graph/topological-sort.js +292 -0
  36. package/dist/graph/topological-sort.js.map +1 -0
  37. package/dist/index.d.ts +6 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +10 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/on.d.ts +35 -10
  42. package/dist/on.d.ts.map +1 -1
  43. package/dist/on.js +52 -18
  44. package/dist/on.js.map +1 -1
  45. package/dist/send.d.ts +0 -5
  46. package/dist/send.d.ts.map +1 -1
  47. package/dist/send.js +1 -14
  48. package/dist/send.js.map +1 -1
  49. package/dist/timer-registry.d.ts +52 -0
  50. package/dist/timer-registry.d.ts.map +1 -0
  51. package/dist/timer-registry.js +120 -0
  52. package/dist/timer-registry.js.map +1 -0
  53. package/dist/types.d.ts +171 -9
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/types.js +17 -1
  56. package/dist/types.js.map +1 -1
  57. package/dist/workflow.d.ts.map +1 -1
  58. package/dist/workflow.js +22 -18
  59. package/dist/workflow.js.map +1 -1
  60. package/package.json +12 -16
  61. package/src/barrier.ts +466 -0
  62. package/src/cascade-context.ts +488 -0
  63. package/src/cascade-executor.ts +587 -0
  64. package/src/context.js +83 -0
  65. package/src/context.ts +12 -7
  66. package/src/dependency-graph.ts +518 -0
  67. package/src/every.js +267 -0
  68. package/src/every.ts +104 -35
  69. package/src/graph/index.ts +19 -0
  70. package/src/graph/topological-sort.ts +414 -0
  71. package/src/index.js +71 -0
  72. package/src/index.ts +78 -0
  73. package/src/on.js +79 -0
  74. package/src/on.ts +81 -25
  75. package/src/send.js +111 -0
  76. package/src/send.ts +1 -16
  77. package/src/timer-registry.ts +145 -0
  78. package/src/types.js +4 -0
  79. package/src/types.ts +218 -11
  80. package/src/workflow.js +455 -0
  81. package/src/workflow.ts +32 -23
  82. package/test/barrier-join.test.ts +434 -0
  83. package/test/barrier-unhandled-rejections.test.ts +359 -0
  84. package/test/cascade-context.test.ts +390 -0
  85. package/test/cascade-executor.test.ts +859 -0
  86. package/test/context.test.js +116 -0
  87. package/test/dependency-graph.test.ts +512 -0
  88. package/test/every.test.js +282 -0
  89. package/test/graph/topological-sort.test.ts +586 -0
  90. package/test/on.test.js +80 -0
  91. package/test/schedule-timer-cleanup.test.ts +344 -0
  92. package/test/send-race-conditions.test.ts +410 -0
  93. package/test/send.test.js +89 -0
  94. package/test/type-safety-every.test.ts +303 -0
  95. package/test/types-event-handler.test.ts +225 -0
  96. package/test/types-proxy-autocomplete.test.ts +345 -0
  97. package/test/workflow.test.js +224 -0
  98. package/vitest.config.js +7 -0
package/src/every.js ADDED
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Schedule registration using natural language
3
+ *
4
+ * Usage:
5
+ * every.hour($ => { ... })
6
+ * every.Thursday.at8am($ => { ... })
7
+ * every.weekday.at9am($ => { ... })
8
+ * every('hour during business hours', $ => { ... })
9
+ * every('first Monday of the month at 9am', $ => { ... })
10
+ */
11
+ /**
12
+ * Registry of schedule handlers
13
+ */
14
+ const scheduleRegistry = [];
15
+ /**
16
+ * Get all registered schedule handlers
17
+ */
18
+ export function getScheduleHandlers() {
19
+ return [...scheduleRegistry];
20
+ }
21
+ /**
22
+ * Clear all registered schedule handlers
23
+ */
24
+ export function clearScheduleHandlers() {
25
+ scheduleRegistry.length = 0;
26
+ }
27
+ /**
28
+ * Register a schedule handler directly
29
+ */
30
+ export function registerScheduleHandler(interval, handler) {
31
+ scheduleRegistry.push({
32
+ interval,
33
+ handler,
34
+ source: handler.toString(),
35
+ });
36
+ }
37
+ /**
38
+ * Well-known cron patterns for common schedules
39
+ */
40
+ const KNOWN_PATTERNS = {
41
+ // Time units
42
+ 'second': '* * * * * *',
43
+ 'minute': '* * * * *',
44
+ 'hour': '0 * * * *',
45
+ 'day': '0 0 * * *',
46
+ 'week': '0 0 * * 0',
47
+ 'month': '0 0 1 * *',
48
+ 'year': '0 0 1 1 *',
49
+ // Days of week
50
+ 'Monday': '0 0 * * 1',
51
+ 'Tuesday': '0 0 * * 2',
52
+ 'Wednesday': '0 0 * * 3',
53
+ 'Thursday': '0 0 * * 4',
54
+ 'Friday': '0 0 * * 5',
55
+ 'Saturday': '0 0 * * 6',
56
+ 'Sunday': '0 0 * * 0',
57
+ // Common patterns
58
+ 'weekday': '0 0 * * 1-5',
59
+ 'weekend': '0 0 * * 0,6',
60
+ 'midnight': '0 0 * * *',
61
+ 'noon': '0 12 * * *',
62
+ };
63
+ /**
64
+ * Time suffixes for day-based schedules
65
+ */
66
+ const TIME_PATTERNS = {
67
+ 'at6am': { hour: 6, minute: 0 },
68
+ 'at7am': { hour: 7, minute: 0 },
69
+ 'at8am': { hour: 8, minute: 0 },
70
+ 'at9am': { hour: 9, minute: 0 },
71
+ 'at10am': { hour: 10, minute: 0 },
72
+ 'at11am': { hour: 11, minute: 0 },
73
+ 'at12pm': { hour: 12, minute: 0 },
74
+ 'atnoon': { hour: 12, minute: 0 },
75
+ 'at1pm': { hour: 13, minute: 0 },
76
+ 'at2pm': { hour: 14, minute: 0 },
77
+ 'at3pm': { hour: 15, minute: 0 },
78
+ 'at4pm': { hour: 16, minute: 0 },
79
+ 'at5pm': { hour: 17, minute: 0 },
80
+ 'at6pm': { hour: 18, minute: 0 },
81
+ 'at7pm': { hour: 19, minute: 0 },
82
+ 'at8pm': { hour: 20, minute: 0 },
83
+ 'at9pm': { hour: 21, minute: 0 },
84
+ 'atmidnight': { hour: 0, minute: 0 },
85
+ };
86
+ /**
87
+ * Parse a known pattern or return null
88
+ */
89
+ function parseKnownPattern(pattern) {
90
+ return KNOWN_PATTERNS[pattern] ?? null;
91
+ }
92
+ /**
93
+ * Combine a day pattern with a time pattern
94
+ */
95
+ function combineWithTime(baseCron, time) {
96
+ const parts = baseCron.split(' ');
97
+ parts[0] = String(time.minute);
98
+ parts[1] = String(time.hour);
99
+ return parts.join(' ');
100
+ }
101
+ /**
102
+ * AI-powered cron conversion (placeholder - will use ai-functions)
103
+ */
104
+ let cronConverter = null;
105
+ /**
106
+ * Set the AI cron converter function
107
+ */
108
+ export function setCronConverter(converter) {
109
+ cronConverter = converter;
110
+ }
111
+ /**
112
+ * Convert natural language to cron expression
113
+ */
114
+ export async function toCron(description) {
115
+ // First check known patterns
116
+ const known = parseKnownPattern(description);
117
+ if (known)
118
+ return known;
119
+ // If we have an AI converter, use it
120
+ if (cronConverter) {
121
+ return cronConverter(description);
122
+ }
123
+ // Otherwise, assume it's already a cron expression or throw
124
+ if (/^[\d\*\-\/\,\s]+$/.test(description)) {
125
+ return description;
126
+ }
127
+ throw new Error(`Unknown schedule pattern: "${description}". ` +
128
+ `Set up AI conversion with setCronConverter() for natural language schedules.`);
129
+ }
130
+ /**
131
+ * Create the `every` proxy
132
+ */
133
+ function createEveryProxy() {
134
+ const handler = {
135
+ get(_target, prop) {
136
+ // Check if it's a known pattern
137
+ const pattern = KNOWN_PATTERNS[prop];
138
+ if (pattern) {
139
+ // Return an object that can either be called directly or have time accessors
140
+ const result = (handlerFn) => {
141
+ registerScheduleHandler({ type: 'cron', expression: pattern, natural: prop }, handlerFn);
142
+ };
143
+ // Add time accessors
144
+ return new Proxy(result, {
145
+ get(_t, timeKey) {
146
+ const time = TIME_PATTERNS[timeKey];
147
+ if (time) {
148
+ const cron = combineWithTime(pattern, time);
149
+ return (handlerFn) => {
150
+ registerScheduleHandler({ type: 'cron', expression: cron, natural: `${prop}.${timeKey}` }, handlerFn);
151
+ };
152
+ }
153
+ return undefined;
154
+ },
155
+ apply(_t, _thisArg, args) {
156
+ registerScheduleHandler({ type: 'cron', expression: pattern, natural: prop }, args[0]);
157
+ }
158
+ });
159
+ }
160
+ // Check for plural time units (e.g., seconds(5), minutes(30))
161
+ const pluralUnits = {
162
+ seconds: 'second',
163
+ minutes: 'minute',
164
+ hours: 'hour',
165
+ days: 'day',
166
+ weeks: 'week',
167
+ };
168
+ if (pluralUnits[prop]) {
169
+ return (value) => (handlerFn) => {
170
+ registerScheduleHandler({ type: pluralUnits[prop], value, natural: `${value} ${prop}` }, handlerFn);
171
+ };
172
+ }
173
+ return undefined;
174
+ },
175
+ apply(_target, _thisArg, args) {
176
+ // Called as every('natural language description', handler)
177
+ const [description, handler] = args;
178
+ if (typeof description === 'string' && typeof handler === 'function') {
179
+ // Register with natural language - will be converted to cron at runtime
180
+ registerScheduleHandler({ type: 'natural', description }, handler);
181
+ }
182
+ }
183
+ };
184
+ return new Proxy(function () { }, handler);
185
+ }
186
+ /**
187
+ * The `every` function/object for registering scheduled handlers
188
+ *
189
+ * @example
190
+ * ```ts
191
+ * import { every } from 'ai-workflows'
192
+ *
193
+ * // Simple intervals
194
+ * every.hour($ => $.log('Hourly task'))
195
+ * every.day($ => $.log('Daily task'))
196
+ *
197
+ * // Day + time combinations
198
+ * every.Monday.at9am($ => $.log('Monday morning standup'))
199
+ * every.weekday.at8am($ => $.log('Workday start'))
200
+ * every.Friday.at5pm($ => $.log('End of week report'))
201
+ *
202
+ * // Plural intervals with values
203
+ * every.minutes(30)($ => $.log('Every 30 minutes'))
204
+ * every.hours(4)($ => $.log('Every 4 hours'))
205
+ *
206
+ * // Natural language (requires AI converter)
207
+ * every('hour during business hours', $ => { ... })
208
+ * every('first Monday of the month at 9am', $ => { ... })
209
+ * every('every 15 minutes between 9am and 5pm on weekdays', $ => { ... })
210
+ * ```
211
+ */
212
+ export const every = createEveryProxy();
213
+ /**
214
+ * Convert interval to milliseconds (for simulation/testing)
215
+ */
216
+ export function intervalToMs(interval) {
217
+ switch (interval.type) {
218
+ case 'second':
219
+ return (interval.value ?? 1) * 1000;
220
+ case 'minute':
221
+ return (interval.value ?? 1) * 60 * 1000;
222
+ case 'hour':
223
+ return (interval.value ?? 1) * 60 * 60 * 1000;
224
+ case 'day':
225
+ return (interval.value ?? 1) * 24 * 60 * 60 * 1000;
226
+ case 'week':
227
+ return (interval.value ?? 1) * 7 * 24 * 60 * 60 * 1000;
228
+ case 'cron':
229
+ case 'natural':
230
+ // Cron/natural expressions need special handling
231
+ return 0;
232
+ }
233
+ }
234
+ /**
235
+ * Format interval for display
236
+ */
237
+ export function formatInterval(interval) {
238
+ if ('natural' in interval && interval.natural) {
239
+ return interval.natural;
240
+ }
241
+ switch (interval.type) {
242
+ case 'second':
243
+ return interval.value && interval.value > 1
244
+ ? `every ${interval.value} seconds`
245
+ : 'every second';
246
+ case 'minute':
247
+ return interval.value && interval.value > 1
248
+ ? `every ${interval.value} minutes`
249
+ : 'every minute';
250
+ case 'hour':
251
+ return interval.value && interval.value > 1
252
+ ? `every ${interval.value} hours`
253
+ : 'every hour';
254
+ case 'day':
255
+ return interval.value && interval.value > 1
256
+ ? `every ${interval.value} days`
257
+ : 'every day';
258
+ case 'week':
259
+ return interval.value && interval.value > 1
260
+ ? `every ${interval.value} weeks`
261
+ : 'every week';
262
+ case 'cron':
263
+ return `cron: ${interval.expression}`;
264
+ case 'natural':
265
+ return interval.description;
266
+ }
267
+ }
package/src/every.ts CHANGED
@@ -9,7 +9,16 @@
9
9
  * every('first Monday of the month at 9am', $ => { ... })
10
10
  */
11
11
 
12
- import type { ScheduleHandler, ScheduleRegistration, ScheduleInterval } from './types.js'
12
+ import type {
13
+ ScheduleHandler,
14
+ ScheduleRegistration,
15
+ ScheduleInterval,
16
+ EveryProxyTarget,
17
+ EveryProxy,
18
+ EveryProxyHandler,
19
+ DayScheduleProxyHandler,
20
+ } from './types.js'
21
+ import { PLURAL_UNITS, isPluralUnitKey } from './types.js'
13
22
 
14
23
  /**
15
24
  * Registry of schedule handlers
@@ -151,65 +160,125 @@ export async function toCron(description: string): Promise<string> {
151
160
  }
152
161
 
153
162
  /**
154
- * Create the `every` proxy
163
+ * Schedule registration callback type
164
+ * Used by createTypedEveryProxy to customize handler registration
155
165
  */
156
- function createEveryProxy() {
157
- const handler = {
158
- get(_target: unknown, prop: string) {
166
+ export type EveryProxyRegistrationCallback = (
167
+ interval: ScheduleInterval,
168
+ handler: ScheduleHandler
169
+ ) => void
170
+
171
+ /**
172
+ * Create a typed EveryProxy with proper TypeScript generics
173
+ *
174
+ * This factory function creates a callable proxy that supports:
175
+ * - Direct calls: every('natural language', handler)
176
+ * - Simple patterns: every.hour(handler)
177
+ * - Day + time: every.Monday.at9am(handler)
178
+ * - Intervals: every.minutes(30)(handler)
179
+ *
180
+ * @param registerCallback - Function called when a handler is registered
181
+ * @returns A properly typed EveryProxy
182
+ *
183
+ * @example
184
+ * ```ts
185
+ * // Create proxy with custom registration
186
+ * const myEvery = createTypedEveryProxy((interval, handler) => {
187
+ * myRegistry.push({ interval, handler })
188
+ * })
189
+ *
190
+ * myEvery.hour(handler) // Properly typed!
191
+ * myEvery.Monday.at9am(handler) // Chained access typed!
192
+ * ```
193
+ */
194
+ export function createTypedEveryProxy(registerCallback: EveryProxyRegistrationCallback): EveryProxy {
195
+ // Create typed handler for day schedule patterns with time modifiers
196
+ const createDayScheduleHandler = (
197
+ pattern: string,
198
+ prop: string
199
+ ): DayScheduleProxyHandler => ({
200
+ get(
201
+ _target: (handler: ScheduleHandler) => void,
202
+ timeKey: string,
203
+ _receiver: unknown
204
+ ): ((handler: ScheduleHandler) => void) | undefined {
205
+ const time = TIME_PATTERNS[timeKey]
206
+ if (time) {
207
+ const cron = combineWithTime(pattern, time)
208
+ return (handlerFn: ScheduleHandler) => {
209
+ registerCallback({ type: 'cron', expression: cron, natural: `${prop}.${timeKey}` }, handlerFn)
210
+ }
211
+ }
212
+ return undefined
213
+ },
214
+ apply(
215
+ _target: (handler: ScheduleHandler) => void,
216
+ _thisArg: unknown,
217
+ args: [ScheduleHandler]
218
+ ): void {
219
+ registerCallback({ type: 'cron', expression: pattern, natural: prop }, args[0])
220
+ }
221
+ })
222
+
223
+ // Create the main EveryProxy handler
224
+ const everyHandler: EveryProxyHandler = {
225
+ get(
226
+ _target: EveryProxyTarget,
227
+ prop: string,
228
+ _receiver: unknown
229
+ ): unknown {
159
230
  // Check if it's a known pattern
160
231
  const pattern = KNOWN_PATTERNS[prop]
161
232
  if (pattern) {
162
233
  // Return an object that can either be called directly or have time accessors
163
234
  const result = (handlerFn: ScheduleHandler) => {
164
- registerScheduleHandler({ type: 'cron', expression: pattern, natural: prop }, handlerFn)
235
+ registerCallback({ type: 'cron', expression: pattern, natural: prop }, handlerFn)
165
236
  }
166
- // Add time accessors
167
- return new Proxy(result, {
168
- get(_t, timeKey: string) {
169
- const time = TIME_PATTERNS[timeKey]
170
- if (time) {
171
- const cron = combineWithTime(pattern, time)
172
- return (handlerFn: ScheduleHandler) => {
173
- registerScheduleHandler({ type: 'cron', expression: cron, natural: `${prop}.${timeKey}` }, handlerFn)
174
- }
175
- }
176
- return undefined
177
- },
178
- apply(_t, _thisArg, args) {
179
- registerScheduleHandler({ type: 'cron', expression: pattern, natural: prop }, args[0])
180
- }
181
- })
237
+ // Add time accessors with typed handler
238
+ return new Proxy(result, createDayScheduleHandler(pattern, prop))
182
239
  }
183
240
 
184
241
  // Check for plural time units (e.g., seconds(5), minutes(30))
185
- const pluralUnits: Record<string, string> = {
186
- seconds: 'second',
187
- minutes: 'minute',
188
- hours: 'hour',
189
- days: 'day',
190
- weeks: 'week',
191
- }
192
- if (pluralUnits[prop]) {
242
+ // Using type guard and typed constant for type-safe interval creation
243
+ if (isPluralUnitKey(prop)) {
244
+ const intervalType = PLURAL_UNITS[prop]
193
245
  return (value: number) => (handlerFn: ScheduleHandler) => {
194
- registerScheduleHandler({ type: pluralUnits[prop] as any, value, natural: `${value} ${prop}` }, handlerFn)
246
+ registerCallback({ type: intervalType, value, natural: `${value} ${prop}` }, handlerFn)
195
247
  }
196
248
  }
197
249
 
198
250
  return undefined
199
251
  },
200
252
 
201
- apply(_target: unknown, _thisArg: unknown, args: unknown[]) {
253
+ apply(
254
+ _target: EveryProxyTarget,
255
+ _thisArg: unknown,
256
+ args: [string, ScheduleHandler]
257
+ ): void {
202
258
  // Called as every('natural language description', handler)
203
- const [description, handler] = args as [string, ScheduleHandler]
259
+ const [description, handler] = args
204
260
 
205
261
  if (typeof description === 'string' && typeof handler === 'function') {
206
262
  // Register with natural language - will be converted to cron at runtime
207
- registerScheduleHandler({ type: 'natural', description }, handler)
263
+ registerCallback({ type: 'natural', description }, handler)
208
264
  }
209
265
  }
210
266
  }
211
267
 
212
- return new Proxy(function() {} as any, handler)
268
+ // Create callable target with proper typing
269
+ // The function serves as the Proxy target - actual behavior is in the handler's apply trap
270
+ const target: EveryProxyTarget = function(_description: string, _handler: ScheduleHandler) {}
271
+ return new Proxy(target, everyHandler) as EveryProxy
272
+ }
273
+
274
+ /**
275
+ * Create the `every` proxy using the global schedule registry
276
+ *
277
+ * This is the default implementation that uses registerScheduleHandler
278
+ * for backward compatibility with the standalone `every` export.
279
+ */
280
+ function createEveryProxy(): EveryProxy {
281
+ return createTypedEveryProxy(registerScheduleHandler)
213
282
  }
214
283
 
215
284
  /**
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Graph algorithms for workflow execution ordering
3
+ *
4
+ * Provides topological sorting and execution level grouping for
5
+ * managing workflow step dependencies.
6
+ */
7
+
8
+ export {
9
+ topologicalSort,
10
+ topologicalSortKahn,
11
+ topologicalSortDFS,
12
+ getExecutionLevels,
13
+ CycleDetectedError,
14
+ MissingNodeError,
15
+ type SortableNode,
16
+ type ExecutionLevel,
17
+ type TopologicalSortOptions,
18
+ type TopologicalSortResult,
19
+ } from './topological-sort.js'