@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
package/index.js
CHANGED
|
@@ -33,6 +33,9 @@ __export(src_exports, {
|
|
|
33
33
|
});
|
|
34
34
|
module.exports = __toCommonJS(src_exports);
|
|
35
35
|
|
|
36
|
+
// src/serve/index.ts
|
|
37
|
+
var import_qstash12 = require("@upstash/qstash");
|
|
38
|
+
|
|
36
39
|
// src/client/utils.ts
|
|
37
40
|
var import_qstash2 = require("@upstash/qstash");
|
|
38
41
|
|
|
@@ -301,6 +304,14 @@ var WORKFLOW_CREATED_AT_HEADER = "Upstash-Workflow-CreatedAt";
|
|
|
301
304
|
var WORKFLOW_FAILURE_HEADER = "Upstash-Workflow-Is-Failure";
|
|
302
305
|
var WORKFLOW_FAILURE_CALLBACK_HEADER = "Upstash-Workflow-Failure-Callback";
|
|
303
306
|
var WORKFLOW_FEATURE_HEADER = "Upstash-Feature-Set";
|
|
307
|
+
var WORKFLOW_FEATURE_SET = "LazyFetch,InitialBody,WF_DetectTrigger,WF_TriggerOnConfig";
|
|
308
|
+
var WORKFLOW_STEP_CONFIG_CALL_TYPE = "stepConfig";
|
|
309
|
+
var WORKFLOW_STEP_CONFIG_HEADER = "Upstash-Workflow-Step-Config";
|
|
310
|
+
var FLOW_CONTROL_KEY_HEADER = "Upstash-Flow-Control-Key";
|
|
311
|
+
var FLOW_CONTROL_VALUE_HEADER = "Upstash-Flow-Control-Value";
|
|
312
|
+
var RETRIES_HEADER = "Upstash-Retries";
|
|
313
|
+
var RETRY_DELAY_HEADER = "Upstash-Retry-Delay";
|
|
314
|
+
var WORKFLOW_STEP_CONFIG_FEATURE = "WF_StepConfig";
|
|
304
315
|
var WORKFLOW_INVOKE_COUNT_HEADER = "Upstash-Workflow-Invoke-Count";
|
|
305
316
|
var WORKFLOW_RETRIED_HEADER = "Upstash-Retried";
|
|
306
317
|
var WORKFLOW_LABEL_HEADER = "Upstash-Label";
|
|
@@ -318,102 +329,6 @@ var TELEMETRY_HEADER_SDK = "Upstash-Telemetry-Sdk";
|
|
|
318
329
|
var TELEMETRY_HEADER_FRAMEWORK = "Upstash-Telemetry-Framework";
|
|
319
330
|
var TELEMETRY_HEADER_RUNTIME = "Upstash-Telemetry-Runtime";
|
|
320
331
|
|
|
321
|
-
// src/context/auto-executor.ts
|
|
322
|
-
var import_qstash5 = require("@upstash/qstash");
|
|
323
|
-
|
|
324
|
-
// src/qstash/headers.ts
|
|
325
|
-
var import_qstash4 = require("@upstash/qstash");
|
|
326
|
-
|
|
327
|
-
// src/utils.ts
|
|
328
|
-
var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
|
|
329
|
-
var NANOID_LENGTH = 21;
|
|
330
|
-
var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
|
|
331
|
-
function validateLabel(label) {
|
|
332
|
-
if (label === void 0) return;
|
|
333
|
-
const labels = Array.isArray(label) ? label : [label];
|
|
334
|
-
if (labels.length === 0) {
|
|
335
|
-
throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
|
|
336
|
-
}
|
|
337
|
-
for (const value of labels) {
|
|
338
|
-
if (!RESOURCE_NAME_PATTERN.test(value)) {
|
|
339
|
-
throw new WorkflowNonRetryableError(
|
|
340
|
-
`Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
function serializeLabel(label) {
|
|
346
|
-
return Array.isArray(label) ? label.join(",") : label;
|
|
347
|
-
}
|
|
348
|
-
function validateFlowControl(flowControl) {
|
|
349
|
-
if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
|
|
350
|
-
throw new WorkflowNonRetryableError(
|
|
351
|
-
`Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
|
|
352
|
-
);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
function getRandomInt() {
|
|
356
|
-
return Math.floor(Math.random() * NANOID_CHARS.length);
|
|
357
|
-
}
|
|
358
|
-
function nanoid(length = NANOID_LENGTH) {
|
|
359
|
-
return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
|
|
360
|
-
}
|
|
361
|
-
function getWorkflowRunId(id) {
|
|
362
|
-
return `wfr_${id ?? nanoid()}`;
|
|
363
|
-
}
|
|
364
|
-
function decodeBase64(base64) {
|
|
365
|
-
const binString = atob(base64);
|
|
366
|
-
try {
|
|
367
|
-
const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
|
|
368
|
-
return new TextDecoder().decode(intArray);
|
|
369
|
-
} catch (error) {
|
|
370
|
-
console.warn(
|
|
371
|
-
`Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
|
|
372
|
-
);
|
|
373
|
-
return binString;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
function getUserIdFromToken(qstashClient) {
|
|
377
|
-
try {
|
|
378
|
-
const token = qstashClient.token;
|
|
379
|
-
const decodedToken = decodeBase64(token);
|
|
380
|
-
const tokenPayload = JSON.parse(decodedToken);
|
|
381
|
-
const userId = tokenPayload.UserID;
|
|
382
|
-
if (!userId) {
|
|
383
|
-
throw new WorkflowError("QStash token payload does not contain userId");
|
|
384
|
-
}
|
|
385
|
-
return userId;
|
|
386
|
-
} catch (error) {
|
|
387
|
-
throw new WorkflowError(
|
|
388
|
-
`Failed to decode QStash token while running create webhook step: ${error.message}`
|
|
389
|
-
);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
function getQStashUrl(qstashClient) {
|
|
393
|
-
try {
|
|
394
|
-
const requester = qstashClient.http;
|
|
395
|
-
const baseUrl = requester.baseUrl;
|
|
396
|
-
if (!baseUrl) {
|
|
397
|
-
throw new WorkflowError("QStash client does not have a baseUrl");
|
|
398
|
-
}
|
|
399
|
-
return baseUrl;
|
|
400
|
-
} catch (error) {
|
|
401
|
-
throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
function getEventId() {
|
|
405
|
-
return `evt_${nanoid(15)}`;
|
|
406
|
-
}
|
|
407
|
-
function stringifyBody(body) {
|
|
408
|
-
if (body === void 0) {
|
|
409
|
-
return void 0;
|
|
410
|
-
}
|
|
411
|
-
if (typeof body === "string") {
|
|
412
|
-
return body;
|
|
413
|
-
}
|
|
414
|
-
return JSON.stringify(body);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
332
|
// node_modules/neverthrow/dist/index.es.js
|
|
418
333
|
var defaultErrorConfig = {
|
|
419
334
|
withStackTrace: false
|
|
@@ -829,6 +744,102 @@ var Err = class {
|
|
|
829
744
|
};
|
|
830
745
|
var fromThrowable = Result.fromThrowable;
|
|
831
746
|
|
|
747
|
+
// src/context/auto-executor.ts
|
|
748
|
+
var import_qstash5 = require("@upstash/qstash");
|
|
749
|
+
|
|
750
|
+
// src/qstash/headers.ts
|
|
751
|
+
var import_qstash4 = require("@upstash/qstash");
|
|
752
|
+
|
|
753
|
+
// src/utils.ts
|
|
754
|
+
var NANOID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
|
|
755
|
+
var NANOID_LENGTH = 21;
|
|
756
|
+
var RESOURCE_NAME_PATTERN = /^[a-zA-Z0-9\-_.]+$/;
|
|
757
|
+
function validateLabel(label) {
|
|
758
|
+
if (label === void 0) return;
|
|
759
|
+
const labels = Array.isArray(label) ? label : [label];
|
|
760
|
+
if (labels.length === 0) {
|
|
761
|
+
throw new WorkflowNonRetryableError("Invalid label: label array must not be empty.");
|
|
762
|
+
}
|
|
763
|
+
for (const value of labels) {
|
|
764
|
+
if (!RESOURCE_NAME_PATTERN.test(value)) {
|
|
765
|
+
throw new WorkflowNonRetryableError(
|
|
766
|
+
`Invalid label "${value}": must be alphanumeric, hyphen, underscore, or period.`
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
function serializeLabel(label) {
|
|
772
|
+
return Array.isArray(label) ? label.join(",") : label;
|
|
773
|
+
}
|
|
774
|
+
function validateFlowControl(flowControl) {
|
|
775
|
+
if (flowControl?.key !== void 0 && !RESOURCE_NAME_PATTERN.test(flowControl.key)) {
|
|
776
|
+
throw new WorkflowNonRetryableError(
|
|
777
|
+
`Invalid flow control key "${flowControl.key}": must be alphanumeric, hyphen, underscore, or period.`
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
function getRandomInt() {
|
|
782
|
+
return Math.floor(Math.random() * NANOID_CHARS.length);
|
|
783
|
+
}
|
|
784
|
+
function nanoid(length = NANOID_LENGTH) {
|
|
785
|
+
return Array.from({ length }).map(() => NANOID_CHARS[getRandomInt()]).join("");
|
|
786
|
+
}
|
|
787
|
+
function getWorkflowRunId(id) {
|
|
788
|
+
return `wfr_${id ?? nanoid()}`;
|
|
789
|
+
}
|
|
790
|
+
function decodeBase64(base64) {
|
|
791
|
+
const binString = atob(base64);
|
|
792
|
+
try {
|
|
793
|
+
const intArray = Uint8Array.from(binString, (m) => m.codePointAt(0));
|
|
794
|
+
return new TextDecoder().decode(intArray);
|
|
795
|
+
} catch (error) {
|
|
796
|
+
console.warn(
|
|
797
|
+
`Upstash Qstash: Failed while decoding base64 "${base64}". Decoding with atob and returning it instead. ${error}`
|
|
798
|
+
);
|
|
799
|
+
return binString;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
function getUserIdFromToken(qstashClient) {
|
|
803
|
+
try {
|
|
804
|
+
const token = qstashClient.token;
|
|
805
|
+
const decodedToken = decodeBase64(token);
|
|
806
|
+
const tokenPayload = JSON.parse(decodedToken);
|
|
807
|
+
const userId = tokenPayload.UserID;
|
|
808
|
+
if (!userId) {
|
|
809
|
+
throw new WorkflowError("QStash token payload does not contain userId");
|
|
810
|
+
}
|
|
811
|
+
return userId;
|
|
812
|
+
} catch (error) {
|
|
813
|
+
throw new WorkflowError(
|
|
814
|
+
`Failed to decode QStash token while running create webhook step: ${error.message}`
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
function getQStashUrl(qstashClient) {
|
|
819
|
+
try {
|
|
820
|
+
const requester = qstashClient.http;
|
|
821
|
+
const baseUrl = requester.baseUrl;
|
|
822
|
+
if (!baseUrl) {
|
|
823
|
+
throw new WorkflowError("QStash client does not have a baseUrl");
|
|
824
|
+
}
|
|
825
|
+
return baseUrl;
|
|
826
|
+
} catch (error) {
|
|
827
|
+
throw new WorkflowError(`Failed to get QStash URL from client: ${error.message}`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
function getEventId() {
|
|
831
|
+
return `evt_${nanoid(15)}`;
|
|
832
|
+
}
|
|
833
|
+
function stringifyBody(body) {
|
|
834
|
+
if (body === void 0) {
|
|
835
|
+
return void 0;
|
|
836
|
+
}
|
|
837
|
+
if (typeof body === "string") {
|
|
838
|
+
return body;
|
|
839
|
+
}
|
|
840
|
+
return JSON.stringify(body);
|
|
841
|
+
}
|
|
842
|
+
|
|
832
843
|
// src/types.ts
|
|
833
844
|
var StepTypes = [
|
|
834
845
|
"Initial",
|
|
@@ -934,6 +945,10 @@ var triggerFirstInvocation = async (params) => {
|
|
|
934
945
|
return err(error_);
|
|
935
946
|
}
|
|
936
947
|
};
|
|
948
|
+
var flushPendingStep = async (workflowContext) => {
|
|
949
|
+
const { executor } = workflowContext;
|
|
950
|
+
return await executor.submitPendingStep();
|
|
951
|
+
};
|
|
937
952
|
var triggerRouteFunction = async ({
|
|
938
953
|
onCleanup,
|
|
939
954
|
onStep,
|
|
@@ -1141,8 +1156,29 @@ If you want to disable QStash Verification, you should clear env variables QSTAS
|
|
|
1141
1156
|
var BaseLazyStep = class _BaseLazyStep {
|
|
1142
1157
|
stepName;
|
|
1143
1158
|
context;
|
|
1144
|
-
|
|
1159
|
+
/**
|
|
1160
|
+
* step-level settings (flow control, retries etc.) which override the
|
|
1161
|
+
* settings the workflow run was triggered with, for this step only.
|
|
1162
|
+
*/
|
|
1163
|
+
stepSettings;
|
|
1164
|
+
/**
|
|
1165
|
+
* whether this step's result can be submitted after the route function
|
|
1166
|
+
* has moved on, instead of right away.
|
|
1167
|
+
*
|
|
1168
|
+
* Enabled for steps whose `getResultStep` produces the final `out` —
|
|
1169
|
+
* which is not the same as "needs no server round trip":
|
|
1170
|
+
* `LazyCreateWebhookStep` builds its result in `getBody` and returns
|
|
1171
|
+
* `out: undefined` from `getResultStep`, so deferring it would hand
|
|
1172
|
+
* `undefined` to the route function.
|
|
1173
|
+
*
|
|
1174
|
+
* Deferring lets the route function continue and reveal the next step,
|
|
1175
|
+
* so that step's settings can ride on this step's submission instead
|
|
1176
|
+
* of needing a step config request of their own.
|
|
1177
|
+
*/
|
|
1178
|
+
supportsDeferredSubmission = false;
|
|
1179
|
+
constructor(context, stepName, stepSettings) {
|
|
1145
1180
|
this.context = context;
|
|
1181
|
+
this.stepSettings = stepSettings;
|
|
1146
1182
|
if (!stepName) {
|
|
1147
1183
|
throw new WorkflowError(
|
|
1148
1184
|
"A workflow step name cannot be undefined or an empty string. Please provide a name for your workflow step."
|
|
@@ -1206,7 +1242,13 @@ var BaseLazyStep = class _BaseLazyStep {
|
|
|
1206
1242
|
step.out = JSON.stringify(step.out);
|
|
1207
1243
|
return JSON.stringify(step);
|
|
1208
1244
|
}
|
|
1209
|
-
getHeaders({
|
|
1245
|
+
getHeaders({
|
|
1246
|
+
context,
|
|
1247
|
+
telemetry,
|
|
1248
|
+
invokeCount,
|
|
1249
|
+
step,
|
|
1250
|
+
stepSettings
|
|
1251
|
+
}) {
|
|
1210
1252
|
return getHeaders({
|
|
1211
1253
|
initHeaderValue: "false",
|
|
1212
1254
|
workflowConfig: {
|
|
@@ -1219,7 +1261,8 @@ var BaseLazyStep = class _BaseLazyStep {
|
|
|
1219
1261
|
stepInfo: {
|
|
1220
1262
|
step,
|
|
1221
1263
|
lazyStep: this
|
|
1222
|
-
}
|
|
1264
|
+
},
|
|
1265
|
+
stepSettings
|
|
1223
1266
|
});
|
|
1224
1267
|
}
|
|
1225
1268
|
async submitStep({ context, body, headers }) {
|
|
@@ -1237,8 +1280,9 @@ var LazyFunctionStep = class extends BaseLazyStep {
|
|
|
1237
1280
|
stepFunction;
|
|
1238
1281
|
stepType = "Run";
|
|
1239
1282
|
allowUndefinedOut = true;
|
|
1240
|
-
|
|
1241
|
-
|
|
1283
|
+
supportsDeferredSubmission = true;
|
|
1284
|
+
constructor(context, stepName, stepFunction, stepSettings) {
|
|
1285
|
+
super(context, stepName, stepSettings);
|
|
1242
1286
|
this.stepFunction = stepFunction;
|
|
1243
1287
|
}
|
|
1244
1288
|
getPlanStep(concurrent, targetStep) {
|
|
@@ -1268,6 +1312,7 @@ var LazySleepStep = class extends BaseLazyStep {
|
|
|
1268
1312
|
sleep;
|
|
1269
1313
|
stepType = "SleepFor";
|
|
1270
1314
|
allowUndefinedOut = true;
|
|
1315
|
+
supportsDeferredSubmission = true;
|
|
1271
1316
|
constructor(context, stepName, sleep) {
|
|
1272
1317
|
super(context, stepName);
|
|
1273
1318
|
this.sleep = sleep;
|
|
@@ -1307,6 +1352,7 @@ var LazySleepUntilStep = class extends BaseLazyStep {
|
|
|
1307
1352
|
sleepUntil;
|
|
1308
1353
|
stepType = "SleepUntil";
|
|
1309
1354
|
allowUndefinedOut = true;
|
|
1355
|
+
supportsDeferredSubmission = true;
|
|
1310
1356
|
constructor(context, stepName, sleepUntil) {
|
|
1311
1357
|
super(context, stepName);
|
|
1312
1358
|
this.sleepUntil = sleepUntil;
|
|
@@ -1799,6 +1845,7 @@ var WorkflowHeaders = class {
|
|
|
1799
1845
|
invokeCount;
|
|
1800
1846
|
initHeaderValue;
|
|
1801
1847
|
stepInfo;
|
|
1848
|
+
stepSettings;
|
|
1802
1849
|
headers;
|
|
1803
1850
|
/**
|
|
1804
1851
|
* @param params workflow header parameters
|
|
@@ -1808,17 +1855,20 @@ var WorkflowHeaders = class {
|
|
|
1808
1855
|
workflowConfig,
|
|
1809
1856
|
invokeCount,
|
|
1810
1857
|
initHeaderValue,
|
|
1811
|
-
stepInfo
|
|
1858
|
+
stepInfo,
|
|
1859
|
+
stepSettings
|
|
1812
1860
|
}) {
|
|
1813
1861
|
this.userHeaders = userHeaders;
|
|
1814
1862
|
this.workflowConfig = workflowConfig;
|
|
1815
1863
|
this.invokeCount = invokeCount;
|
|
1816
1864
|
this.initHeaderValue = initHeaderValue;
|
|
1817
1865
|
this.stepInfo = stepInfo;
|
|
1866
|
+
this.stepSettings = stepSettings;
|
|
1818
1867
|
this.headers = {
|
|
1819
1868
|
rawHeaders: {},
|
|
1820
1869
|
workflowHeaders: {},
|
|
1821
|
-
failureHeaders: {}
|
|
1870
|
+
failureHeaders: {},
|
|
1871
|
+
stepSettingsHeaders: {}
|
|
1822
1872
|
};
|
|
1823
1873
|
}
|
|
1824
1874
|
getHeaders() {
|
|
@@ -1829,6 +1879,7 @@ var WorkflowHeaders = class {
|
|
|
1829
1879
|
this.addUserHeaders();
|
|
1830
1880
|
this.addInvokeCount();
|
|
1831
1881
|
this.addFailureUrl();
|
|
1882
|
+
this.addStepSettings();
|
|
1832
1883
|
const contentType = this.addContentType();
|
|
1833
1884
|
return this.prefixHeaders(contentType);
|
|
1834
1885
|
}
|
|
@@ -1838,7 +1889,7 @@ var WorkflowHeaders = class {
|
|
|
1838
1889
|
[WORKFLOW_INIT_HEADER]: this.initHeaderValue,
|
|
1839
1890
|
[WORKFLOW_ID_HEADER]: this.workflowConfig.workflowRunId,
|
|
1840
1891
|
[WORKFLOW_URL_HEADER]: this.workflowConfig.workflowUrl,
|
|
1841
|
-
[WORKFLOW_FEATURE_HEADER]:
|
|
1892
|
+
[WORKFLOW_FEATURE_HEADER]: WORKFLOW_FEATURE_SET,
|
|
1842
1893
|
[WORKFLOW_PROTOCOL_VERSION_HEADER]: WORKFLOW_PROTOCOL_VERSION,
|
|
1843
1894
|
...this.workflowConfig.telemetry ? getTelemetryHeaders(this.workflowConfig.telemetry) : {}
|
|
1844
1895
|
};
|
|
@@ -1916,7 +1967,7 @@ var WorkflowHeaders = class {
|
|
|
1916
1967
|
this.headers.failureHeaders["Workflow-Init"] = "false";
|
|
1917
1968
|
this.headers.failureHeaders["Workflow-Url"] = this.workflowConfig.workflowUrl;
|
|
1918
1969
|
this.headers.failureHeaders["Workflow-Calltype"] = "failureCall";
|
|
1919
|
-
this.headers.failureHeaders["Feature-Set"] =
|
|
1970
|
+
this.headers.failureHeaders["Feature-Set"] = WORKFLOW_FEATURE_SET;
|
|
1920
1971
|
if (this.workflowConfig.retries !== void 0 && this.workflowConfig.retries !== DEFAULT_RETRIES) {
|
|
1921
1972
|
this.headers.failureHeaders["Retries"] = this.workflowConfig.retries.toString();
|
|
1922
1973
|
}
|
|
@@ -1924,6 +1975,42 @@ var WorkflowHeaders = class {
|
|
|
1924
1975
|
this.headers.failureHeaders["Retry-Delay"] = this.workflowConfig.retryDelay.toString();
|
|
1925
1976
|
}
|
|
1926
1977
|
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Applies the step-level settings of a step to the message.
|
|
1980
|
+
*
|
|
1981
|
+
* These headers configure the message itself, so that QStash uses them
|
|
1982
|
+
* instead of the settings the run was triggered with. The delivery of
|
|
1983
|
+
* the message is what executes the step, which is how a step's
|
|
1984
|
+
* settings reach it.
|
|
1985
|
+
*
|
|
1986
|
+
* When any setting is present the feature set is extended with
|
|
1987
|
+
* `WF_StepConfig`, which is what tells QStash to keep them. QStash
|
|
1988
|
+
* reports that back on the delivery as `Upstash-Workflow-Step-Config`.
|
|
1989
|
+
*/
|
|
1990
|
+
addStepSettings() {
|
|
1991
|
+
if (!this.stepSettings) {
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
const headers = {};
|
|
1995
|
+
if (this.stepSettings.flowControl) {
|
|
1996
|
+
const { flowControlKey, flowControlValue } = prepareFlowControl(
|
|
1997
|
+
this.stepSettings.flowControl
|
|
1998
|
+
);
|
|
1999
|
+
headers[FLOW_CONTROL_KEY_HEADER] = flowControlKey;
|
|
2000
|
+
headers[FLOW_CONTROL_VALUE_HEADER] = flowControlValue;
|
|
2001
|
+
}
|
|
2002
|
+
if (this.stepSettings.retries !== void 0) {
|
|
2003
|
+
headers[RETRIES_HEADER] = this.stepSettings.retries.toString();
|
|
2004
|
+
}
|
|
2005
|
+
if (this.stepSettings.retryDelay) {
|
|
2006
|
+
headers[RETRY_DELAY_HEADER] = this.stepSettings.retryDelay;
|
|
2007
|
+
}
|
|
2008
|
+
if (Object.keys(headers).length === 0) {
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
headers[WORKFLOW_FEATURE_HEADER] = `${WORKFLOW_FEATURE_SET},${WORKFLOW_STEP_CONFIG_FEATURE}`;
|
|
2012
|
+
this.headers.stepSettingsHeaders = headers;
|
|
2013
|
+
}
|
|
1927
2014
|
addContentType() {
|
|
1928
2015
|
if (this.workflowConfig.useJSONContent) {
|
|
1929
2016
|
this.headers.rawHeaders["content-type"] = "application/json";
|
|
@@ -1937,14 +2024,17 @@ var WorkflowHeaders = class {
|
|
|
1937
2024
|
return contentType;
|
|
1938
2025
|
}
|
|
1939
2026
|
prefixHeaders(contentType) {
|
|
1940
|
-
const { rawHeaders, workflowHeaders, failureHeaders } = this.headers;
|
|
2027
|
+
const { rawHeaders, workflowHeaders, failureHeaders, stepSettingsHeaders } = this.headers;
|
|
1941
2028
|
const isCall = this.stepInfo?.lazyStep.stepType === "Call";
|
|
1942
2029
|
return {
|
|
1943
2030
|
headers: {
|
|
1944
2031
|
...rawHeaders,
|
|
1945
2032
|
...addPrefixToHeaders(workflowHeaders, isCall ? "Upstash-Callback-" : "Upstash-"),
|
|
1946
2033
|
...addPrefixToHeaders(failureHeaders, "Upstash-Failure-Callback-"),
|
|
1947
|
-
...isCall ? addPrefixToHeaders(failureHeaders, "Upstash-Callback-Failure-Callback-") : {}
|
|
2034
|
+
...isCall ? addPrefixToHeaders(failureHeaders, "Upstash-Callback-Failure-Callback-") : {},
|
|
2035
|
+
// last, so they override the run's settings above. Never
|
|
2036
|
+
// prefixed: they configure this message, not its callback.
|
|
2037
|
+
...stepSettingsHeaders
|
|
1948
2038
|
},
|
|
1949
2039
|
contentType
|
|
1950
2040
|
};
|
|
@@ -1995,7 +2085,7 @@ var submitParallelSteps = async ({
|
|
|
1995
2085
|
info: `Submitting ${planSteps.length} parallel steps.`
|
|
1996
2086
|
});
|
|
1997
2087
|
const result = await context.qstashClient.batch(
|
|
1998
|
-
planSteps.map((planStep) => {
|
|
2088
|
+
planSteps.map((planStep, index) => {
|
|
1999
2089
|
const { headers } = getHeaders({
|
|
2000
2090
|
initHeaderValue: "false",
|
|
2001
2091
|
workflowConfig: {
|
|
@@ -2003,7 +2093,8 @@ var submitParallelSteps = async ({
|
|
|
2003
2093
|
workflowUrl: context.url,
|
|
2004
2094
|
telemetry
|
|
2005
2095
|
},
|
|
2006
|
-
invokeCount
|
|
2096
|
+
invokeCount,
|
|
2097
|
+
stepSettings: steps[index].stepSettings
|
|
2007
2098
|
});
|
|
2008
2099
|
return {
|
|
2009
2100
|
headers,
|
|
@@ -2022,31 +2113,38 @@ var submitParallelSteps = async ({
|
|
|
2022
2113
|
}
|
|
2023
2114
|
throw new WorkflowAbort(planSteps[0].stepName, planSteps[0]);
|
|
2024
2115
|
};
|
|
2025
|
-
var
|
|
2026
|
-
context,
|
|
2116
|
+
var executeStep = async ({
|
|
2027
2117
|
lazyStep,
|
|
2028
2118
|
stepId,
|
|
2029
|
-
invokeCount,
|
|
2030
2119
|
concurrency,
|
|
2031
|
-
telemetry,
|
|
2032
|
-
dispatchDebug,
|
|
2033
2120
|
dispatchLifecycle
|
|
2034
2121
|
}) => {
|
|
2035
2122
|
await dispatchLifecycle("beforeExecution", {
|
|
2036
2123
|
stepName: lazyStep.stepName
|
|
2037
2124
|
});
|
|
2038
|
-
let resultStep;
|
|
2039
2125
|
try {
|
|
2040
|
-
|
|
2126
|
+
return await lazyStep.getResultStep(concurrency, stepId);
|
|
2041
2127
|
} catch (error) {
|
|
2042
2128
|
attachStepNameToError(error, lazyStep.stepName);
|
|
2043
2129
|
throw error;
|
|
2044
2130
|
}
|
|
2131
|
+
};
|
|
2132
|
+
var submitStepResult = async ({
|
|
2133
|
+
context,
|
|
2134
|
+
lazyStep,
|
|
2135
|
+
resultStep,
|
|
2136
|
+
invokeCount,
|
|
2137
|
+
concurrency,
|
|
2138
|
+
telemetry,
|
|
2139
|
+
dispatchDebug,
|
|
2140
|
+
nextStepSettings
|
|
2141
|
+
}) => {
|
|
2045
2142
|
const { headers } = lazyStep.getHeaders({
|
|
2046
2143
|
context,
|
|
2047
2144
|
step: resultStep,
|
|
2048
2145
|
invokeCount,
|
|
2049
|
-
telemetry
|
|
2146
|
+
telemetry,
|
|
2147
|
+
stepSettings: nextStepSettings
|
|
2050
2148
|
});
|
|
2051
2149
|
const body = lazyStep.getBody({
|
|
2052
2150
|
context,
|
|
@@ -2071,6 +2169,138 @@ var submitSingleStep = async ({
|
|
|
2071
2169
|
}
|
|
2072
2170
|
return resultStep;
|
|
2073
2171
|
};
|
|
2172
|
+
var publishStepConfigRequest = async ({
|
|
2173
|
+
context,
|
|
2174
|
+
lazyStep,
|
|
2175
|
+
targetStep,
|
|
2176
|
+
invokeCount,
|
|
2177
|
+
telemetry,
|
|
2178
|
+
dispatchDebug
|
|
2179
|
+
}) => {
|
|
2180
|
+
const { headers } = getHeaders({
|
|
2181
|
+
initHeaderValue: "false",
|
|
2182
|
+
workflowConfig: {
|
|
2183
|
+
workflowRunId: context.workflowRunId,
|
|
2184
|
+
workflowUrl: context.url,
|
|
2185
|
+
telemetry
|
|
2186
|
+
},
|
|
2187
|
+
invokeCount,
|
|
2188
|
+
stepSettings: lazyStep.stepSettings
|
|
2189
|
+
});
|
|
2190
|
+
await dispatchDebug("onInfo", {
|
|
2191
|
+
info: `Step "${lazyStep.stepName}" (${targetStep}) has step-level settings which the current delivery does not carry. Requesting a delivery which does.`
|
|
2192
|
+
});
|
|
2193
|
+
const result = await context.qstashClient.publishJSON({
|
|
2194
|
+
headers: {
|
|
2195
|
+
...headers,
|
|
2196
|
+
"Upstash-Workflow-CallType": WORKFLOW_STEP_CONFIG_CALL_TYPE
|
|
2197
|
+
},
|
|
2198
|
+
method: "POST",
|
|
2199
|
+
body: { targetStep, invokeCount },
|
|
2200
|
+
url: context.url,
|
|
2201
|
+
contentBasedDeduplication: true
|
|
2202
|
+
});
|
|
2203
|
+
if (result?.deduplicated) {
|
|
2204
|
+
await dispatchDebug("onWarning", {
|
|
2205
|
+
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.`
|
|
2206
|
+
});
|
|
2207
|
+
} else if (result?.messageId) {
|
|
2208
|
+
await dispatchDebug("onInfo", {
|
|
2209
|
+
info: `Requested a delivery under the settings of step "${lazyStep.stepName}" with messageId: ${result.messageId}.`
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
};
|
|
2213
|
+
|
|
2214
|
+
// src/qstash/step-config.ts
|
|
2215
|
+
var DEFAULT_PARALLELISM = 0;
|
|
2216
|
+
var DEFAULT_RATE = 0;
|
|
2217
|
+
var DEFAULT_PERIOD_SECONDS = 1;
|
|
2218
|
+
var SECONDS_PER_UNIT = {
|
|
2219
|
+
s: 1,
|
|
2220
|
+
m: 60,
|
|
2221
|
+
h: 60 * 60,
|
|
2222
|
+
d: 24 * 60 * 60
|
|
2223
|
+
};
|
|
2224
|
+
var durationToSeconds = (duration) => {
|
|
2225
|
+
if (typeof duration === "number") {
|
|
2226
|
+
return Number.isFinite(duration) ? duration : void 0;
|
|
2227
|
+
}
|
|
2228
|
+
const match = /^(\d+)([smhd])$/.exec(duration.trim());
|
|
2229
|
+
if (match) {
|
|
2230
|
+
return Number(match[1]) * SECONDS_PER_UNIT[match[2]];
|
|
2231
|
+
}
|
|
2232
|
+
const seconds = Number(duration.trim());
|
|
2233
|
+
return Number.isFinite(seconds) ? seconds : void 0;
|
|
2234
|
+
};
|
|
2235
|
+
var normalizeFlowControl = (flowControl) => {
|
|
2236
|
+
const period = flowControl.period === void 0 ? void 0 : durationToSeconds(flowControl.period);
|
|
2237
|
+
return {
|
|
2238
|
+
key: flowControl.key,
|
|
2239
|
+
parallelism: flowControl.parallelism ?? DEFAULT_PARALLELISM,
|
|
2240
|
+
rate: flowControl.rate ?? flowControl.ratePerSecond ?? DEFAULT_RATE,
|
|
2241
|
+
period: period ?? DEFAULT_PERIOD_SECONDS
|
|
2242
|
+
};
|
|
2243
|
+
};
|
|
2244
|
+
var parseFlowControlHeaders = (key, value) => {
|
|
2245
|
+
if (!key) {
|
|
2246
|
+
return void 0;
|
|
2247
|
+
}
|
|
2248
|
+
const flowControl = {
|
|
2249
|
+
key,
|
|
2250
|
+
parallelism: DEFAULT_PARALLELISM,
|
|
2251
|
+
rate: DEFAULT_RATE,
|
|
2252
|
+
period: DEFAULT_PERIOD_SECONDS
|
|
2253
|
+
};
|
|
2254
|
+
for (const entry of (value ?? "").split(",")) {
|
|
2255
|
+
const [name, rawValue] = entry.split("=").map((part) => part.trim());
|
|
2256
|
+
if (!name || rawValue === void 0) {
|
|
2257
|
+
continue;
|
|
2258
|
+
}
|
|
2259
|
+
switch (name) {
|
|
2260
|
+
case "parallelism": {
|
|
2261
|
+
flowControl.parallelism = Number(rawValue);
|
|
2262
|
+
break;
|
|
2263
|
+
}
|
|
2264
|
+
case "rate": {
|
|
2265
|
+
flowControl.rate = Number(rawValue);
|
|
2266
|
+
break;
|
|
2267
|
+
}
|
|
2268
|
+
case "period": {
|
|
2269
|
+
flowControl.period = durationToSeconds(rawValue) ?? DEFAULT_PERIOD_SECONDS;
|
|
2270
|
+
break;
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
return flowControl;
|
|
2275
|
+
};
|
|
2276
|
+
var getEffectiveConfig = (headers) => {
|
|
2277
|
+
const retriesHeader = headers.get(RETRIES_HEADER);
|
|
2278
|
+
return {
|
|
2279
|
+
flowControl: parseFlowControlHeaders(
|
|
2280
|
+
headers.get(FLOW_CONTROL_KEY_HEADER),
|
|
2281
|
+
headers.get(FLOW_CONTROL_VALUE_HEADER)
|
|
2282
|
+
),
|
|
2283
|
+
retries: retriesHeader === null ? void 0 : Number(retriesHeader),
|
|
2284
|
+
retryDelay: headers.get(RETRY_DELAY_HEADER) ?? void 0,
|
|
2285
|
+
hasStepConfig: headers.get(WORKFLOW_STEP_CONFIG_HEADER) === "true"
|
|
2286
|
+
};
|
|
2287
|
+
};
|
|
2288
|
+
var describeStepSettingsMismatch = (stepSettings, effectiveConfig) => {
|
|
2289
|
+
if (stepSettings.flowControl) {
|
|
2290
|
+
const wanted = normalizeFlowControl(stepSettings.flowControl);
|
|
2291
|
+
const applied = effectiveConfig.flowControl;
|
|
2292
|
+
if (!applied || applied.key !== wanted.key || applied.parallelism !== wanted.parallelism || applied.rate !== wanted.rate || applied.period !== wanted.period) {
|
|
2293
|
+
return `flow control: expected ${JSON.stringify(wanted)}, delivery has ${JSON.stringify(applied)}`;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
if (stepSettings.retries !== void 0 && effectiveConfig.retries !== void 0 && stepSettings.retries !== effectiveConfig.retries) {
|
|
2297
|
+
return `retries: expected ${stepSettings.retries}, delivery has ${effectiveConfig.retries}`;
|
|
2298
|
+
}
|
|
2299
|
+
if (stepSettings.retryDelay !== void 0 && stepSettings.retryDelay !== effectiveConfig.retryDelay) {
|
|
2300
|
+
return `retry delay: expected '${stepSettings.retryDelay}', delivery has '${effectiveConfig.retryDelay ?? ""}'`;
|
|
2301
|
+
}
|
|
2302
|
+
return void 0;
|
|
2303
|
+
};
|
|
2074
2304
|
|
|
2075
2305
|
// src/context/auto-executor.ts
|
|
2076
2306
|
var AutoExecutor = class _AutoExecutor {
|
|
@@ -2087,6 +2317,25 @@ var AutoExecutor = class _AutoExecutor {
|
|
|
2087
2317
|
stepCount = 0;
|
|
2088
2318
|
planStepCount = 0;
|
|
2089
2319
|
executingStep = false;
|
|
2320
|
+
/**
|
|
2321
|
+
* an executed step whose result hasn't been submitted to QStash yet.
|
|
2322
|
+
*
|
|
2323
|
+
* Set when a step which can produce its result in-process executes. The
|
|
2324
|
+
* submission waits so that the route function can continue and reveal
|
|
2325
|
+
* what comes next: if that is a single step with step-level settings,
|
|
2326
|
+
* the settings ride on this submission and no step config request is
|
|
2327
|
+
* needed, since the delivery of the submission is what executes it.
|
|
2328
|
+
*
|
|
2329
|
+
* Flushed when the next step is reached (see `addStep`) or when the
|
|
2330
|
+
* route function ends (see `flushPendingStep` in `serve`).
|
|
2331
|
+
*/
|
|
2332
|
+
pendingStep;
|
|
2333
|
+
/**
|
|
2334
|
+
* configuration QStash applied to the delivery being handled. A step
|
|
2335
|
+
* with step-level settings is settled by comparing its settings against
|
|
2336
|
+
* this.
|
|
2337
|
+
*/
|
|
2338
|
+
effectiveConfig;
|
|
2090
2339
|
/**
|
|
2091
2340
|
* @param context workflow context
|
|
2092
2341
|
* @param steps list of steps
|
|
@@ -2094,14 +2343,17 @@ var AutoExecutor = class _AutoExecutor {
|
|
|
2094
2343
|
* @param dispatchLifecycle lifecycle event dispatcher
|
|
2095
2344
|
* @param telemetry optional telemetry information
|
|
2096
2345
|
* @param invokeCount optional invoke count
|
|
2346
|
+
* @param effectiveConfig configuration QStash applied to the delivery
|
|
2347
|
+
* being handled
|
|
2097
2348
|
*/
|
|
2098
|
-
constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry, invokeCount) {
|
|
2349
|
+
constructor(context, steps, dispatchDebug, dispatchLifecycle, telemetry, invokeCount, effectiveConfig) {
|
|
2099
2350
|
this.context = context;
|
|
2100
2351
|
this.steps = steps;
|
|
2101
2352
|
this.dispatchDebug = dispatchDebug;
|
|
2102
2353
|
this.dispatchLifecycle = dispatchLifecycle;
|
|
2103
2354
|
this.telemetry = telemetry;
|
|
2104
2355
|
this.invokeCount = invokeCount ?? 0;
|
|
2356
|
+
this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
|
|
2105
2357
|
this.nonPlanStepCount = this.steps.filter((step) => !step.targetStep).length;
|
|
2106
2358
|
}
|
|
2107
2359
|
/**
|
|
@@ -2136,6 +2388,15 @@ var AutoExecutor = class _AutoExecutor {
|
|
|
2136
2388
|
lazyStepList.push(stepInfo);
|
|
2137
2389
|
const index = this.indexInCurrentList++;
|
|
2138
2390
|
const requestComplete = this.deferExecution().then(async () => {
|
|
2391
|
+
const submitted = await this.submitPendingStep(
|
|
2392
|
+
lazyStepList.length === 1 ? lazyStepList[0].stepSettings : void 0
|
|
2393
|
+
);
|
|
2394
|
+
if (submitted.isErr()) {
|
|
2395
|
+
throw submitted.error;
|
|
2396
|
+
}
|
|
2397
|
+
if (submitted.value.result === "submitted-step") {
|
|
2398
|
+
throw submitted.value.abort;
|
|
2399
|
+
}
|
|
2139
2400
|
if (!this.promises.has(lazyStepList)) {
|
|
2140
2401
|
const promise2 = this.getExecutionPromise(lazyStepList);
|
|
2141
2402
|
this.promises.set(lazyStepList, promise2);
|
|
@@ -2189,18 +2450,100 @@ var AutoExecutor = class _AutoExecutor {
|
|
|
2189
2450
|
}
|
|
2190
2451
|
return parsedOut;
|
|
2191
2452
|
}
|
|
2192
|
-
const
|
|
2193
|
-
|
|
2453
|
+
const mismatch = lazyStep.stepSettings ? describeStepSettingsMismatch(lazyStep.stepSettings, this.effectiveConfig) : void 0;
|
|
2454
|
+
if (mismatch) {
|
|
2455
|
+
if (!this.effectiveConfig.hasStepConfig) {
|
|
2456
|
+
await publishStepConfigRequest({
|
|
2457
|
+
context: this.context,
|
|
2458
|
+
lazyStep,
|
|
2459
|
+
targetStep: this.stepCount,
|
|
2460
|
+
invokeCount: this.invokeCount,
|
|
2461
|
+
telemetry: this.telemetry,
|
|
2462
|
+
dispatchDebug: this.dispatchDebug
|
|
2463
|
+
});
|
|
2464
|
+
throw new WorkflowAbort(lazyStep.stepName);
|
|
2465
|
+
} else {
|
|
2466
|
+
await this.dispatchDebug("onWarning", {
|
|
2467
|
+
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.`
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
const resultStep = await executeStep({
|
|
2194
2472
|
lazyStep,
|
|
2195
2473
|
stepId: this.stepCount,
|
|
2474
|
+
concurrency: NO_CONCURRENCY,
|
|
2475
|
+
dispatchLifecycle: this.dispatchLifecycle
|
|
2476
|
+
});
|
|
2477
|
+
if (lazyStep.supportsDeferredSubmission) {
|
|
2478
|
+
this.pendingStep = { status: "held", lazyStep, resultStep };
|
|
2479
|
+
return lazyStep.parseOut({
|
|
2480
|
+
...resultStep,
|
|
2481
|
+
out: resultStep.out === void 0 ? void 0 : JSON.stringify(resultStep.out)
|
|
2482
|
+
});
|
|
2483
|
+
}
|
|
2484
|
+
await submitStepResult({
|
|
2485
|
+
context: this.context,
|
|
2486
|
+
lazyStep,
|
|
2487
|
+
resultStep,
|
|
2196
2488
|
invokeCount: this.invokeCount,
|
|
2197
|
-
concurrency:
|
|
2489
|
+
concurrency: NO_CONCURRENCY,
|
|
2198
2490
|
telemetry: this.telemetry,
|
|
2199
|
-
dispatchDebug: this.dispatchDebug
|
|
2200
|
-
dispatchLifecycle: this.dispatchLifecycle
|
|
2491
|
+
dispatchDebug: this.dispatchDebug
|
|
2201
2492
|
});
|
|
2202
2493
|
throw new WorkflowAbort(lazyStep.stepName, resultStep);
|
|
2203
2494
|
}
|
|
2495
|
+
/**
|
|
2496
|
+
* Submits the result of the step being held, if there is one, and
|
|
2497
|
+
* returns the abort the caller has to throw to end the invocation.
|
|
2498
|
+
*
|
|
2499
|
+
* Returns undefined when no step is being held, which is the caller's
|
|
2500
|
+
* signal that there is nothing to end. Throws instead of returning
|
|
2501
|
+
* when the submission itself fails, so that error reaches the caller
|
|
2502
|
+
* rather than an abort claiming the step was submitted.
|
|
2503
|
+
*
|
|
2504
|
+
* Callers which arrive while a submission is already under way (the
|
|
2505
|
+
* steps of a parallel group, added together) wait for that one and get
|
|
2506
|
+
* the same abort, rather than submitting a second time.
|
|
2507
|
+
*
|
|
2508
|
+
* The next step's settings are attached whenever it has any, without
|
|
2509
|
+
* checking them against the current delivery: the executor only knows
|
|
2510
|
+
* the configuration of the delivery in hand, which inside a
|
|
2511
|
+
* delivery is the *previous* step's, so such a check would be wrong
|
|
2512
|
+
* exactly when two steps with settings follow each other.
|
|
2513
|
+
*
|
|
2514
|
+
* @param nextStepSettings step-level settings of the next step
|
|
2515
|
+
* @returns `submitted-step` with the abort which ends this invocation,
|
|
2516
|
+
* or `no-pending-step` when nothing was held and there is nothing to
|
|
2517
|
+
* end
|
|
2518
|
+
*/
|
|
2519
|
+
async submitPendingStep(nextStepSettings) {
|
|
2520
|
+
if (!this.pendingStep) {
|
|
2521
|
+
return ok({ result: "no-pending-step" });
|
|
2522
|
+
}
|
|
2523
|
+
try {
|
|
2524
|
+
if (this.pendingStep.status === "submitting") {
|
|
2525
|
+
return ok({ result: "submitted-step", abort: await this.pendingStep.submitted });
|
|
2526
|
+
}
|
|
2527
|
+
const { lazyStep, resultStep } = this.pendingStep;
|
|
2528
|
+
const submitted = (async () => {
|
|
2529
|
+
await submitStepResult({
|
|
2530
|
+
context: this.context,
|
|
2531
|
+
lazyStep,
|
|
2532
|
+
resultStep,
|
|
2533
|
+
invokeCount: this.invokeCount,
|
|
2534
|
+
concurrency: NO_CONCURRENCY,
|
|
2535
|
+
telemetry: this.telemetry,
|
|
2536
|
+
dispatchDebug: this.dispatchDebug,
|
|
2537
|
+
nextStepSettings
|
|
2538
|
+
});
|
|
2539
|
+
return new WorkflowAbort(lazyStep.stepName, resultStep);
|
|
2540
|
+
})();
|
|
2541
|
+
this.pendingStep = { status: "submitting", submitted };
|
|
2542
|
+
return ok({ result: "submitted-step", abort: await submitted });
|
|
2543
|
+
} catch (error) {
|
|
2544
|
+
return err(error);
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2204
2547
|
/**
|
|
2205
2548
|
* Runs steps in parallel.
|
|
2206
2549
|
*
|
|
@@ -2249,15 +2592,20 @@ var AutoExecutor = class _AutoExecutor {
|
|
|
2249
2592
|
validateStep(parallelSteps[stepIndex], planStep);
|
|
2250
2593
|
try {
|
|
2251
2594
|
const parallelStep = parallelSteps[stepIndex];
|
|
2252
|
-
const resultStep = await
|
|
2253
|
-
context: this.context,
|
|
2595
|
+
const resultStep = await executeStep({
|
|
2254
2596
|
lazyStep: parallelStep,
|
|
2255
2597
|
stepId: planStep.targetStep,
|
|
2598
|
+
concurrency: parallelSteps.length,
|
|
2599
|
+
dispatchLifecycle: this.dispatchLifecycle
|
|
2600
|
+
});
|
|
2601
|
+
await submitStepResult({
|
|
2602
|
+
context: this.context,
|
|
2603
|
+
lazyStep: parallelStep,
|
|
2604
|
+
resultStep,
|
|
2256
2605
|
invokeCount: this.invokeCount,
|
|
2257
2606
|
concurrency: parallelSteps.length,
|
|
2258
2607
|
telemetry: this.telemetry,
|
|
2259
|
-
dispatchDebug: this.dispatchDebug
|
|
2260
|
-
dispatchLifecycle: this.dispatchLifecycle
|
|
2608
|
+
dispatchDebug: this.dispatchDebug
|
|
2261
2609
|
});
|
|
2262
2610
|
throw new WorkflowAbort(parallelStep.stepName, resultStep);
|
|
2263
2611
|
} catch (error) {
|
|
@@ -2806,6 +3154,46 @@ var WorkflowContext = class {
|
|
|
2806
3154
|
* `1` on the first retry, `2` on the second, and so on.
|
|
2807
3155
|
*/
|
|
2808
3156
|
retried;
|
|
3157
|
+
/**
|
|
3158
|
+
* Configuration QStash applied to the request being handled.
|
|
3159
|
+
*
|
|
3160
|
+
* This is the configuration of *this delivery*, not of the run: it is
|
|
3161
|
+
* the configuration the run was triggered with, except inside the
|
|
3162
|
+
* delivery which executes a step that has step-level settings, where
|
|
3163
|
+
* it is that step's settings. The fields below expose it; the executor
|
|
3164
|
+
* gets it directly rather than reaching through the context.
|
|
3165
|
+
*/
|
|
3166
|
+
effectiveConfig;
|
|
3167
|
+
/**
|
|
3168
|
+
* Flow control QStash applied to the request being handled, if any.
|
|
3169
|
+
*
|
|
3170
|
+
* This is the flow control of the request in hand, which is the one the
|
|
3171
|
+
* run was triggered with except inside a step carrying its own.
|
|
3172
|
+
*/
|
|
3173
|
+
get flowControl() {
|
|
3174
|
+
return this.effectiveConfig.flowControl;
|
|
3175
|
+
}
|
|
3176
|
+
/**
|
|
3177
|
+
* Retry limit QStash applied to the request being handled, if it
|
|
3178
|
+
* reported one.
|
|
3179
|
+
*
|
|
3180
|
+
* Not to be confused with {@link retried}, which is how many retries
|
|
3181
|
+
* have already happened.
|
|
3182
|
+
*
|
|
3183
|
+
* @see {@link flowControl} for what "the request being handled" means
|
|
3184
|
+
*/
|
|
3185
|
+
get retries() {
|
|
3186
|
+
return this.effectiveConfig.retries;
|
|
3187
|
+
}
|
|
3188
|
+
/**
|
|
3189
|
+
* Retry delay expression QStash applied to the request being handled,
|
|
3190
|
+
* if any.
|
|
3191
|
+
*
|
|
3192
|
+
* @see {@link flowControl} for what "the request being handled" means
|
|
3193
|
+
*/
|
|
3194
|
+
get retryDelay() {
|
|
3195
|
+
return this.effectiveConfig.retryDelay;
|
|
3196
|
+
}
|
|
2809
3197
|
constructor({
|
|
2810
3198
|
qstashClient,
|
|
2811
3199
|
workflowRunId,
|
|
@@ -2819,7 +3207,8 @@ var WorkflowContext = class {
|
|
|
2819
3207
|
invokeCount,
|
|
2820
3208
|
label,
|
|
2821
3209
|
retried,
|
|
2822
|
-
middlewareManager
|
|
3210
|
+
middlewareManager,
|
|
3211
|
+
effectiveConfig
|
|
2823
3212
|
}) {
|
|
2824
3213
|
this.qstashClient = qstashClient;
|
|
2825
3214
|
this.workflowRunId = workflowRunId;
|
|
@@ -2831,6 +3220,7 @@ var WorkflowContext = class {
|
|
|
2831
3220
|
this.env = env ?? {};
|
|
2832
3221
|
this.labels = label === void 0 ? [] : Array.isArray(label) ? label : label ? label.split(",") : [];
|
|
2833
3222
|
this.retried = retried ?? 0;
|
|
3223
|
+
this.effectiveConfig = effectiveConfig ?? { hasStepConfig: false };
|
|
2834
3224
|
const middlewareManagerInstance = middlewareManager ?? new MiddlewareManager([]);
|
|
2835
3225
|
middlewareManagerInstance.assignContext(this);
|
|
2836
3226
|
this.executor = new AutoExecutor(
|
|
@@ -2839,7 +3229,8 @@ var WorkflowContext = class {
|
|
|
2839
3229
|
middlewareManagerInstance.dispatchDebug.bind(middlewareManagerInstance),
|
|
2840
3230
|
middlewareManagerInstance.dispatchLifecycle.bind(middlewareManagerInstance),
|
|
2841
3231
|
telemetry,
|
|
2842
|
-
invokeCount
|
|
3232
|
+
invokeCount,
|
|
3233
|
+
this.effectiveConfig
|
|
2843
3234
|
);
|
|
2844
3235
|
}
|
|
2845
3236
|
/**
|
|
@@ -2865,13 +3256,35 @@ var WorkflowContext = class {
|
|
|
2865
3256
|
* ])
|
|
2866
3257
|
* ```
|
|
2867
3258
|
*
|
|
3259
|
+
* Step-level settings can be passed as a third argument, overriding
|
|
3260
|
+
* the settings the workflow run was triggered with for this step only:
|
|
3261
|
+
*
|
|
3262
|
+
* ```typescript
|
|
3263
|
+
* const result = await context.run(
|
|
3264
|
+
* "step 1",
|
|
3265
|
+
* () => {
|
|
3266
|
+
* return "result"
|
|
3267
|
+
* },
|
|
3268
|
+
* {
|
|
3269
|
+
* flowControl: { key: "custom-key", parallelism: 3 },
|
|
3270
|
+
* retries: 5,
|
|
3271
|
+
* }
|
|
3272
|
+
* )
|
|
3273
|
+
* ```
|
|
3274
|
+
*
|
|
2868
3275
|
* @param stepName name of the step
|
|
2869
3276
|
* @param stepFunction step function to be executed
|
|
3277
|
+
* @param stepSettings step-level settings for this step
|
|
2870
3278
|
* @returns result of the step function
|
|
2871
3279
|
*/
|
|
2872
|
-
async run(stepName, stepFunction) {
|
|
3280
|
+
async run(stepName, stepFunction, stepSettings) {
|
|
3281
|
+
if (stepSettings) {
|
|
3282
|
+
validateFlowControl(stepSettings.flowControl);
|
|
3283
|
+
}
|
|
2873
3284
|
const wrappedStepFunction = (() => this.executor.wrapStep(stepName, stepFunction));
|
|
2874
|
-
return await this.addStep(
|
|
3285
|
+
return await this.addStep(
|
|
3286
|
+
new LazyFunctionStep(this, stepName, wrappedStepFunction, stepSettings)
|
|
3287
|
+
);
|
|
2875
3288
|
}
|
|
2876
3289
|
/**
|
|
2877
3290
|
* Stops the execution for the duration provided.
|
|
@@ -3094,7 +3507,7 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
|
|
|
3094
3507
|
*
|
|
3095
3508
|
* @param routeFunction
|
|
3096
3509
|
*/
|
|
3097
|
-
static async tryAuthentication(routeFunction, context) {
|
|
3510
|
+
static async tryAuthentication(routeFunction, context, effectiveConfig) {
|
|
3098
3511
|
const disabledContext = new _DisabledWorkflowContext({
|
|
3099
3512
|
qstashClient: new import_qstash10.Client({
|
|
3100
3513
|
baseUrl: "disabled-client",
|
|
@@ -3109,7 +3522,10 @@ var DisabledWorkflowContext = class _DisabledWorkflowContext extends WorkflowCon
|
|
|
3109
3522
|
initialPayload: context.requestPayload,
|
|
3110
3523
|
env: context.env,
|
|
3111
3524
|
label: context.labels,
|
|
3112
|
-
retried: context.retried
|
|
3525
|
+
retried: context.retried,
|
|
3526
|
+
// the route function runs for real here until it reaches a step, so
|
|
3527
|
+
// it observes the same configuration as it will on the real context
|
|
3528
|
+
effectiveConfig
|
|
3113
3529
|
});
|
|
3114
3530
|
try {
|
|
3115
3531
|
await routeFunction(disabledContext);
|
|
@@ -3326,6 +3742,7 @@ var handleFailure = async ({
|
|
|
3326
3742
|
}
|
|
3327
3743
|
const userHeaders = recreateUserHeaders(request.headers);
|
|
3328
3744
|
const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
|
|
3745
|
+
const effectiveConfig = getEffectiveConfig(request.headers);
|
|
3329
3746
|
const workflowContext = new WorkflowContext({
|
|
3330
3747
|
qstashClient,
|
|
3331
3748
|
workflowRunId,
|
|
@@ -3339,11 +3756,13 @@ var handleFailure = async ({
|
|
|
3339
3756
|
label: userHeaders.get(WORKFLOW_LABEL_HEADER) ?? void 0,
|
|
3340
3757
|
retried,
|
|
3341
3758
|
workflowRunCreatedAt: workflowCreatedAt,
|
|
3342
|
-
middlewareManager: void 0
|
|
3759
|
+
middlewareManager: void 0,
|
|
3760
|
+
effectiveConfig
|
|
3343
3761
|
});
|
|
3344
3762
|
const authCheck = await DisabledWorkflowContext.tryAuthentication(
|
|
3345
3763
|
routeFunction,
|
|
3346
|
-
workflowContext
|
|
3764
|
+
workflowContext,
|
|
3765
|
+
effectiveConfig
|
|
3347
3766
|
);
|
|
3348
3767
|
if (authCheck.isErr()) {
|
|
3349
3768
|
await dispatchDebug?.("onError", {
|
|
@@ -3435,7 +3854,13 @@ var createRegionalHandler = (environment, receiverConfig, region, clientOptions)
|
|
|
3435
3854
|
const client = new import_qstash11.Client({
|
|
3436
3855
|
...clientOptions,
|
|
3437
3856
|
baseUrl: clientEnv.QSTASH_URL,
|
|
3438
|
-
token: clientEnv.QSTASH_TOKEN
|
|
3857
|
+
token: clientEnv.QSTASH_TOKEN,
|
|
3858
|
+
// Mirror the receiver's dev-mode decision so a single QSTASH_DEV=true points
|
|
3859
|
+
// the client at the local dev server too. Without this the QStash Client
|
|
3860
|
+
// re-derives dev mode from its own process.env, which is blind to the
|
|
3861
|
+
// Cloudflare worker `env` binding that actually carries QSTASH_DEV there.
|
|
3862
|
+
// An explicit devMode in the user's client config still wins.
|
|
3863
|
+
devMode: clientOptions?.devMode ?? isQStashDevModeEnabled(environment)
|
|
3439
3864
|
});
|
|
3440
3865
|
const receiver = getReceiver(environment, receiverConfig, region);
|
|
3441
3866
|
return { client, receiver };
|
|
@@ -3485,7 +3910,9 @@ var getQStashHandlers = ({
|
|
|
3485
3910
|
client: qstashClientOption && "http" in qstashClientOption ? qstashClientOption : new import_qstash11.Client({
|
|
3486
3911
|
...qstashClientOption,
|
|
3487
3912
|
baseUrl: environment.QSTASH_URL,
|
|
3488
|
-
token: environment.QSTASH_TOKEN
|
|
3913
|
+
token: environment.QSTASH_TOKEN,
|
|
3914
|
+
// Mirror the receiver's dev-mode decision (see createRegionalHandler).
|
|
3915
|
+
devMode: qstashClientOption?.devMode ?? isQStashDevModeEnabled(environment)
|
|
3489
3916
|
}),
|
|
3490
3917
|
receiver: getReceiver(environment, receiverConfig)
|
|
3491
3918
|
}
|
|
@@ -3825,12 +4252,14 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
|
|
|
3825
4252
|
const retried = Number(request.headers.get(WORKFLOW_RETRIED_HEADER) ?? "0");
|
|
3826
4253
|
const label = request.headers.get(WORKFLOW_LABEL_HEADER) ?? void 0;
|
|
3827
4254
|
const workflowRunCreatedAt = request.headers.get(WORKFLOW_CREATED_AT_HEADER);
|
|
4255
|
+
const effectiveConfig = getEffectiveConfig(request.headers);
|
|
3828
4256
|
const workflowContext = new WorkflowContext({
|
|
3829
4257
|
qstashClient: regionalClient,
|
|
3830
4258
|
workflowRunId,
|
|
3831
4259
|
initialPayload: isThirdPartyCallResult(request) ? JSON.parse(rawInitialPayload) : initialPayloadParser(rawInitialPayload),
|
|
3832
4260
|
headers: recreateUserHeaders(request.headers),
|
|
3833
4261
|
steps,
|
|
4262
|
+
effectiveConfig,
|
|
3834
4263
|
url: workflowUrl,
|
|
3835
4264
|
env,
|
|
3836
4265
|
telemetry,
|
|
@@ -3842,7 +4271,8 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
|
|
|
3842
4271
|
});
|
|
3843
4272
|
const authCheck = await DisabledWorkflowContext.tryAuthentication(
|
|
3844
4273
|
routeFunction,
|
|
3845
|
-
workflowContext
|
|
4274
|
+
workflowContext,
|
|
4275
|
+
effectiveConfig
|
|
3846
4276
|
);
|
|
3847
4277
|
if (authCheck.isErr()) {
|
|
3848
4278
|
throw authCheck.error;
|
|
@@ -3879,7 +4309,23 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
|
|
|
3879
4309
|
if (steps.length === 1) {
|
|
3880
4310
|
await middlewareManager.dispatchLifecycle("runStarted", {});
|
|
3881
4311
|
}
|
|
3882
|
-
|
|
4312
|
+
let outcome;
|
|
4313
|
+
try {
|
|
4314
|
+
outcome = { ran: true, result: await routeFunction(workflowContext) };
|
|
4315
|
+
} catch (error) {
|
|
4316
|
+
outcome = { ran: false, error };
|
|
4317
|
+
}
|
|
4318
|
+
const submitted = await flushPendingStep(workflowContext);
|
|
4319
|
+
if (submitted.isErr()) {
|
|
4320
|
+
throw submitted.error;
|
|
4321
|
+
}
|
|
4322
|
+
if (submitted.value.result === "submitted-step") {
|
|
4323
|
+
throw submitted.value.abort;
|
|
4324
|
+
}
|
|
4325
|
+
if (!outcome.ran) {
|
|
4326
|
+
throw outcome.error;
|
|
4327
|
+
}
|
|
4328
|
+
return outcome.result;
|
|
3883
4329
|
},
|
|
3884
4330
|
onCleanup: async (result2) => {
|
|
3885
4331
|
await middlewareManager.dispatchLifecycle("runCompleted", {
|
|
@@ -3959,6 +4405,7 @@ var serveBase = (routeFunction, telemetry, options, internalOptions) => {
|
|
|
3959
4405
|
return { handler: safeHandler };
|
|
3960
4406
|
};
|
|
3961
4407
|
var serve = (routeFunction, options) => {
|
|
4408
|
+
void (0, import_qstash12.startDevServer)();
|
|
3962
4409
|
return serveBase(
|
|
3963
4410
|
routeFunction,
|
|
3964
4411
|
{
|
|
@@ -3970,9 +4417,10 @@ var serve = (routeFunction, options) => {
|
|
|
3970
4417
|
};
|
|
3971
4418
|
|
|
3972
4419
|
// src/client/index.ts
|
|
3973
|
-
var
|
|
4420
|
+
var import_qstash14 = require("@upstash/qstash");
|
|
3974
4421
|
|
|
3975
4422
|
// src/client/dlq.ts
|
|
4423
|
+
var import_qstash13 = require("@upstash/qstash");
|
|
3976
4424
|
function buildResumeRestartHeaders(options) {
|
|
3977
4425
|
const headers = {};
|
|
3978
4426
|
if (options?.flowControl) {
|
|
@@ -4090,6 +4538,27 @@ var DLQ = class {
|
|
|
4090
4538
|
});
|
|
4091
4539
|
return response;
|
|
4092
4540
|
}
|
|
4541
|
+
/**
|
|
4542
|
+
* Cancel the in-progress failure function (failureUrl/failureFunction) call
|
|
4543
|
+
* of a workflow run.
|
|
4544
|
+
*
|
|
4545
|
+
* Fails if the run has no failure function call in progress.
|
|
4546
|
+
*
|
|
4547
|
+
* @param dlqId - The ID of the DLQ message whose failure function call will be canceled
|
|
4548
|
+
*/
|
|
4549
|
+
async cancelFailureFunction({ dlqId }) {
|
|
4550
|
+
assertNonEmptyId(dlqId, "DLQ id");
|
|
4551
|
+
try {
|
|
4552
|
+
await this.client.http.request({
|
|
4553
|
+
path: ["v2", "workflows", "dlq", "callback", dlqId],
|
|
4554
|
+
method: "DELETE",
|
|
4555
|
+
parseResponseAsJson: false
|
|
4556
|
+
});
|
|
4557
|
+
} catch (error) {
|
|
4558
|
+
if (isInstanceOf(error, import_qstash13.QstashError) && error.status === 302) return;
|
|
4559
|
+
throw error;
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4093
4562
|
/**
|
|
4094
4563
|
* Delete DLQ messages.
|
|
4095
4564
|
*
|
|
@@ -4131,7 +4600,7 @@ var DLQ = class {
|
|
|
4131
4600
|
var Client4 = class {
|
|
4132
4601
|
client;
|
|
4133
4602
|
constructor(clientConfig) {
|
|
4134
|
-
this.client = new
|
|
4603
|
+
this.client = new import_qstash14.Client(clientConfig);
|
|
4135
4604
|
}
|
|
4136
4605
|
async cancel(request) {
|
|
4137
4606
|
if (typeof request === "object" && !Array.isArray(request) && ("ids" in request || "urlStartingWith" in request)) {
|