@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/index.js CHANGED
@@ -32,11 +32,13 @@ var src_exports = {};
32
32
  __export(src_exports, {
33
33
  Chat: () => Chat,
34
34
  Client: () => Client,
35
+ FlowControlApi: () => FlowControlApi,
35
36
  Messages: () => Messages,
36
37
  QStashWorkflowAbort: () => QStashWorkflowAbort,
37
38
  QStashWorkflowError: () => QStashWorkflowError,
38
39
  QstashChatRatelimitError: () => QstashChatRatelimitError,
39
40
  QstashDailyRatelimitError: () => QstashDailyRatelimitError,
41
+ QstashEmptyArrayError: () => QstashEmptyArrayError,
40
42
  QstashError: () => QstashError,
41
43
  QstashRatelimitError: () => QstashRatelimitError,
42
44
  Receiver: () => Receiver,
@@ -274,6 +276,14 @@ var QstashDailyRatelimitError = class extends QstashError {
274
276
  this.reset = args.reset;
275
277
  }
276
278
  };
279
+ var QstashEmptyArrayError = class extends QstashError {
280
+ constructor(parameterName) {
281
+ super(
282
+ `Empty array provided for query parameter "${parameterName}". This would result in no filter being applied, which could affect all resources.`
283
+ );
284
+ this.name = "QstashEmptyArrayError";
285
+ }
286
+ };
277
287
  var QStashWorkflowError = class extends QstashError {
278
288
  constructor(message) {
279
289
  super(message);
@@ -303,6 +313,7 @@ var formatWorkflowError = (error) => {
303
313
  };
304
314
 
305
315
  // src/client/utils.ts
316
+ var DEFAULT_BULK_COUNT = 100;
306
317
  var isIgnoredHeader = (header) => {
307
318
  const lowerCaseHeader = header.toLowerCase();
308
319
  return lowerCaseHeader.startsWith("content-type") || lowerCaseHeader.startsWith("upstash-");
@@ -389,6 +400,24 @@ function processHeaders(request) {
389
400
  if (request.label !== void 0) {
390
401
  headers.set("Upstash-Label", request.label);
391
402
  }
403
+ if (request.redact !== void 0) {
404
+ const redactParts = [];
405
+ if (request.redact.body) {
406
+ redactParts.push("body");
407
+ }
408
+ if (request.redact.header !== void 0) {
409
+ if (request.redact.header === true) {
410
+ redactParts.push("header");
411
+ } else if (Array.isArray(request.redact.header) && request.redact.header.length > 0) {
412
+ for (const headerName of request.redact.header) {
413
+ redactParts.push(`header[${headerName}]`);
414
+ }
415
+ }
416
+ }
417
+ if (redactParts.length > 0) {
418
+ headers.set("Upstash-Redact-Fields", redactParts.join(","));
419
+ }
420
+ }
392
421
  return headers;
393
422
  }
394
423
  function getRequestPath(request) {
@@ -423,38 +452,39 @@ function decodeBase64(base64) {
423
452
  }
424
453
  }
425
454
  }
426
- function buildBulkActionFilterPayload(request, options) {
427
- const hasDlqIds = "dlqIds" in request && request.dlqIds !== void 0;
428
- const hasAll = "all" in request && Boolean(request.all);
429
- const filterKeys = Object.keys(request).filter((k) => k !== "dlqIds" && k !== "all");
430
- const hasFilters = filterKeys.length > 0;
431
- if (hasDlqIds && hasAll) {
432
- throw new QstashError("dlqIds and all: true are mutually exclusive.");
433
- }
434
- if (hasDlqIds && hasFilters) {
435
- throw new QstashError(
436
- `dlqIds cannot be combined with filter fields: ${filterKeys.join(", ")}.`
437
- );
455
+ function buildBulkActionFilterPayload(request) {
456
+ const cursor = "cursor" in request ? request.cursor : void 0;
457
+ if ("all" in request) {
458
+ const count2 = "count" in request ? request.count ?? DEFAULT_BULK_COUNT : DEFAULT_BULK_COUNT;
459
+ return { count: count2, cursor };
460
+ }
461
+ if ("dlqIds" in request) {
462
+ const ids = request.dlqIds;
463
+ if (Array.isArray(ids) && ids.length === 0) {
464
+ throw new QstashError(
465
+ "Empty dlqIds array provided. If you intend to target all DLQ messages, use { all: true } explicitly."
466
+ );
467
+ }
468
+ return { dlqIds: ids, cursor };
438
469
  }
439
- if (hasAll && hasFilters) {
440
- throw new QstashError(
441
- `all: true cannot be combined with filter fields: ${filterKeys.join(", ")}.`
442
- );
470
+ if ("messageIds" in request && request.messageIds) {
471
+ if (request.messageIds.length === 0) {
472
+ throw new QstashError(
473
+ "Empty messageIds array provided. If you intend to target all messages, use { all: true } explicitly."
474
+ );
475
+ }
476
+ return { messageIds: request.messageIds, cursor };
443
477
  }
444
- if (hasAll)
445
- return {};
446
- const { urlGroup, callerIp, ...rest } = request;
447
- const payload = {
448
- ...rest,
449
- ...urlGroup === void 0 || typeof urlGroup !== "string" ? {} : { topicName: urlGroup },
450
- ...callerIp === void 0 || typeof callerIp !== "string" ? {} : options?.callerIpCasing ? { callerIP: callerIp } : { callerIp }
478
+ const count = "count" in request ? request.count ?? DEFAULT_BULK_COUNT : DEFAULT_BULK_COUNT;
479
+ return {
480
+ ...renameUrlGroup(request.filter),
481
+ count,
482
+ cursor
451
483
  };
452
- if (Object.keys(payload).length === 0) {
453
- throw new QstashError(
454
- "No filters provided. Pass { all: true } to explicitly target all messages."
455
- );
456
- }
457
- return payload;
484
+ }
485
+ function renameUrlGroup(filter) {
486
+ const { urlGroup, api, ...rest } = filter;
487
+ return { ...rest, ...urlGroup === void 0 ? {} : { topicName: urlGroup } };
458
488
  }
459
489
  function normalizeCursor(response) {
460
490
  const cursor = response.cursor;
@@ -688,23 +718,21 @@ var DLQ = class {
688
718
  }
689
719
  /**
690
720
  * List messages in the dlq
721
+ *
722
+ * Can be called with:
723
+ * - Filters: `listMessages({ filter: { url: "https://example.com" } })`
724
+ * - DLQ IDs: `listMessages({ dlqIds: ["id1", "id2"] })`
725
+ * - No filter (list all): `listMessages()`
691
726
  */
692
727
  async listMessages(options = {}) {
693
- const { urlGroup, ...restFilter } = options.filter ?? {};
694
- const filterPayload = {
695
- ...restFilter,
696
- ...urlGroup === void 0 ? {} : { topicName: urlGroup }
728
+ const query = {
729
+ count: options.count,
730
+ ..."dlqIds" in options ? { dlqIds: options.dlqIds } : { ...renameUrlGroup(options.filter ?? {}), cursor: options.cursor }
697
731
  };
698
732
  const messagesPayload = await this.http.request({
699
733
  method: "GET",
700
734
  path: ["v2", "dlq"],
701
- query: {
702
- cursor: options.cursor,
703
- count: options.count,
704
- order: options.order,
705
- trimBody: options.trimBody,
706
- ...filterPayload
707
- }
735
+ query
708
736
  });
709
737
  return {
710
738
  messages: messagesPayload.messages.map((message) => {
@@ -724,10 +752,19 @@ var DLQ = class {
724
752
  * - A single dlqId: `delete("id")`
725
753
  * - An array of dlqIds: `delete(["id1", "id2"])`
726
754
  * - An object with dlqIds: `delete({ dlqIds: ["id1", "id2"] })`
727
- * - A filter object: `delete({ url: "https://example.com", label: "label" })`
755
+ * - A filter object: `delete({ filter: { url: "https://example.com", label: "label" } })`
728
756
  * - All messages: `delete({ all: true })`
729
757
  *
730
- * Note: passing an empty array returns `{ deleted: 0 }` without making a request.
758
+ * Pass `count` to limit the number of messages processed per call (defaults to 100).
759
+ * Call in a loop until cursor is undefined:
760
+ *
761
+ * ```ts
762
+ * let cursor: string | undefined;
763
+ * do {
764
+ * const result = await dlq.delete({ all: true, count: 100, cursor });
765
+ * cursor = result.cursor;
766
+ * } while (cursor);
767
+ * ```
731
768
  */
732
769
  async delete(request) {
733
770
  if (typeof request === "string") {
@@ -741,13 +778,11 @@ var DLQ = class {
741
778
  if (Array.isArray(request) && request.length === 0)
742
779
  return { deleted: 0 };
743
780
  const filters = Array.isArray(request) ? { dlqIds: request } : request;
744
- return normalizeCursor(
745
- await this.http.request({
746
- method: "DELETE",
747
- path: ["v2", "dlq"],
748
- query: buildBulkActionFilterPayload(filters)
749
- })
750
- );
781
+ return await this.http.request({
782
+ method: "DELETE",
783
+ path: ["v2", "dlq"],
784
+ query: buildBulkActionFilterPayload(filters)
785
+ });
751
786
  }
752
787
  /**
753
788
  * Remove multiple messages from the dlq using their `dlqId`s
@@ -764,10 +799,19 @@ var DLQ = class {
764
799
  * - A single dlqId: `retry("id")`
765
800
  * - An array of dlqIds: `retry(["id1", "id2"])`
766
801
  * - An object with dlqIds: `retry({ dlqIds: ["id1", "id2"] })`
767
- * - A filter object: `retry({ url: "https://example.com", label: "label" })`
802
+ * - A filter object: `retry({ filter: { url: "https://example.com", label: "label" } })`
768
803
  * - All messages: `retry({ all: true })`
769
804
  *
770
- * Note: passing an empty array returns `{ responses: [] }` without making a request.
805
+ * Pass `count` to limit the number of messages processed per call (defaults to 100).
806
+ * Call in a loop until cursor is undefined:
807
+ *
808
+ * ```ts
809
+ * let cursor: string | undefined;
810
+ * do {
811
+ * const result = await dlq.retry({ all: true, count: 100, cursor });
812
+ * cursor = result.cursor;
813
+ * } while (cursor);
814
+ * ```
771
815
  */
772
816
  async retry(request) {
773
817
  if (typeof request === "string")
@@ -785,6 +829,107 @@ var DLQ = class {
785
829
  }
786
830
  };
787
831
 
832
+ // src/client/flow-control.ts
833
+ var FlowControlApi = class {
834
+ http;
835
+ constructor(http) {
836
+ this.http = http;
837
+ }
838
+ /**
839
+ * Get a single flow control by key.
840
+ */
841
+ async get(flowControlKey) {
842
+ return await this.http.request({
843
+ method: "GET",
844
+ path: ["v2", "flowControl", flowControlKey]
845
+ });
846
+ }
847
+ /**
848
+ * Get the global parallelism info.
849
+ */
850
+ async getGlobalParallelism() {
851
+ const response = await this.http.request({
852
+ method: "GET",
853
+ path: ["v2", "globalParallelism"]
854
+ });
855
+ return {
856
+ parallelismMax: response.parallelismMax ?? 0,
857
+ parallelismCount: response.parallelismCount ?? 0
858
+ };
859
+ }
860
+ /**
861
+ * Pause message delivery for a flow-control key.
862
+ *
863
+ * Messages already in the waitlist will remain there.
864
+ * New incoming messages will be added directly to the waitlist.
865
+ */
866
+ async pause(flowControlKey) {
867
+ await this.http.request({
868
+ method: "POST",
869
+ path: ["v2", "flowControl", flowControlKey, "pause"],
870
+ parseResponseAsJson: false
871
+ });
872
+ }
873
+ /**
874
+ * Resume message delivery for a flow-control key.
875
+ */
876
+ async resume(flowControlKey) {
877
+ await this.http.request({
878
+ method: "POST",
879
+ path: ["v2", "flowControl", flowControlKey, "resume"],
880
+ parseResponseAsJson: false
881
+ });
882
+ }
883
+ /**
884
+ * Pin a processing configuration for a flow-control key.
885
+ *
886
+ * While pinned, the system ignores configurations provided by incoming
887
+ * messages and uses the pinned configuration instead.
888
+ */
889
+ async pin(flowControlKey, options) {
890
+ await this.http.request({
891
+ method: "POST",
892
+ path: ["v2", "flowControl", flowControlKey, "pin"],
893
+ query: {
894
+ parallelism: options.parallelism,
895
+ rate: options.rate,
896
+ period: options.period
897
+ },
898
+ parseResponseAsJson: false
899
+ });
900
+ }
901
+ /**
902
+ * Remove the pinned configuration for a flow-control key.
903
+ *
904
+ * After unpinning, the system resumes updating the configuration
905
+ * based on incoming messages.
906
+ */
907
+ async unpin(flowControlKey, options) {
908
+ await this.http.request({
909
+ method: "POST",
910
+ path: ["v2", "flowControl", flowControlKey, "unpin"],
911
+ query: {
912
+ parallelism: options.parallelism,
913
+ rate: options.rate
914
+ },
915
+ parseResponseAsJson: false
916
+ });
917
+ }
918
+ /**
919
+ * Reset the rate configuration state for a flow-control key.
920
+ *
921
+ * Clears the current rate count and immediately ends the current period.
922
+ * The current timestamp becomes the start of the new rate period.
923
+ */
924
+ async resetRate(flowControlKey) {
925
+ await this.http.request({
926
+ method: "POST",
927
+ path: ["v2", "flowControl", flowControlKey, "resetRate"],
928
+ parseResponseAsJson: false
929
+ });
930
+ }
931
+ };
932
+
788
933
  // src/client/http.ts
789
934
  var HttpClient = class {
790
935
  baseUrl;
@@ -885,6 +1030,9 @@ var HttpClient = class {
885
1030
  if (value === void 0)
886
1031
  continue;
887
1032
  if (Array.isArray(value)) {
1033
+ if (value.length === 0) {
1034
+ throw new QstashEmptyArrayError(key);
1035
+ }
888
1036
  for (const item of value) {
889
1037
  url.searchParams.append(key, item);
890
1038
  }
@@ -1126,8 +1274,19 @@ var Messages = class {
1126
1274
  * Can be called with:
1127
1275
  * - A single messageId: `cancel("id")`
1128
1276
  * - An array of messageIds: `cancel(["id1", "id2"])`
1129
- * - A filter object: `cancel({ flowControlKey: "key", label: "label" })`
1277
+ * - A filter object: `cancel({ filter: { flowControlKey: "key", label: "label" } })`
1130
1278
  * - All messages: `cancel({ all: true })`
1279
+ *
1280
+ * Pass `count` to limit the number of messages processed per call (defaults to 100).
1281
+ * Call in a loop until `cancelled` is 0:
1282
+ *
1283
+ * ```ts
1284
+ * let cancelled: number;
1285
+ * do {
1286
+ * const result = await messages.cancel({ all: true, count: 100 });
1287
+ * cancelled = result.cancelled;
1288
+ * } while (cancelled > 0);
1289
+ * ```
1131
1290
  */
1132
1291
  async cancel(request) {
1133
1292
  if (typeof request === "string") {
@@ -1142,7 +1301,7 @@ var Messages = class {
1142
1301
  return await this.http.request({
1143
1302
  method: "DELETE",
1144
1303
  path: ["v2", "messages"],
1145
- query: buildBulkActionFilterPayload(filters, { callerIpCasing: true })
1304
+ query: buildBulkActionFilterPayload(filters)
1146
1305
  });
1147
1306
  }
1148
1307
  /**
@@ -1376,6 +1535,24 @@ var Schedules = class {
1376
1535
  if (request.label !== void 0) {
1377
1536
  headers.set("Upstash-Label", request.label);
1378
1537
  }
1538
+ if (request.redact !== void 0) {
1539
+ const redactParts = [];
1540
+ if (request.redact.body) {
1541
+ redactParts.push("body");
1542
+ }
1543
+ if (request.redact.header !== void 0) {
1544
+ if (request.redact.header === true) {
1545
+ redactParts.push("header");
1546
+ } else if (Array.isArray(request.redact.header) && request.redact.header.length > 0) {
1547
+ for (const headerName of request.redact.header) {
1548
+ redactParts.push(`header[${headerName}]`);
1549
+ }
1550
+ }
1551
+ }
1552
+ if (redactParts.length > 0) {
1553
+ headers.set("Upstash-Redact-Fields", redactParts.join(","));
1554
+ }
1555
+ }
1379
1556
  return await this.http.request({
1380
1557
  method: "POST",
1381
1558
  headers: wrapWithGlobalHeaders(headers, this.http.headers, this.http.telemetryHeaders),
@@ -1536,7 +1713,7 @@ var Workflow = class {
1536
1713
  };
1537
1714
 
1538
1715
  // version.ts
1539
- var VERSION = "v2.9.1-rc.1";
1716
+ var VERSION = "2.10.0";
1540
1717
 
1541
1718
  // src/client/client.ts
1542
1719
  var Client = class {
@@ -1607,6 +1784,14 @@ var Client = class {
1607
1784
  get schedules() {
1608
1785
  return new Schedules(this.http);
1609
1786
  }
1787
+ /**
1788
+ * Access the flow control API.
1789
+ *
1790
+ * List, get, or reset flow controls.
1791
+ */
1792
+ get flowControl() {
1793
+ return new FlowControlApi(this.http);
1794
+ }
1610
1795
  /**
1611
1796
  * Access the workflow API.
1612
1797
  *
@@ -1732,38 +1917,9 @@ var Client = class {
1732
1917
  * ```
1733
1918
  */
1734
1919
  async logs(request = {}) {
1735
- const {
1736
- urlGroup,
1737
- // eslint-disable-next-line @typescript-eslint/no-deprecated
1738
- topicName,
1739
- fromDate,
1740
- toDate,
1741
- callerIp,
1742
- messageIds,
1743
- // eslint-disable-next-line @typescript-eslint/no-deprecated
1744
- count: filterCount,
1745
- ...restFilter
1746
- } = request.filter ?? {};
1747
- const filterPayload = {
1748
- ...restFilter,
1749
- topicName: urlGroup ?? topicName,
1750
- fromDate,
1751
- toDate,
1752
- callerIp
1753
- };
1754
- let cursorString;
1755
- if (typeof request.cursor === "number" && request.cursor > 0) {
1756
- cursorString = request.cursor.toString();
1757
- } else if (typeof request.cursor === "string" && request.cursor !== "") {
1758
- cursorString = request.cursor;
1759
- }
1760
1920
  const query = {
1761
- cursor: cursorString,
1762
- count: request.count ?? filterCount,
1763
- order: request.order,
1764
- trimBody: request.trimBody,
1765
- messageIds,
1766
- ...filterPayload
1921
+ count: request.count,
1922
+ ..."messageIds" in request ? { messageIds: request.messageIds } : { ...renameUrlGroup(request.filter ?? {}), cursor: request.cursor }
1767
1923
  };
1768
1924
  const responsePayload = await this.http.request({
1769
1925
  path: ["v2", "events"],
@@ -1830,11 +1986,13 @@ var resend = ({
1830
1986
  0 && (module.exports = {
1831
1987
  Chat,
1832
1988
  Client,
1989
+ FlowControlApi,
1833
1990
  Messages,
1834
1991
  QStashWorkflowAbort,
1835
1992
  QStashWorkflowError,
1836
1993
  QstashChatRatelimitError,
1837
1994
  QstashDailyRatelimitError,
1995
+ QstashEmptyArrayError,
1838
1996
  QstashError,
1839
1997
  QstashRatelimitError,
1840
1998
  Receiver,
package/index.mjs CHANGED
@@ -1,14 +1,16 @@
1
1
  import {
2
2
  resend
3
- } from "./chunk-SN6OPGRS.mjs";
3
+ } from "./chunk-FAMFGAMR.mjs";
4
4
  import {
5
5
  Chat,
6
6
  Client,
7
+ FlowControlApi,
7
8
  Messages,
8
9
  QStashWorkflowAbort,
9
10
  QStashWorkflowError,
10
11
  QstashChatRatelimitError,
11
12
  QstashDailyRatelimitError,
13
+ QstashEmptyArrayError,
12
14
  QstashError,
13
15
  QstashRatelimitError,
14
16
  Receiver,
@@ -22,15 +24,17 @@ import {
22
24
  openai,
23
25
  setupAnalytics,
24
26
  upstash
25
- } from "./chunk-RUCOF5QZ.mjs";
27
+ } from "./chunk-X3MMU3BQ.mjs";
26
28
  export {
27
29
  Chat,
28
30
  Client,
31
+ FlowControlApi,
29
32
  Messages,
30
33
  QStashWorkflowAbort,
31
34
  QStashWorkflowError,
32
35
  QstashChatRatelimitError,
33
36
  QstashDailyRatelimitError,
37
+ QstashEmptyArrayError,
34
38
  QstashError,
35
39
  QstashRatelimitError,
36
40
  Receiver,
package/nextjs.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { NextApiHandler } from 'next';
2
2
  import { NextRequest, NextFetchEvent } from 'next/server';
3
- import { aa as RouteFunction, ab as WorkflowServeOptions } from './client-Gv4WRTxB.mjs';
3
+ import { ae as RouteFunction, af as WorkflowServeOptions } from './client-CsM1dTnz.mjs';
4
4
  import 'neverthrow';
5
5
 
6
6
  type VerifySignatureConfig = {
package/nextjs.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { NextApiHandler } from 'next';
2
2
  import { NextRequest, NextFetchEvent } from 'next/server';
3
- import { aa as RouteFunction, ab as WorkflowServeOptions } from './client-Gv4WRTxB.js';
3
+ import { ae as RouteFunction, af as WorkflowServeOptions } from './client-CsM1dTnz.js';
4
4
  import 'neverthrow';
5
5
 
6
6
  type VerifySignatureConfig = {