@upstash/workflow 1.3.2 → 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 +595 -140
- package/astro.mjs +3 -1
- package/{chunk-ICDNN4UW.mjs → chunk-UH6O5IJY.mjs} +751 -281
- package/cloudflare.d.mts +3 -2
- package/cloudflare.d.ts +3 -2
- package/cloudflare.js +595 -140
- package/cloudflare.mjs +3 -1
- package/express.d.mts +3 -2
- package/express.d.ts +3 -2
- package/express.js +596 -140
- package/express.mjs +4 -1
- package/h3.d.mts +3 -2
- package/h3.d.ts +3 -2
- package/h3.js +595 -140
- package/h3.mjs +3 -1
- package/hono.d.mts +3 -2
- package/hono.d.ts +3 -2
- package/hono.js +595 -140
- package/hono.mjs +3 -1
- package/index.d.mts +64 -17
- package/index.d.ts +64 -17
- package/index.js +652 -150
- package/index.mjs +49 -11
- package/nextjs.d.mts +3 -2
- package/nextjs.d.ts +3 -2
- package/nextjs.js +596 -140
- 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 +595 -140
- 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 +595 -140
- package/solidjs.mjs +3 -1
- package/svelte.d.mts +3 -2
- package/svelte.d.ts +3 -2
- package/svelte.js +595 -140
- package/svelte.mjs +3 -1
- package/tanstack.d.mts +3 -2
- package/tanstack.d.ts +3 -2
- package/tanstack.js +595 -140
- 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
package/h3.js
CHANGED
|
@@ -335,6 +335,9 @@ async function _callHandler(event, handler, hooks) {
|
|
|
335
335
|
var H3Headers = globalThis.Headers;
|
|
336
336
|
var H3Response = globalThis.Response;
|
|
337
337
|
|
|
338
|
+
// src/serve/index.ts
|
|
339
|
+
var import_qstash12 = require("@upstash/qstash");
|
|
340
|
+
|
|
338
341
|
// src/client/utils.ts
|
|
339
342
|
var import_qstash2 = require("@upstash/qstash");
|
|
340
343
|
|
|
@@ -451,8 +454,15 @@ function isInstanceOf(v, ctor) {
|
|
|
451
454
|
}
|
|
452
455
|
|
|
453
456
|
// src/client/utils.ts
|
|
457
|
+
var assertNonEmptyId = (id, label = "id") => {
|
|
458
|
+
if (!id) {
|
|
459
|
+
throw new import_qstash2.QstashError(`${label} cannot be empty`);
|
|
460
|
+
}
|
|
461
|
+
};
|
|
454
462
|
var makeNotifyRequest = async (requester, eventId, eventData, workflowRunId) => {
|
|
455
|
-
|
|
463
|
+
assertNonEmptyId(eventId, "Event id");
|
|
464
|
+
if (workflowRunId !== void 0) assertNonEmptyId(workflowRunId, "Workflow run id");
|
|
465
|
+
const path = workflowRunId === void 0 ? ["v2", "notify", eventId] : ["v2", "notify", workflowRunId, eventId];
|
|
456
466
|
const result = await requester.request({
|
|
457
467
|
path,
|
|
458
468
|
method: "POST",
|
|
@@ -516,6 +526,14 @@ var WORKFLOW_CREATED_AT_HEADER = "Upstash-Workflow-CreatedAt";
|
|
|
516
526
|
var WORKFLOW_FAILURE_HEADER = "Upstash-Workflow-Is-Failure";
|
|
517
527
|
var WORKFLOW_FAILURE_CALLBACK_HEADER = "Upstash-Workflow-Failure-Callback";
|
|
518
528
|
var WORKFLOW_FEATURE_HEADER = "Upstash-Feature-Set";
|
|
529
|
+
var WORKFLOW_FEATURE_SET = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
|
|
530
|
+
var WORKFLOW_STEP_CONFIG_CALL_TYPE = "stepConfig";
|
|
531
|
+
var WORKFLOW_STEP_CONFIG_HEADER = "Upstash-Workflow-Step-Config";
|
|
532
|
+
var FLOW_CONTROL_KEY_HEADER = "Upstash-Flow-Control-Key";
|
|
533
|
+
var FLOW_CONTROL_VALUE_HEADER = "Upstash-Flow-Control-Value";
|
|
534
|
+
var RETRIES_HEADER = "Upstash-Retries";
|
|
535
|
+
var RETRY_DELAY_HEADER = "Upstash-Retry-Delay";
|
|
536
|
+
var WORKFLOW_STEP_CONFIG_FEATURE = "WF_StepConfig";
|
|
519
537
|
var WORKFLOW_INVOKE_COUNT_HEADER = "Upstash-Workflow-Invoke-Count";
|
|
520
538
|
var WORKFLOW_RETRIED_HEADER = "Upstash-Retried";
|
|
521
539
|
var WORKFLOW_LABEL_HEADER = "Upstash-Label";
|
|
@@ -533,102 +551,6 @@ var TELEMETRY_HEADER_SDK = "Upstash-Telemetry-Sdk";
|
|
|
533
551
|
var TELEMETRY_HEADER_FRAMEWORK = "Upstash-Telemetry-Framework";
|
|
534
552
|
var TELEMETRY_HEADER_RUNTIME = "Upstash-Telemetry-Runtime";
|
|
535
553
|
|
|
536
|
-
// src/context/auto-executor.ts
|
|
537
|
-
var import_qstash5 = require("@upstash/qstash");
|
|
538
|
-
|
|
539
|
-
// src/qstash/headers.ts
|
|
540
|
-
var import_qstash4 = require("@upstash/qstash");
|
|
541
|
-
|
|
542
|
-
// src/utils.ts
|
|
543
|
-
var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
|
|
544
|
-
var NANOID_LENGTH = 21;
|
|
545
|
-
var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
|
|
546
|
-
function validateLabel(label) {
|
|
547
|
-
if (label === void 0) return;
|
|
548
|
-
const labels = Array.isArray(label) ? label : [label];
|
|
549
|
-
if (labels.length === 0) {
|
|
550
|
-
throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
|
|
551
|
-
}
|
|
552
|
-
for (const value of labels) {
|
|
553
|
-
if (!RESOURCE_NAME_PATTERN.test(value)) {
|
|
554
|
-
throw new WorkflowNonRetryableError(
|
|
555
|
-
`Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
|
|
556
|
-
);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
function serializeLabel(label) {
|
|
561
|
-
return Array.isArray(label) ? label.join(",") : label;
|
|
562
|
-
}
|
|
563
|
-
function validateFlowControl(flowControl) {
|
|
564
|
-
if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
|
|
565
|
-
throw new WorkflowNonRetryableError(
|
|
566
|
-
`Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
|
|
567
|
-
);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
function getRandomInt() {
|
|
571
|
-
return Math.floor(Math.random() * NANOID_CHARS.length);
|
|
572
|
-
}
|
|
573
|
-
function nanoid(length = NANOID_LENGTH) {
|
|
574
|
-
return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
|
|
575
|
-
}
|
|
576
|
-
function getWorkflowRunId(id) {
|
|
577
|
-
return `wfr_${id ?? nanoid()}`;
|
|
578
|
-
}
|
|
579
|
-
function decodeBase64(base64) {
|
|
580
|
-
const binString = atob(base64);
|
|
581
|
-
try {
|
|
582
|
-
const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
|
|
583
|
-
return new TextDecoder().decode(intArray);
|
|
584
|
-
} catch (error) {
|
|
585
|
-
console.warn(
|
|
586
|
-
`Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
|
|
587
|
-
);
|
|
588
|
-
return binString;
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
function getUserIdFromToken(qstashClient) {
|
|
592
|
-
try {
|
|
593
|
-
const token = qstashClient.token;
|
|
594
|
-
const decodedToken = decodeBase64(token);
|
|
595
|
-
const tokenPayload = JSON.parse(decodedToken);
|
|
596
|
-
const userId = tokenPayload.UserID;
|
|
597
|
-
if (!userId) {
|
|
598
|
-
throw new WorkflowError("QStash token payload does not contain userId");
|
|
599
|
-
}
|
|
600
|
-
return userId;
|
|
601
|
-
} catch (error) {
|
|
602
|
-
throw new WorkflowError(
|
|
603
|
-
`Failed to decode QStash token while running create webhook step: ${error.message}`
|
|
604
|
-
);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
function getQStashUrl(qstashClient) {
|
|
608
|
-
try {
|
|
609
|
-
const requester = qstashClient.http;
|
|
610
|
-
const baseUrl = requester.baseUrl;
|
|
611
|
-
if (!baseUrl) {
|
|
612
|
-
throw new WorkflowError("QStash client does not have a baseUrl");
|
|
613
|
-
}
|
|
614
|
-
return baseUrl;
|
|
615
|
-
} catch (error) {
|
|
616
|
-
throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
function getEventId() {
|
|
620
|
-
return `evt_${nanoid(15)}`;
|
|
621
|
-
}
|
|
622
|
-
function stringifyBody(body) {
|
|
623
|
-
if (body === void 0) {
|
|
624
|
-
return void 0;
|
|
625
|
-
}
|
|
626
|
-
if (typeof body === "string") {
|
|
627
|
-
return body;
|
|
628
|
-
}
|
|
629
|
-
return JSON.stringify(body);
|
|
630
|
-
}
|
|
631
|
-
|
|
632
554
|
// node_modules/neverthrow/dist/index.es.js
|
|
633
555
|
var defaultErrorConfig = {
|
|
634
556
|
withStackTrace: false
|
|
@@ -1044,6 +966,102 @@ var Err = class {
|
|
|
1044
966
|
};
|
|
1045
967
|
var fromThrowable = Result.fromThrowable;
|
|
1046
968
|
|
|
969
|
+
// src/context/auto-executor.ts
|
|
970
|
+
var import_qstash5 = require("@upstash/qstash");
|
|
971
|
+
|
|
972
|
+
// src/qstash/headers.ts
|
|
973
|
+
var import_qstash4 = require("@upstash/qstash");
|
|
974
|
+
|
|
975
|
+
// src/utils.ts
|
|
976
|
+
var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
|
|
977
|
+
var NANOID_LENGTH = 21;
|
|
978
|
+
var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
|
|
979
|
+
function validateLabel(label) {
|
|
980
|
+
if (label === void 0) return;
|
|
981
|
+
const labels = Array.isArray(label) ? label : [label];
|
|
982
|
+
if (labels.length === 0) {
|
|
983
|
+
throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
|
|
984
|
+
}
|
|
985
|
+
for (const value of labels) {
|
|
986
|
+
if (!RESOURCE_NAME_PATTERN.test(value)) {
|
|
987
|
+
throw new WorkflowNonRetryableError(
|
|
988
|
+
`Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
function serializeLabel(label) {
|
|
994
|
+
return Array.isArray(label) ? label.join(",") : label;
|
|
995
|
+
}
|
|
996
|
+
function validateFlowControl(flowControl) {
|
|
997
|
+
if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
|
|
998
|
+
throw new WorkflowNonRetryableError(
|
|
999
|
+
`Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
function getRandomInt() {
|
|
1004
|
+
return Math.floor(Math.random() * NANOID_CHARS.length);
|
|
1005
|
+
}
|
|
1006
|
+
function nanoid(length = NANOID_LENGTH) {
|
|
1007
|
+
return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
|
|
1008
|
+
}
|
|
1009
|
+
function getWorkflowRunId(id) {
|
|
1010
|
+
return `wfr_${id ?? nanoid()}`;
|
|
1011
|
+
}
|
|
1012
|
+
function decodeBase64(base64) {
|
|
1013
|
+
const binString = atob(base64);
|
|
1014
|
+
try {
|
|
1015
|
+
const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
|
|
1016
|
+
return new TextDecoder().decode(intArray);
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
console.warn(
|
|
1019
|
+
`Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
|
|
1020
|
+
);
|
|
1021
|
+
return binString;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
function getUserIdFromToken(qstashClient) {
|
|
1025
|
+
try {
|
|
1026
|
+
const token = qstashClient.token;
|
|
1027
|
+
const decodedToken = decodeBase64(token);
|
|
1028
|
+
const tokenPayload = JSON.parse(decodedToken);
|
|
1029
|
+
const userId = tokenPayload.UserID;
|
|
1030
|
+
if (!userId) {
|
|
1031
|
+
throw new WorkflowError("QStash token payload does not contain userId");
|
|
1032
|
+
}
|
|
1033
|
+
return userId;
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
throw new WorkflowError(
|
|
1036
|
+
`Failed to decode QStash token while running create webhook step: ${error.message}`
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
function getQStashUrl(qstashClient) {
|
|
1041
|
+
try {
|
|
1042
|
+
const requester = qstashClient.http;
|
|
1043
|
+
const baseUrl = requester.baseUrl;
|
|
1044
|
+
if (!baseUrl) {
|
|
1045
|
+
throw new WorkflowError("QStash client does not have a baseUrl");
|
|
1046
|
+
}
|
|
1047
|
+
return baseUrl;
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
function getEventId() {
|
|
1053
|
+
return `evt_${nanoid(15)}`;
|
|
1054
|
+
}
|
|
1055
|
+
function stringifyBody(body) {
|
|
1056
|
+
if (body === void 0) {
|
|
1057
|
+
return void 0;
|
|
1058
|
+
}
|
|
1059
|
+
if (typeof body === "string") {
|
|
1060
|
+
return body;
|
|
1061
|
+
}
|
|
1062
|
+
return JSON.stringify(body);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1047
1065
|
// src/types.ts
|
|
1048
1066
|
var StepTypes = [
|
|
1049
1067
|
"Initial",
|
|
@@ -1149,6 +1167,10 @@ var triggerFirstInvocation = async (params) => {
|
|
|
1149
1167
|
return err(error_);
|
|
1150
1168
|
}
|
|
1151
1169
|
};
|
|
1170
|
+
var flushPendingStep = async (workflowContext) => {
|
|
1171
|
+
const { executor } = workflowContext;
|
|
1172
|
+
return await executor.submitPendingStep();
|
|
1173
|
+
};
|
|
1152
1174
|
var triggerRouteFunction = async ({
|
|
1153
1175
|
onCleanup,
|
|
1154
1176
|
onStep,
|
|
@@ -1356,8 +1378,29 @@ If you want to disable QStash Verification, you should clear env variables QSTAS
|
|
|
1356
1378
|
var BaseLazyStep = class _BaseLazyStep {
|
|
1357
1379
|
stepName;
|
|
1358
1380
|
context;
|
|
1359
|
-
|
|
1381
|
+
/**
|
|
1382
|
+
* step-level settings (flow control, retries etc.) which override the
|
|
1383
|
+
* settings the workflow run was triggered with, for this step only.
|
|
1384
|
+
*/
|
|
1385
|
+
stepSettings;
|
|
1386
|
+
/**
|
|
1387
|
+
* whether this step's result can be submitted after the route function
|
|
1388
|
+
* has moved on, instead of right away.
|
|
1389
|
+
*
|
|
1390
|
+
* Enabled for steps whose `getResultStep` produces the final `out` —
|
|
1391
|
+
* which is not the same as "needs no server round trip":
|
|
1392
|
+
* `LazyCreateWebhookStep` builds its result in `getBody` and returns
|
|
1393
|
+
* `out: undefined` from `getResultStep`, so deferring it would hand
|
|
1394
|
+
* `undefined` to the route function.
|
|
1395
|
+
*
|
|
1396
|
+
* Deferring lets the route function continue and reveal the next step,
|
|
1397
|
+
* so that step's settings can ride on this step's submission instead
|
|
1398
|
+
* of needing a step config request of their own.
|
|
1399
|
+
*/
|
|
1400
|
+
supportsDeferredSubmission = false;
|
|
1401
|
+
constructor(context, stepName, stepSettings) {
|
|
1360
1402
|
this.context = context;
|
|
1403
|
+
this.stepSettings = stepSettings;
|
|
1361
1404
|
if (!stepName) {
|
|
1362
1405
|
throw new WorkflowError(
|
|
1363
1406
|
"A workflow step name cannot be undefined or an empty string. Please provide a name for your workflow step."
|
|
@@ -1421,7 +1464,13 @@ var BaseLazyStep = class _BaseLazyStep {
|
|
|
1421
1464
|
step.out = JSON.stringify(step.out);
|
|
1422
1465
|
return JSON.stringify(step);
|
|
1423
1466
|
}
|
|
1424
|
-
getHeaders({
|
|
1467
|
+
getHeaders({
|
|
1468
|
+
context,
|
|
1469
|
+
telemetry: telemetry2,
|
|
1470
|
+
invokeCount,
|
|
1471
|
+
step,
|
|
1472
|
+
stepSettings
|
|
1473
|
+
}) {
|
|
1425
1474
|
return getHeaders({
|
|
1426
1475
|
initHeaderValue: "false",
|
|
1427
1476
|
workflowConfig: {
|
|
@@ -1434,7 +1483,8 @@ var BaseLazyStep = class _BaseLazyStep {
|
|
|
1434
1483
|
stepInfo: {
|
|
1435
1484
|
step,
|
|
1436
1485
|
lazyStep: this
|
|
1437
|
-
}
|
|
1486
|
+
},
|
|
1487
|
+
stepSettings
|
|
1438
1488
|
});
|
|
1439
1489
|
}
|
|
1440
1490
|
async submitStep({ context, body, headers }) {
|
|
@@ -1452,8 +1502,9 @@ var LazyFunctionStep = class extends BaseLazyStep {
|
|
|
1452
1502
|
stepFunction;
|
|
1453
1503
|
stepType = "Run";
|
|
1454
1504
|
allowUndefinedOut = true;
|
|
1455
|
-
|
|
1456
|
-
|
|
1505
|
+
supportsDeferredSubmission = true;
|
|
1506
|
+
constructor(context, stepName, stepFunction, stepSettings) {
|
|
1507
|
+
super(context, stepName, stepSettings);
|
|
1457
1508
|
this.stepFunction = stepFunction;
|
|
1458
1509
|
}
|
|
1459
1510
|
getPlanStep(concurrent, targetStep) {
|
|
@@ -1483,6 +1534,7 @@ var LazySleepStep = class extends BaseLazyStep {
|
|
|
1483
1534
|
sleep;
|
|
1484
1535
|
stepType = "SleepFor";
|
|
1485
1536
|
allowUndefinedOut = true;
|
|
1537
|
+
supportsDeferredSubmission = true;
|
|
1486
1538
|
constructor(context, stepName, sleep) {
|
|
1487
1539
|
super(context, stepName);
|
|
1488
1540
|
this.sleep = sleep;
|
|
@@ -1522,6 +1574,7 @@ var LazySleepUntilStep = class extends BaseLazyStep {
|
|
|
1522
1574
|
sleepUntil;
|
|
1523
1575
|
stepType = "SleepUntil";
|
|
1524
1576
|
allowUndefinedOut = true;
|
|
1577
|
+
supportsDeferredSubmission = true;
|
|
1525
1578
|
constructor(context, stepName, sleepUntil) {
|
|
1526
1579
|
super(context, stepName);
|
|
1527
1580
|
this.sleepUntil = sleepUntil;
|
|
@@ -2014,6 +2067,7 @@ var WorkflowHeaders = class {
|
|
|
2014
2067
|
invokeCount;
|
|
2015
2068
|
initHeaderValue;
|
|
2016
2069
|
stepInfo;
|
|
2070
|
+
stepSettings;
|
|
2017
2071
|
headers;
|
|
2018
2072
|
/**
|
|
2019
2073
|
* @param params workflow header parameters
|
|
@@ -2023,17 +2077,20 @@ var WorkflowHeaders = class {
|
|
|
2023
2077
|
workflowConfig,
|
|
2024
2078
|
invokeCount,
|
|
2025
2079
|
initHeaderValue,
|
|
2026
|
-
stepInfo
|
|
2080
|
+
stepInfo,
|
|
2081
|
+
stepSettings
|
|
2027
2082
|
}) {
|
|
2028
2083
|
this.userHeaders = userHeaders;
|
|
2029
2084
|
this.workflowConfig = workflowConfig;
|
|
2030
2085
|
this.invokeCount = invokeCount;
|
|
2031
2086
|
this.initHeaderValue = initHeaderValue;
|
|
2032
2087
|
this.stepInfo = stepInfo;
|
|
2088
|
+
this.stepSettings = stepSettings;
|
|
2033
2089
|
this.headers = {
|
|
2034
2090
|
rawHeaders: {},
|
|
2035
2091
|
workflowHeaders: {},
|
|
2036
|
-
failureHeaders: {}
|
|
2092
|
+
failureHeaders: {},
|
|
2093
|
+
stepSettingsHeaders: {}
|
|
2037
2094
|
};
|
|
2038
2095
|
}
|
|
2039
2096
|
getHeaders() {
|
|
@@ -2044,6 +2101,7 @@ var WorkflowHeaders = class {
|
|
|
2044
2101
|
this.addUserHeaders();
|
|
2045
2102
|
this.addInvokeCount();
|
|
2046
2103
|
this.addFailureUrl();
|
|
2104
|
+
this.addStepSettings();
|
|
2047
2105
|
const contentType = this.addContentType();
|
|
2048
2106
|
return this.prefixHeaders(contentType);
|
|
2049
2107
|
}
|
|
@@ -2053,7 +2111,7 @@ var WorkflowHeaders = class {
|
|
|
2053
2111
|
[WORKFLOW_INIT_HEADER]: this.initHeaderValue,
|
|
2054
2112
|
[WORKFLOW_ID_HEADER]: this.workflowConfig.workflowRunId,
|
|
2055
2113
|
[WORKFLOW_URL_HEADER]: this.workflowConfig.workflowUrl,
|
|
2056
|
-
[WORKFLOW_FEATURE_HEADER]:
|
|
2114
|
+
[WORKFLOW_FEATURE_HEADER]: WORKFLOW_FEATURE_SET,
|
|
2057
2115
|
[WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
|
|
2058
2116
|
...this.workflowConfig.telemetry ? getTelemetryHeaders(this.workflowConfig.telemetry) : {}
|
|
2059
2117
|
};
|
|
@@ -2131,7 +2189,7 @@ var WorkflowHeaders = class {
|
|
|
2131
2189
|
this.headers.failureHeaders["Workflow-Init"] = "false";
|
|
2132
2190
|
this.headers.failureHeaders["Workflow-Url"] = this.workflowConfig.workflowUrl;
|
|
2133
2191
|
this.headers.failureHeaders["Workflow-Calltype"] = "failureCall";
|
|
2134
|
-
this.headers.failureHeaders["Feature-Set"] =
|
|
2192
|
+
this.headers.failureHeaders["Feature-Set"] = WORKFLOW_FEATURE_SET;
|
|
2135
2193
|
if (this.workflowConfig.retries !== void 0 && this.workflowConfig.retries !== DEFAULT_RETRIES) {
|
|
2136
2194
|
this.headers.failureHeaders["Retries"] = this.workflowConfig.retries.toString();
|
|
2137
2195
|
}
|
|
@@ -2139,6 +2197,42 @@ var WorkflowHeaders = class {
|
|
|
2139
2197
|
this.headers.failureHeaders["Retry-Delay"] = this.workflowConfig.retryDelay.toString();
|
|
2140
2198
|
}
|
|
2141
2199
|
}
|
|
2200
|
+
/**
|
|
2201
|
+
* Applies the step-level settings of a step to the message.
|
|
2202
|
+
*
|
|
2203
|
+
* These headers configure the message itself, so that QStash uses them
|
|
2204
|
+
* instead of the settings the run was triggered with. The delivery of
|
|
2205
|
+
* the message is what executes the step, which is how a step's
|
|
2206
|
+
* settings reach it.
|
|
2207
|
+
*
|
|
2208
|
+
* When any setting is present the feature set is extended with
|
|
2209
|
+
* `WF_StepConfig`, which is what tells QStash to keep them. QStash
|
|
2210
|
+
* reports that back on the delivery as `Upstash-Workflow-Step-Config`.
|
|
2211
|
+
*/
|
|
2212
|
+
addStepSettings() {
|
|
2213
|
+
if (!this.stepSettings) {
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
const headers = {};
|
|
2217
|
+
if (this.stepSettings.flowControl) {
|
|
2218
|
+
const { flowControlKey, flowControlValue } = prepareFlowControl(
|
|
2219
|
+
this.stepSettings.flowControl
|
|
2220
|
+
);
|
|
2221
|
+
headers[FLOW_CONTROL_KEY_HEADER] = flowControlKey;
|
|
2222
|
+
headers[FLOW_CONTROL_VALUE_HEADER] = flowControlValue;
|
|
2223
|
+
}
|
|
2224
|
+
if (this.stepSettings.retries !== void 0) {
|
|
2225
|
+
headers[RETRIES_HEADER] = this.stepSettings.retries.toString();
|
|
2226
|
+
}
|
|
2227
|
+
if (this.stepSettings.retryDelay) {
|
|
2228
|
+
headers[RETRY_DELAY_HEADER] = this.stepSettings.retryDelay;
|
|
2229
|
+
}
|
|
2230
|
+
if (Object.keys(headers).length === 0) {
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
headers[WORKFLOW_FEATURE_HEADER] = `${WORKFLOW_FEATURE_SET},${WORKFLOW_STEP_CONFIG_FEATURE}`;
|
|
2234
|
+
this.headers.stepSettingsHeaders = headers;
|
|
2235
|
+
}
|
|
2142
2236
|
addContentType() {
|
|
2143
2237
|
if (this.workflowConfig.useJSONContent) {
|
|
2144
2238
|
this.headers.rawHeaders["content-type"] = "application/json";
|
|
@@ -2152,14 +2246,17 @@ var WorkflowHeaders = class {
|
|
|
2152
2246
|
return contentType;
|
|
2153
2247
|
}
|
|
2154
2248
|
prefixHeaders(contentType) {
|
|
2155
|
-
const { rawHeaders, workflowHeaders, failureHeaders } = this.headers;
|
|
2249
|
+
const { rawHeaders, workflowHeaders, failureHeaders, stepSettingsHeaders } = this.headers;
|
|
2156
2250
|
const isCall = this.stepInfo?.lazyStep.stepType === "Call";
|
|
2157
2251
|
return {
|
|
2158
2252
|
headers: {
|
|
2159
2253
|
...rawHeaders,
|
|
2160
2254
|
...addPrefixToHeaders(workflowHeaders, isCall ? "Upstash-Callback-" : "Upstash-"),
|
|
2161
2255
|
...addPrefixToHeaders(failureHeaders, "Upstash-Failure-Callback-"),
|
|
2162
|
-
...isCall ? addPrefixToHeaders(failureHeaders, "Upstash-Callback-Failure-Callback-") : {}
|
|
2256
|
+
...isCall ? addPrefixToHeaders(failureHeaders, "Upstash-Callback-Failure-Callback-") : {},
|
|
2257
|
+
// last, so they override the run's settings above. Never
|
|
2258
|
+
// prefixed: they configure this message, not its callback.
|
|
2259
|
+
...stepSettingsHeaders
|
|
2163
2260
|
},
|
|
2164
2261
|
contentType
|
|
2165
2262
|
};
|
|
@@ -2210,7 +2307,7 @@ var submitParallelSteps = async ({
|
|
|
2210
2307
|
info: `Submitting ${planSteps.length} parallel steps.`
|
|
2211
2308
|
});
|
|
2212
2309
|
const result = await context.qstashClient.batch(
|
|
2213
|
-
planSteps.map((planStep) => {
|
|
2310
|
+
planSteps.map((planStep, index) => {
|
|
2214
2311
|
const { headers } = getHeaders({
|
|
2215
2312
|
initHeaderValue: "false",
|
|
2216
2313
|
workflowConfig: {
|
|
@@ -2218,7 +2315,8 @@ var submitParallelSteps = async ({
|
|
|
2218
2315
|
workflowUrl: context.url,
|
|
2219
2316
|
telemetry: telemetry2
|
|
2220
2317
|
},
|
|
2221
|
-
invokeCount
|
|
2318
|
+
invokeCount,
|
|
2319
|
+
stepSettings: steps[index].stepSettings
|
|
2222
2320
|
});
|
|
2223
2321
|
return {
|
|
2224
2322
|
headers,
|
|
@@ -2237,31 +2335,38 @@ var submitParallelSteps = async ({
|
|
|
2237
2335
|
}
|
|
2238
2336
|
throw new WorkflowAbort(planSteps[0].stepName, planSteps[0]);
|
|
2239
2337
|
};
|
|
2240
|
-
var
|
|
2241
|
-
context,
|
|
2338
|
+
var executeStep = async ({
|
|
2242
2339
|
lazyStep,
|
|
2243
2340
|
stepId,
|
|
2244
|
-
invokeCount,
|
|
2245
2341
|
concurrency,
|
|
2246
|
-
telemetry: telemetry2,
|
|
2247
|
-
dispatchDebug,
|
|
2248
2342
|
dispatchLifecycle
|
|
2249
2343
|
}) => {
|
|
2250
2344
|
await dispatchLifecycle("beforeExecution", {
|
|
2251
2345
|
stepName: lazyStep.stepName
|
|
2252
2346
|
});
|
|
2253
|
-
let resultStep;
|
|
2254
2347
|
try {
|
|
2255
|
-
|
|
2348
|
+
return await lazyStep.getResultStep(concurrency, stepId);
|
|
2256
2349
|
} catch (error) {
|
|
2257
2350
|
attachStepNameToError(error, lazyStep.stepName);
|
|
2258
2351
|
throw error;
|
|
2259
2352
|
}
|
|
2353
|
+
};
|
|
2354
|
+
var submitStepResult = async ({
|
|
2355
|
+
context,
|
|
2356
|
+
lazyStep,
|
|
2357
|
+
resultStep,
|
|
2358
|
+
invokeCount,
|
|
2359
|
+
concurrency,
|
|
2360
|
+
telemetry: telemetry2,
|
|
2361
|
+
dispatchDebug,
|
|
2362
|
+
nextStepSettings
|
|
2363
|
+
}) => {
|
|
2260
2364
|
const { headers } = lazyStep.getHeaders({
|
|
2261
2365
|
context,
|
|
2262
2366
|
step: resultStep,
|
|
2263
2367
|
invokeCount,
|
|
2264
|
-
telemetry: telemetry2
|
|
2368
|
+
telemetry: telemetry2,
|
|
2369
|
+
stepSettings: nextStepSettings
|
|
2265
2370
|
});
|
|
2266
2371
|
const body = lazyStep.getBody({
|
|
2267
2372
|
context,
|
|
@@ -2286,6 +2391,138 @@ var submitSingleStep = async ({
|
|
|
2286
2391
|
}
|
|
2287
2392
|
return resultStep;
|
|
2288
2393
|
};
|
|
2394
|
+
var publishStepConfigRequest = async ({
|
|
2395
|
+
context,
|
|
2396
|
+
lazyStep,
|
|
2397
|
+
targetStep,
|
|
2398
|
+
invokeCount,
|
|
2399
|
+
telemetry: telemetry2,
|
|
2400
|
+
dispatchDebug
|
|
2401
|
+
}) => {
|
|
2402
|
+
const { headers } = getHeaders({
|
|
2403
|
+
initHeaderValue: "false",
|
|
2404
|
+
workflowConfig: {
|
|
2405
|
+
workflowRunId: context.workflowRunId,
|
|
2406
|
+
workflowUrl: context.url,
|
|
2407
|
+
telemetry: telemetry2
|
|
2408
|
+
},
|
|
2409
|
+
invokeCount,
|
|
2410
|
+
stepSettings: lazyStep.stepSettings
|
|
2411
|
+
});
|
|
2412
|
+
await dispatchDebug("onInfo", {
|
|
2413
|
+
info: `Step "${lazyStep.stepName}" (${targetStep}) has step-level settings which the current delivery does not carry. Requesting a delivery which does.`
|
|
2414
|
+
});
|
|
2415
|
+
const result = await context.qstashClient.publishJSON({
|
|
2416
|
+
headers: {
|
|
2417
|
+
...headers,
|
|
2418
|
+
"Upstash-Workflow-CallType": WORKFLOW_STEP_CONFIG_CALL_TYPE
|
|
2419
|
+
},
|
|
2420
|
+
method: "POST",
|
|
2421
|
+
body: { targetStep, invokeCount },
|
|
2422
|
+
url: context.url,
|
|
2423
|
+
contentBasedDeduplication: true
|
|
2424
|
+
});
|
|
2425
|
+
if (result?.deduplicated) {
|
|
2426
|
+
await dispatchDebug("onWarning", {
|
|
2427
|
+
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.`
|
|
2428
|
+
});
|
|
2429
|
+
} else if (result?.messageId) {
|
|
2430
|
+
await dispatchDebug("onInfo", {
|
|
2431
|
+
info: `Requested a delivery under the settings of step "${lazyStep.stepName}" with messageId: ${result.messageId}.`
|
|
2432
|
+
});
|
|
2433
|
+
}
|
|
2434
|
+
};
|
|
2435
|
+
|
|
2436
|
+
// src/qstash/step-config.ts
|
|
2437
|
+
var DEFAULT_PARALLELISM = 0;
|
|
2438
|
+
var DEFAULT_RATE = 0;
|
|
2439
|
+
var DEFAULT_PERIOD_SECONDS = 1;
|
|
2440
|
+
var SECONDS_PER_UNIT = {
|
|
2441
|
+
s: 1,
|
|
2442
|
+
m: 60,
|
|
2443
|
+
h: 60 * 60,
|
|
2444
|
+
d: 24 * 60 * 60
|
|
2445
|
+
};
|
|
2446
|
+
var durationToSeconds = (duration) => {
|
|
2447
|
+
if (typeof duration === "number") {
|
|
2448
|
+
return Number.isFinite(duration) ? duration : void 0;
|
|
2449
|
+
}
|
|
2450
|
+
const match = /^(\d+)([smhd])$/.exec(duration.trim());
|
|
2451
|
+
if (match) {
|
|
2452
|
+
return Number(match[1]) * SECONDS_PER_UNIT[match[2]];
|
|
2453
|
+
}
|
|
2454
|
+
const seconds = Number(duration.trim());
|
|
2455
|
+
return Number.isFinite(seconds) ? seconds : void 0;
|
|
2456
|
+
};
|
|
2457
|
+
var normalizeFlowControl = (flowControl) => {
|
|
2458
|
+
const period = flowControl.period === void 0 ? void 0 : durationToSeconds(flowControl.period);
|
|
2459
|
+
return {
|
|
2460
|
+
key: flowControl.key,
|
|
2461
|
+
parallelism: flowControl.parallelism ?? DEFAULT_PARALLELISM,
|
|
2462
|
+
rate: flowControl.rate ?? flowControl.ratePerSecond ?? DEFAULT_RATE,
|
|
2463
|
+
period: period ?? DEFAULT_PERIOD_SECONDS
|
|
2464
|
+
};
|
|
2465
|
+
};
|
|
2466
|
+
var parseFlowControlHeaders = (key, value) => {
|
|
2467
|
+
if (!key) {
|
|
2468
|
+
return void 0;
|
|
2469
|
+
}
|
|
2470
|
+
const flowControl = {
|
|
2471
|
+
key,
|
|
2472
|
+
parallelism: DEFAULT_PARALLELISM,
|
|
2473
|
+
rate: DEFAULT_RATE,
|
|
2474
|
+
period: DEFAULT_PERIOD_SECONDS
|
|
2475
|
+
};
|
|
2476
|
+
for (const entry of (value ?? "").split(",")) {
|
|
2477
|
+
const [name, rawValue] = entry.split("=").map((part) => part.trim());
|
|
2478
|
+
if (!name || rawValue === void 0) {
|
|
2479
|
+
continue;
|
|
2480
|
+
}
|
|
2481
|
+
switch (name) {
|
|
2482
|
+
case "parallelism": {
|
|
2483
|
+
flowControl.parallelism = Number(rawValue);
|
|
2484
|
+
break;
|
|
2485
|
+
}
|
|
2486
|
+
case "rate": {
|
|
2487
|
+
flowControl.rate = Number(rawValue);
|
|
2488
|
+
break;
|
|
2489
|
+
}
|
|
2490
|
+
case "period": {
|
|
2491
|
+
flowControl.period = durationToSeconds(rawValue) ?? DEFAULT_PERIOD_SECONDS;
|
|
2492
|
+
break;
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
return flowControl;
|
|
2497
|
+
};
|
|
2498
|
+
var getEffectiveConfig = (headers) => {
|
|
2499
|
+
const retriesHeader = headers.get(RETRIES_HEADER);
|
|
2500
|
+
return {
|
|
2501
|
+
flowControl: parseFlowControlHeaders(
|
|
2502
|
+
headers.get(FLOW_CONTROL_KEY_HEADER),
|
|
2503
|
+
headers.get(FLOW_CONTROL_VALUE_HEADER)
|
|
2504
|
+
),
|
|
2505
|
+
retries: retriesHeader === null ? void 0 : Number(retriesHeader),
|
|
2506
|
+
retryDelay: headers.get(RETRY_DELAY_HEADER) ?? void 0,
|
|
2507
|
+
hasStepConfig: headers.get(WORKFLOW_STEP_CONFIG_HEADER) === "true"
|
|
2508
|
+
};
|
|
2509
|
+
};
|
|
2510
|
+
var describeStepSettingsMismatch = (stepSettings, effectiveConfig) => {
|
|
2511
|
+
if (stepSettings.flowControl) {
|
|
2512
|
+
const wanted = normalizeFlowControl(stepSettings.flowControl);
|
|
2513
|
+
const applied = effectiveConfig.flowControl;
|
|
2514
|
+
if (!applied || applied.key !== wanted.key || applied.parallelism !== wanted.parallelism || applied.rate !== wanted.rate || applied.period !== wanted.period) {
|
|
2515
|
+
return `flow control: expected ${JSON.stringify(wanted)}, delivery has ${JSON.stringify(applied)}`;
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
if (stepSettings.retries !== void 0 && effectiveConfig.retries !== void 0 && stepSettings.retries !== effectiveConfig.retries) {
|
|
2519
|
+
return `retries: expected ${stepSettings.retries}, delivery has ${effectiveConfig.retries}`;
|
|
2520
|
+
}
|
|
2521
|
+
if (stepSettings.retryDelay !== void 0 && stepSettings.retryDelay !== effectiveConfig.retryDelay) {
|
|
2522
|
+
return `retry delay: expected '${stepSettings.retryDelay}', delivery has '${effectiveConfig.retryDelay ?? ""}'`;
|
|
2523
|
+
}
|
|
2524
|
+
return void 0;
|
|
2525
|
+
};
|
|
2289
2526
|
|
|
2290
2527
|
// src/context/auto-executor.ts
|
|
2291
2528
|
var AutoExecutor = class _AutoExecutor {
|
|
@@ -2302,6 +2539,25 @@ var AutoExecutor = class _AutoExecutor {
|
|
|
2302
2539
|
stepCount = 0;
|
|
2303
2540
|
planStepCount = 0;
|
|
2304
2541
|
executingStep = false;
|
|
2542
|
+
/**
|
|
2543
|
+
* an executed step whose result hasn't been submitted to QStash yet.
|
|
2544
|
+
*
|
|
2545
|
+
* Set when a step which can produce its result in-process executes. The
|
|
2546
|
+
* submission waits so that the route function can continue and reveal
|
|
2547
|
+
* what comes next: if that is a single step with step-level settings,
|
|
2548
|
+
* the settings ride on this submission and no step config request is
|
|
2549
|
+
* needed, since the delivery of the submission is what executes it.
|
|
2550
|
+
*
|
|
2551
|
+
* Flushed when the next step is reached (see `addStep`) or when the
|
|
2552
|
+
* route function ends (see `flushPendingStep` in `serve`).
|
|
2553
|
+
*/
|
|
2554
|
+
pendingStep;
|
|
2555
|
+
/**
|
|
2556
|
+
* configuration QStash applied to the delivery being handled. A step
|
|
2557
|
+
* with step-level settings is settled by comparing its settings against
|
|
2558
|
+
* this.
|
|
2559
|
+
*/
|
|
2560
|
+
effectiveConfig;
|
|
2305
2561
|
/**
|
|
2306
2562
|
* @param context workflow context
|
|
2307
2563
|
* @param steps list of steps
|
|
@@ -2309,14 +2565,17 @@ var AutoExecutor = class _AutoExecutor {
|
|
|
2309
2565
|
* @param dispatchLifecycle lifecycle event dispatcher
|
|
2310
2566
|
* @param telemetry optional telemetry information
|
|
2311
2567
|
* @param invokeCount optional invoke count
|
|
2568
|
+
* @param effectiveConfig configuration QStash applied to the delivery
|
|
2569
|
+
* being handled
|
|
2312
2570
|
*/
|
|
2313
|
-
constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry2, invokeCount) {
|
|
2571
|
+
constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry2, invokeCount, effectiveConfig) {
|
|
2314
2572
|
this.context = context;
|
|
2315
2573
|
this.steps = steps;
|
|
2316
2574
|
this.dispatchDebug = dispatchDebug;
|
|
2317
2575
|
this.dispatchLifecycle = dispatchLifecycle;
|
|
2318
2576
|
this.telemetry = telemetry2;
|
|
2319
2577
|
this.invokeCount = invokeCount ?? 0;
|
|
2578
|
+
this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
|
|
2320
2579
|
this.nonPlanStepCount = this.steps.filter((step) => !step.targetStep).length;
|
|
2321
2580
|
}
|
|
2322
2581
|
/**
|
|
@@ -2351,6 +2610,15 @@ var AutoExecutor = class _AutoExecutor {
|
|
|
2351
2610
|
lazyStepList.push(stepInfo);
|
|
2352
2611
|
const index = this.indexInCurrentList++;
|
|
2353
2612
|
const requestComplete = this.deferExecution().then(async () => {
|
|
2613
|
+
const submitted = await this.submitPendingStep(
|
|
2614
|
+
lazyStepList.length === 1 ? lazyStepList[0].stepSettings : void 0
|
|
2615
|
+
);
|
|
2616
|
+
if (submitted.isErr()) {
|
|
2617
|
+
throw submitted.error;
|
|
2618
|
+
}
|
|
2619
|
+
if (submitted.value.result === "submitted-step") {
|
|
2620
|
+
throw submitted.value.abort;
|
|
2621
|
+
}
|
|
2354
2622
|
if (!this.promises.has(lazyStepList)) {
|
|
2355
2623
|
const promise2 = this.getExecutionPromise(lazyStepList);
|
|
2356
2624
|
this.promises.set(lazyStepList, promise2);
|
|
@@ -2404,18 +2672,100 @@ var AutoExecutor = class _AutoExecutor {
|
|
|
2404
2672
|
}
|
|
2405
2673
|
return parsedOut;
|
|
2406
2674
|
}
|
|
2407
|
-
const
|
|
2408
|
-
|
|
2675
|
+
const mismatch = lazyStep.stepSettings ? describeStepSettingsMismatch(lazyStep.stepSettings, this.effectiveConfig) : void 0;
|
|
2676
|
+
if (mismatch) {
|
|
2677
|
+
if (!this.effectiveConfig.hasStepConfig) {
|
|
2678
|
+
await publishStepConfigRequest({
|
|
2679
|
+
context: this.context,
|
|
2680
|
+
lazyStep,
|
|
2681
|
+
targetStep: this.stepCount,
|
|
2682
|
+
invokeCount: this.invokeCount,
|
|
2683
|
+
telemetry: this.telemetry,
|
|
2684
|
+
dispatchDebug: this.dispatchDebug
|
|
2685
|
+
});
|
|
2686
|
+
throw new WorkflowAbort(lazyStep.stepName);
|
|
2687
|
+
} else {
|
|
2688
|
+
await this.dispatchDebug("onWarning", {
|
|
2689
|
+
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.`
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
const resultStep = await executeStep({
|
|
2409
2694
|
lazyStep,
|
|
2410
2695
|
stepId: this.stepCount,
|
|
2696
|
+
concurrency: NO_CONCURRENCY,
|
|
2697
|
+
dispatchLifecycle: this.dispatchLifecycle
|
|
2698
|
+
});
|
|
2699
|
+
if (lazyStep.supportsDeferredSubmission) {
|
|
2700
|
+
this.pendingStep = { status: "held", lazyStep, resultStep };
|
|
2701
|
+
return lazyStep.parseOut({
|
|
2702
|
+
...resultStep,
|
|
2703
|
+
out: resultStep.out === void 0 ? void 0 : JSON.stringify(resultStep.out)
|
|
2704
|
+
});
|
|
2705
|
+
}
|
|
2706
|
+
await submitStepResult({
|
|
2707
|
+
context: this.context,
|
|
2708
|
+
lazyStep,
|
|
2709
|
+
resultStep,
|
|
2411
2710
|
invokeCount: this.invokeCount,
|
|
2412
|
-
concurrency:
|
|
2711
|
+
concurrency: NO_CONCURRENCY,
|
|
2413
2712
|
telemetry: this.telemetry,
|
|
2414
|
-
dispatchDebug: this.dispatchDebug
|
|
2415
|
-
dispatchLifecycle: this.dispatchLifecycle
|
|
2713
|
+
dispatchDebug: this.dispatchDebug
|
|
2416
2714
|
});
|
|
2417
2715
|
throw new WorkflowAbort(lazyStep.stepName, resultStep);
|
|
2418
2716
|
}
|
|
2717
|
+
/**
|
|
2718
|
+
* Submits the result of the step being held, if there is one, and
|
|
2719
|
+
* returns the abort the caller has to throw to end the invocation.
|
|
2720
|
+
*
|
|
2721
|
+
* Returns undefined when no step is being held, which is the caller's
|
|
2722
|
+
* signal that there is nothing to end. Throws instead of returning
|
|
2723
|
+
* when the submission itself fails, so that error reaches the caller
|
|
2724
|
+
* rather than an abort claiming the step was submitted.
|
|
2725
|
+
*
|
|
2726
|
+
* Callers which arrive while a submission is already under way (the
|
|
2727
|
+
* steps of a parallel group, added together) wait for that one and get
|
|
2728
|
+
* the same abort, rather than submitting a second time.
|
|
2729
|
+
*
|
|
2730
|
+
* The next step's settings are attached whenever it has any, without
|
|
2731
|
+
* checking them against the current delivery: the executor only knows
|
|
2732
|
+
* the configuration of the delivery in hand, which inside a
|
|
2733
|
+
* delivery is the *previous* step's, so such a check would be wrong
|
|
2734
|
+
* exactly when two steps with settings follow each other.
|
|
2735
|
+
*
|
|
2736
|
+
* @param nextStepSettings step-level settings of the next step
|
|
2737
|
+
* @returns `submitted-step` with the abort which ends this invocation,
|
|
2738
|
+
* or `no-pending-step` when nothing was held and there is nothing to
|
|
2739
|
+
* end
|
|
2740
|
+
*/
|
|
2741
|
+
async submitPendingStep(nextStepSettings) {
|
|
2742
|
+
if (!this.pendingStep) {
|
|
2743
|
+
return ok({ result: "no-pending-step" });
|
|
2744
|
+
}
|
|
2745
|
+
try {
|
|
2746
|
+
if (this.pendingStep.status === "submitting") {
|
|
2747
|
+
return ok({ result: "submitted-step", abort: await this.pendingStep.submitted });
|
|
2748
|
+
}
|
|
2749
|
+
const { lazyStep, resultStep } = this.pendingStep;
|
|
2750
|
+
const submitted = (async () => {
|
|
2751
|
+
await submitStepResult({
|
|
2752
|
+
context: this.context,
|
|
2753
|
+
lazyStep,
|
|
2754
|
+
resultStep,
|
|
2755
|
+
invokeCount: this.invokeCount,
|
|
2756
|
+
concurrency: NO_CONCURRENCY,
|
|
2757
|
+
telemetry: this.telemetry,
|
|
2758
|
+
dispatchDebug: this.dispatchDebug,
|
|
2759
|
+
nextStepSettings
|
|
2760
|
+
});
|
|
2761
|
+
return new WorkflowAbort(lazyStep.stepName, resultStep);
|
|
2762
|
+
})();
|
|
2763
|
+
this.pendingStep = { status: "submitting", submitted };
|
|
2764
|
+
return ok({ result: "submitted-step", abort: await submitted });
|
|
2765
|
+
} catch (error) {
|
|
2766
|
+
return err(error);
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2419
2769
|
/**
|
|
2420
2770
|
* Runs steps in parallel.
|
|
2421
2771
|
*
|
|
@@ -2464,15 +2814,20 @@ var AutoExecutor = class _AutoExecutor {
|
|
|
2464
2814
|
validateStep(parallelSteps[stepIndex], planStep);
|
|
2465
2815
|
try {
|
|
2466
2816
|
const parallelStep = parallelSteps[stepIndex];
|
|
2467
|
-
const resultStep = await
|
|
2468
|
-
context: this.context,
|
|
2817
|
+
const resultStep = await executeStep({
|
|
2469
2818
|
lazyStep: parallelStep,
|
|
2470
2819
|
stepId: planStep.targetStep,
|
|
2820
|
+
concurrency: parallelSteps.length,
|
|
2821
|
+
dispatchLifecycle: this.dispatchLifecycle
|
|
2822
|
+
});
|
|
2823
|
+
await submitStepResult({
|
|
2824
|
+
context: this.context,
|
|
2825
|
+
lazyStep: parallelStep,
|
|
2826
|
+
resultStep,
|
|
2471
2827
|
invokeCount: this.invokeCount,
|
|
2472
2828
|
concurrency: parallelSteps.length,
|
|
2473
2829
|
telemetry: this.telemetry,
|
|
2474
|
-
dispatchDebug: this.dispatchDebug
|
|
2475
|
-
dispatchLifecycle: this.dispatchLifecycle
|
|
2830
|
+
dispatchDebug: this.dispatchDebug
|
|
2476
2831
|
});
|
|
2477
2832
|
throw new WorkflowAbort(parallelStep.stepName, resultStep);
|
|
2478
2833
|
} catch (error) {
|
|
@@ -3088,6 +3443,46 @@ var WorkflowContext = class {
|
|
|
3088
3443
|
* `1` on the first retry, `2` on the second, and so on.
|
|
3089
3444
|
*/
|
|
3090
3445
|
retried;
|
|
3446
|
+
/**
|
|
3447
|
+
* Configuration QStash applied to the request being handled.
|
|
3448
|
+
*
|
|
3449
|
+
* This is the configuration of *this delivery*, not of the run: it is
|
|
3450
|
+
* the configuration the run was triggered with, except inside the
|
|
3451
|
+
* delivery which executes a step that has step-level settings, where
|
|
3452
|
+
* it is that step's settings. The fields below expose it; the executor
|
|
3453
|
+
* gets it directly rather than reaching through the context.
|
|
3454
|
+
*/
|
|
3455
|
+
effectiveConfig;
|
|
3456
|
+
/**
|
|
3457
|
+
* Flow control QStash applied to the request being handled, if any.
|
|
3458
|
+
*
|
|
3459
|
+
* This is the flow control of the request in hand, which is the one the
|
|
3460
|
+
* run was triggered with except inside a step carrying its own.
|
|
3461
|
+
*/
|
|
3462
|
+
get flowControl() {
|
|
3463
|
+
return this.effectiveConfig.flowControl;
|
|
3464
|
+
}
|
|
3465
|
+
/**
|
|
3466
|
+
* Retry limit QStash applied to the request being handled, if it
|
|
3467
|
+
* reported one.
|
|
3468
|
+
*
|
|
3469
|
+
* Not to be confused with {@link retried}, which is how many retries
|
|
3470
|
+
* have already happened.
|
|
3471
|
+
*
|
|
3472
|
+
* @see {@link flowControl} for what "the request being handled" means
|
|
3473
|
+
*/
|
|
3474
|
+
get retries() {
|
|
3475
|
+
return this.effectiveConfig.retries;
|
|
3476
|
+
}
|
|
3477
|
+
/**
|
|
3478
|
+
* Retry delay expression QStash applied to the request being handled,
|
|
3479
|
+
* if any.
|
|
3480
|
+
*
|
|
3481
|
+
* @see {@link flowControl} for what "the request being handled" means
|
|
3482
|
+
*/
|
|
3483
|
+
get retryDelay() {
|
|
3484
|
+
return this.effectiveConfig.retryDelay;
|
|
3485
|
+
}
|
|
3091
3486
|
constructor({
|
|
3092
3487
|
qstashClient,
|
|
3093
3488
|
workflowRunId,
|
|
@@ -3101,7 +3496,8 @@ var WorkflowContext = class {
|
|
|
3101
3496
|
invokeCount,
|
|
3102
3497
|
label,
|
|
3103
3498
|
retried,
|
|
3104
|
-
middlewareManager
|
|
3499
|
+
middlewareManager,
|
|
3500
|
+
effectiveConfig
|
|
3105
3501
|
}) {
|
|
3106
3502
|
this.qstashClient = qstashClient;
|
|
3107
3503
|
this.workflowRunId = workflowRunId;
|
|
@@ -3113,6 +3509,7 @@ var WorkflowContext = class {
|
|
|
3113
3509
|
this.env = env ?? {};
|
|
3114
3510
|
this.labels = label === void 0 ? [] : Array.isArray(label) ? label : label ? label.split(",") : [];
|
|
3115
3511
|
this.retried = retried ?? 0;
|
|
3512
|
+
this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
|
|
3116
3513
|
const middlewareManagerInstance = middlewareManager ?? new MiddlewareManager([]);
|
|
3117
3514
|
middlewareManagerInstance.assignContext(this);
|
|
3118
3515
|
this.executor = new AutoExecutor(
|
|
@@ -3121,7 +3518,8 @@ var WorkflowContext = class {
|
|
|
3121
3518
|
middlewareManagerInstance.dispatchDebug.bind(middlewareManagerInstance),
|
|
3122
3519
|
middlewareManagerInstance.dispatchLifecycle.bind(middlewareManagerInstance),
|
|
3123
3520
|
telemetry2,
|
|
3124
|
-
invokeCount
|
|
3521
|
+
invokeCount,
|
|
3522
|
+
this.effectiveConfig
|
|
3125
3523
|
);
|
|
3126
3524
|
}
|
|
3127
3525
|
/**
|
|
@@ -3147,13 +3545,35 @@ var WorkflowContext = class {
|
|
|
3147
3545
|
* ])
|
|
3148
3546
|
* ```
|
|
3149
3547
|
*
|
|
3548
|
+
* Step-level settings can be passed as a third argument, overriding
|
|
3549
|
+
* the settings the workflow run was triggered with for this step only:
|
|
3550
|
+
*
|
|
3551
|
+
* ```typescript
|
|
3552
|
+
* const result = await context.run(
|
|
3553
|
+
* "step 1",
|
|
3554
|
+
* () => {
|
|
3555
|
+
* return "result"
|
|
3556
|
+
* },
|
|
3557
|
+
* {
|
|
3558
|
+
* flowControl: { key: "custom-key", parallelism: 3 },
|
|
3559
|
+
* retries: 5,
|
|
3560
|
+
* }
|
|
3561
|
+
* )
|
|
3562
|
+
* ```
|
|
3563
|
+
*
|
|
3150
3564
|
* @param stepName name of the step
|
|
3151
3565
|
* @param stepFunction step function to be executed
|
|
3566
|
+
* @param stepSettings step-level settings for this step
|
|
3152
3567
|
* @returns result of the step function
|
|
3153
3568
|
*/
|
|
3154
|
-
async run(stepName, stepFunction) {
|
|
3569
|
+
async run(stepName, stepFunction, stepSettings) {
|
|
3570
|
+
if (stepSettings) {
|
|
3571
|
+
validateFlowControl(stepSettings.flowControl);
|
|
3572
|
+
}
|
|
3155
3573
|
const wrappedStepFunction = (() => this.executor.wrapStep(stepName, stepFunction));
|
|
3156
|
-
return await this.addStep(
|
|
3574
|
+
return await this.addStep(
|
|
3575
|
+
new LazyFunctionStep(this, stepName, wrappedStepFunction, stepSettings)
|
|
3576
|
+
);
|
|
3157
3577
|
}
|
|
3158
3578
|
/**
|
|
3159
3579
|
* Stops the execution for the duration provided.
|
|
@@ -3376,7 +3796,7 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
|
|
|
3376
3796
|
*
|
|
3377
3797
|
* @param routeFunction
|
|
3378
3798
|
*/
|
|
3379
|
-
static async tryAuthentication(routeFunction, context) {
|
|
3799
|
+
static async tryAuthentication(routeFunction, context, effectiveConfig) {
|
|
3380
3800
|
const disabledContext = new _DisabledWorkflowContext({
|
|
3381
3801
|
qstashClient: new import_qstash10.Client({
|
|
3382
3802
|
baseUrl: "disabled-client",
|
|
@@ -3391,7 +3811,10 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
|
|
|
3391
3811
|
initialPayload: context.requestPayload,
|
|
3392
3812
|
env: context.env,
|
|
3393
3813
|
label: context.labels,
|
|
3394
|
-
retried: context.retried
|
|
3814
|
+
retried: context.retried,
|
|
3815
|
+
// the route function runs for real here until it reaches a step, so
|
|
3816
|
+
// it observes the same configuration as it will on the real context
|
|
3817
|
+
effectiveConfig
|
|
3395
3818
|
});
|
|
3396
3819
|
try {
|
|
3397
3820
|
await routeFunction(disabledContext);
|
|
@@ -3608,6 +4031,7 @@ var handleFailure = async ({
|
|
|
3608
4031
|
}
|
|
3609
4032
|
const userHeaders = recreateUserHeaders(request.headers);
|
|
3610
4033
|
const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
|
|
4034
|
+
const effectiveConfig = getEffectiveConfig(request.headers);
|
|
3611
4035
|
const workflowContext = new WorkflowContext({
|
|
3612
4036
|
qstashClient,
|
|
3613
4037
|
workflowRunId,
|
|
@@ -3621,11 +4045,13 @@ var handleFailure = async ({
|
|
|
3621
4045
|
label: userHeaders.get(WORKFLOW_LABEL_HEADER) ?? void 0,
|
|
3622
4046
|
retried,
|
|
3623
4047
|
workflowRunCreatedAt: workflowCreatedAt,
|
|
3624
|
-
middlewareManager: void 0
|
|
4048
|
+
middlewareManager: void 0,
|
|
4049
|
+
effectiveConfig
|
|
3625
4050
|
});
|
|
3626
4051
|
const authCheck = await DisabledWorkflowContext.tryAuthentication(
|
|
3627
4052
|
routeFunction,
|
|
3628
|
-
workflowContext
|
|
4053
|
+
workflowContext,
|
|
4054
|
+
effectiveConfig
|
|
3629
4055
|
);
|
|
3630
4056
|
if (authCheck.isErr()) {
|
|
3631
4057
|
await dispatchDebug?.("onError", {
|
|
@@ -3717,7 +4143,13 @@ var createRegionalHandler = (environment, receiverConfig, region, clientOptions)
|
|
|
3717
4143
|
const client = new import_qstash11.Client({
|
|
3718
4144
|
...clientOptions,
|
|
3719
4145
|
baseUrl: clientEnv.QSTASH_URL,
|
|
3720
|
-
token: clientEnv.QSTASH_TOKEN
|
|
4146
|
+
token: clientEnv.QSTASH_TOKEN,
|
|
4147
|
+
// Mirror the receiver's dev-mode decision so a single QSTASH_DEV=true points
|
|
4148
|
+
// the client at the local dev server too. Without this the QStash Client
|
|
4149
|
+
// re-derives dev mode from its own process.env, which is blind to the
|
|
4150
|
+
// Cloudflare worker `env` binding that actually carries QSTASH_DEV there.
|
|
4151
|
+
// An explicit devMode in the user's client config still wins.
|
|
4152
|
+
devMode: clientOptions?.devMode ?? isQStashDevModeEnabled(environment)
|
|
3721
4153
|
});
|
|
3722
4154
|
const receiver = getReceiver(environment, receiverConfig, region);
|
|
3723
4155
|
return { client, receiver };
|
|
@@ -3767,7 +4199,9 @@ var getQStashHandlers = ({
|
|
|
3767
4199
|
client: qstashClientOption && "http" in qstashClientOption ? qstashClientOption : new import_qstash11.Client({
|
|
3768
4200
|
...qstashClientOption,
|
|
3769
4201
|
baseUrl: environment.QSTASH_URL,
|
|
3770
|
-
token: environment.QSTASH_TOKEN
|
|
4202
|
+
token: environment.QSTASH_TOKEN,
|
|
4203
|
+
// Mirror the receiver's dev-mode decision (see createRegionalHandler).
|
|
4204
|
+
devMode: qstashClientOption?.devMode ?? isQStashDevModeEnabled(environment)
|
|
3771
4205
|
}),
|
|
3772
4206
|
receiver: getReceiver(environment, receiverConfig)
|
|
3773
4207
|
}
|
|
@@ -4107,12 +4541,14 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
|
|
|
4107
4541
|
const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
|
|
4108
4542
|
const label = request.headers.get(WORKFLOW_LABEL_HEADER) ?? void 0;
|
|
4109
4543
|
const workflowRunCreatedAt = request.headers.get(WORKFLOW_CREATED_AT_HEADER);
|
|
4544
|
+
const effectiveConfig = getEffectiveConfig(request.headers);
|
|
4110
4545
|
const workflowContext = new WorkflowContext({
|
|
4111
4546
|
qstashClient: regionalClient,
|
|
4112
4547
|
workflowRunId,
|
|
4113
4548
|
initialPayload: isThirdPartyCallResult(request) ? JSON.parse(rawInitialPayload) : initialPayloadParser(rawInitialPayload),
|
|
4114
4549
|
headers: recreateUserHeaders(request.headers),
|
|
4115
4550
|
steps,
|
|
4551
|
+
effectiveConfig,
|
|
4116
4552
|
url: workflowUrl,
|
|
4117
4553
|
env,
|
|
4118
4554
|
telemetry: telemetry2,
|
|
@@ -4124,7 +4560,8 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
|
|
|
4124
4560
|
});
|
|
4125
4561
|
const authCheck = await DisabledWorkflowContext.tryAuthentication(
|
|
4126
4562
|
routeFunction,
|
|
4127
|
-
workflowContext
|
|
4563
|
+
workflowContext,
|
|
4564
|
+
effectiveConfig
|
|
4128
4565
|
);
|
|
4129
4566
|
if (authCheck.isErr()) {
|
|
4130
4567
|
throw authCheck.error;
|
|
@@ -4161,7 +4598,23 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
|
|
|
4161
4598
|
if (steps.length === 1) {
|
|
4162
4599
|
await middlewareManager.dispatchLifecycle("runStarted", {});
|
|
4163
4600
|
}
|
|
4164
|
-
|
|
4601
|
+
let outcome;
|
|
4602
|
+
try {
|
|
4603
|
+
outcome = { ran: true, result: await routeFunction(workflowContext) };
|
|
4604
|
+
} catch (error) {
|
|
4605
|
+
outcome = { ran: false, error };
|
|
4606
|
+
}
|
|
4607
|
+
const submitted = await flushPendingStep(workflowContext);
|
|
4608
|
+
if (submitted.isErr()) {
|
|
4609
|
+
throw submitted.error;
|
|
4610
|
+
}
|
|
4611
|
+
if (submitted.value.result === "submitted-step") {
|
|
4612
|
+
throw submitted.value.abort;
|
|
4613
|
+
}
|
|
4614
|
+
if (!outcome.ran) {
|
|
4615
|
+
throw outcome.error;
|
|
4616
|
+
}
|
|
4617
|
+
return outcome.result;
|
|
4165
4618
|
},
|
|
4166
4619
|
onCleanup: async (result2) => {
|
|
4167
4620
|
await middlewareManager.dispatchLifecycle("runCompleted", {
|
|
@@ -4242,6 +4695,7 @@ var serveBase = (routeFunction, telemetry2, options, internalOptions) => {
|
|
|
4242
4695
|
};
|
|
4243
4696
|
|
|
4244
4697
|
// platforms/h3.ts
|
|
4698
|
+
var import_qstash13 = require("@upstash/qstash");
|
|
4245
4699
|
function transformHeaders(headers) {
|
|
4246
4700
|
const formattedHeaders = Object.entries(headers).map(([key, value]) => [
|
|
4247
4701
|
key,
|
|
@@ -4262,6 +4716,7 @@ var telemetry = {
|
|
|
4262
4716
|
runtime: process.versions.bun ? `bun@${process.versions.bun}/node@${process.version}` : `node@${process.version}`
|
|
4263
4717
|
};
|
|
4264
4718
|
var serve = (routeFunction, options) => {
|
|
4719
|
+
void (0, import_qstash13.startDevServer)();
|
|
4265
4720
|
const handler = defineEventHandler(async (event) => {
|
|
4266
4721
|
const method = event.node.req.method;
|
|
4267
4722
|
if (method?.toUpperCase() !== "POST") {
|