ai-workflows 2.0.1 → 2.1.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/.turbo/turbo-build.log +4 -5
- package/.turbo/turbo-test.log +7 -0
- package/CHANGELOG.md +22 -0
- package/dist/send.d.ts +0 -5
- package/dist/send.d.ts.map +1 -1
- package/dist/send.js +1 -14
- package/dist/send.js.map +1 -1
- package/dist/types.d.ts +83 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +7 -7
- package/dist/workflow.js.map +1 -1
- package/package.json +2 -6
- package/src/context.js +83 -0
- package/src/every.js +267 -0
- package/src/index.js +71 -0
- package/src/on.js +79 -0
- package/src/send.js +111 -0
- package/src/send.ts +1 -16
- package/src/types.js +4 -0
- package/src/types.ts +97 -11
- package/src/workflow.js +455 -0
- package/src/workflow.ts +9 -7
- package/test/context.test.js +116 -0
- package/test/every.test.js +282 -0
- package/test/on.test.js +80 -0
- package/test/send.test.js +89 -0
- package/test/types-event-handler.test.ts +225 -0
- package/test/types-proxy-autocomplete.test.ts +345 -0
- package/test/workflow.test.js +224 -0
- package/vitest.config.js +7 -0
package/src/types.ts
CHANGED
|
@@ -17,11 +17,15 @@ export interface HandlerFunction<T = unknown> {
|
|
|
17
17
|
/**
|
|
18
18
|
* Event handler function type
|
|
19
19
|
* Can return void (for send) or a result (for do/try)
|
|
20
|
+
*
|
|
21
|
+
* Generic order follows Promise<T> convention:
|
|
22
|
+
* - TOutput (first) is what the handler returns
|
|
23
|
+
* - TInput (second) is what the handler receives
|
|
20
24
|
*/
|
|
21
|
-
export type EventHandler<
|
|
22
|
-
data:
|
|
25
|
+
export type EventHandler<TOutput = unknown, TInput = unknown> = (
|
|
26
|
+
data: TInput,
|
|
23
27
|
$: WorkflowContext
|
|
24
|
-
) =>
|
|
28
|
+
) => TOutput | void | Promise<TOutput | void>
|
|
25
29
|
|
|
26
30
|
/**
|
|
27
31
|
* Schedule handler function type
|
|
@@ -43,14 +47,22 @@ export interface WorkflowContext {
|
|
|
43
47
|
/**
|
|
44
48
|
* Do an action (durable, waits for result)
|
|
45
49
|
* Retries on failure, stores result durably
|
|
50
|
+
*
|
|
51
|
+
* Generic order follows Promise<T> convention:
|
|
52
|
+
* - TResult (first) is what the action returns
|
|
53
|
+
* - TInput (second) is what data is passed to the action
|
|
46
54
|
*/
|
|
47
|
-
do: <
|
|
55
|
+
do: <TResult = unknown, TInput = unknown>(event: string, data: TInput) => Promise<TResult>
|
|
48
56
|
|
|
49
57
|
/**
|
|
50
58
|
* Try an action (non-durable, waits for result)
|
|
51
59
|
* Simple execution without durability guarantees
|
|
60
|
+
*
|
|
61
|
+
* Generic order follows Promise<T> convention:
|
|
62
|
+
* - TResult (first) is what the action returns
|
|
63
|
+
* - TInput (second) is what data is passed to the action
|
|
52
64
|
*/
|
|
53
|
-
try: <
|
|
65
|
+
try: <TResult = unknown, TInput = unknown>(event: string, data: TInput) => Promise<TResult>
|
|
54
66
|
|
|
55
67
|
/** Register event handler ($.on.Noun.event) */
|
|
56
68
|
on: OnProxy
|
|
@@ -142,24 +154,98 @@ export interface ArtifactData {
|
|
|
142
154
|
metadata?: Record<string, unknown>
|
|
143
155
|
}
|
|
144
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Event handler proxy for a specific noun
|
|
159
|
+
* Allows $.on.Noun.event(handler) pattern
|
|
160
|
+
*/
|
|
161
|
+
export type NounEventProxy = {
|
|
162
|
+
[event: string]: (handler: EventHandler) => void
|
|
163
|
+
}
|
|
164
|
+
|
|
145
165
|
/**
|
|
146
166
|
* Event proxy type for $.on.Noun.event pattern
|
|
167
|
+
*
|
|
168
|
+
* Includes explicit known nouns for IDE autocomplete while
|
|
169
|
+
* preserving index signature for dynamic noun access.
|
|
147
170
|
*/
|
|
148
171
|
export type OnProxy = {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
172
|
+
// Known nouns (for autocomplete)
|
|
173
|
+
Customer: NounEventProxy
|
|
174
|
+
Order: NounEventProxy
|
|
175
|
+
Payment: NounEventProxy
|
|
176
|
+
User: NounEventProxy
|
|
177
|
+
Email: NounEventProxy
|
|
178
|
+
Invoice: NounEventProxy
|
|
179
|
+
Product: NounEventProxy
|
|
180
|
+
Subscription: NounEventProxy
|
|
181
|
+
// Index signature for dynamic nouns
|
|
182
|
+
[noun: string]: NounEventProxy
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Schedule handler with optional time modifiers
|
|
187
|
+
* Allows $.every.Monday.at9am(handler) pattern
|
|
188
|
+
*/
|
|
189
|
+
export type DayScheduleProxy = ((handler: ScheduleHandler) => void) & {
|
|
190
|
+
at6am: (handler: ScheduleHandler) => void
|
|
191
|
+
at7am: (handler: ScheduleHandler) => void
|
|
192
|
+
at8am: (handler: ScheduleHandler) => void
|
|
193
|
+
at9am: (handler: ScheduleHandler) => void
|
|
194
|
+
at10am: (handler: ScheduleHandler) => void
|
|
195
|
+
at11am: (handler: ScheduleHandler) => void
|
|
196
|
+
at12pm: (handler: ScheduleHandler) => void
|
|
197
|
+
atnoon: (handler: ScheduleHandler) => void
|
|
198
|
+
at1pm: (handler: ScheduleHandler) => void
|
|
199
|
+
at2pm: (handler: ScheduleHandler) => void
|
|
200
|
+
at3pm: (handler: ScheduleHandler) => void
|
|
201
|
+
at4pm: (handler: ScheduleHandler) => void
|
|
202
|
+
at5pm: (handler: ScheduleHandler) => void
|
|
203
|
+
at6pm: (handler: ScheduleHandler) => void
|
|
204
|
+
atmidnight: (handler: ScheduleHandler) => void
|
|
205
|
+
[timeKey: string]: (handler: ScheduleHandler) => void
|
|
152
206
|
}
|
|
153
207
|
|
|
154
208
|
/**
|
|
155
209
|
* Every proxy type for $.every patterns
|
|
210
|
+
*
|
|
211
|
+
* Includes explicit known schedule patterns for IDE autocomplete while
|
|
212
|
+
* preserving index signature for dynamic patterns.
|
|
156
213
|
*/
|
|
157
214
|
export type EveryProxy = {
|
|
215
|
+
// Callable for natural language schedules
|
|
158
216
|
(description: string, handler: ScheduleHandler): void
|
|
159
217
|
} & {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
218
|
+
// Known time units (for autocomplete)
|
|
219
|
+
second: (handler: ScheduleHandler) => void
|
|
220
|
+
minute: (handler: ScheduleHandler) => void
|
|
221
|
+
hour: (handler: ScheduleHandler) => void
|
|
222
|
+
day: (handler: ScheduleHandler) => void
|
|
223
|
+
week: (handler: ScheduleHandler) => void
|
|
224
|
+
month: (handler: ScheduleHandler) => void
|
|
225
|
+
year: (handler: ScheduleHandler) => void
|
|
226
|
+
|
|
227
|
+
// Known days of week with time modifiers
|
|
228
|
+
Monday: DayScheduleProxy
|
|
229
|
+
Tuesday: DayScheduleProxy
|
|
230
|
+
Wednesday: DayScheduleProxy
|
|
231
|
+
Thursday: DayScheduleProxy
|
|
232
|
+
Friday: DayScheduleProxy
|
|
233
|
+
Saturday: DayScheduleProxy
|
|
234
|
+
Sunday: DayScheduleProxy
|
|
235
|
+
weekday: DayScheduleProxy
|
|
236
|
+
weekend: DayScheduleProxy
|
|
237
|
+
midnight: (handler: ScheduleHandler) => void
|
|
238
|
+
noon: (handler: ScheduleHandler) => void
|
|
239
|
+
|
|
240
|
+
// Plural forms for specifying intervals
|
|
241
|
+
seconds: (value: number) => (handler: ScheduleHandler) => void
|
|
242
|
+
minutes: (value: number) => (handler: ScheduleHandler) => void
|
|
243
|
+
hours: (value: number) => (handler: ScheduleHandler) => void
|
|
244
|
+
days: (value: number) => (handler: ScheduleHandler) => void
|
|
245
|
+
weeks: (value: number) => (handler: ScheduleHandler) => void
|
|
246
|
+
|
|
247
|
+
// Index signature for dynamic patterns
|
|
248
|
+
[key: string]: ((handler: ScheduleHandler) => void) | ((value: number) => (handler: ScheduleHandler) => void) | DayScheduleProxy
|
|
163
249
|
}
|
|
164
250
|
|
|
165
251
|
/**
|
package/src/workflow.js
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Workflow API
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* Workflow($ => {
|
|
6
|
+
* $.on.Customer.created(async (customer, $) => {
|
|
7
|
+
* $.log('Customer created', customer)
|
|
8
|
+
* await $.send('Email.welcome', { to: customer.email })
|
|
9
|
+
* })
|
|
10
|
+
*
|
|
11
|
+
* $.every.Monday.at9am(async ($) => {
|
|
12
|
+
* $.log('Weekly standup reminder')
|
|
13
|
+
* })
|
|
14
|
+
* })
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Well-known cron patterns for common schedules
|
|
18
|
+
*/
|
|
19
|
+
const KNOWN_PATTERNS = {
|
|
20
|
+
second: '* * * * * *',
|
|
21
|
+
minute: '* * * * *',
|
|
22
|
+
hour: '0 * * * *',
|
|
23
|
+
day: '0 0 * * *',
|
|
24
|
+
week: '0 0 * * 0',
|
|
25
|
+
month: '0 0 1 * *',
|
|
26
|
+
year: '0 0 1 1 *',
|
|
27
|
+
Monday: '0 0 * * 1',
|
|
28
|
+
Tuesday: '0 0 * * 2',
|
|
29
|
+
Wednesday: '0 0 * * 3',
|
|
30
|
+
Thursday: '0 0 * * 4',
|
|
31
|
+
Friday: '0 0 * * 5',
|
|
32
|
+
Saturday: '0 0 * * 6',
|
|
33
|
+
Sunday: '0 0 * * 0',
|
|
34
|
+
weekday: '0 0 * * 1-5',
|
|
35
|
+
weekend: '0 0 * * 0,6',
|
|
36
|
+
midnight: '0 0 * * *',
|
|
37
|
+
noon: '0 12 * * *',
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Time suffixes for day-based schedules
|
|
41
|
+
*/
|
|
42
|
+
const TIME_PATTERNS = {
|
|
43
|
+
at6am: { hour: 6, minute: 0 },
|
|
44
|
+
at7am: { hour: 7, minute: 0 },
|
|
45
|
+
at8am: { hour: 8, minute: 0 },
|
|
46
|
+
at9am: { hour: 9, minute: 0 },
|
|
47
|
+
at10am: { hour: 10, minute: 0 },
|
|
48
|
+
at11am: { hour: 11, minute: 0 },
|
|
49
|
+
at12pm: { hour: 12, minute: 0 },
|
|
50
|
+
atnoon: { hour: 12, minute: 0 },
|
|
51
|
+
at1pm: { hour: 13, minute: 0 },
|
|
52
|
+
at2pm: { hour: 14, minute: 0 },
|
|
53
|
+
at3pm: { hour: 15, minute: 0 },
|
|
54
|
+
at4pm: { hour: 16, minute: 0 },
|
|
55
|
+
at5pm: { hour: 17, minute: 0 },
|
|
56
|
+
at6pm: { hour: 18, minute: 0 },
|
|
57
|
+
at7pm: { hour: 19, minute: 0 },
|
|
58
|
+
at8pm: { hour: 20, minute: 0 },
|
|
59
|
+
at9pm: { hour: 21, minute: 0 },
|
|
60
|
+
atmidnight: { hour: 0, minute: 0 },
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Combine a day pattern with a time pattern
|
|
64
|
+
*/
|
|
65
|
+
function combineWithTime(baseCron, time) {
|
|
66
|
+
const parts = baseCron.split(' ');
|
|
67
|
+
parts[0] = String(time.minute);
|
|
68
|
+
parts[1] = String(time.hour);
|
|
69
|
+
return parts.join(' ');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Parse event string into noun and event
|
|
73
|
+
*/
|
|
74
|
+
export function parseEvent(event) {
|
|
75
|
+
const parts = event.split('.');
|
|
76
|
+
if (parts.length !== 2) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const [noun, eventName] = parts;
|
|
80
|
+
if (!noun || !eventName) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return { noun, event: eventName };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Create a workflow with the $ context
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* const workflow = Workflow($ => {
|
|
91
|
+
* $.on.Customer.created(async (customer, $) => {
|
|
92
|
+
* $.log('New customer:', customer.name)
|
|
93
|
+
* await $.send('Email.welcome', { to: customer.email })
|
|
94
|
+
* })
|
|
95
|
+
*
|
|
96
|
+
* $.every.hour(async ($) => {
|
|
97
|
+
* $.log('Hourly check')
|
|
98
|
+
* })
|
|
99
|
+
*
|
|
100
|
+
* $.every.Monday.at9am(async ($) => {
|
|
101
|
+
* $.log('Weekly standup')
|
|
102
|
+
* })
|
|
103
|
+
*
|
|
104
|
+
* $.every('first Monday of the month', async ($) => {
|
|
105
|
+
* $.log('Monthly report')
|
|
106
|
+
* })
|
|
107
|
+
* })
|
|
108
|
+
*
|
|
109
|
+
* await workflow.start()
|
|
110
|
+
* await workflow.send('Customer.created', { name: 'John', email: 'john@example.com' })
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export function Workflow(setup, options = {}) {
|
|
114
|
+
// Registries for handlers captured during setup
|
|
115
|
+
const eventRegistry = [];
|
|
116
|
+
const scheduleRegistry = [];
|
|
117
|
+
// State
|
|
118
|
+
const state = {
|
|
119
|
+
context: { ...options.context },
|
|
120
|
+
history: [],
|
|
121
|
+
};
|
|
122
|
+
// Schedule timers
|
|
123
|
+
let scheduleTimers = [];
|
|
124
|
+
/**
|
|
125
|
+
* Add to history
|
|
126
|
+
*/
|
|
127
|
+
const addHistory = (entry) => {
|
|
128
|
+
state.history.push({
|
|
129
|
+
...entry,
|
|
130
|
+
timestamp: Date.now(),
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Register an event handler
|
|
135
|
+
*/
|
|
136
|
+
const registerEventHandler = (noun, event, handler) => {
|
|
137
|
+
eventRegistry.push({
|
|
138
|
+
noun,
|
|
139
|
+
event,
|
|
140
|
+
handler,
|
|
141
|
+
source: handler.toString(),
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Register a schedule handler
|
|
146
|
+
*/
|
|
147
|
+
const registerScheduleHandler = (interval, handler) => {
|
|
148
|
+
scheduleRegistry.push({
|
|
149
|
+
interval,
|
|
150
|
+
handler,
|
|
151
|
+
source: handler.toString(),
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Create the $.on proxy
|
|
156
|
+
*/
|
|
157
|
+
const createOnProxy = () => {
|
|
158
|
+
return new Proxy({}, {
|
|
159
|
+
get(_target, noun) {
|
|
160
|
+
return new Proxy({}, {
|
|
161
|
+
get(_eventTarget, event) {
|
|
162
|
+
return (handler) => {
|
|
163
|
+
registerEventHandler(noun, event, handler);
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* Create the $.every proxy
|
|
172
|
+
*/
|
|
173
|
+
const createEveryProxy = () => {
|
|
174
|
+
const handler = {
|
|
175
|
+
get(_target, prop) {
|
|
176
|
+
const pattern = KNOWN_PATTERNS[prop];
|
|
177
|
+
if (pattern) {
|
|
178
|
+
const result = (handlerFn) => {
|
|
179
|
+
registerScheduleHandler({ type: 'cron', expression: pattern, natural: prop }, handlerFn);
|
|
180
|
+
};
|
|
181
|
+
return new Proxy(result, {
|
|
182
|
+
get(_t, timeKey) {
|
|
183
|
+
const time = TIME_PATTERNS[timeKey];
|
|
184
|
+
if (time) {
|
|
185
|
+
const cron = combineWithTime(pattern, time);
|
|
186
|
+
return (handlerFn) => {
|
|
187
|
+
registerScheduleHandler({ type: 'cron', expression: cron, natural: `${prop}.${timeKey}` }, handlerFn);
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return undefined;
|
|
191
|
+
},
|
|
192
|
+
apply(_t, _thisArg, args) {
|
|
193
|
+
registerScheduleHandler({ type: 'cron', expression: pattern, natural: prop }, args[0]);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
// Plural units (seconds, minutes, hours, days, weeks)
|
|
198
|
+
const pluralUnits = {
|
|
199
|
+
seconds: 'second',
|
|
200
|
+
minutes: 'minute',
|
|
201
|
+
hours: 'hour',
|
|
202
|
+
days: 'day',
|
|
203
|
+
weeks: 'week',
|
|
204
|
+
};
|
|
205
|
+
if (pluralUnits[prop]) {
|
|
206
|
+
return (value) => (handlerFn) => {
|
|
207
|
+
registerScheduleHandler({ type: pluralUnits[prop], value, natural: `${value} ${prop}` }, handlerFn);
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
return undefined;
|
|
211
|
+
},
|
|
212
|
+
apply(_target, _thisArg, args) {
|
|
213
|
+
const [description, handler] = args;
|
|
214
|
+
if (typeof description === 'string' && typeof handler === 'function') {
|
|
215
|
+
registerScheduleHandler({ type: 'natural', description }, handler);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
return new Proxy(function () { }, handler);
|
|
220
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* Deliver an event to matching handlers (fire and forget)
|
|
223
|
+
*/
|
|
224
|
+
const deliverEvent = async (event, data) => {
|
|
225
|
+
const parsed = parseEvent(event);
|
|
226
|
+
if (!parsed) {
|
|
227
|
+
console.warn(`Invalid event format: ${event}. Expected Noun.event`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const matching = eventRegistry.filter(h => h.noun === parsed.noun && h.event === parsed.event);
|
|
231
|
+
if (matching.length === 0) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
await Promise.all(matching.map(async ({ handler }) => {
|
|
235
|
+
try {
|
|
236
|
+
await handler(data, $);
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
console.error(`Error in handler for ${event}:`, error);
|
|
240
|
+
}
|
|
241
|
+
}));
|
|
242
|
+
};
|
|
243
|
+
/**
|
|
244
|
+
* Execute an event and wait for result from first matching handler
|
|
245
|
+
*/
|
|
246
|
+
const executeEvent = async (event, data, durable) => {
|
|
247
|
+
const parsed = parseEvent(event);
|
|
248
|
+
if (!parsed) {
|
|
249
|
+
throw new Error(`Invalid event format: ${event}. Expected Noun.event`);
|
|
250
|
+
}
|
|
251
|
+
const matching = eventRegistry.filter(h => h.noun === parsed.noun && h.event === parsed.event);
|
|
252
|
+
if (matching.length === 0) {
|
|
253
|
+
throw new Error(`No handler registered for ${event}`);
|
|
254
|
+
}
|
|
255
|
+
// Use first matching handler for result
|
|
256
|
+
const { handler } = matching[0];
|
|
257
|
+
if (durable && options.db) {
|
|
258
|
+
// Create action for durability tracking
|
|
259
|
+
await options.db.createAction({
|
|
260
|
+
actor: 'workflow',
|
|
261
|
+
object: event,
|
|
262
|
+
action: 'execute',
|
|
263
|
+
metadata: { data },
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const result = await handler(data, $);
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
if (durable) {
|
|
272
|
+
// Could implement retry logic here
|
|
273
|
+
console.error(`[workflow] Durable action failed for ${event}:`, error);
|
|
274
|
+
}
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
/**
|
|
279
|
+
* Create the $ context
|
|
280
|
+
*/
|
|
281
|
+
const $ = {
|
|
282
|
+
async send(event, data) {
|
|
283
|
+
addHistory({ type: 'event', name: event, data });
|
|
284
|
+
// Record to database if connected (durable)
|
|
285
|
+
if (options.db) {
|
|
286
|
+
await options.db.recordEvent(event, data);
|
|
287
|
+
}
|
|
288
|
+
await deliverEvent(event, data);
|
|
289
|
+
},
|
|
290
|
+
async do(event, data) {
|
|
291
|
+
addHistory({ type: 'action', name: `do:${event}`, data });
|
|
292
|
+
// Record to database (durable)
|
|
293
|
+
if (options.db) {
|
|
294
|
+
await options.db.recordEvent(event, data);
|
|
295
|
+
}
|
|
296
|
+
return executeEvent(event, data, true);
|
|
297
|
+
},
|
|
298
|
+
async try(event, data) {
|
|
299
|
+
addHistory({ type: 'action', name: `try:${event}`, data });
|
|
300
|
+
// Non-durable - no database recording
|
|
301
|
+
return executeEvent(event, data, false);
|
|
302
|
+
},
|
|
303
|
+
on: createOnProxy(),
|
|
304
|
+
every: createEveryProxy(),
|
|
305
|
+
// Direct access to state context
|
|
306
|
+
state: state.context,
|
|
307
|
+
getState() {
|
|
308
|
+
// Return a deep copy to prevent mutation
|
|
309
|
+
return {
|
|
310
|
+
current: state.current,
|
|
311
|
+
context: { ...state.context },
|
|
312
|
+
history: [...state.history],
|
|
313
|
+
};
|
|
314
|
+
},
|
|
315
|
+
set(key, value) {
|
|
316
|
+
state.context[key] = value;
|
|
317
|
+
},
|
|
318
|
+
get(key) {
|
|
319
|
+
return state.context[key];
|
|
320
|
+
},
|
|
321
|
+
log(message, data) {
|
|
322
|
+
addHistory({ type: 'action', name: 'log', data: { message, data } });
|
|
323
|
+
console.log(`[workflow] ${message}`, data ?? '');
|
|
324
|
+
},
|
|
325
|
+
db: options.db,
|
|
326
|
+
};
|
|
327
|
+
// Run setup to capture handlers
|
|
328
|
+
setup($);
|
|
329
|
+
/**
|
|
330
|
+
* Start schedule handlers
|
|
331
|
+
*/
|
|
332
|
+
const startSchedules = async () => {
|
|
333
|
+
for (const schedule of scheduleRegistry) {
|
|
334
|
+
const { interval, handler } = schedule;
|
|
335
|
+
let ms = 0;
|
|
336
|
+
switch (interval.type) {
|
|
337
|
+
case 'second':
|
|
338
|
+
ms = (interval.value ?? 1) * 1000;
|
|
339
|
+
break;
|
|
340
|
+
case 'minute':
|
|
341
|
+
ms = (interval.value ?? 1) * 60 * 1000;
|
|
342
|
+
break;
|
|
343
|
+
case 'hour':
|
|
344
|
+
ms = (interval.value ?? 1) * 60 * 60 * 1000;
|
|
345
|
+
break;
|
|
346
|
+
case 'day':
|
|
347
|
+
ms = (interval.value ?? 1) * 24 * 60 * 60 * 1000;
|
|
348
|
+
break;
|
|
349
|
+
case 'week':
|
|
350
|
+
ms = (interval.value ?? 1) * 7 * 24 * 60 * 60 * 1000;
|
|
351
|
+
break;
|
|
352
|
+
case 'cron':
|
|
353
|
+
case 'natural':
|
|
354
|
+
// Cron/natural need special handling
|
|
355
|
+
console.log(`[workflow] Cron/natural scheduling not yet implemented: ${interval.type === 'cron' ? interval.expression : interval.description}`);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (ms > 0) {
|
|
359
|
+
const timer = setInterval(async () => {
|
|
360
|
+
try {
|
|
361
|
+
addHistory({ type: 'schedule', name: interval.natural ?? interval.type });
|
|
362
|
+
await handler($);
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
console.error('[workflow] Schedule handler error:', error);
|
|
366
|
+
}
|
|
367
|
+
}, ms);
|
|
368
|
+
scheduleTimers.push(timer);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
const instance = {
|
|
373
|
+
definition: {
|
|
374
|
+
name: 'workflow',
|
|
375
|
+
events: eventRegistry,
|
|
376
|
+
schedules: scheduleRegistry,
|
|
377
|
+
initialContext: options.context,
|
|
378
|
+
},
|
|
379
|
+
get state() {
|
|
380
|
+
return state;
|
|
381
|
+
},
|
|
382
|
+
$,
|
|
383
|
+
async send(event, data) {
|
|
384
|
+
await $.send(event, data);
|
|
385
|
+
},
|
|
386
|
+
async start() {
|
|
387
|
+
console.log(`[workflow] Starting with ${eventRegistry.length} event handlers and ${scheduleRegistry.length} schedules`);
|
|
388
|
+
await startSchedules();
|
|
389
|
+
},
|
|
390
|
+
async stop() {
|
|
391
|
+
console.log('[workflow] Stopping');
|
|
392
|
+
for (const timer of scheduleTimers) {
|
|
393
|
+
clearInterval(timer);
|
|
394
|
+
}
|
|
395
|
+
scheduleTimers = [];
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
return instance;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Create an isolated $ context for testing
|
|
402
|
+
*/
|
|
403
|
+
export function createTestContext() {
|
|
404
|
+
const emittedEvents = [];
|
|
405
|
+
const stateContext = {};
|
|
406
|
+
const history = [];
|
|
407
|
+
const $ = {
|
|
408
|
+
emittedEvents,
|
|
409
|
+
async send(event, data) {
|
|
410
|
+
emittedEvents.push({ event, data });
|
|
411
|
+
},
|
|
412
|
+
async do(_event, _data) {
|
|
413
|
+
throw new Error('$.do not implemented in test context - register handlers via Workflow()');
|
|
414
|
+
},
|
|
415
|
+
async try(_event, _data) {
|
|
416
|
+
throw new Error('$.try not implemented in test context - register handlers via Workflow()');
|
|
417
|
+
},
|
|
418
|
+
on: new Proxy({}, {
|
|
419
|
+
get() {
|
|
420
|
+
return new Proxy({}, {
|
|
421
|
+
get() {
|
|
422
|
+
return () => { }; // No-op for testing
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}),
|
|
427
|
+
every: new Proxy(function () { }, {
|
|
428
|
+
get() {
|
|
429
|
+
return () => () => { }; // No-op for testing
|
|
430
|
+
},
|
|
431
|
+
apply() { }
|
|
432
|
+
}),
|
|
433
|
+
state: stateContext,
|
|
434
|
+
getState() {
|
|
435
|
+
return {
|
|
436
|
+
context: { ...stateContext },
|
|
437
|
+
history: [...history],
|
|
438
|
+
};
|
|
439
|
+
},
|
|
440
|
+
set(key, value) {
|
|
441
|
+
stateContext[key] = value;
|
|
442
|
+
},
|
|
443
|
+
get(key) {
|
|
444
|
+
return stateContext[key];
|
|
445
|
+
},
|
|
446
|
+
log(message, data) {
|
|
447
|
+
console.log(`[test] ${message}`, data ?? '');
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
return $;
|
|
451
|
+
}
|
|
452
|
+
// Also export standalone on/every for import { on, every } usage
|
|
453
|
+
export { on, registerEventHandler, getEventHandlers, clearEventHandlers } from './on.js';
|
|
454
|
+
export { every, registerScheduleHandler, getScheduleHandlers, clearScheduleHandlers, toCron, intervalToMs, formatInterval, setCronConverter } from './every.js';
|
|
455
|
+
export { send } from './send.js';
|
package/src/workflow.ts
CHANGED
|
@@ -392,11 +392,11 @@ export function Workflow(
|
|
|
392
392
|
|
|
393
393
|
getState(): WorkflowState {
|
|
394
394
|
// Return a deep copy to prevent mutation
|
|
395
|
-
return {
|
|
395
|
+
return structuredClone({
|
|
396
396
|
current: state.current,
|
|
397
|
-
context:
|
|
398
|
-
history:
|
|
399
|
-
}
|
|
397
|
+
context: state.context,
|
|
398
|
+
history: state.history,
|
|
399
|
+
})
|
|
400
400
|
},
|
|
401
401
|
|
|
402
402
|
set<T = unknown>(key: string, value: T): void {
|
|
@@ -444,9 +444,11 @@ export function Workflow(
|
|
|
444
444
|
break
|
|
445
445
|
case 'cron':
|
|
446
446
|
case 'natural':
|
|
447
|
-
// Cron/natural need special handling
|
|
448
|
-
|
|
449
|
-
|
|
447
|
+
// Cron/natural need special handling - throw error to avoid silent failures
|
|
448
|
+
throw new Error(
|
|
449
|
+
`Cron scheduling not yet implemented: "${interval.type === 'cron' ? interval.expression : interval.description}". ` +
|
|
450
|
+
`Use interval-based patterns like $.every.seconds(30), $.every.minutes(5), or $.every.hours(1) instead.`
|
|
451
|
+
)
|
|
450
452
|
}
|
|
451
453
|
|
|
452
454
|
if (ms > 0) {
|