@pylonsync/functions 0.20.0 → 0.21.0
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/dist/define.d.ts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/response.d.ts +39 -0
- package/dist/runtime.d.ts +3 -1
- package/dist/ssr-runtime.d.ts +7 -0
- package/dist/types.d.ts +202 -8
- package/dist/workflows.d.ts +19 -2
- package/package.json +1 -1
- package/src/define.ts +2 -1
- package/src/index.ts +7 -0
- package/src/response.test.ts +42 -0
- package/src/response.ts +109 -0
- package/src/runtime.ts +70 -5
- package/src/ssr-runtime.test.ts +25 -0
- package/src/ssr-runtime.ts +35 -3
- package/src/types.ts +211 -9
- package/src/workflows.test.ts +74 -7
- package/src/workflows.ts +81 -19
package/src/workflows.ts
CHANGED
|
@@ -53,7 +53,7 @@ export interface WorkflowRunRequest {
|
|
|
53
53
|
export type WorkflowRunnerResponse =
|
|
54
54
|
| { action: "step_complete"; step_name: string; output: unknown; duration_ms: number }
|
|
55
55
|
| { action: "sleep"; duration: string }
|
|
56
|
-
| { action: "wait_event"; event: string }
|
|
56
|
+
| { action: "wait_event"; event: string; timeout?: string }
|
|
57
57
|
| { action: "complete"; output: unknown }
|
|
58
58
|
| { action: "fail"; error: string; step_name?: string };
|
|
59
59
|
|
|
@@ -72,12 +72,43 @@ export interface WorkflowRun<TInput = unknown> {
|
|
|
72
72
|
/** Pause the workflow for a duration ("30s", "5m", "24h", "7d"). */
|
|
73
73
|
sleep(duration: string): Promise<void>;
|
|
74
74
|
/**
|
|
75
|
-
* Pause until
|
|
76
|
-
* Resolves with the event's data
|
|
75
|
+
* Pause until the event arrives through `ctx.workflows.sendEvent` or
|
|
76
|
+
* `POST /api/workflows/<id>/event`. Resolves with the event's data
|
|
77
|
+
* (`{}` when it was sent without data).
|
|
78
|
+
*
|
|
79
|
+
* Events sent while the run is not waiting for them are buffered. The
|
|
80
|
+
* next `waitForEvent` with that name consumes the oldest buffered
|
|
81
|
+
* event, so an event that arrives during a step is not lost.
|
|
82
|
+
*
|
|
83
|
+
* The same event name can be awaited more than once in a run; each
|
|
84
|
+
* call consumes one event.
|
|
77
85
|
*/
|
|
78
86
|
waitForEvent<T = unknown>(eventName: string): Promise<T>;
|
|
87
|
+
/**
|
|
88
|
+
* Same as above, but resolves with `null` if no event arrives within
|
|
89
|
+
* `timeout` ("60s", "5m", "24h", "7d"). The timeout is durable: it
|
|
90
|
+
* survives restarts, like `sleep`.
|
|
91
|
+
*/
|
|
92
|
+
waitForEvent<T = unknown>(
|
|
93
|
+
eventName: string,
|
|
94
|
+
opts: { timeout: string },
|
|
95
|
+
): Promise<T | null>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const DURATION_RE = /^\s*\d+\s*[smhd]?\s*$/;
|
|
99
|
+
|
|
100
|
+
/** Throw unless `value` is a duration the engine accepts. */
|
|
101
|
+
function assertDuration(what: string, value: unknown): asserts value is string {
|
|
102
|
+
if (typeof value !== "string" || !DURATION_RE.test(value)) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`${what}: invalid duration ${JSON.stringify(value)} — use a string like "30s", "5m", "24h", or "7d"`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
79
107
|
}
|
|
80
108
|
|
|
109
|
+
/** Step names the engine uses for recorded event waits. */
|
|
110
|
+
const RESERVED_STEP_PREFIXES = ["event:", "timeout:"];
|
|
111
|
+
|
|
81
112
|
export interface WorkflowDefinition<TInput = unknown> {
|
|
82
113
|
readonly __pylonWorkflow: true;
|
|
83
114
|
name: string;
|
|
@@ -159,12 +190,13 @@ export async function executeWorkflowSlice(
|
|
|
159
190
|
): Promise<WorkflowRunnerResponse> {
|
|
160
191
|
let index = 0;
|
|
161
192
|
let currentStepName = "";
|
|
162
|
-
//
|
|
163
|
-
// is name-keyed, so a duplicate step name would silently replay
|
|
164
|
-
// FIRST record's output into the second call
|
|
165
|
-
//
|
|
193
|
+
// Step name uniqueness is enforced, not just documented: the replay
|
|
194
|
+
// cache is name-keyed, so a duplicate step name would silently replay
|
|
195
|
+
// the FIRST record's output into the second call. Event waits replay
|
|
196
|
+
// by order instead, so one event name can be awaited many times.
|
|
166
197
|
const seenNames = new Set<string>();
|
|
167
|
-
const
|
|
198
|
+
const eventOccurrences = new Map<string, number>();
|
|
199
|
+
const claimName = (kind: "step", name: string) => {
|
|
168
200
|
const key = `${kind}:${name}`;
|
|
169
201
|
if (seenNames.has(key)) {
|
|
170
202
|
throw new Error(
|
|
@@ -179,6 +211,11 @@ export async function executeWorkflowSlice(
|
|
|
179
211
|
input: request.input,
|
|
180
212
|
|
|
181
213
|
async step<T>(name: string, fn: () => Promise<T> | T): Promise<T> {
|
|
214
|
+
if (RESERVED_STEP_PREFIXES.some((p) => name.startsWith(p))) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`step name "${name}" is reserved — names starting with "event:" or "timeout:" record event waits`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
182
219
|
claimName("step", name);
|
|
183
220
|
const myIndex = index++;
|
|
184
221
|
if (myIndex < request.current_step) {
|
|
@@ -215,25 +252,50 @@ export async function executeWorkflowSlice(
|
|
|
215
252
|
},
|
|
216
253
|
|
|
217
254
|
async sleep(duration: string): Promise<void> {
|
|
255
|
+
assertDuration("wf.sleep", duration);
|
|
218
256
|
const myIndex = index++;
|
|
219
257
|
if (myIndex < request.current_step) return; // already slept
|
|
220
258
|
throw new WorkflowPaused({ action: "sleep", duration });
|
|
221
259
|
},
|
|
222
260
|
|
|
223
|
-
async waitForEvent<T>(
|
|
224
|
-
|
|
261
|
+
async waitForEvent<T>(
|
|
262
|
+
eventName: string,
|
|
263
|
+
opts?: { timeout: string },
|
|
264
|
+
): Promise<T | null> {
|
|
265
|
+
if (typeof eventName !== "string" || eventName.length === 0) {
|
|
266
|
+
throw new Error("wf.waitForEvent: event name must be a non-empty string");
|
|
267
|
+
}
|
|
268
|
+
const timeout = opts?.timeout;
|
|
269
|
+
if (timeout !== undefined) assertDuration("wf.waitForEvent timeout", timeout);
|
|
225
270
|
const myIndex = index++;
|
|
226
|
-
//
|
|
227
|
-
// `event:<name>` (the
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
);
|
|
231
|
-
if (myIndex < request.current_step
|
|
232
|
-
|
|
271
|
+
// The engine records each resolved wait as a completed step named
|
|
272
|
+
// `event:<name>` (with the event data) or `timeout:<name>` (no
|
|
273
|
+
// output). The k-th wait on a name replays the k-th record for it.
|
|
274
|
+
const occurrence = eventOccurrences.get(eventName) ?? 0;
|
|
275
|
+
eventOccurrences.set(eventName, occurrence + 1);
|
|
276
|
+
if (myIndex < request.current_step) {
|
|
277
|
+
const records = request.completed_steps.filter(
|
|
278
|
+
(s) =>
|
|
279
|
+
s.status === "completed" &&
|
|
280
|
+
(s.name === `event:${eventName}` || s.name === `timeout:${eventName}`),
|
|
281
|
+
);
|
|
282
|
+
const record = records[occurrence];
|
|
283
|
+
if (!record) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`workflow replay mismatch: waitForEvent("${eventName}") #${occurrence + 1} (index ${myIndex}) has no recorded result — ` +
|
|
286
|
+
"the step sequence must be deterministic across replays",
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
if (record.name.startsWith("timeout:")) return null;
|
|
290
|
+
return record.output as T;
|
|
233
291
|
}
|
|
234
|
-
throw new WorkflowPaused({
|
|
292
|
+
throw new WorkflowPaused({
|
|
293
|
+
action: "wait_event",
|
|
294
|
+
event: eventName,
|
|
295
|
+
...(timeout !== undefined ? { timeout } : {}),
|
|
296
|
+
});
|
|
235
297
|
},
|
|
236
|
-
};
|
|
298
|
+
} as WorkflowRun;
|
|
237
299
|
|
|
238
300
|
try {
|
|
239
301
|
const output = await def.fn(wf, ctx);
|