@upstash/qstash 2.9.1-rc.1 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/workflow.js CHANGED
@@ -244,6 +244,14 @@ var QstashDailyRatelimitError = class extends QstashError {
244
244
  this.reset = args.reset;
245
245
  }
246
246
  };
247
+ var QstashEmptyArrayError = class extends QstashError {
248
+ constructor(parameterName) {
249
+ super(
250
+ `Empty array provided for query parameter "${parameterName}". This would result in no filter being applied, which could affect all resources.`
251
+ );
252
+ this.name = "QstashEmptyArrayError";
253
+ }
254
+ };
247
255
  var QStashWorkflowError = class extends QstashError {
248
256
  constructor(message) {
249
257
  super(message);
@@ -273,6 +281,7 @@ var formatWorkflowError = (error) => {
273
281
  };
274
282
 
275
283
  // src/client/utils.ts
284
+ var DEFAULT_BULK_COUNT = 100;
276
285
  var isIgnoredHeader = (header) => {
277
286
  const lowerCaseHeader = header.toLowerCase();
278
287
  return lowerCaseHeader.startsWith("content-type") || lowerCaseHeader.startsWith("upstash-");
@@ -359,6 +368,24 @@ function processHeaders(request) {
359
368
  if (request.label !== void 0) {
360
369
  headers.set("Upstash-Label", request.label);
361
370
  }
371
+ if (request.redact !== void 0) {
372
+ const redactParts = [];
373
+ if (request.redact.body) {
374
+ redactParts.push("body");
375
+ }
376
+ if (request.redact.header !== void 0) {
377
+ if (request.redact.header === true) {
378
+ redactParts.push("header");
379
+ } else if (Array.isArray(request.redact.header) && request.redact.header.length > 0) {
380
+ for (const headerName of request.redact.header) {
381
+ redactParts.push(`header[${headerName}]`);
382
+ }
383
+ }
384
+ }
385
+ if (redactParts.length > 0) {
386
+ headers.set("Upstash-Redact-Fields", redactParts.join(","));
387
+ }
388
+ }
362
389
  return headers;
363
390
  }
364
391
  function getRequestPath(request) {
@@ -398,38 +425,39 @@ function decodeBase64(base64) {
398
425
  }
399
426
  }
400
427
  }
401
- function buildBulkActionFilterPayload(request, options) {
402
- const hasDlqIds = "dlqIds" in request && request.dlqIds !== void 0;
403
- const hasAll = "all" in request && Boolean(request.all);
404
- const filterKeys = Object.keys(request).filter((k) => k !== "dlqIds" && k !== "all");
405
- const hasFilters = filterKeys.length > 0;
406
- if (hasDlqIds && hasAll) {
407
- throw new QstashError("dlqIds and all: true are mutually exclusive.");
408
- }
409
- if (hasDlqIds && hasFilters) {
410
- throw new QstashError(
411
- `dlqIds cannot be combined with filter fields: ${filterKeys.join(", ")}.`
412
- );
428
+ function buildBulkActionFilterPayload(request) {
429
+ const cursor = "cursor" in request ? request.cursor : void 0;
430
+ if ("all" in request) {
431
+ const count2 = "count" in request ? request.count ?? DEFAULT_BULK_COUNT : DEFAULT_BULK_COUNT;
432
+ return { count: count2, cursor };
433
+ }
434
+ if ("dlqIds" in request) {
435
+ const ids = request.dlqIds;
436
+ if (Array.isArray(ids) && ids.length === 0) {
437
+ throw new QstashError(
438
+ "Empty dlqIds array provided. If you intend to target all DLQ messages, use { all: true } explicitly."
439
+ );
440
+ }
441
+ return { dlqIds: ids, cursor };
413
442
  }
414
- if (hasAll && hasFilters) {
415
- throw new QstashError(
416
- `all: true cannot be combined with filter fields: ${filterKeys.join(", ")}.`
417
- );
443
+ if ("messageIds" in request && request.messageIds) {
444
+ if (request.messageIds.length === 0) {
445
+ throw new QstashError(
446
+ "Empty messageIds array provided. If you intend to target all messages, use { all: true } explicitly."
447
+ );
448
+ }
449
+ return { messageIds: request.messageIds, cursor };
418
450
  }
419
- if (hasAll)
420
- return {};
421
- const { urlGroup, callerIp, ...rest } = request;
422
- const payload = {
423
- ...rest,
424
- ...urlGroup === void 0 || typeof urlGroup !== "string" ? {} : { topicName: urlGroup },
425
- ...callerIp === void 0 || typeof callerIp !== "string" ? {} : options?.callerIpCasing ? { callerIP: callerIp } : { callerIp }
451
+ const count = "count" in request ? request.count ?? DEFAULT_BULK_COUNT : DEFAULT_BULK_COUNT;
452
+ return {
453
+ ...renameUrlGroup(request.filter),
454
+ count,
455
+ cursor
426
456
  };
427
- if (Object.keys(payload).length === 0) {
428
- throw new QstashError(
429
- "No filters provided. Pass { all: true } to explicitly target all messages."
430
- );
431
- }
432
- return payload;
457
+ }
458
+ function renameUrlGroup(filter) {
459
+ const { urlGroup, api, ...rest } = filter;
460
+ return { ...rest, ...urlGroup === void 0 ? {} : { topicName: urlGroup } };
433
461
  }
434
462
  function normalizeCursor(response) {
435
463
  const cursor = response.cursor;
@@ -663,23 +691,21 @@ var DLQ = class {
663
691
  }
664
692
  /**
665
693
  * List messages in the dlq
694
+ *
695
+ * Can be called with:
696
+ * - Filters: `listMessages({ filter: { url: "https://example.com" } })`
697
+ * - DLQ IDs: `listMessages({ dlqIds: ["id1", "id2"] })`
698
+ * - No filter (list all): `listMessages()`
666
699
  */
667
700
  async listMessages(options = {}) {
668
- const { urlGroup, ...restFilter } = options.filter ?? {};
669
- const filterPayload = {
670
- ...restFilter,
671
- ...urlGroup === void 0 ? {} : { topicName: urlGroup }
701
+ const query = {
702
+ count: options.count,
703
+ ..."dlqIds" in options ? { dlqIds: options.dlqIds } : { ...renameUrlGroup(options.filter ?? {}), cursor: options.cursor }
672
704
  };
673
705
  const messagesPayload = await this.http.request({
674
706
  method: "GET",
675
707
  path: ["v2", "dlq"],
676
- query: {
677
- cursor: options.cursor,
678
- count: options.count,
679
- order: options.order,
680
- trimBody: options.trimBody,
681
- ...filterPayload
682
- }
708
+ query
683
709
  });
684
710
  return {
685
711
  messages: messagesPayload.messages.map((message) => {
@@ -699,10 +725,19 @@ var DLQ = class {
699
725
  * - A single dlqId: `delete("id")`
700
726
  * - An array of dlqIds: `delete(["id1", "id2"])`
701
727
  * - An object with dlqIds: `delete({ dlqIds: ["id1", "id2"] })`
702
- * - A filter object: `delete({ url: "https://example.com", label: "label" })`
728
+ * - A filter object: `delete({ filter: { url: "https://example.com", label: "label" } })`
703
729
  * - All messages: `delete({ all: true })`
704
730
  *
705
- * Note: passing an empty array returns `{ deleted: 0 }` without making a request.
731
+ * Pass `count` to limit the number of messages processed per call (defaults to 100).
732
+ * Call in a loop until cursor is undefined:
733
+ *
734
+ * ```ts
735
+ * let cursor: string | undefined;
736
+ * do {
737
+ * const result = await dlq.delete({ all: true, count: 100, cursor });
738
+ * cursor = result.cursor;
739
+ * } while (cursor);
740
+ * ```
706
741
  */
707
742
  async delete(request) {
708
743
  if (typeof request === "string") {
@@ -716,13 +751,11 @@ var DLQ = class {
716
751
  if (Array.isArray(request) && request.length === 0)
717
752
  return { deleted: 0 };
718
753
  const filters = Array.isArray(request) ? { dlqIds: request } : request;
719
- return normalizeCursor(
720
- await this.http.request({
721
- method: "DELETE",
722
- path: ["v2", "dlq"],
723
- query: buildBulkActionFilterPayload(filters)
724
- })
725
- );
754
+ return await this.http.request({
755
+ method: "DELETE",
756
+ path: ["v2", "dlq"],
757
+ query: buildBulkActionFilterPayload(filters)
758
+ });
726
759
  }
727
760
  /**
728
761
  * Remove multiple messages from the dlq using their `dlqId`s
@@ -739,10 +772,19 @@ var DLQ = class {
739
772
  * - A single dlqId: `retry("id")`
740
773
  * - An array of dlqIds: `retry(["id1", "id2"])`
741
774
  * - An object with dlqIds: `retry({ dlqIds: ["id1", "id2"] })`
742
- * - A filter object: `retry({ url: "https://example.com", label: "label" })`
775
+ * - A filter object: `retry({ filter: { url: "https://example.com", label: "label" } })`
743
776
  * - All messages: `retry({ all: true })`
744
777
  *
745
- * Note: passing an empty array returns `{ responses: [] }` without making a request.
778
+ * Pass `count` to limit the number of messages processed per call (defaults to 100).
779
+ * Call in a loop until cursor is undefined:
780
+ *
781
+ * ```ts
782
+ * let cursor: string | undefined;
783
+ * do {
784
+ * const result = await dlq.retry({ all: true, count: 100, cursor });
785
+ * cursor = result.cursor;
786
+ * } while (cursor);
787
+ * ```
746
788
  */
747
789
  async retry(request) {
748
790
  if (typeof request === "string")
@@ -760,6 +802,107 @@ var DLQ = class {
760
802
  }
761
803
  };
762
804
 
805
+ // src/client/flow-control.ts
806
+ var FlowControlApi = class {
807
+ http;
808
+ constructor(http) {
809
+ this.http = http;
810
+ }
811
+ /**
812
+ * Get a single flow control by key.
813
+ */
814
+ async get(flowControlKey) {
815
+ return await this.http.request({
816
+ method: "GET",
817
+ path: ["v2", "flowControl", flowControlKey]
818
+ });
819
+ }
820
+ /**
821
+ * Get the global parallelism info.
822
+ */
823
+ async getGlobalParallelism() {
824
+ const response = await this.http.request({
825
+ method: "GET",
826
+ path: ["v2", "globalParallelism"]
827
+ });
828
+ return {
829
+ parallelismMax: response.parallelismMax ?? 0,
830
+ parallelismCount: response.parallelismCount ?? 0
831
+ };
832
+ }
833
+ /**
834
+ * Pause message delivery for a flow-control key.
835
+ *
836
+ * Messages already in the waitlist will remain there.
837
+ * New incoming messages will be added directly to the waitlist.
838
+ */
839
+ async pause(flowControlKey) {
840
+ await this.http.request({
841
+ method: "POST",
842
+ path: ["v2", "flowControl", flowControlKey, "pause"],
843
+ parseResponseAsJson: false
844
+ });
845
+ }
846
+ /**
847
+ * Resume message delivery for a flow-control key.
848
+ */
849
+ async resume(flowControlKey) {
850
+ await this.http.request({
851
+ method: "POST",
852
+ path: ["v2", "flowControl", flowControlKey, "resume"],
853
+ parseResponseAsJson: false
854
+ });
855
+ }
856
+ /**
857
+ * Pin a processing configuration for a flow-control key.
858
+ *
859
+ * While pinned, the system ignores configurations provided by incoming
860
+ * messages and uses the pinned configuration instead.
861
+ */
862
+ async pin(flowControlKey, options) {
863
+ await this.http.request({
864
+ method: "POST",
865
+ path: ["v2", "flowControl", flowControlKey, "pin"],
866
+ query: {
867
+ parallelism: options.parallelism,
868
+ rate: options.rate,
869
+ period: options.period
870
+ },
871
+ parseResponseAsJson: false
872
+ });
873
+ }
874
+ /**
875
+ * Remove the pinned configuration for a flow-control key.
876
+ *
877
+ * After unpinning, the system resumes updating the configuration
878
+ * based on incoming messages.
879
+ */
880
+ async unpin(flowControlKey, options) {
881
+ await this.http.request({
882
+ method: "POST",
883
+ path: ["v2", "flowControl", flowControlKey, "unpin"],
884
+ query: {
885
+ parallelism: options.parallelism,
886
+ rate: options.rate
887
+ },
888
+ parseResponseAsJson: false
889
+ });
890
+ }
891
+ /**
892
+ * Reset the rate configuration state for a flow-control key.
893
+ *
894
+ * Clears the current rate count and immediately ends the current period.
895
+ * The current timestamp becomes the start of the new rate period.
896
+ */
897
+ async resetRate(flowControlKey) {
898
+ await this.http.request({
899
+ method: "POST",
900
+ path: ["v2", "flowControl", flowControlKey, "resetRate"],
901
+ parseResponseAsJson: false
902
+ });
903
+ }
904
+ };
905
+
763
906
  // src/client/http.ts
764
907
  var HttpClient = class {
765
908
  baseUrl;
@@ -860,6 +1003,9 @@ var HttpClient = class {
860
1003
  if (value === void 0)
861
1004
  continue;
862
1005
  if (Array.isArray(value)) {
1006
+ if (value.length === 0) {
1007
+ throw new QstashEmptyArrayError(key);
1008
+ }
863
1009
  for (const item of value) {
864
1010
  url.searchParams.append(key, item);
865
1011
  }
@@ -1101,8 +1247,19 @@ var Messages = class {
1101
1247
  * Can be called with:
1102
1248
  * - A single messageId: `cancel("id")`
1103
1249
  * - An array of messageIds: `cancel(["id1", "id2"])`
1104
- * - A filter object: `cancel({ flowControlKey: "key", label: "label" })`
1250
+ * - A filter object: `cancel({ filter: { flowControlKey: "key", label: "label" } })`
1105
1251
  * - All messages: `cancel({ all: true })`
1252
+ *
1253
+ * Pass `count` to limit the number of messages processed per call (defaults to 100).
1254
+ * Call in a loop until `cancelled` is 0:
1255
+ *
1256
+ * ```ts
1257
+ * let cancelled: number;
1258
+ * do {
1259
+ * const result = await messages.cancel({ all: true, count: 100 });
1260
+ * cancelled = result.cancelled;
1261
+ * } while (cancelled > 0);
1262
+ * ```
1106
1263
  */
1107
1264
  async cancel(request) {
1108
1265
  if (typeof request === "string") {
@@ -1117,7 +1274,7 @@ var Messages = class {
1117
1274
  return await this.http.request({
1118
1275
  method: "DELETE",
1119
1276
  path: ["v2", "messages"],
1120
- query: buildBulkActionFilterPayload(filters, { callerIpCasing: true })
1277
+ query: buildBulkActionFilterPayload(filters)
1121
1278
  });
1122
1279
  }
1123
1280
  /**
@@ -1351,6 +1508,24 @@ var Schedules = class {
1351
1508
  if (request.label !== void 0) {
1352
1509
  headers.set("Upstash-Label", request.label);
1353
1510
  }
1511
+ if (request.redact !== void 0) {
1512
+ const redactParts = [];
1513
+ if (request.redact.body) {
1514
+ redactParts.push("body");
1515
+ }
1516
+ if (request.redact.header !== void 0) {
1517
+ if (request.redact.header === true) {
1518
+ redactParts.push("header");
1519
+ } else if (Array.isArray(request.redact.header) && request.redact.header.length > 0) {
1520
+ for (const headerName of request.redact.header) {
1521
+ redactParts.push(`header[${headerName}]`);
1522
+ }
1523
+ }
1524
+ }
1525
+ if (redactParts.length > 0) {
1526
+ headers.set("Upstash-Redact-Fields", redactParts.join(","));
1527
+ }
1528
+ }
1354
1529
  return await this.http.request({
1355
1530
  method: "POST",
1356
1531
  headers: wrapWithGlobalHeaders(headers, this.http.headers, this.http.telemetryHeaders),
@@ -1480,7 +1655,7 @@ var UrlGroups = class {
1480
1655
  };
1481
1656
 
1482
1657
  // version.ts
1483
- var VERSION = "v2.9.1-rc.1";
1658
+ var VERSION = "2.10.0";
1484
1659
 
1485
1660
  // src/client/client.ts
1486
1661
  var Client = class {
@@ -1551,6 +1726,14 @@ var Client = class {
1551
1726
  get schedules() {
1552
1727
  return new Schedules(this.http);
1553
1728
  }
1729
+ /**
1730
+ * Access the flow control API.
1731
+ *
1732
+ * List, get, or reset flow controls.
1733
+ */
1734
+ get flowControl() {
1735
+ return new FlowControlApi(this.http);
1736
+ }
1554
1737
  /**
1555
1738
  * Access the workflow API.
1556
1739
  *
@@ -1676,38 +1859,9 @@ var Client = class {
1676
1859
  * ```
1677
1860
  */
1678
1861
  async logs(request = {}) {
1679
- const {
1680
- urlGroup,
1681
- // eslint-disable-next-line @typescript-eslint/no-deprecated
1682
- topicName,
1683
- fromDate,
1684
- toDate,
1685
- callerIp,
1686
- messageIds,
1687
- // eslint-disable-next-line @typescript-eslint/no-deprecated
1688
- count: filterCount,
1689
- ...restFilter
1690
- } = request.filter ?? {};
1691
- const filterPayload = {
1692
- ...restFilter,
1693
- topicName: urlGroup ?? topicName,
1694
- fromDate,
1695
- toDate,
1696
- callerIp
1697
- };
1698
- let cursorString;
1699
- if (typeof request.cursor === "number" && request.cursor > 0) {
1700
- cursorString = request.cursor.toString();
1701
- } else if (typeof request.cursor === "string" && request.cursor !== "") {
1702
- cursorString = request.cursor;
1703
- }
1704
1862
  const query = {
1705
- cursor: cursorString,
1706
- count: request.count ?? filterCount,
1707
- order: request.order,
1708
- trimBody: request.trimBody,
1709
- messageIds,
1710
- ...filterPayload
1863
+ count: request.count,
1864
+ ..."messageIds" in request ? { messageIds: request.messageIds } : { ...renameUrlGroup(request.filter ?? {}), cursor: request.cursor }
1711
1865
  };
1712
1866
  const responsePayload = await this.http.request({
1713
1867
  path: ["v2", "events"],
package/workflow.mjs CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  WorkflowLogger,
7
7
  processOptions,
8
8
  serve
9
- } from "./chunk-RUCOF5QZ.mjs";
9
+ } from "./chunk-X3MMU3BQ.mjs";
10
10
  export {
11
11
  DisabledWorkflowContext,
12
12
  StepTypes,