@upstash/workflow 1.3.3 → 1.4.0-rc
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/astro.d.mts +3 -2
- package/astro.d.ts +3 -2
- package/astro.js +587 -139
- package/astro.mjs +3 -1
- package/{chunk-WEQGCISK.mjs → chunk-UH6O5IJY.mjs} +749 -301
- package/cloudflare.d.mts +3 -2
- package/cloudflare.d.ts +3 -2
- package/cloudflare.js +587 -139
- package/cloudflare.mjs +3 -1
- package/express.d.mts +3 -2
- package/express.d.ts +3 -2
- package/express.js +588 -139
- package/express.mjs +4 -1
- package/h3.d.mts +3 -2
- package/h3.d.ts +3 -2
- package/h3.js +587 -139
- package/h3.mjs +3 -1
- package/hono.d.mts +3 -2
- package/hono.d.ts +3 -2
- package/hono.js +587 -139
- package/hono.mjs +3 -1
- package/index.d.mts +25 -6
- package/index.d.ts +25 -6
- package/index.js +610 -141
- package/index.mjs +26 -3
- package/nextjs.d.mts +3 -2
- package/nextjs.d.ts +3 -2
- package/nextjs.js +588 -139
- package/nextjs.mjs +4 -1
- package/package.json +1 -1
- package/react-router.d.mts +3 -2
- package/react-router.d.ts +3 -2
- package/react-router.js +587 -139
- package/react-router.mjs +3 -1
- package/{serve-many-iJF1IUXk.d.ts → serve-many-BCtZg31b.d.ts} +1 -1
- package/{serve-many-CG3BFvO3.d.mts → serve-many-CbIV7145.d.mts} +1 -1
- package/solidjs.d.mts +2 -1
- package/solidjs.d.ts +2 -1
- package/solidjs.js +587 -139
- package/solidjs.mjs +3 -1
- package/svelte.d.mts +3 -2
- package/svelte.d.ts +3 -2
- package/svelte.js +587 -139
- package/svelte.mjs +3 -1
- package/tanstack.d.mts +3 -2
- package/tanstack.d.ts +3 -2
- package/tanstack.js +587 -139
- package/tanstack.mjs +3 -1
- package/{types-CekOpKvz.d.ts → types-ffLPVG5_.d.mts} +248 -8
- package/{types-CekOpKvz.d.mts → types-ffLPVG5_.d.ts} +248 -8
|
@@ -118,6 +118,14 @@ var WORKFLOW_CREATED_AT_HEADER = "Upstash-Workflow-CreatedAt";
|
|
|
118
118
|
var WORKFLOW_FAILURE_HEADER = "Upstash-Workflow-Is-Failure";
|
|
119
119
|
var WORKFLOW_FAILURE_CALLBACK_HEADER = "Upstash-Workflow-Failure-Callback";
|
|
120
120
|
var WORKFLOW_FEATURE_HEADER = "Upstash-Feature-Set";
|
|
121
|
+
var WORKFLOW_FEATURE_SET = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
|
|
122
|
+
var WORKFLOW_STEP_CONFIG_CALL_TYPE = "stepConfig";
|
|
123
|
+
var WORKFLOW_STEP_CONFIG_HEADER = "Upstash-Workflow-Step-Config";
|
|
124
|
+
var FLOW_CONTROL_KEY_HEADER = "Upstash-Flow-Control-Key";
|
|
125
|
+
var FLOW_CONTROL_VALUE_HEADER = "Upstash-Flow-Control-Value";
|
|
126
|
+
var RETRIES_HEADER = "Upstash-Retries";
|
|
127
|
+
var RETRY_DELAY_HEADER = "Upstash-Retry-Delay";
|
|
128
|
+
var WORKFLOW_STEP_CONFIG_FEATURE = "WF_StepConfig";
|
|
121
129
|
var WORKFLOW_INVOKE_COUNT_HEADER = "Upstash-Workflow-Invoke-Count";
|
|
122
130
|
var WORKFLOW_RETRIED_HEADER = "Upstash-Retried";
|
|
123
131
|
var WORKFLOW_LABEL_HEADER = "Upstash-Label";
|
|
@@ -224,248 +232,6 @@ var getNewUrlFromWorkflowId = (url, workflowId) => {
|
|
|
224
232
|
return url.replace(/[^/]+$/, workflowId);
|
|
225
233
|
};
|
|
226
234
|
|
|
227
|
-
// src/context/auto-executor.ts
|
|
228
|
-
import { QstashError as QstashError5 } from "@upstash/qstash";
|
|
229
|
-
|
|
230
|
-
// src/qstash/headers.ts
|
|
231
|
-
import { QstashError as QstashError4 } from "@upstash/qstash";
|
|
232
|
-
|
|
233
|
-
// src/client/utils.ts
|
|
234
|
-
import { QstashError as QstashError2 } from "@upstash/qstash";
|
|
235
|
-
var assertNonEmptyId = (id, label = "id") => {
|
|
236
|
-
if (!id) {
|
|
237
|
-
throw new QstashError2(`${label} cannot be empty`);
|
|
238
|
-
}
|
|
239
|
-
};
|
|
240
|
-
var toNonEmptyIdArray = (request, label = "id") => {
|
|
241
|
-
const ids = typeof request === "string" ? [request] : request;
|
|
242
|
-
for (const id of ids) assertNonEmptyId(id, label);
|
|
243
|
-
return ids;
|
|
244
|
-
};
|
|
245
|
-
var makeNotifyRequest = async (requester, eventId, eventData, workflowRunId) => {
|
|
246
|
-
assertNonEmptyId(eventId, "Event id");
|
|
247
|
-
if (workflowRunId !== void 0) assertNonEmptyId(workflowRunId, "Workflow run id");
|
|
248
|
-
const path = workflowRunId === void 0 ? ["v2", "notify", eventId] : ["v2", "notify", workflowRunId, eventId];
|
|
249
|
-
const result = await requester.request({
|
|
250
|
-
path,
|
|
251
|
-
method: "POST",
|
|
252
|
-
body: typeof eventData === "string" ? eventData : JSON.stringify(eventData)
|
|
253
|
-
});
|
|
254
|
-
return result;
|
|
255
|
-
};
|
|
256
|
-
var makeGetWaitersRequest = async (requester, eventId) => {
|
|
257
|
-
assertNonEmptyId(eventId, "Event id");
|
|
258
|
-
const result = await requester.request({
|
|
259
|
-
path: ["v2", "waiters", eventId],
|
|
260
|
-
method: "GET"
|
|
261
|
-
});
|
|
262
|
-
return result;
|
|
263
|
-
};
|
|
264
|
-
var makeCancelRequest = async (requester, workflowRunId) => {
|
|
265
|
-
await requester.request({
|
|
266
|
-
path: ["v2", "workflows", "runs", `${workflowRunId}?cancel=true`],
|
|
267
|
-
method: "DELETE",
|
|
268
|
-
parseResponseAsJson: false
|
|
269
|
-
});
|
|
270
|
-
return true;
|
|
271
|
-
};
|
|
272
|
-
var getSteps = async (requester, workflowRunId, messageId, dispatchDebug) => {
|
|
273
|
-
try {
|
|
274
|
-
const steps = await requester.request({
|
|
275
|
-
path: ["v2", "workflows", "runs", workflowRunId],
|
|
276
|
-
parseResponseAsJson: true
|
|
277
|
-
});
|
|
278
|
-
if (steps.length === 1) {
|
|
279
|
-
return {
|
|
280
|
-
steps,
|
|
281
|
-
workflowRunEnded: false
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
if (!messageId) {
|
|
285
|
-
await dispatchDebug?.("onInfo", {
|
|
286
|
-
info: `Pulled ${steps.length} steps from QStashand returned them without filtering with messageId.`
|
|
287
|
-
});
|
|
288
|
-
return { steps, workflowRunEnded: false };
|
|
289
|
-
} else {
|
|
290
|
-
const index = steps.findIndex((item) => item.messageId === messageId);
|
|
291
|
-
if (index === -1) {
|
|
292
|
-
return { steps: [], workflowRunEnded: false };
|
|
293
|
-
}
|
|
294
|
-
const filteredSteps = steps.slice(0, index + 1);
|
|
295
|
-
await dispatchDebug?.("onInfo", {
|
|
296
|
-
info: `Pulled ${steps.length} steps from QStash and filtered them to ${filteredSteps.length} using messageId.`
|
|
297
|
-
});
|
|
298
|
-
return { steps: filteredSteps, workflowRunEnded: false };
|
|
299
|
-
}
|
|
300
|
-
} catch (error) {
|
|
301
|
-
if (isInstanceOf(error, QstashError2) && error.status === 404) {
|
|
302
|
-
await dispatchDebug?.("onWarning", {
|
|
303
|
-
warning: "Couldn't fetch workflow run steps. This can happen if the workflow run succesfully ends before some callback is executed."
|
|
304
|
-
});
|
|
305
|
-
return { steps: void 0, workflowRunEnded: true };
|
|
306
|
-
} else {
|
|
307
|
-
throw error;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
};
|
|
311
|
-
function normalizeCursor(response) {
|
|
312
|
-
const cursor = response.cursor;
|
|
313
|
-
return { ...response, cursor: cursor || void 0 };
|
|
314
|
-
}
|
|
315
|
-
var DEFAULT_BULK_COUNT = 100;
|
|
316
|
-
function buildBulkActionQueryParameters(request, options) {
|
|
317
|
-
const cursor = "cursor" in request ? request.cursor : void 0;
|
|
318
|
-
if ("all" in request) {
|
|
319
|
-
return { count: request.count ?? DEFAULT_BULK_COUNT, cursor };
|
|
320
|
-
}
|
|
321
|
-
if ("dlqIds" in request) {
|
|
322
|
-
const ids = request.dlqIds;
|
|
323
|
-
if (Array.isArray(ids) && ids.length === 0) {
|
|
324
|
-
throw new QstashError2(
|
|
325
|
-
"Empty dlqIds array provided. If you intend to target all DLQ messages, use { all: true } explicitly."
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
return { dlqIds: ids, cursor };
|
|
329
|
-
}
|
|
330
|
-
if ("workflowRunIds" in request && request.workflowRunIds) {
|
|
331
|
-
if (request.workflowRunIds.length === 0) {
|
|
332
|
-
throw new QstashError2(
|
|
333
|
-
"Empty workflowRunIds array provided. If you intend to target all workflow runs, use { all: true } explicitly."
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
return { workflowRunIds: request.workflowRunIds };
|
|
337
|
-
}
|
|
338
|
-
const filter = request.filter;
|
|
339
|
-
if (!filter) {
|
|
340
|
-
throw new QstashError2(
|
|
341
|
-
"No filter provided. Use { filter: { ... } } with at least one filter field, or { all: true }."
|
|
342
|
-
);
|
|
343
|
-
}
|
|
344
|
-
for (const [field, value] of Object.entries(filter)) {
|
|
345
|
-
if (Array.isArray(value) && value.length === 0) {
|
|
346
|
-
throw new QstashError2(
|
|
347
|
-
`Empty array provided for filter field '${field}'. If you intend to target all records, use { all: true } explicitly.`
|
|
348
|
-
);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
if (options?.translateWorkflowUrl) {
|
|
352
|
-
const { workflowUrlStartingWith, workflowUrl, ...rest } = filter;
|
|
353
|
-
if (workflowUrlStartingWith && workflowUrl) {
|
|
354
|
-
throw new QstashError2(
|
|
355
|
-
"workflowUrl and workflowUrlStartingWith are mutually exclusive. Use workflowUrl for exact match or workflowUrlStartingWith for prefix match."
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
const urlParams = {};
|
|
359
|
-
if (workflowUrlStartingWith) {
|
|
360
|
-
urlParams.workflowUrl = workflowUrlStartingWith;
|
|
361
|
-
} else if (workflowUrl) {
|
|
362
|
-
urlParams.workflowUrl = workflowUrl;
|
|
363
|
-
urlParams.workflowUrlExactMatch = true;
|
|
364
|
-
}
|
|
365
|
-
return {
|
|
366
|
-
...rest,
|
|
367
|
-
...urlParams,
|
|
368
|
-
count: request.count ?? DEFAULT_BULK_COUNT,
|
|
369
|
-
cursor
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
return {
|
|
373
|
-
...filter,
|
|
374
|
-
count: request.count ?? DEFAULT_BULK_COUNT,
|
|
375
|
-
cursor
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// src/utils.ts
|
|
380
|
-
var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
|
|
381
|
-
var NANOID_LENGTH = 21;
|
|
382
|
-
var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
|
|
383
|
-
function validateLabel(label) {
|
|
384
|
-
if (label === void 0) return;
|
|
385
|
-
const labels = Array.isArray(label) ? label : [label];
|
|
386
|
-
if (labels.length === 0) {
|
|
387
|
-
throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
|
|
388
|
-
}
|
|
389
|
-
for (const value of labels) {
|
|
390
|
-
if (!RESOURCE_NAME_PATTERN.test(value)) {
|
|
391
|
-
throw new WorkflowNonRetryableError(
|
|
392
|
-
`Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
function serializeLabel(label) {
|
|
398
|
-
return Array.isArray(label) ? label.join(",") : label;
|
|
399
|
-
}
|
|
400
|
-
function validateFlowControl(flowControl) {
|
|
401
|
-
if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
|
|
402
|
-
throw new WorkflowNonRetryableError(
|
|
403
|
-
`Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
function getRandomInt() {
|
|
408
|
-
return Math.floor(Math.random() * NANOID_CHARS.length);
|
|
409
|
-
}
|
|
410
|
-
function nanoid(length = NANOID_LENGTH) {
|
|
411
|
-
return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
|
|
412
|
-
}
|
|
413
|
-
function getWorkflowRunId(id) {
|
|
414
|
-
return `wfr_${id ?? nanoid()}`;
|
|
415
|
-
}
|
|
416
|
-
function decodeBase64(base64) {
|
|
417
|
-
const binString = atob(base64);
|
|
418
|
-
try {
|
|
419
|
-
const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
|
|
420
|
-
return new TextDecoder().decode(intArray);
|
|
421
|
-
} catch (error) {
|
|
422
|
-
console.warn(
|
|
423
|
-
`Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
|
|
424
|
-
);
|
|
425
|
-
return binString;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
function getUserIdFromToken(qstashClient) {
|
|
429
|
-
try {
|
|
430
|
-
const token = qstashClient.token;
|
|
431
|
-
const decodedToken = decodeBase64(token);
|
|
432
|
-
const tokenPayload = JSON.parse(decodedToken);
|
|
433
|
-
const userId = tokenPayload.UserID;
|
|
434
|
-
if (!userId) {
|
|
435
|
-
throw new WorkflowError("QStash token payload does not contain userId");
|
|
436
|
-
}
|
|
437
|
-
return userId;
|
|
438
|
-
} catch (error) {
|
|
439
|
-
throw new WorkflowError(
|
|
440
|
-
`Failed to decode QStash token while running create webhook step: ${error.message}`
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
function getQStashUrl(qstashClient) {
|
|
445
|
-
try {
|
|
446
|
-
const requester = qstashClient.http;
|
|
447
|
-
const baseUrl = requester.baseUrl;
|
|
448
|
-
if (!baseUrl) {
|
|
449
|
-
throw new WorkflowError("QStash client does not have a baseUrl");
|
|
450
|
-
}
|
|
451
|
-
return baseUrl;
|
|
452
|
-
} catch (error) {
|
|
453
|
-
throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
function getEventId() {
|
|
457
|
-
return `evt_${nanoid(15)}`;
|
|
458
|
-
}
|
|
459
|
-
function stringifyBody(body) {
|
|
460
|
-
if (body === void 0) {
|
|
461
|
-
return void 0;
|
|
462
|
-
}
|
|
463
|
-
if (typeof body === "string") {
|
|
464
|
-
return body;
|
|
465
|
-
}
|
|
466
|
-
return JSON.stringify(body);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
235
|
// node_modules/neverthrow/dist/index.es.js
|
|
470
236
|
var defaultErrorConfig = {
|
|
471
237
|
withStackTrace: false
|
|
@@ -859,27 +625,269 @@ var Err = class {
|
|
|
859
625
|
asyncMap(_f) {
|
|
860
626
|
return errAsync(this.error);
|
|
861
627
|
}
|
|
862
|
-
unwrapOr(v) {
|
|
863
|
-
return v;
|
|
628
|
+
unwrapOr(v) {
|
|
629
|
+
return v;
|
|
630
|
+
}
|
|
631
|
+
match(_ok, err2) {
|
|
632
|
+
return err2(this.error);
|
|
633
|
+
}
|
|
634
|
+
safeUnwrap() {
|
|
635
|
+
const error = this.error;
|
|
636
|
+
return (function* () {
|
|
637
|
+
yield err(error);
|
|
638
|
+
throw new Error("Do not use this generator out of `safeTry`");
|
|
639
|
+
})();
|
|
640
|
+
}
|
|
641
|
+
_unsafeUnwrap(config) {
|
|
642
|
+
throw createNeverThrowError("Called `_unsafeUnwrap` on an Err", this, config);
|
|
643
|
+
}
|
|
644
|
+
_unsafeUnwrapErr(_) {
|
|
645
|
+
return this.error;
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
var fromThrowable = Result.fromThrowable;
|
|
649
|
+
|
|
650
|
+
// src/context/auto-executor.ts
|
|
651
|
+
import { QstashError as QstashError5 } from "@upstash/qstash";
|
|
652
|
+
|
|
653
|
+
// src/qstash/headers.ts
|
|
654
|
+
import { QstashError as QstashError4 } from "@upstash/qstash";
|
|
655
|
+
|
|
656
|
+
// src/client/utils.ts
|
|
657
|
+
import { QstashError as QstashError2 } from "@upstash/qstash";
|
|
658
|
+
var assertNonEmptyId = (id, label = "id") => {
|
|
659
|
+
if (!id) {
|
|
660
|
+
throw new QstashError2(`${label} cannot be empty`);
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
var toNonEmptyIdArray = (request, label = "id") => {
|
|
664
|
+
const ids = typeof request === "string" ? [request] : request;
|
|
665
|
+
for (const id of ids) assertNonEmptyId(id, label);
|
|
666
|
+
return ids;
|
|
667
|
+
};
|
|
668
|
+
var makeNotifyRequest = async (requester, eventId, eventData, workflowRunId) => {
|
|
669
|
+
assertNonEmptyId(eventId, "Event id");
|
|
670
|
+
if (workflowRunId !== void 0) assertNonEmptyId(workflowRunId, "Workflow run id");
|
|
671
|
+
const path = workflowRunId === void 0 ? ["v2", "notify", eventId] : ["v2", "notify", workflowRunId, eventId];
|
|
672
|
+
const result = await requester.request({
|
|
673
|
+
path,
|
|
674
|
+
method: "POST",
|
|
675
|
+
body: typeof eventData === "string" ? eventData : JSON.stringify(eventData)
|
|
676
|
+
});
|
|
677
|
+
return result;
|
|
678
|
+
};
|
|
679
|
+
var makeGetWaitersRequest = async (requester, eventId) => {
|
|
680
|
+
assertNonEmptyId(eventId, "Event id");
|
|
681
|
+
const result = await requester.request({
|
|
682
|
+
path: ["v2", "waiters", eventId],
|
|
683
|
+
method: "GET"
|
|
684
|
+
});
|
|
685
|
+
return result;
|
|
686
|
+
};
|
|
687
|
+
var makeCancelRequest = async (requester, workflowRunId) => {
|
|
688
|
+
await requester.request({
|
|
689
|
+
path: ["v2", "workflows", "runs", `${workflowRunId}?cancel=true`],
|
|
690
|
+
method: "DELETE",
|
|
691
|
+
parseResponseAsJson: false
|
|
692
|
+
});
|
|
693
|
+
return true;
|
|
694
|
+
};
|
|
695
|
+
var getSteps = async (requester, workflowRunId, messageId, dispatchDebug) => {
|
|
696
|
+
try {
|
|
697
|
+
const steps = await requester.request({
|
|
698
|
+
path: ["v2", "workflows", "runs", workflowRunId],
|
|
699
|
+
parseResponseAsJson: true
|
|
700
|
+
});
|
|
701
|
+
if (steps.length === 1) {
|
|
702
|
+
return {
|
|
703
|
+
steps,
|
|
704
|
+
workflowRunEnded: false
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
if (!messageId) {
|
|
708
|
+
await dispatchDebug?.("onInfo", {
|
|
709
|
+
info: `Pulled ${steps.length} steps from QStashand returned them without filtering with messageId.`
|
|
710
|
+
});
|
|
711
|
+
return { steps, workflowRunEnded: false };
|
|
712
|
+
} else {
|
|
713
|
+
const index = steps.findIndex((item) => item.messageId === messageId);
|
|
714
|
+
if (index === -1) {
|
|
715
|
+
return { steps: [], workflowRunEnded: false };
|
|
716
|
+
}
|
|
717
|
+
const filteredSteps = steps.slice(0, index + 1);
|
|
718
|
+
await dispatchDebug?.("onInfo", {
|
|
719
|
+
info: `Pulled ${steps.length} steps from QStash and filtered them to ${filteredSteps.length} using messageId.`
|
|
720
|
+
});
|
|
721
|
+
return { steps: filteredSteps, workflowRunEnded: false };
|
|
722
|
+
}
|
|
723
|
+
} catch (error) {
|
|
724
|
+
if (isInstanceOf(error, QstashError2) && error.status === 404) {
|
|
725
|
+
await dispatchDebug?.("onWarning", {
|
|
726
|
+
warning: "Couldn't fetch workflow run steps. This can happen if the workflow run succesfully ends before some callback is executed."
|
|
727
|
+
});
|
|
728
|
+
return { steps: void 0, workflowRunEnded: true };
|
|
729
|
+
} else {
|
|
730
|
+
throw error;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
function normalizeCursor(response) {
|
|
735
|
+
const cursor = response.cursor;
|
|
736
|
+
return { ...response, cursor: cursor || void 0 };
|
|
737
|
+
}
|
|
738
|
+
var DEFAULT_BULK_COUNT = 100;
|
|
739
|
+
function buildBulkActionQueryParameters(request, options) {
|
|
740
|
+
const cursor = "cursor" in request ? request.cursor : void 0;
|
|
741
|
+
if ("all" in request) {
|
|
742
|
+
return { count: request.count ?? DEFAULT_BULK_COUNT, cursor };
|
|
743
|
+
}
|
|
744
|
+
if ("dlqIds" in request) {
|
|
745
|
+
const ids = request.dlqIds;
|
|
746
|
+
if (Array.isArray(ids) && ids.length === 0) {
|
|
747
|
+
throw new QstashError2(
|
|
748
|
+
"Empty dlqIds array provided. If you intend to target all DLQ messages, use { all: true } explicitly."
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
return { dlqIds: ids, cursor };
|
|
752
|
+
}
|
|
753
|
+
if ("workflowRunIds" in request && request.workflowRunIds) {
|
|
754
|
+
if (request.workflowRunIds.length === 0) {
|
|
755
|
+
throw new QstashError2(
|
|
756
|
+
"Empty workflowRunIds array provided. If you intend to target all workflow runs, use { all: true } explicitly."
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
return { workflowRunIds: request.workflowRunIds };
|
|
760
|
+
}
|
|
761
|
+
const filter = request.filter;
|
|
762
|
+
if (!filter) {
|
|
763
|
+
throw new QstashError2(
|
|
764
|
+
"No filter provided. Use { filter: { ... } } with at least one filter field, or { all: true }."
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
for (const [field, value] of Object.entries(filter)) {
|
|
768
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
769
|
+
throw new QstashError2(
|
|
770
|
+
`Empty array provided for filter field '${field}'. If you intend to target all records, use { all: true } explicitly.`
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (options?.translateWorkflowUrl) {
|
|
775
|
+
const { workflowUrlStartingWith, workflowUrl, ...rest } = filter;
|
|
776
|
+
if (workflowUrlStartingWith && workflowUrl) {
|
|
777
|
+
throw new QstashError2(
|
|
778
|
+
"workflowUrl and workflowUrlStartingWith are mutually exclusive. Use workflowUrl for exact match or workflowUrlStartingWith for prefix match."
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
const urlParams = {};
|
|
782
|
+
if (workflowUrlStartingWith) {
|
|
783
|
+
urlParams.workflowUrl = workflowUrlStartingWith;
|
|
784
|
+
} else if (workflowUrl) {
|
|
785
|
+
urlParams.workflowUrl = workflowUrl;
|
|
786
|
+
urlParams.workflowUrlExactMatch = true;
|
|
787
|
+
}
|
|
788
|
+
return {
|
|
789
|
+
...rest,
|
|
790
|
+
...urlParams,
|
|
791
|
+
count: request.count ?? DEFAULT_BULK_COUNT,
|
|
792
|
+
cursor
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
...filter,
|
|
797
|
+
count: request.count ?? DEFAULT_BULK_COUNT,
|
|
798
|
+
cursor
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// src/utils.ts
|
|
803
|
+
var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
|
|
804
|
+
var NANOID_LENGTH = 21;
|
|
805
|
+
var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
|
|
806
|
+
function validateLabel(label) {
|
|
807
|
+
if (label === void 0) return;
|
|
808
|
+
const labels = Array.isArray(label) ? label : [label];
|
|
809
|
+
if (labels.length === 0) {
|
|
810
|
+
throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
|
|
811
|
+
}
|
|
812
|
+
for (const value of labels) {
|
|
813
|
+
if (!RESOURCE_NAME_PATTERN.test(value)) {
|
|
814
|
+
throw new WorkflowNonRetryableError(
|
|
815
|
+
`Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
function serializeLabel(label) {
|
|
821
|
+
return Array.isArray(label) ? label.join(",") : label;
|
|
822
|
+
}
|
|
823
|
+
function validateFlowControl(flowControl) {
|
|
824
|
+
if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
|
|
825
|
+
throw new WorkflowNonRetryableError(
|
|
826
|
+
`Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
function getRandomInt() {
|
|
831
|
+
return Math.floor(Math.random() * NANOID_CHARS.length);
|
|
832
|
+
}
|
|
833
|
+
function nanoid(length = NANOID_LENGTH) {
|
|
834
|
+
return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
|
|
835
|
+
}
|
|
836
|
+
function getWorkflowRunId(id) {
|
|
837
|
+
return `wfr_${id ?? nanoid()}`;
|
|
838
|
+
}
|
|
839
|
+
function decodeBase64(base64) {
|
|
840
|
+
const binString = atob(base64);
|
|
841
|
+
try {
|
|
842
|
+
const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
|
|
843
|
+
return new TextDecoder().decode(intArray);
|
|
844
|
+
} catch (error) {
|
|
845
|
+
console.warn(
|
|
846
|
+
`Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
|
|
847
|
+
);
|
|
848
|
+
return binString;
|
|
864
849
|
}
|
|
865
|
-
|
|
866
|
-
|
|
850
|
+
}
|
|
851
|
+
function getUserIdFromToken(qstashClient) {
|
|
852
|
+
try {
|
|
853
|
+
const token = qstashClient.token;
|
|
854
|
+
const decodedToken = decodeBase64(token);
|
|
855
|
+
const tokenPayload = JSON.parse(decodedToken);
|
|
856
|
+
const userId = tokenPayload.UserID;
|
|
857
|
+
if (!userId) {
|
|
858
|
+
throw new WorkflowError("QStash token payload does not contain userId");
|
|
859
|
+
}
|
|
860
|
+
return userId;
|
|
861
|
+
} catch (error) {
|
|
862
|
+
throw new WorkflowError(
|
|
863
|
+
`Failed to decode QStash token while running create webhook step: ${error.message}`
|
|
864
|
+
);
|
|
867
865
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
866
|
+
}
|
|
867
|
+
function getQStashUrl(qstashClient) {
|
|
868
|
+
try {
|
|
869
|
+
const requester = qstashClient.http;
|
|
870
|
+
const baseUrl = requester.baseUrl;
|
|
871
|
+
if (!baseUrl) {
|
|
872
|
+
throw new WorkflowError("QStash client does not have a baseUrl");
|
|
873
|
+
}
|
|
874
|
+
return baseUrl;
|
|
875
|
+
} catch (error) {
|
|
876
|
+
throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
|
|
874
877
|
}
|
|
875
|
-
|
|
876
|
-
|
|
878
|
+
}
|
|
879
|
+
function getEventId() {
|
|
880
|
+
return `evt_${nanoid(15)}`;
|
|
881
|
+
}
|
|
882
|
+
function stringifyBody(body) {
|
|
883
|
+
if (body === void 0) {
|
|
884
|
+
return void 0;
|
|
877
885
|
}
|
|
878
|
-
|
|
879
|
-
return
|
|
886
|
+
if (typeof body === "string") {
|
|
887
|
+
return body;
|
|
880
888
|
}
|
|
881
|
-
|
|
882
|
-
|
|
889
|
+
return JSON.stringify(body);
|
|
890
|
+
}
|
|
883
891
|
|
|
884
892
|
// src/workflow-requests.ts
|
|
885
893
|
import { QstashError as QstashError3 } from "@upstash/qstash";
|
|
@@ -972,6 +980,10 @@ var triggerFirstInvocation = async (params) => {
|
|
|
972
980
|
return err(error_);
|
|
973
981
|
}
|
|
974
982
|
};
|
|
983
|
+
var flushPendingStep = async (workflowContext) => {
|
|
984
|
+
const { executor } = workflowContext;
|
|
985
|
+
return await executor.submitPendingStep();
|
|
986
|
+
};
|
|
975
987
|
var triggerRouteFunction = async ({
|
|
976
988
|
onCleanup,
|
|
977
989
|
onStep,
|
|
@@ -1179,8 +1191,29 @@ If you want to disable QStash Verification, you should clear env variables QSTAS
|
|
|
1179
1191
|
var BaseLazyStep = class _BaseLazyStep {
|
|
1180
1192
|
stepName;
|
|
1181
1193
|
context;
|
|
1182
|
-
|
|
1194
|
+
/**
|
|
1195
|
+
* step-level settings (flow control, retries etc.) which override the
|
|
1196
|
+
* settings the workflow run was triggered with, for this step only.
|
|
1197
|
+
*/
|
|
1198
|
+
stepSettings;
|
|
1199
|
+
/**
|
|
1200
|
+
* whether this step's result can be submitted after the route function
|
|
1201
|
+
* has moved on, instead of right away.
|
|
1202
|
+
*
|
|
1203
|
+
* Enabled for steps whose `getResultStep` produces the final `out` —
|
|
1204
|
+
* which is not the same as "needs no server round trip":
|
|
1205
|
+
* `LazyCreateWebhookStep` builds its result in `getBody` and returns
|
|
1206
|
+
* `out: undefined` from `getResultStep`, so deferring it would hand
|
|
1207
|
+
* `undefined` to the route function.
|
|
1208
|
+
*
|
|
1209
|
+
* Deferring lets the route function continue and reveal the next step,
|
|
1210
|
+
* so that step's settings can ride on this step's submission instead
|
|
1211
|
+
* of needing a step config request of their own.
|
|
1212
|
+
*/
|
|
1213
|
+
supportsDeferredSubmission = false;
|
|
1214
|
+
constructor(context, stepName, stepSettings) {
|
|
1183
1215
|
this.context = context;
|
|
1216
|
+
this.stepSettings = stepSettings;
|
|
1184
1217
|
if (!stepName) {
|
|
1185
1218
|
throw new WorkflowError(
|
|
1186
1219
|
"A workflow step name cannot be undefined or an empty string. Please provide a name for your workflow step."
|
|
@@ -1244,7 +1277,13 @@ var BaseLazyStep = class _BaseLazyStep {
|
|
|
1244
1277
|
step.out = JSON.stringify(step.out);
|
|
1245
1278
|
return JSON.stringify(step);
|
|
1246
1279
|
}
|
|
1247
|
-
getHeaders({
|
|
1280
|
+
getHeaders({
|
|
1281
|
+
context,
|
|
1282
|
+
telemetry,
|
|
1283
|
+
invokeCount,
|
|
1284
|
+
step,
|
|
1285
|
+
stepSettings
|
|
1286
|
+
}) {
|
|
1248
1287
|
return getHeaders({
|
|
1249
1288
|
initHeaderValue: "false",
|
|
1250
1289
|
workflowConfig: {
|
|
@@ -1257,7 +1296,8 @@ var BaseLazyStep = class _BaseLazyStep {
|
|
|
1257
1296
|
stepInfo: {
|
|
1258
1297
|
step,
|
|
1259
1298
|
lazyStep: this
|
|
1260
|
-
}
|
|
1299
|
+
},
|
|
1300
|
+
stepSettings
|
|
1261
1301
|
});
|
|
1262
1302
|
}
|
|
1263
1303
|
async submitStep({ context, body, headers }) {
|
|
@@ -1275,8 +1315,9 @@ var LazyFunctionStep = class extends BaseLazyStep {
|
|
|
1275
1315
|
stepFunction;
|
|
1276
1316
|
stepType = "Run";
|
|
1277
1317
|
allowUndefinedOut = true;
|
|
1278
|
-
|
|
1279
|
-
|
|
1318
|
+
supportsDeferredSubmission = true;
|
|
1319
|
+
constructor(context, stepName, stepFunction, stepSettings) {
|
|
1320
|
+
super(context, stepName, stepSettings);
|
|
1280
1321
|
this.stepFunction = stepFunction;
|
|
1281
1322
|
}
|
|
1282
1323
|
getPlanStep(concurrent, targetStep) {
|
|
@@ -1306,6 +1347,7 @@ var LazySleepStep = class extends BaseLazyStep {
|
|
|
1306
1347
|
sleep;
|
|
1307
1348
|
stepType = "SleepFor";
|
|
1308
1349
|
allowUndefinedOut = true;
|
|
1350
|
+
supportsDeferredSubmission = true;
|
|
1309
1351
|
constructor(context, stepName, sleep) {
|
|
1310
1352
|
super(context, stepName);
|
|
1311
1353
|
this.sleep = sleep;
|
|
@@ -1345,6 +1387,7 @@ var LazySleepUntilStep = class extends BaseLazyStep {
|
|
|
1345
1387
|
sleepUntil;
|
|
1346
1388
|
stepType = "SleepUntil";
|
|
1347
1389
|
allowUndefinedOut = true;
|
|
1390
|
+
supportsDeferredSubmission = true;
|
|
1348
1391
|
constructor(context, stepName, sleepUntil) {
|
|
1349
1392
|
super(context, stepName);
|
|
1350
1393
|
this.sleepUntil = sleepUntil;
|
|
@@ -1837,6 +1880,7 @@ var WorkflowHeaders = class {
|
|
|
1837
1880
|
invokeCount;
|
|
1838
1881
|
initHeaderValue;
|
|
1839
1882
|
stepInfo;
|
|
1883
|
+
stepSettings;
|
|
1840
1884
|
headers;
|
|
1841
1885
|
/**
|
|
1842
1886
|
* @param params workflow header parameters
|
|
@@ -1846,17 +1890,20 @@ var WorkflowHeaders = class {
|
|
|
1846
1890
|
workflowConfig,
|
|
1847
1891
|
invokeCount,
|
|
1848
1892
|
initHeaderValue,
|
|
1849
|
-
stepInfo
|
|
1893
|
+
stepInfo,
|
|
1894
|
+
stepSettings
|
|
1850
1895
|
}) {
|
|
1851
1896
|
this.userHeaders = userHeaders;
|
|
1852
1897
|
this.workflowConfig = workflowConfig;
|
|
1853
1898
|
this.invokeCount = invokeCount;
|
|
1854
1899
|
this.initHeaderValue = initHeaderValue;
|
|
1855
1900
|
this.stepInfo = stepInfo;
|
|
1901
|
+
this.stepSettings = stepSettings;
|
|
1856
1902
|
this.headers = {
|
|
1857
1903
|
rawHeaders: {},
|
|
1858
1904
|
workflowHeaders: {},
|
|
1859
|
-
failureHeaders: {}
|
|
1905
|
+
failureHeaders: {},
|
|
1906
|
+
stepSettingsHeaders: {}
|
|
1860
1907
|
};
|
|
1861
1908
|
}
|
|
1862
1909
|
getHeaders() {
|
|
@@ -1867,6 +1914,7 @@ var WorkflowHeaders = class {
|
|
|
1867
1914
|
this.addUserHeaders();
|
|
1868
1915
|
this.addInvokeCount();
|
|
1869
1916
|
this.addFailureUrl();
|
|
1917
|
+
this.addStepSettings();
|
|
1870
1918
|
const contentType = this.addContentType();
|
|
1871
1919
|
return this.prefixHeaders(contentType);
|
|
1872
1920
|
}
|
|
@@ -1876,7 +1924,7 @@ var WorkflowHeaders = class {
|
|
|
1876
1924
|
[WORKFLOW_INIT_HEADER]: this.initHeaderValue,
|
|
1877
1925
|
[WORKFLOW_ID_HEADER]: this.workflowConfig.workflowRunId,
|
|
1878
1926
|
[WORKFLOW_URL_HEADER]: this.workflowConfig.workflowUrl,
|
|
1879
|
-
[WORKFLOW_FEATURE_HEADER]:
|
|
1927
|
+
[WORKFLOW_FEATURE_HEADER]: WORKFLOW_FEATURE_SET,
|
|
1880
1928
|
[WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
|
|
1881
1929
|
...this.workflowConfig.telemetry ? getTelemetryHeaders(this.workflowConfig.telemetry) : {}
|
|
1882
1930
|
};
|
|
@@ -1954,7 +2002,7 @@ var WorkflowHeaders = class {
|
|
|
1954
2002
|
this.headers.failureHeaders["Workflow-Init"] = "false";
|
|
1955
2003
|
this.headers.failureHeaders["Workflow-Url"] = this.workflowConfig.workflowUrl;
|
|
1956
2004
|
this.headers.failureHeaders["Workflow-Calltype"] = "failureCall";
|
|
1957
|
-
this.headers.failureHeaders["Feature-Set"] =
|
|
2005
|
+
this.headers.failureHeaders["Feature-Set"] = WORKFLOW_FEATURE_SET;
|
|
1958
2006
|
if (this.workflowConfig.retries !== void 0 && this.workflowConfig.retries !== DEFAULT_RETRIES) {
|
|
1959
2007
|
this.headers.failureHeaders["Retries"] = this.workflowConfig.retries.toString();
|
|
1960
2008
|
}
|
|
@@ -1962,6 +2010,42 @@ var WorkflowHeaders = class {
|
|
|
1962
2010
|
this.headers.failureHeaders["Retry-Delay"] = this.workflowConfig.retryDelay.toString();
|
|
1963
2011
|
}
|
|
1964
2012
|
}
|
|
2013
|
+
/**
|
|
2014
|
+
* Applies the step-level settings of a step to the message.
|
|
2015
|
+
*
|
|
2016
|
+
* These headers configure the message itself, so that QStash uses them
|
|
2017
|
+
* instead of the settings the run was triggered with. The delivery of
|
|
2018
|
+
* the message is what executes the step, which is how a step's
|
|
2019
|
+
* settings reach it.
|
|
2020
|
+
*
|
|
2021
|
+
* When any setting is present the feature set is extended with
|
|
2022
|
+
* `WF_StepConfig`, which is what tells QStash to keep them. QStash
|
|
2023
|
+
* reports that back on the delivery as `Upstash-Workflow-Step-Config`.
|
|
2024
|
+
*/
|
|
2025
|
+
addStepSettings() {
|
|
2026
|
+
if (!this.stepSettings) {
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
const headers = {};
|
|
2030
|
+
if (this.stepSettings.flowControl) {
|
|
2031
|
+
const { flowControlKey, flowControlValue } = prepareFlowControl(
|
|
2032
|
+
this.stepSettings.flowControl
|
|
2033
|
+
);
|
|
2034
|
+
headers[FLOW_CONTROL_KEY_HEADER] = flowControlKey;
|
|
2035
|
+
headers[FLOW_CONTROL_VALUE_HEADER] = flowControlValue;
|
|
2036
|
+
}
|
|
2037
|
+
if (this.stepSettings.retries !== void 0) {
|
|
2038
|
+
headers[RETRIES_HEADER] = this.stepSettings.retries.toString();
|
|
2039
|
+
}
|
|
2040
|
+
if (this.stepSettings.retryDelay) {
|
|
2041
|
+
headers[RETRY_DELAY_HEADER] = this.stepSettings.retryDelay;
|
|
2042
|
+
}
|
|
2043
|
+
if (Object.keys(headers).length === 0) {
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
headers[WORKFLOW_FEATURE_HEADER] = `${WORKFLOW_FEATURE_SET},${WORKFLOW_STEP_CONFIG_FEATURE}`;
|
|
2047
|
+
this.headers.stepSettingsHeaders = headers;
|
|
2048
|
+
}
|
|
1965
2049
|
addContentType() {
|
|
1966
2050
|
if (this.workflowConfig.useJSONContent) {
|
|
1967
2051
|
this.headers.rawHeaders["content-type"] = "application/json";
|
|
@@ -1975,14 +2059,17 @@ var WorkflowHeaders = class {
|
|
|
1975
2059
|
return contentType;
|
|
1976
2060
|
}
|
|
1977
2061
|
prefixHeaders(contentType) {
|
|
1978
|
-
const { rawHeaders, workflowHeaders, failureHeaders } = this.headers;
|
|
2062
|
+
const { rawHeaders, workflowHeaders, failureHeaders, stepSettingsHeaders } = this.headers;
|
|
1979
2063
|
const isCall = this.stepInfo?.lazyStep.stepType === "Call";
|
|
1980
2064
|
return {
|
|
1981
2065
|
headers: {
|
|
1982
2066
|
...rawHeaders,
|
|
1983
2067
|
...addPrefixToHeaders(workflowHeaders, isCall ? "Upstash-Callback-" : "Upstash-"),
|
|
1984
2068
|
...addPrefixToHeaders(failureHeaders, "Upstash-Failure-Callback-"),
|
|
1985
|
-
...isCall ? addPrefixToHeaders(failureHeaders, "Upstash-Callback-Failure-Callback-") : {}
|
|
2069
|
+
...isCall ? addPrefixToHeaders(failureHeaders, "Upstash-Callback-Failure-Callback-") : {},
|
|
2070
|
+
// last, so they override the run's settings above. Never
|
|
2071
|
+
// prefixed: they configure this message, not its callback.
|
|
2072
|
+
...stepSettingsHeaders
|
|
1986
2073
|
},
|
|
1987
2074
|
contentType
|
|
1988
2075
|
};
|
|
@@ -2033,7 +2120,7 @@ var submitParallelSteps = async ({
|
|
|
2033
2120
|
info: `Submitting ${planSteps.length} parallel steps.`
|
|
2034
2121
|
});
|
|
2035
2122
|
const result = await context.qstashClient.batch(
|
|
2036
|
-
planSteps.map((planStep) => {
|
|
2123
|
+
planSteps.map((planStep, index) => {
|
|
2037
2124
|
const { headers } = getHeaders({
|
|
2038
2125
|
initHeaderValue: "false",
|
|
2039
2126
|
workflowConfig: {
|
|
@@ -2041,7 +2128,8 @@ var submitParallelSteps = async ({
|
|
|
2041
2128
|
workflowUrl: context.url,
|
|
2042
2129
|
telemetry
|
|
2043
2130
|
},
|
|
2044
|
-
invokeCount
|
|
2131
|
+
invokeCount,
|
|
2132
|
+
stepSettings: steps[index].stepSettings
|
|
2045
2133
|
});
|
|
2046
2134
|
return {
|
|
2047
2135
|
headers,
|
|
@@ -2060,31 +2148,38 @@ var submitParallelSteps = async ({
|
|
|
2060
2148
|
}
|
|
2061
2149
|
throw new WorkflowAbort(planSteps[0].stepName, planSteps[0]);
|
|
2062
2150
|
};
|
|
2063
|
-
var
|
|
2064
|
-
context,
|
|
2151
|
+
var executeStep = async ({
|
|
2065
2152
|
lazyStep,
|
|
2066
2153
|
stepId,
|
|
2067
|
-
invokeCount,
|
|
2068
2154
|
concurrency,
|
|
2069
|
-
telemetry,
|
|
2070
|
-
dispatchDebug,
|
|
2071
2155
|
dispatchLifecycle
|
|
2072
2156
|
}) => {
|
|
2073
2157
|
await dispatchLifecycle("beforeExecution", {
|
|
2074
2158
|
stepName: lazyStep.stepName
|
|
2075
2159
|
});
|
|
2076
|
-
let resultStep;
|
|
2077
2160
|
try {
|
|
2078
|
-
|
|
2161
|
+
return await lazyStep.getResultStep(concurrency, stepId);
|
|
2079
2162
|
} catch (error) {
|
|
2080
2163
|
attachStepNameToError(error, lazyStep.stepName);
|
|
2081
2164
|
throw error;
|
|
2082
2165
|
}
|
|
2166
|
+
};
|
|
2167
|
+
var submitStepResult = async ({
|
|
2168
|
+
context,
|
|
2169
|
+
lazyStep,
|
|
2170
|
+
resultStep,
|
|
2171
|
+
invokeCount,
|
|
2172
|
+
concurrency,
|
|
2173
|
+
telemetry,
|
|
2174
|
+
dispatchDebug,
|
|
2175
|
+
nextStepSettings
|
|
2176
|
+
}) => {
|
|
2083
2177
|
const { headers } = lazyStep.getHeaders({
|
|
2084
2178
|
context,
|
|
2085
2179
|
step: resultStep,
|
|
2086
2180
|
invokeCount,
|
|
2087
|
-
telemetry
|
|
2181
|
+
telemetry,
|
|
2182
|
+
stepSettings: nextStepSettings
|
|
2088
2183
|
});
|
|
2089
2184
|
const body = lazyStep.getBody({
|
|
2090
2185
|
context,
|
|
@@ -2109,6 +2204,138 @@ var submitSingleStep = async ({
|
|
|
2109
2204
|
}
|
|
2110
2205
|
return resultStep;
|
|
2111
2206
|
};
|
|
2207
|
+
var publishStepConfigRequest = async ({
|
|
2208
|
+
context,
|
|
2209
|
+
lazyStep,
|
|
2210
|
+
targetStep,
|
|
2211
|
+
invokeCount,
|
|
2212
|
+
telemetry,
|
|
2213
|
+
dispatchDebug
|
|
2214
|
+
}) => {
|
|
2215
|
+
const { headers } = getHeaders({
|
|
2216
|
+
initHeaderValue: "false",
|
|
2217
|
+
workflowConfig: {
|
|
2218
|
+
workflowRunId: context.workflowRunId,
|
|
2219
|
+
workflowUrl: context.url,
|
|
2220
|
+
telemetry
|
|
2221
|
+
},
|
|
2222
|
+
invokeCount,
|
|
2223
|
+
stepSettings: lazyStep.stepSettings
|
|
2224
|
+
});
|
|
2225
|
+
await dispatchDebug("onInfo", {
|
|
2226
|
+
info: `Step "${lazyStep.stepName}" (${targetStep}) has step-level settings which the current delivery does not carry. Requesting a delivery which does.`
|
|
2227
|
+
});
|
|
2228
|
+
const result = await context.qstashClient.publishJSON({
|
|
2229
|
+
headers: {
|
|
2230
|
+
...headers,
|
|
2231
|
+
"Upstash-Workflow-CallType": WORKFLOW_STEP_CONFIG_CALL_TYPE
|
|
2232
|
+
},
|
|
2233
|
+
method: "POST",
|
|
2234
|
+
body: { targetStep, invokeCount },
|
|
2235
|
+
url: context.url,
|
|
2236
|
+
contentBasedDeduplication: true
|
|
2237
|
+
});
|
|
2238
|
+
if (result?.deduplicated) {
|
|
2239
|
+
await dispatchDebug("onWarning", {
|
|
2240
|
+
warning: `A step config request for step "${lazyStep.stepName}" (${targetStep}) was already published and this one was deduplicated. Expected when the delivery which published it is being retried; otherwise the run stops here.`
|
|
2241
|
+
});
|
|
2242
|
+
} else if (result?.messageId) {
|
|
2243
|
+
await dispatchDebug("onInfo", {
|
|
2244
|
+
info: `Requested a delivery under the settings of step "${lazyStep.stepName}" with messageId: ${result.messageId}.`
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
};
|
|
2248
|
+
|
|
2249
|
+
// src/qstash/step-config.ts
|
|
2250
|
+
var DEFAULT_PARALLELISM = 0;
|
|
2251
|
+
var DEFAULT_RATE = 0;
|
|
2252
|
+
var DEFAULT_PERIOD_SECONDS = 1;
|
|
2253
|
+
var SECONDS_PER_UNIT = {
|
|
2254
|
+
s: 1,
|
|
2255
|
+
m: 60,
|
|
2256
|
+
h: 60 * 60,
|
|
2257
|
+
d: 24 * 60 * 60
|
|
2258
|
+
};
|
|
2259
|
+
var durationToSeconds = (duration) => {
|
|
2260
|
+
if (typeof duration === "number") {
|
|
2261
|
+
return Number.isFinite(duration) ? duration : void 0;
|
|
2262
|
+
}
|
|
2263
|
+
const match = /^(\d+)([smhd])$/.exec(duration.trim());
|
|
2264
|
+
if (match) {
|
|
2265
|
+
return Number(match[1]) * SECONDS_PER_UNIT[match[2]];
|
|
2266
|
+
}
|
|
2267
|
+
const seconds = Number(duration.trim());
|
|
2268
|
+
return Number.isFinite(seconds) ? seconds : void 0;
|
|
2269
|
+
};
|
|
2270
|
+
var normalizeFlowControl = (flowControl) => {
|
|
2271
|
+
const period = flowControl.period === void 0 ? void 0 : durationToSeconds(flowControl.period);
|
|
2272
|
+
return {
|
|
2273
|
+
key: flowControl.key,
|
|
2274
|
+
parallelism: flowControl.parallelism ?? DEFAULT_PARALLELISM,
|
|
2275
|
+
rate: flowControl.rate ?? flowControl.ratePerSecond ?? DEFAULT_RATE,
|
|
2276
|
+
period: period ?? DEFAULT_PERIOD_SECONDS
|
|
2277
|
+
};
|
|
2278
|
+
};
|
|
2279
|
+
var parseFlowControlHeaders = (key, value) => {
|
|
2280
|
+
if (!key) {
|
|
2281
|
+
return void 0;
|
|
2282
|
+
}
|
|
2283
|
+
const flowControl = {
|
|
2284
|
+
key,
|
|
2285
|
+
parallelism: DEFAULT_PARALLELISM,
|
|
2286
|
+
rate: DEFAULT_RATE,
|
|
2287
|
+
period: DEFAULT_PERIOD_SECONDS
|
|
2288
|
+
};
|
|
2289
|
+
for (const entry of (value ?? "").split(",")) {
|
|
2290
|
+
const [name, rawValue] = entry.split("=").map((part) => part.trim());
|
|
2291
|
+
if (!name || rawValue === void 0) {
|
|
2292
|
+
continue;
|
|
2293
|
+
}
|
|
2294
|
+
switch (name) {
|
|
2295
|
+
case "parallelism": {
|
|
2296
|
+
flowControl.parallelism = Number(rawValue);
|
|
2297
|
+
break;
|
|
2298
|
+
}
|
|
2299
|
+
case "rate": {
|
|
2300
|
+
flowControl.rate = Number(rawValue);
|
|
2301
|
+
break;
|
|
2302
|
+
}
|
|
2303
|
+
case "period": {
|
|
2304
|
+
flowControl.period = durationToSeconds(rawValue) ?? DEFAULT_PERIOD_SECONDS;
|
|
2305
|
+
break;
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
return flowControl;
|
|
2310
|
+
};
|
|
2311
|
+
var getEffectiveConfig = (headers) => {
|
|
2312
|
+
const retriesHeader = headers.get(RETRIES_HEADER);
|
|
2313
|
+
return {
|
|
2314
|
+
flowControl: parseFlowControlHeaders(
|
|
2315
|
+
headers.get(FLOW_CONTROL_KEY_HEADER),
|
|
2316
|
+
headers.get(FLOW_CONTROL_VALUE_HEADER)
|
|
2317
|
+
),
|
|
2318
|
+
retries: retriesHeader === null ? void 0 : Number(retriesHeader),
|
|
2319
|
+
retryDelay: headers.get(RETRY_DELAY_HEADER) ?? void 0,
|
|
2320
|
+
hasStepConfig: headers.get(WORKFLOW_STEP_CONFIG_HEADER) === "true"
|
|
2321
|
+
};
|
|
2322
|
+
};
|
|
2323
|
+
var describeStepSettingsMismatch = (stepSettings, effectiveConfig) => {
|
|
2324
|
+
if (stepSettings.flowControl) {
|
|
2325
|
+
const wanted = normalizeFlowControl(stepSettings.flowControl);
|
|
2326
|
+
const applied = effectiveConfig.flowControl;
|
|
2327
|
+
if (!applied || applied.key !== wanted.key || applied.parallelism !== wanted.parallelism || applied.rate !== wanted.rate || applied.period !== wanted.period) {
|
|
2328
|
+
return `flow control: expected ${JSON.stringify(wanted)}, delivery has ${JSON.stringify(applied)}`;
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
if (stepSettings.retries !== void 0 && effectiveConfig.retries !== void 0 && stepSettings.retries !== effectiveConfig.retries) {
|
|
2332
|
+
return `retries: expected ${stepSettings.retries}, delivery has ${effectiveConfig.retries}`;
|
|
2333
|
+
}
|
|
2334
|
+
if (stepSettings.retryDelay !== void 0 && stepSettings.retryDelay !== effectiveConfig.retryDelay) {
|
|
2335
|
+
return `retry delay: expected '${stepSettings.retryDelay}', delivery has '${effectiveConfig.retryDelay ?? ""}'`;
|
|
2336
|
+
}
|
|
2337
|
+
return void 0;
|
|
2338
|
+
};
|
|
2112
2339
|
|
|
2113
2340
|
// src/context/auto-executor.ts
|
|
2114
2341
|
var AutoExecutor = class _AutoExecutor {
|
|
@@ -2125,6 +2352,25 @@ var AutoExecutor = class _AutoExecutor {
|
|
|
2125
2352
|
stepCount = 0;
|
|
2126
2353
|
planStepCount = 0;
|
|
2127
2354
|
executingStep = false;
|
|
2355
|
+
/**
|
|
2356
|
+
* an executed step whose result hasn't been submitted to QStash yet.
|
|
2357
|
+
*
|
|
2358
|
+
* Set when a step which can produce its result in-process executes. The
|
|
2359
|
+
* submission waits so that the route function can continue and reveal
|
|
2360
|
+
* what comes next: if that is a single step with step-level settings,
|
|
2361
|
+
* the settings ride on this submission and no step config request is
|
|
2362
|
+
* needed, since the delivery of the submission is what executes it.
|
|
2363
|
+
*
|
|
2364
|
+
* Flushed when the next step is reached (see `addStep`) or when the
|
|
2365
|
+
* route function ends (see `flushPendingStep` in `serve`).
|
|
2366
|
+
*/
|
|
2367
|
+
pendingStep;
|
|
2368
|
+
/**
|
|
2369
|
+
* configuration QStash applied to the delivery being handled. A step
|
|
2370
|
+
* with step-level settings is settled by comparing its settings against
|
|
2371
|
+
* this.
|
|
2372
|
+
*/
|
|
2373
|
+
effectiveConfig;
|
|
2128
2374
|
/**
|
|
2129
2375
|
* @param context workflow context
|
|
2130
2376
|
* @param steps list of steps
|
|
@@ -2132,14 +2378,17 @@ var AutoExecutor = class _AutoExecutor {
|
|
|
2132
2378
|
* @param dispatchLifecycle lifecycle event dispatcher
|
|
2133
2379
|
* @param telemetry optional telemetry information
|
|
2134
2380
|
* @param invokeCount optional invoke count
|
|
2381
|
+
* @param effectiveConfig configuration QStash applied to the delivery
|
|
2382
|
+
* being handled
|
|
2135
2383
|
*/
|
|
2136
|
-
constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry, invokeCount) {
|
|
2384
|
+
constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry, invokeCount, effectiveConfig) {
|
|
2137
2385
|
this.context = context;
|
|
2138
2386
|
this.steps = steps;
|
|
2139
2387
|
this.dispatchDebug = dispatchDebug;
|
|
2140
2388
|
this.dispatchLifecycle = dispatchLifecycle;
|
|
2141
2389
|
this.telemetry = telemetry;
|
|
2142
2390
|
this.invokeCount = invokeCount ?? 0;
|
|
2391
|
+
this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
|
|
2143
2392
|
this.nonPlanStepCount = this.steps.filter((step) => !step.targetStep).length;
|
|
2144
2393
|
}
|
|
2145
2394
|
/**
|
|
@@ -2174,6 +2423,15 @@ var AutoExecutor = class _AutoExecutor {
|
|
|
2174
2423
|
lazyStepList.push(stepInfo);
|
|
2175
2424
|
const index = this.indexInCurrentList++;
|
|
2176
2425
|
const requestComplete = this.deferExecution().then(async () => {
|
|
2426
|
+
const submitted = await this.submitPendingStep(
|
|
2427
|
+
lazyStepList.length === 1 ? lazyStepList[0].stepSettings : void 0
|
|
2428
|
+
);
|
|
2429
|
+
if (submitted.isErr()) {
|
|
2430
|
+
throw submitted.error;
|
|
2431
|
+
}
|
|
2432
|
+
if (submitted.value.result === "submitted-step") {
|
|
2433
|
+
throw submitted.value.abort;
|
|
2434
|
+
}
|
|
2177
2435
|
if (!this.promises.has(lazyStepList)) {
|
|
2178
2436
|
const promise2 = this.getExecutionPromise(lazyStepList);
|
|
2179
2437
|
this.promises.set(lazyStepList, promise2);
|
|
@@ -2227,18 +2485,100 @@ var AutoExecutor = class _AutoExecutor {
|
|
|
2227
2485
|
}
|
|
2228
2486
|
return parsedOut;
|
|
2229
2487
|
}
|
|
2230
|
-
const
|
|
2231
|
-
|
|
2488
|
+
const mismatch = lazyStep.stepSettings ? describeStepSettingsMismatch(lazyStep.stepSettings, this.effectiveConfig) : void 0;
|
|
2489
|
+
if (mismatch) {
|
|
2490
|
+
if (!this.effectiveConfig.hasStepConfig) {
|
|
2491
|
+
await publishStepConfigRequest({
|
|
2492
|
+
context: this.context,
|
|
2493
|
+
lazyStep,
|
|
2494
|
+
targetStep: this.stepCount,
|
|
2495
|
+
invokeCount: this.invokeCount,
|
|
2496
|
+
telemetry: this.telemetry,
|
|
2497
|
+
dispatchDebug: this.dispatchDebug
|
|
2498
|
+
});
|
|
2499
|
+
throw new WorkflowAbort(lazyStep.stepName);
|
|
2500
|
+
} else {
|
|
2501
|
+
await this.dispatchDebug("onWarning", {
|
|
2502
|
+
warning: `The step-level settings of step '${lazyStep.stepName}' were published but are not recognized on the delivery which carries them (${mismatch}). Executing the step with the settings of the delivery instead. This is a bug in @upstash/workflow, please report it.`
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
const resultStep = await executeStep({
|
|
2232
2507
|
lazyStep,
|
|
2233
2508
|
stepId: this.stepCount,
|
|
2509
|
+
concurrency: NO_CONCURRENCY,
|
|
2510
|
+
dispatchLifecycle: this.dispatchLifecycle
|
|
2511
|
+
});
|
|
2512
|
+
if (lazyStep.supportsDeferredSubmission) {
|
|
2513
|
+
this.pendingStep = { status: "held", lazyStep, resultStep };
|
|
2514
|
+
return lazyStep.parseOut({
|
|
2515
|
+
...resultStep,
|
|
2516
|
+
out: resultStep.out === void 0 ? void 0 : JSON.stringify(resultStep.out)
|
|
2517
|
+
});
|
|
2518
|
+
}
|
|
2519
|
+
await submitStepResult({
|
|
2520
|
+
context: this.context,
|
|
2521
|
+
lazyStep,
|
|
2522
|
+
resultStep,
|
|
2234
2523
|
invokeCount: this.invokeCount,
|
|
2235
|
-
concurrency:
|
|
2524
|
+
concurrency: NO_CONCURRENCY,
|
|
2236
2525
|
telemetry: this.telemetry,
|
|
2237
|
-
dispatchDebug: this.dispatchDebug
|
|
2238
|
-
dispatchLifecycle: this.dispatchLifecycle
|
|
2526
|
+
dispatchDebug: this.dispatchDebug
|
|
2239
2527
|
});
|
|
2240
2528
|
throw new WorkflowAbort(lazyStep.stepName, resultStep);
|
|
2241
2529
|
}
|
|
2530
|
+
/**
|
|
2531
|
+
* Submits the result of the step being held, if there is one, and
|
|
2532
|
+
* returns the abort the caller has to throw to end the invocation.
|
|
2533
|
+
*
|
|
2534
|
+
* Returns undefined when no step is being held, which is the caller's
|
|
2535
|
+
* signal that there is nothing to end. Throws instead of returning
|
|
2536
|
+
* when the submission itself fails, so that error reaches the caller
|
|
2537
|
+
* rather than an abort claiming the step was submitted.
|
|
2538
|
+
*
|
|
2539
|
+
* Callers which arrive while a submission is already under way (the
|
|
2540
|
+
* steps of a parallel group, added together) wait for that one and get
|
|
2541
|
+
* the same abort, rather than submitting a second time.
|
|
2542
|
+
*
|
|
2543
|
+
* The next step's settings are attached whenever it has any, without
|
|
2544
|
+
* checking them against the current delivery: the executor only knows
|
|
2545
|
+
* the configuration of the delivery in hand, which inside a
|
|
2546
|
+
* delivery is the *previous* step's, so such a check would be wrong
|
|
2547
|
+
* exactly when two steps with settings follow each other.
|
|
2548
|
+
*
|
|
2549
|
+
* @param nextStepSettings step-level settings of the next step
|
|
2550
|
+
* @returns `submitted-step` with the abort which ends this invocation,
|
|
2551
|
+
* or `no-pending-step` when nothing was held and there is nothing to
|
|
2552
|
+
* end
|
|
2553
|
+
*/
|
|
2554
|
+
async submitPendingStep(nextStepSettings) {
|
|
2555
|
+
if (!this.pendingStep) {
|
|
2556
|
+
return ok({ result: "no-pending-step" });
|
|
2557
|
+
}
|
|
2558
|
+
try {
|
|
2559
|
+
if (this.pendingStep.status === "submitting") {
|
|
2560
|
+
return ok({ result: "submitted-step", abort: await this.pendingStep.submitted });
|
|
2561
|
+
}
|
|
2562
|
+
const { lazyStep, resultStep } = this.pendingStep;
|
|
2563
|
+
const submitted = (async () => {
|
|
2564
|
+
await submitStepResult({
|
|
2565
|
+
context: this.context,
|
|
2566
|
+
lazyStep,
|
|
2567
|
+
resultStep,
|
|
2568
|
+
invokeCount: this.invokeCount,
|
|
2569
|
+
concurrency: NO_CONCURRENCY,
|
|
2570
|
+
telemetry: this.telemetry,
|
|
2571
|
+
dispatchDebug: this.dispatchDebug,
|
|
2572
|
+
nextStepSettings
|
|
2573
|
+
});
|
|
2574
|
+
return new WorkflowAbort(lazyStep.stepName, resultStep);
|
|
2575
|
+
})();
|
|
2576
|
+
this.pendingStep = { status: "submitting", submitted };
|
|
2577
|
+
return ok({ result: "submitted-step", abort: await submitted });
|
|
2578
|
+
} catch (error) {
|
|
2579
|
+
return err(error);
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2242
2582
|
/**
|
|
2243
2583
|
* Runs steps in parallel.
|
|
2244
2584
|
*
|
|
@@ -2287,15 +2627,20 @@ var AutoExecutor = class _AutoExecutor {
|
|
|
2287
2627
|
validateStep(parallelSteps[stepIndex], planStep);
|
|
2288
2628
|
try {
|
|
2289
2629
|
const parallelStep = parallelSteps[stepIndex];
|
|
2290
|
-
const resultStep = await
|
|
2291
|
-
context: this.context,
|
|
2630
|
+
const resultStep = await executeStep({
|
|
2292
2631
|
lazyStep: parallelStep,
|
|
2293
2632
|
stepId: planStep.targetStep,
|
|
2633
|
+
concurrency: parallelSteps.length,
|
|
2634
|
+
dispatchLifecycle: this.dispatchLifecycle
|
|
2635
|
+
});
|
|
2636
|
+
await submitStepResult({
|
|
2637
|
+
context: this.context,
|
|
2638
|
+
lazyStep: parallelStep,
|
|
2639
|
+
resultStep,
|
|
2294
2640
|
invokeCount: this.invokeCount,
|
|
2295
2641
|
concurrency: parallelSteps.length,
|
|
2296
2642
|
telemetry: this.telemetry,
|
|
2297
|
-
dispatchDebug: this.dispatchDebug
|
|
2298
|
-
dispatchLifecycle: this.dispatchLifecycle
|
|
2643
|
+
dispatchDebug: this.dispatchDebug
|
|
2299
2644
|
});
|
|
2300
2645
|
throw new WorkflowAbort(parallelStep.stepName, resultStep);
|
|
2301
2646
|
} catch (error) {
|
|
@@ -2836,6 +3181,46 @@ var WorkflowContext = class {
|
|
|
2836
3181
|
* `1` on the first retry, `2` on the second, and so on.
|
|
2837
3182
|
*/
|
|
2838
3183
|
retried;
|
|
3184
|
+
/**
|
|
3185
|
+
* Configuration QStash applied to the request being handled.
|
|
3186
|
+
*
|
|
3187
|
+
* This is the configuration of *this delivery*, not of the run: it is
|
|
3188
|
+
* the configuration the run was triggered with, except inside the
|
|
3189
|
+
* delivery which executes a step that has step-level settings, where
|
|
3190
|
+
* it is that step's settings. The fields below expose it; the executor
|
|
3191
|
+
* gets it directly rather than reaching through the context.
|
|
3192
|
+
*/
|
|
3193
|
+
effectiveConfig;
|
|
3194
|
+
/**
|
|
3195
|
+
* Flow control QStash applied to the request being handled, if any.
|
|
3196
|
+
*
|
|
3197
|
+
* This is the flow control of the request in hand, which is the one the
|
|
3198
|
+
* run was triggered with except inside a step carrying its own.
|
|
3199
|
+
*/
|
|
3200
|
+
get flowControl() {
|
|
3201
|
+
return this.effectiveConfig.flowControl;
|
|
3202
|
+
}
|
|
3203
|
+
/**
|
|
3204
|
+
* Retry limit QStash applied to the request being handled, if it
|
|
3205
|
+
* reported one.
|
|
3206
|
+
*
|
|
3207
|
+
* Not to be confused with {@link retried}, which is how many retries
|
|
3208
|
+
* have already happened.
|
|
3209
|
+
*
|
|
3210
|
+
* @see {@link flowControl} for what "the request being handled" means
|
|
3211
|
+
*/
|
|
3212
|
+
get retries() {
|
|
3213
|
+
return this.effectiveConfig.retries;
|
|
3214
|
+
}
|
|
3215
|
+
/**
|
|
3216
|
+
* Retry delay expression QStash applied to the request being handled,
|
|
3217
|
+
* if any.
|
|
3218
|
+
*
|
|
3219
|
+
* @see {@link flowControl} for what "the request being handled" means
|
|
3220
|
+
*/
|
|
3221
|
+
get retryDelay() {
|
|
3222
|
+
return this.effectiveConfig.retryDelay;
|
|
3223
|
+
}
|
|
2839
3224
|
constructor({
|
|
2840
3225
|
qstashClient,
|
|
2841
3226
|
workflowRunId,
|
|
@@ -2849,7 +3234,8 @@ var WorkflowContext = class {
|
|
|
2849
3234
|
invokeCount,
|
|
2850
3235
|
label,
|
|
2851
3236
|
retried,
|
|
2852
|
-
middlewareManager
|
|
3237
|
+
middlewareManager,
|
|
3238
|
+
effectiveConfig
|
|
2853
3239
|
}) {
|
|
2854
3240
|
this.qstashClient = qstashClient;
|
|
2855
3241
|
this.workflowRunId = workflowRunId;
|
|
@@ -2861,6 +3247,7 @@ var WorkflowContext = class {
|
|
|
2861
3247
|
this.env = env ?? {};
|
|
2862
3248
|
this.labels = label === void 0 ? [] : Array.isArray(label) ? label : label ? label.split(",") : [];
|
|
2863
3249
|
this.retried = retried ?? 0;
|
|
3250
|
+
this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
|
|
2864
3251
|
const middlewareManagerInstance = middlewareManager ?? new MiddlewareManager([]);
|
|
2865
3252
|
middlewareManagerInstance.assignContext(this);
|
|
2866
3253
|
this.executor = new AutoExecutor(
|
|
@@ -2869,7 +3256,8 @@ var WorkflowContext = class {
|
|
|
2869
3256
|
middlewareManagerInstance.dispatchDebug.bind(middlewareManagerInstance),
|
|
2870
3257
|
middlewareManagerInstance.dispatchLifecycle.bind(middlewareManagerInstance),
|
|
2871
3258
|
telemetry,
|
|
2872
|
-
invokeCount
|
|
3259
|
+
invokeCount,
|
|
3260
|
+
this.effectiveConfig
|
|
2873
3261
|
);
|
|
2874
3262
|
}
|
|
2875
3263
|
/**
|
|
@@ -2895,13 +3283,35 @@ var WorkflowContext = class {
|
|
|
2895
3283
|
* ])
|
|
2896
3284
|
* ```
|
|
2897
3285
|
*
|
|
3286
|
+
* Step-level settings can be passed as a third argument, overriding
|
|
3287
|
+
* the settings the workflow run was triggered with for this step only:
|
|
3288
|
+
*
|
|
3289
|
+
* ```typescript
|
|
3290
|
+
* const result = await context.run(
|
|
3291
|
+
* "step 1",
|
|
3292
|
+
* () => {
|
|
3293
|
+
* return "result"
|
|
3294
|
+
* },
|
|
3295
|
+
* {
|
|
3296
|
+
* flowControl: { key: "custom-key", parallelism: 3 },
|
|
3297
|
+
* retries: 5,
|
|
3298
|
+
* }
|
|
3299
|
+
* )
|
|
3300
|
+
* ```
|
|
3301
|
+
*
|
|
2898
3302
|
* @param stepName name of the step
|
|
2899
3303
|
* @param stepFunction step function to be executed
|
|
3304
|
+
* @param stepSettings step-level settings for this step
|
|
2900
3305
|
* @returns result of the step function
|
|
2901
3306
|
*/
|
|
2902
|
-
async run(stepName, stepFunction) {
|
|
3307
|
+
async run(stepName, stepFunction, stepSettings) {
|
|
3308
|
+
if (stepSettings) {
|
|
3309
|
+
validateFlowControl(stepSettings.flowControl);
|
|
3310
|
+
}
|
|
2903
3311
|
const wrappedStepFunction = (() => this.executor.wrapStep(stepName, stepFunction));
|
|
2904
|
-
return await this.addStep(
|
|
3312
|
+
return await this.addStep(
|
|
3313
|
+
new LazyFunctionStep(this, stepName, wrappedStepFunction, stepSettings)
|
|
3314
|
+
);
|
|
2905
3315
|
}
|
|
2906
3316
|
/**
|
|
2907
3317
|
* Stops the execution for the duration provided.
|
|
@@ -3167,6 +3577,9 @@ var loggingMiddleware = new WorkflowMiddleware({
|
|
|
3167
3577
|
}
|
|
3168
3578
|
});
|
|
3169
3579
|
|
|
3580
|
+
// src/serve/index.ts
|
|
3581
|
+
import { startDevServer } from "@upstash/qstash";
|
|
3582
|
+
|
|
3170
3583
|
// src/serve/authorization.ts
|
|
3171
3584
|
import { Client as Client2 } from "@upstash/qstash";
|
|
3172
3585
|
var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowContext {
|
|
@@ -3198,7 +3611,7 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
|
|
|
3198
3611
|
*
|
|
3199
3612
|
* @param routeFunction
|
|
3200
3613
|
*/
|
|
3201
|
-
static async tryAuthentication(routeFunction, context) {
|
|
3614
|
+
static async tryAuthentication(routeFunction, context, effectiveConfig) {
|
|
3202
3615
|
const disabledContext = new _DisabledWorkflowContext({
|
|
3203
3616
|
qstashClient: new Client2({
|
|
3204
3617
|
baseUrl: "disabled-client",
|
|
@@ -3213,7 +3626,10 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
|
|
|
3213
3626
|
initialPayload: context.requestPayload,
|
|
3214
3627
|
env: context.env,
|
|
3215
3628
|
label: context.labels,
|
|
3216
|
-
retried: context.retried
|
|
3629
|
+
retried: context.retried,
|
|
3630
|
+
// the route function runs for real here until it reaches a step, so
|
|
3631
|
+
// it observes the same configuration as it will on the real context
|
|
3632
|
+
effectiveConfig
|
|
3217
3633
|
});
|
|
3218
3634
|
try {
|
|
3219
3635
|
await routeFunction(disabledContext);
|
|
@@ -3430,6 +3846,7 @@ var handleFailure = async ({
|
|
|
3430
3846
|
}
|
|
3431
3847
|
const userHeaders = recreateUserHeaders(request.headers);
|
|
3432
3848
|
const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
|
|
3849
|
+
const effectiveConfig = getEffectiveConfig(request.headers);
|
|
3433
3850
|
const workflowContext = new WorkflowContext({
|
|
3434
3851
|
qstashClient,
|
|
3435
3852
|
workflowRunId,
|
|
@@ -3443,11 +3860,13 @@ var handleFailure = async ({
|
|
|
3443
3860
|
label: userHeaders.get(WORKFLOW_LABEL_HEADER) ?? void 0,
|
|
3444
3861
|
retried,
|
|
3445
3862
|
workflowRunCreatedAt: workflowCreatedAt,
|
|
3446
|
-
middlewareManager: void 0
|
|
3863
|
+
middlewareManager: void 0,
|
|
3864
|
+
effectiveConfig
|
|
3447
3865
|
});
|
|
3448
3866
|
const authCheck = await DisabledWorkflowContext.tryAuthentication(
|
|
3449
3867
|
routeFunction,
|
|
3450
|
-
workflowContext
|
|
3868
|
+
workflowContext,
|
|
3869
|
+
effectiveConfig
|
|
3451
3870
|
);
|
|
3452
3871
|
if (authCheck.isErr()) {
|
|
3453
3872
|
await dispatchDebug?.("onError", {
|
|
@@ -3539,7 +3958,13 @@ var createRegionalHandler = (environment, receiverConfig, region, clientOptions)
|
|
|
3539
3958
|
const client = new Client3({
|
|
3540
3959
|
...clientOptions,
|
|
3541
3960
|
baseUrl: clientEnv.QSTASH_URL,
|
|
3542
|
-
token: clientEnv.QSTASH_TOKEN
|
|
3961
|
+
token: clientEnv.QSTASH_TOKEN,
|
|
3962
|
+
// Mirror the receiver's dev-mode decision so a single QSTASH_DEV=true points
|
|
3963
|
+
// the client at the local dev server too. Without this the QStash Client
|
|
3964
|
+
// re-derives dev mode from its own process.env, which is blind to the
|
|
3965
|
+
// Cloudflare worker `env` binding that actually carries QSTASH_DEV there.
|
|
3966
|
+
// An explicit devMode in the user's client config still wins.
|
|
3967
|
+
devMode: clientOptions?.devMode ?? isQStashDevModeEnabled(environment)
|
|
3543
3968
|
});
|
|
3544
3969
|
const receiver = getReceiver(environment, receiverConfig, region);
|
|
3545
3970
|
return { client, receiver };
|
|
@@ -3589,7 +4014,9 @@ var getQStashHandlers = ({
|
|
|
3589
4014
|
client: qstashClientOption && "http" in qstashClientOption ? qstashClientOption : new Client3({
|
|
3590
4015
|
...qstashClientOption,
|
|
3591
4016
|
baseUrl: environment.QSTASH_URL,
|
|
3592
|
-
token: environment.QSTASH_TOKEN
|
|
4017
|
+
token: environment.QSTASH_TOKEN,
|
|
4018
|
+
// Mirror the receiver's dev-mode decision (see createRegionalHandler).
|
|
4019
|
+
devMode: qstashClientOption?.devMode ?? isQStashDevModeEnabled(environment)
|
|
3593
4020
|
}),
|
|
3594
4021
|
receiver: getReceiver(environment, receiverConfig)
|
|
3595
4022
|
}
|
|
@@ -3855,12 +4282,14 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
|
|
|
3855
4282
|
const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
|
|
3856
4283
|
const label = request.headers.get(WORKFLOW_LABEL_HEADER) ?? void 0;
|
|
3857
4284
|
const workflowRunCreatedAt = request.headers.get(WORKFLOW_CREATED_AT_HEADER);
|
|
4285
|
+
const effectiveConfig = getEffectiveConfig(request.headers);
|
|
3858
4286
|
const workflowContext = new WorkflowContext({
|
|
3859
4287
|
qstashClient: regionalClient,
|
|
3860
4288
|
workflowRunId,
|
|
3861
4289
|
initialPayload: isThirdPartyCallResult(request) ? JSON.parse(rawInitialPayload) : initialPayloadParser(rawInitialPayload),
|
|
3862
4290
|
headers: recreateUserHeaders(request.headers),
|
|
3863
4291
|
steps,
|
|
4292
|
+
effectiveConfig,
|
|
3864
4293
|
url: workflowUrl,
|
|
3865
4294
|
env,
|
|
3866
4295
|
telemetry,
|
|
@@ -3872,7 +4301,8 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
|
|
|
3872
4301
|
});
|
|
3873
4302
|
const authCheck = await DisabledWorkflowContext.tryAuthentication(
|
|
3874
4303
|
routeFunction,
|
|
3875
|
-
workflowContext
|
|
4304
|
+
workflowContext,
|
|
4305
|
+
effectiveConfig
|
|
3876
4306
|
);
|
|
3877
4307
|
if (authCheck.isErr()) {
|
|
3878
4308
|
throw authCheck.error;
|
|
@@ -3909,7 +4339,23 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
|
|
|
3909
4339
|
if (steps.length === 1) {
|
|
3910
4340
|
await middlewareManager.dispatchLifecycle("runStarted", {});
|
|
3911
4341
|
}
|
|
3912
|
-
|
|
4342
|
+
let outcome;
|
|
4343
|
+
try {
|
|
4344
|
+
outcome = { ran: true, result: await routeFunction(workflowContext) };
|
|
4345
|
+
} catch (error) {
|
|
4346
|
+
outcome = { ran: false, error };
|
|
4347
|
+
}
|
|
4348
|
+
const submitted = await flushPendingStep(workflowContext);
|
|
4349
|
+
if (submitted.isErr()) {
|
|
4350
|
+
throw submitted.error;
|
|
4351
|
+
}
|
|
4352
|
+
if (submitted.value.result === "submitted-step") {
|
|
4353
|
+
throw submitted.value.abort;
|
|
4354
|
+
}
|
|
4355
|
+
if (!outcome.ran) {
|
|
4356
|
+
throw outcome.error;
|
|
4357
|
+
}
|
|
4358
|
+
return outcome.result;
|
|
3913
4359
|
},
|
|
3914
4360
|
onCleanup: async (result2) => {
|
|
3915
4361
|
await middlewareManager.dispatchLifecycle("runCompleted", {
|
|
@@ -3989,6 +4435,7 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
|
|
|
3989
4435
|
return { handler: safeHandler };
|
|
3990
4436
|
};
|
|
3991
4437
|
var serve = (routeFunction, options) => {
|
|
4438
|
+
void startDevServer();
|
|
3992
4439
|
return serveBase(
|
|
3993
4440
|
routeFunction,
|
|
3994
4441
|
{
|
|
@@ -4004,6 +4451,7 @@ export {
|
|
|
4004
4451
|
WorkflowAbort,
|
|
4005
4452
|
WorkflowNonRetryableError,
|
|
4006
4453
|
WorkflowRetryAfterError,
|
|
4454
|
+
isInstanceOf,
|
|
4007
4455
|
assertNonEmptyId,
|
|
4008
4456
|
toNonEmptyIdArray,
|
|
4009
4457
|
makeNotifyRequest,
|