@spooled/sdk 1.2.1 → 1.3.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/dist/index.js CHANGED
@@ -1,5 +1,3 @@
1
- import { createHmac } from 'crypto';
2
- import { Buffer } from 'buffer';
3
1
  import * as grpc2 from '@grpc/grpc-js';
4
2
  import * as protoLoader from '@grpc/proto-loader';
5
3
  import { existsSync } from 'fs';
@@ -26,10 +24,10 @@ var DEFAULT_CONFIG = {
26
24
  successThreshold: 3,
27
25
  timeout: 3e4
28
26
  },
29
- userAgent: "@spooled/sdk-nodejs/1.2.1",
27
+ userAgent: "@spooled/sdk-nodejs/1.3.0",
30
28
  autoRefreshToken: true
31
29
  };
32
- var SDK_VERSION = "1.2.1";
30
+ var SDK_VERSION = "1.3.0";
33
31
  var API_VERSION = "v1";
34
32
  var API_BASE_PATH = `/api/${API_VERSION}`;
35
33
  function normalizeCredential(value) {
@@ -86,9 +84,9 @@ function resolveConfig(options) {
86
84
  };
87
85
  }
88
86
  function validateConfig(config) {
89
- if (!config.apiKey && !config.accessToken) {
87
+ if (!config.apiKey && !config.accessToken && !config.adminKey) {
90
88
  throw new Error(
91
- "SpooledClient requires either apiKey or accessToken for authentication"
89
+ "SpooledClient requires either apiKey or accessToken for authentication (or adminKey for admin-only use)"
92
90
  );
93
91
  }
94
92
  if (config.apiKey && !config.apiKey.startsWith("sp_") && !config.apiKey.startsWith("sk_")) {
@@ -151,9 +149,15 @@ var SpooledError = class extends Error {
151
149
  Error.captureStackTrace(this, this.constructor);
152
150
  }
153
151
  }
154
- /** Whether this error is retryable */
152
+ /**
153
+ * Whether this error is retryable.
154
+ *
155
+ * Only 408 (Request Timeout) is retryable among the plain 4xx statuses: the
156
+ * server gave up waiting for the request, so it never ran. Subclasses
157
+ * override this (5xx, network, timeout, rate limit).
158
+ */
155
159
  isRetryable() {
156
- return false;
160
+ return this.statusCode === 408;
157
161
  }
158
162
  /** Convert to plain object for logging */
159
163
  toJSON() {
@@ -192,8 +196,8 @@ var ConflictError = class extends SpooledError {
192
196
  }
193
197
  };
194
198
  var ValidationError = class extends SpooledError {
195
- constructor(message = "Validation failed", code = "VALIDATION_ERROR", details, requestId) {
196
- super(message, 400, code, details, requestId);
199
+ constructor(message = "Validation failed", code = "VALIDATION_ERROR", details, requestId, statusCode = 400) {
200
+ super(message, statusCode, code, details, requestId);
197
201
  this.name = "ValidationError";
198
202
  }
199
203
  };
@@ -203,6 +207,7 @@ var PayloadTooLargeError = class extends SpooledError {
203
207
  this.name = "PayloadTooLargeError";
204
208
  }
205
209
  };
210
+ var QUOTA_MESSAGE = /\blimit reached\b/i;
206
211
  var RateLimitError = class extends SpooledError {
207
212
  /** Rate limit information */
208
213
  rateLimitInfo;
@@ -211,8 +216,27 @@ var RateLimitError = class extends SpooledError {
211
216
  this.name = "RateLimitError";
212
217
  this.rateLimitInfo = rateLimitInfo;
213
218
  }
219
+ /**
220
+ * True when this 429 is a plan quota or limit (daily job cap, active jobs,
221
+ * queue/worker/webhook caps) rather than per-second rate limiting.
222
+ *
223
+ * A quota does not clear for minutes or hours, so retrying it only blocks the
224
+ * caller for the whole backoff and, via the circuit breaker, takes every
225
+ * other call on the client down with it. It is never retried and never
226
+ * counted as a circuit breaker failure.
227
+ *
228
+ * Detection: the `QUOTA_EXCEEDED` code, or, for older plan-limit paths that
229
+ * answer with plain text wrapped as `RATE_LIMIT_EXCEEDED`, a "... limit
230
+ * reached" message that carries no `Retry-After`.
231
+ */
232
+ get isQuota() {
233
+ if (this.code === "QUOTA_EXCEEDED") {
234
+ return true;
235
+ }
236
+ return this.rateLimitInfo.retryAfter === void 0 && QUOTA_MESSAGE.test(this.message);
237
+ }
214
238
  isRetryable() {
215
- return true;
239
+ return !this.isQuota;
216
240
  }
217
241
  /** Get the number of seconds to wait before retrying */
218
242
  getRetryAfter() {
@@ -272,6 +296,20 @@ var CircuitBreakerOpenError = class extends SpooledError {
272
296
  return false;
273
297
  }
274
298
  };
299
+ var UnsupportedOperationError = class extends SpooledError {
300
+ /** The SDK method that is not supported, e.g. `ingest.github` */
301
+ operation;
302
+ constructor(operation, reason) {
303
+ super(
304
+ `${operation} is not supported by the Spooled API: ${reason}`,
305
+ 0,
306
+ "UNSUPPORTED_OPERATION",
307
+ { operation }
308
+ );
309
+ this.name = "UnsupportedOperationError";
310
+ this.operation = operation;
311
+ }
312
+ };
275
313
  function parseRateLimitHeaders(headers) {
276
314
  const info = {};
277
315
  const retryAfter = headers.get("Retry-After");
@@ -343,6 +381,10 @@ async function createErrorFromResponse(response) {
343
381
  switch (response.status) {
344
382
  case 400:
345
383
  return new ValidationError(message, code, details, requestId);
384
+ // A missing or mistyped JSON field is a 422 from the API's extractor; it is
385
+ // the same class of caller error as a 400 field-rule failure.
386
+ case 422:
387
+ return new ValidationError(message, code, details, requestId, 422);
346
388
  case 401:
347
389
  return new AuthenticationError(message, code, details, requestId);
348
390
  case 403:
@@ -571,8 +613,12 @@ var HttpClient = class {
571
613
  authToken;
572
614
  /** Optional function to mint a fresh access token (set when auto-refresh is enabled) */
573
615
  refreshTokenFn = null;
574
- /** Guard to prevent a refresh request from recursively triggering another refresh */
575
- isRefreshing = false;
616
+ /**
617
+ * The refresh currently in flight, shared by every request that gets a 401
618
+ * meanwhile. A boolean guard here made the second concurrent 401 skip the
619
+ * refresh and fail even though the token was about to be replaced.
620
+ */
621
+ refreshInFlight = null;
576
622
  constructor(config, circuitBreaker) {
577
623
  this.config = config;
578
624
  this.circuitBreaker = circuitBreaker;
@@ -594,6 +640,30 @@ var HttpClient = class {
594
640
  setRefreshTokenFn(fn) {
595
641
  this.refreshTokenFn = fn;
596
642
  }
643
+ /**
644
+ * Refresh the access token once for all callers.
645
+ *
646
+ * Resolves true when a new token is installed, false when the refresh failed
647
+ * (the caller then surfaces its original 401).
648
+ */
649
+ refreshAuthToken() {
650
+ if (!this.refreshInFlight) {
651
+ const refresh = this.refreshTokenFn;
652
+ this.refreshInFlight = (async () => {
653
+ try {
654
+ this.setAuthToken(await refresh());
655
+ return true;
656
+ } catch (refreshError) {
657
+ const message = refreshError instanceof Error ? refreshError.message : "unknown";
658
+ this.debug?.("Token refresh failed", { error: message });
659
+ return false;
660
+ } finally {
661
+ this.refreshInFlight = null;
662
+ }
663
+ })();
664
+ }
665
+ return this.refreshInFlight;
666
+ }
597
667
  /**
598
668
  * Build full URL with query parameters
599
669
  */
@@ -669,6 +739,7 @@ var HttpClient = class {
669
739
  */
670
740
  async executeRequest(url, options, hasRefreshed = false) {
671
741
  const method = options.method ?? "GET";
742
+ const tokenUsed = this.authToken;
672
743
  const headers = this.buildHeaders(options.headers);
673
744
  const timeout = options.timeout ?? this.config.timeout;
674
745
  const init = {
@@ -683,6 +754,9 @@ var HttpClient = class {
683
754
  init.body = JSON.stringify(body);
684
755
  }
685
756
  }
757
+ if (init.body === void 0) {
758
+ headers.delete("Content-Type");
759
+ }
686
760
  this.debug?.(`${method} ${url}`, { timeout, hasBody: !!options.body });
687
761
  let response;
688
762
  try {
@@ -704,20 +778,9 @@ var HttpClient = class {
704
778
  }
705
779
  this.debug?.(`Response: ${response.status}`, { url });
706
780
  if (!response.ok) {
707
- if (response.status === 401 && this.refreshTokenFn && !hasRefreshed && !this.isRefreshing) {
708
- let refreshed = false;
709
- this.isRefreshing = true;
710
- try {
711
- const newToken = await this.refreshTokenFn();
712
- this.setAuthToken(newToken);
713
- refreshed = true;
714
- } catch (refreshError) {
715
- const message = refreshError instanceof Error ? refreshError.message : "unknown";
716
- this.debug?.("Token refresh failed", { error: message });
717
- } finally {
718
- this.isRefreshing = false;
719
- }
720
- if (refreshed) {
781
+ if (response.status === 401 && this.refreshTokenFn && !hasRefreshed && !options.skipAuthRefresh) {
782
+ const alreadyRefreshed = this.authToken !== tokenUsed;
783
+ if (alreadyRefreshed || await this.refreshAuthToken()) {
721
784
  return this.executeRequest(url, options, true);
722
785
  }
723
786
  }
@@ -779,22 +842,26 @@ var HttpClient = class {
779
842
  /**
780
843
  * Resolve the retry configuration for a request.
781
844
  *
782
- * Idempotent methods (GET/PUT/DELETE) and any request carrying an
783
- * idempotency key retry normally. Non-idempotent methods (POST/PATCH)
784
- * without an idempotency key must NOT be retried on ambiguous failures
845
+ * Idempotent methods (GET/PUT/DELETE), any request carrying an idempotency
846
+ * key and any request marked `idempotent` retry normally. Non-idempotent
847
+ * methods (POST/PATCH) otherwise must NOT be retried on ambiguous failures
785
848
  * (network/timeout/5xx) — doing so risks duplicate jobs, double claims,
786
- * etc. They are still retried on 429, which is rejected before the
787
- * server processes the request and is therefore side-effect-free.
849
+ * etc. They are still retried on a rate-limit 429, which is rejected before
850
+ * the server processes the request and is therefore side-effect-free. A
851
+ * quota 429 is never retried: it does not clear for hours.
788
852
  */
789
853
  resolveRetryConfig(method, options) {
790
854
  const base = this.config.retry;
791
- if (IDEMPOTENT_METHODS.has(method) || this.hasIdempotencyKey(options)) {
855
+ if (options.idempotent || IDEMPOTENT_METHODS.has(method) || this.hasIdempotencyKey(options)) {
792
856
  return base;
793
857
  }
794
858
  return {
795
859
  ...base,
796
860
  retryOn: (error, attempt) => {
797
861
  if (error instanceof RateLimitError) {
862
+ if (error.isQuota) {
863
+ return false;
864
+ }
798
865
  return base.retryOn ? base.retryOn(error, attempt) : true;
799
866
  }
800
867
  return false;
@@ -1025,13 +1092,17 @@ var AuthResource = class {
1025
1092
  * Exchange API key for JWT tokens
1026
1093
  */
1027
1094
  async login(params) {
1028
- return this.http.post("/auth/login", params);
1095
+ return this.http.post("/auth/login", params, {
1096
+ skipAuthRefresh: true
1097
+ });
1029
1098
  }
1030
1099
  /**
1031
1100
  * Refresh access token using refresh token
1032
1101
  */
1033
1102
  async refresh(params) {
1034
- return this.http.post("/auth/refresh", params);
1103
+ return this.http.post("/auth/refresh", params, {
1104
+ skipAuthRefresh: true
1105
+ });
1035
1106
  }
1036
1107
  /**
1037
1108
  * Logout and revoke the refresh token server-side.
@@ -1102,6 +1173,21 @@ function mapValidate(raw) {
1102
1173
  };
1103
1174
  }
1104
1175
 
1176
+ // src/utils/failure.ts
1177
+ var MAX_FAILURE_MESSAGE_CHARS = 2048;
1178
+ var DEFAULT_FAILURE_MESSAGE = "Job failed";
1179
+ function coerceFailureMessage(message) {
1180
+ const text = typeof message === "string" ? message : message == null ? "" : `${message}`;
1181
+ if (text === "") {
1182
+ return DEFAULT_FAILURE_MESSAGE;
1183
+ }
1184
+ if (text.length <= MAX_FAILURE_MESSAGE_CHARS) {
1185
+ return text;
1186
+ }
1187
+ const codePoints = Array.from(text);
1188
+ return codePoints.length <= MAX_FAILURE_MESSAGE_CHARS ? text : codePoints.slice(0, MAX_FAILURE_MESSAGE_CHARS).join("");
1189
+ }
1190
+
1105
1191
  // src/resources/jobs.ts
1106
1192
  var JobsResource = class {
1107
1193
  constructor(http) {
@@ -1118,7 +1204,9 @@ var JobsResource = class {
1118
1204
  * Create a new job
1119
1205
  */
1120
1206
  async create(params) {
1121
- return this.http.post("/jobs", params);
1207
+ return this.http.post("/jobs", params, {
1208
+ idempotent: params.idempotencyKey ? true : void 0
1209
+ });
1122
1210
  }
1123
1211
  /**
1124
1212
  * Create a job and return the full job object
@@ -1202,26 +1290,43 @@ var JobsResource = class {
1202
1290
  }
1203
1291
  /**
1204
1292
  * Complete a job (worker ack)
1293
+ *
1294
+ * Lease-fenced and idempotent on the server, so a network error, timeout or
1295
+ * 5xx is retried: a lost ack would otherwise re-run a finished job.
1205
1296
  */
1206
1297
  async complete(id, params) {
1207
- return this.http.post(`/jobs/${id}/complete`, params);
1298
+ return this.http.post(
1299
+ `/jobs/${id}/complete`,
1300
+ params,
1301
+ { idempotent: true }
1302
+ );
1208
1303
  }
1209
1304
  /**
1210
1305
  * Fail a job (worker nack)
1306
+ *
1307
+ * `params.error` is made acceptable first: empty text becomes "Job failed"
1308
+ * and text over 2048 characters is truncated, because the API rejects both
1309
+ * with a 400 that would leave the job processing until its lease expires.
1310
+ * Retried like {@link JobsResource.complete}.
1211
1311
  */
1212
1312
  async fail(id, params) {
1213
1313
  return this.http.post(
1214
1314
  `/jobs/${id}/fail`,
1215
- params
1315
+ { ...params, error: coerceFailureMessage(params.error) },
1316
+ { idempotent: true }
1216
1317
  );
1217
1318
  }
1218
1319
  /**
1219
1320
  * Extend job lease (heartbeat)
1321
+ *
1322
+ * Retried like {@link JobsResource.complete}: with the default 30 s lease and
1323
+ * 15 s heartbeat, one lost heartbeat leaves no slack.
1220
1324
  */
1221
1325
  async heartbeat(id, params) {
1222
1326
  return this.http.post(
1223
1327
  `/jobs/${id}/heartbeat`,
1224
- params
1328
+ params,
1329
+ { idempotent: true }
1225
1330
  );
1226
1331
  }
1227
1332
  // DLQ operations (private implementations)
@@ -1317,9 +1422,17 @@ var QueuesResource = class {
1317
1422
  }
1318
1423
  /**
1319
1424
  * Delete a queue configuration
1320
- */
1321
- async delete(name) {
1322
- await this.http.delete(`/queues/${encodeURIComponent(name)}`);
1425
+ *
1426
+ * Without `deleteJobs` the API only deletes an explicit queue config of an
1427
+ * idle queue, and answers 409 otherwise: a queue that exists only because
1428
+ * jobs were posted to it has no config to delete, and a queue with pending
1429
+ * or processing jobs is not idle. Pass `{ deleteJobs: true }` to delete the
1430
+ * queue's jobs (and DLQ rows) along with it.
1431
+ */
1432
+ async delete(name, options) {
1433
+ await this.http.delete(`/queues/${encodeURIComponent(name)}`, {
1434
+ params: options?.deleteJobs ? { deleteJobs: true } : void 0
1435
+ });
1323
1436
  }
1324
1437
  };
1325
1438
 
@@ -1348,9 +1461,13 @@ var WorkersResource = class {
1348
1461
  }
1349
1462
  /**
1350
1463
  * Send worker heartbeat
1464
+ *
1465
+ * Idempotent on the server, so network errors, timeouts and 5xx are retried.
1351
1466
  */
1352
1467
  async heartbeat(id, params) {
1353
- await this.http.post(`/workers/${id}/heartbeat`, params);
1468
+ await this.http.post(`/workers/${id}/heartbeat`, params, {
1469
+ idempotent: true
1470
+ });
1354
1471
  }
1355
1472
  /**
1356
1473
  * Deregister a worker
@@ -1499,7 +1616,7 @@ var WorkflowsResource = class {
1499
1616
  const jobs = await this.listWorkflowJobs(workflowId);
1500
1617
  return jobs.map((job) => ({
1501
1618
  jobId: job.id,
1502
- key: job.key,
1619
+ ...job.key !== void 0 && { key: job.key },
1503
1620
  status: job.status
1504
1621
  }));
1505
1622
  }
@@ -1557,7 +1674,10 @@ function mapWorkflowDetailJobs(detail) {
1557
1674
  return {
1558
1675
  id,
1559
1676
  workflowId: String(job.workflowId ?? workflowId),
1560
- key: String(job.key ?? ""),
1677
+ // The API does not return the creation-time key (jobs store only a
1678
+ // workflow step), so there is nothing to put here unless a future
1679
+ // version sends one. Never fabricate "".
1680
+ ...typeof job.key === "string" && job.key !== "" && { key: job.key },
1561
1681
  queueName: String(job.queueName ?? job.queue ?? ""),
1562
1682
  status: job.status,
1563
1683
  payload: job.payload ?? {},
@@ -1769,19 +1889,18 @@ var OrganizationsResource = class {
1769
1889
  /**
1770
1890
  * Clear/delete the webhook token for the current organization.
1771
1891
  *
1772
- * POST /organizations/webhook-token/clear requires `{ confirm: true }`.
1773
- * An empty body 422s. The API currently rejects the operation and tells
1774
- * callers to use regenerateWebhookToken() instead.
1775
- *
1776
- * @example
1777
- * ```typescript
1778
- * await client.organizations.clearWebhookToken();
1779
- * ```
1892
+ * @deprecated The API refuses this operation on purpose (an org without a
1893
+ * webhook token would have an open public ingestion endpoint) and always
1894
+ * answers with an error. This method now throws
1895
+ * {@link UnsupportedOperationError} without a network call; rotate the token
1896
+ * with {@link OrganizationsResource.regenerateWebhookToken} instead. It will
1897
+ * be removed in the next major release.
1780
1898
  */
1781
1899
  async clearWebhookToken() {
1782
- await this.http.post("/organizations/webhook-token/clear", {
1783
- confirm: true
1784
- });
1900
+ throw new UnsupportedOperationError(
1901
+ "organizations.clearWebhookToken",
1902
+ "clearing the webhook token is not allowed; use organizations.regenerateWebhookToken() to rotate it"
1903
+ );
1785
1904
  }
1786
1905
  };
1787
1906
 
@@ -1989,24 +2108,8 @@ var AdminResource = class {
1989
2108
  });
1990
2109
  }
1991
2110
  };
1992
- function toBytes(body) {
1993
- if (typeof body === "string") {
1994
- return Buffer.from(body, "utf8");
1995
- }
1996
- if (body instanceof Uint8Array) {
1997
- return body;
1998
- }
1999
- return new Uint8Array(body);
2000
- }
2001
- function githubSignature(secret, body) {
2002
- const hex = createHmac("sha256", secret).update(body).digest("hex");
2003
- return `sha256=${hex}`;
2004
- }
2005
- function stripeSignature(secret, body, timestamp) {
2006
- const payload = `${timestamp}.${Buffer.from(body).toString("utf8")}`;
2007
- const hex = createHmac("sha256", secret).update(payload, "utf8").digest("hex");
2008
- return `t=${timestamp},v1=${hex}`;
2009
- }
2111
+
2112
+ // src/resources/webhook-ingestion.ts
2010
2113
  var WebhookIngestionResource = class {
2011
2114
  constructor(http) {
2012
2115
  this.http = http;
@@ -2014,66 +2117,32 @@ var WebhookIngestionResource = class {
2014
2117
  /**
2015
2118
  * Ingest a GitHub webhook.
2016
2119
  *
2017
- * POST /api/v1/webhooks/{org_id}/github
2018
- */
2019
- async github(orgId, body, options) {
2020
- const bytes = toBytes(body);
2021
- const signature = options.signature ?? (options.secret ? githubSignature(options.secret, bytes) : void 0);
2022
- if (!signature) {
2023
- throw new ValidationError(
2024
- "GitHub webhook signature is required (provide `signature` or `secret`)"
2025
- );
2026
- }
2027
- const headers = {
2028
- "Content-Type": "application/json",
2029
- "X-GitHub-Event": options.githubEvent,
2030
- "X-Hub-Signature-256": signature
2031
- };
2032
- if (options.webhookToken) {
2033
- headers["X-Webhook-Token"] = options.webhookToken;
2034
- }
2035
- if (options.forwardedProto) {
2036
- headers["X-Forwarded-Proto"] = options.forwardedProto;
2037
- }
2038
- await this.http.request(`/webhooks/${orgId}/github`, {
2039
- method: "POST",
2040
- rawBody: bytes,
2041
- headers,
2042
- skipRequestConversion: true,
2043
- skipResponseConversion: true
2044
- });
2120
+ * @deprecated The API has no `/webhooks/{org_id}/github` route (it always
2121
+ * answered 404). This method now throws {@link UnsupportedOperationError}
2122
+ * without a network call; point the GitHub webhook at `ingest.custom` /
2123
+ * `POST /api/v1/webhooks/{org_id}/custom` instead. It will be removed in the
2124
+ * next major release.
2125
+ */
2126
+ async github(_orgId, _body, _options) {
2127
+ throw new UnsupportedOperationError(
2128
+ "ingest.github",
2129
+ "the API has no GitHub webhook route; use ingest.custom (POST /webhooks/{org_id}/custom)"
2130
+ );
2045
2131
  }
2046
2132
  /**
2047
2133
  * Ingest a Stripe webhook.
2048
2134
  *
2049
- * POST /api/v1/webhooks/{org_id}/stripe
2050
- */
2051
- async stripe(orgId, body, options = {}) {
2052
- const bytes = toBytes(body);
2053
- const timestamp = options.timestamp ?? Math.floor(Date.now() / 1e3);
2054
- const signature = options.signature ?? (options.secret ? stripeSignature(options.secret, bytes, timestamp) : void 0);
2055
- if (!signature) {
2056
- throw new ValidationError(
2057
- "Stripe webhook signature is required (provide `signature` or `secret`)"
2058
- );
2059
- }
2060
- const headers = {
2061
- "Content-Type": "application/json",
2062
- "Stripe-Signature": signature
2063
- };
2064
- if (options.webhookToken) {
2065
- headers["X-Webhook-Token"] = options.webhookToken;
2066
- }
2067
- if (options.forwardedProto) {
2068
- headers["X-Forwarded-Proto"] = options.forwardedProto;
2069
- }
2070
- await this.http.request(`/webhooks/${orgId}/stripe`, {
2071
- method: "POST",
2072
- rawBody: bytes,
2073
- headers,
2074
- skipRequestConversion: true,
2075
- skipResponseConversion: true
2076
- });
2135
+ * @deprecated The API has no `/webhooks/{org_id}/stripe` route (it always
2136
+ * answered 404). This method now throws {@link UnsupportedOperationError}
2137
+ * without a network call; point the Stripe webhook at `ingest.custom` /
2138
+ * `POST /api/v1/webhooks/{org_id}/custom` instead. It will be removed in the
2139
+ * next major release.
2140
+ */
2141
+ async stripe(_orgId, _body, _options = {}) {
2142
+ throw new UnsupportedOperationError(
2143
+ "ingest.stripe",
2144
+ "the API has no Stripe webhook route; use ingest.custom (POST /webhooks/{org_id}/custom)"
2145
+ );
2077
2146
  }
2078
2147
  /**
2079
2148
  * Ingest a custom webhook.
@@ -2450,6 +2519,13 @@ var SSE_EVENT_NAMES = [
2450
2519
  "ping",
2451
2520
  "error"
2452
2521
  ];
2522
+ var TERMINAL_JOB_STATUSES = /* @__PURE__ */ new Set([
2523
+ "completed",
2524
+ "failed",
2525
+ "deadletter",
2526
+ "cancelled"
2527
+ ]);
2528
+ var STABLE_CONNECTION_MS = 3e4;
2453
2529
  var SseRealtimeClient = class {
2454
2530
  options;
2455
2531
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -2458,6 +2534,12 @@ var SseRealtimeClient = class {
2458
2534
  reconnectAttempts = 0;
2459
2535
  reconnectTimer = null;
2460
2536
  filter = null;
2537
+ /** When the current connection opened (for the stable-connection check). */
2538
+ connectedAt = null;
2539
+ /** The server finished this stream on purpose (terminal job); do not reconnect. */
2540
+ streamEnded = false;
2541
+ /** The server rejected our token; mint a fresh one for the next connect. */
2542
+ forceTokenRefresh = false;
2461
2543
  // Event handlers
2462
2544
  eventHandlers = /* @__PURE__ */ new Map();
2463
2545
  allEventsHandlers = /* @__PURE__ */ new Set();
@@ -2496,6 +2578,7 @@ var SseRealtimeClient = class {
2496
2578
  return;
2497
2579
  }
2498
2580
  this.filter = filter || null;
2581
+ this.streamEnded = false;
2499
2582
  this.setState("connecting");
2500
2583
  const sseUrl = this.buildSseUrl(filter);
2501
2584
  this.options.debug(`Connecting to SSE: ${sseUrl}`);
@@ -2515,7 +2598,9 @@ var SseRealtimeClient = class {
2515
2598
  }
2516
2599
  let token;
2517
2600
  try {
2518
- token = await this.options.tokenProvider(this.reconnectAttempts > 0);
2601
+ const force = this.forceTokenRefresh;
2602
+ this.forceTokenRefresh = false;
2603
+ token = await this.options.tokenProvider(force);
2519
2604
  } catch (error) {
2520
2605
  this.setState("disconnected");
2521
2606
  throw new Error(`Failed to acquire realtime token: ${error}`);
@@ -2535,7 +2620,7 @@ var SseRealtimeClient = class {
2535
2620
  this.eventSource.onopen = () => {
2536
2621
  this.options.debug("SSE connected");
2537
2622
  this.setState("connected");
2538
- this.reconnectAttempts = 0;
2623
+ this.connectedAt = Date.now();
2539
2624
  resolve();
2540
2625
  };
2541
2626
  this.eventSource.onmessage = (event) => {
@@ -2546,8 +2631,11 @@ var SseRealtimeClient = class {
2546
2631
  if (typeof event?.data === "string") this.handleMessage(event.data);
2547
2632
  });
2548
2633
  }
2549
- this.eventSource.onerror = () => {
2634
+ this.eventSource.onerror = (event) => {
2550
2635
  this.options.debug("SSE error");
2636
+ if (event?.code === 401 || event?.code === 403) {
2637
+ this.forceTokenRefresh = true;
2638
+ }
2551
2639
  if (this.state === "connecting") {
2552
2640
  this.setState("disconnected");
2553
2641
  if (this.eventSource) {
@@ -2579,6 +2667,8 @@ var SseRealtimeClient = class {
2579
2667
  }
2580
2668
  this.setState("disconnected");
2581
2669
  this.filter = null;
2670
+ this.connectedAt = null;
2671
+ this.streamEnded = false;
2582
2672
  }
2583
2673
  /**
2584
2674
  * Reconnect with a new filter (SSE requires reconnection to change subscriptions)
@@ -2643,6 +2733,9 @@ var SseRealtimeClient = class {
2643
2733
  parsed.type = eventType;
2644
2734
  const event = parsed;
2645
2735
  this.options.debug(`Received SSE event: ${eventType}`, event);
2736
+ if (this.filter?.jobId && isStreamEndingEvent(eventType, parsed.data)) {
2737
+ this.streamEnded = true;
2738
+ }
2646
2739
  const handlers = this.eventHandlers.get(eventType);
2647
2740
  if (handlers) {
2648
2741
  const eventData = event.data;
@@ -2670,6 +2763,15 @@ var SseRealtimeClient = class {
2670
2763
  this.eventSource.close();
2671
2764
  this.eventSource = null;
2672
2765
  }
2766
+ if (this.streamEnded) {
2767
+ this.connectedAt = null;
2768
+ this.setState("disconnected");
2769
+ return;
2770
+ }
2771
+ if (this.connectedAt !== null && Date.now() - this.connectedAt >= STABLE_CONNECTION_MS) {
2772
+ this.reconnectAttempts = 0;
2773
+ }
2774
+ this.connectedAt = null;
2673
2775
  if (this.options.autoReconnect && this.reconnectAttempts < this.options.maxReconnectAttempts) {
2674
2776
  this.scheduleReconnect();
2675
2777
  } else {
@@ -2701,6 +2803,19 @@ var SseRealtimeClient = class {
2701
2803
  }, delay);
2702
2804
  }
2703
2805
  };
2806
+ function isStreamEndingEvent(eventType, data) {
2807
+ if (eventType === "error" || eventType === "job.completed") {
2808
+ return true;
2809
+ }
2810
+ const record = data ?? {};
2811
+ if (eventType === "job.failed") {
2812
+ return record.willRetry === false;
2813
+ }
2814
+ if (eventType === "job.status_changed" || eventType === "job.status") {
2815
+ return typeof record.newStatus === "string" && TERMINAL_JOB_STATUSES.has(record.newStatus);
2816
+ }
2817
+ return false;
2818
+ }
2704
2819
 
2705
2820
  // src/realtime/index.ts
2706
2821
  var SpooledRealtime = class {
@@ -2887,6 +3002,11 @@ function clearProtoCache() {
2887
3002
  cachedDefinition = null;
2888
3003
  }
2889
3004
 
3005
+ // src/grpc/encode.ts
3006
+ function encodeFailRequest(params) {
3007
+ return { ...params, retry: params.retry ?? true };
3008
+ }
3009
+
2890
3010
  // src/grpc/streaming.ts
2891
3011
  async function* asyncIterableFromStream(stream, options) {
2892
3012
  const { signal, onConnected, onError, onEnd } = options ?? {};
@@ -2959,7 +3079,9 @@ function createProcessJobsStream(call, options) {
2959
3079
  }
2960
3080
  return {
2961
3081
  send(request) {
2962
- call.write(request);
3082
+ call.write(
3083
+ request.fail ? { ...request, fail: encodeFailRequest(request.fail) } : request
3084
+ );
2963
3085
  },
2964
3086
  receive() {
2965
3087
  return new AsyncIterableStream(call, { onError, onEnd });
@@ -3147,12 +3269,19 @@ function mapGrpcError(error, timeoutMs) {
3147
3269
  return new ServerError(message, 500, "GRPC_ERROR");
3148
3270
  }
3149
3271
  }
3272
+ function grpcHost(address) {
3273
+ if (address.startsWith("[")) {
3274
+ const end = address.indexOf("]");
3275
+ return end === -1 ? address : address.slice(1, end);
3276
+ }
3277
+ return address.split(":").length > 2 ? address : address.split(":")[0];
3278
+ }
3150
3279
  function shouldUseTls(address, explicitTls) {
3151
3280
  if (explicitTls !== void 0) {
3152
3281
  return explicitTls;
3153
3282
  }
3154
- const host = address.split(":")[0].toLowerCase();
3155
- return host !== "localhost" && host !== "127.0.0.1" && !host.startsWith("[::1]");
3283
+ const host = grpcHost(address).toLowerCase();
3284
+ return host !== "localhost" && host !== "127.0.0.1" && host !== "::1";
3156
3285
  }
3157
3286
  var GrpcQueueOperations = class {
3158
3287
  constructor(client, metadata, timeoutMs) {
@@ -3204,7 +3333,7 @@ var GrpcQueueOperations = class {
3204
3333
  return promisify(
3205
3334
  this.client,
3206
3335
  this.client.Fail,
3207
- params,
3336
+ encodeFailRequest(params),
3208
3337
  this.metadata,
3209
3338
  this.timeoutMs
3210
3339
  );
@@ -3594,6 +3723,13 @@ var SpooledClient = class _SpooledClient {
3594
3723
  /**
3595
3724
  * Exchange the API key for a JWT via /auth/login and cache the result.
3596
3725
  *
3726
+ * The JWT belongs to the realtime transport only. It must NOT replace the
3727
+ * REST bearer: an API-key client has no refresh token wired into its HTTP
3728
+ * layer, so after the JWT's 24 h lifetime every REST call (claim, complete,
3729
+ * heartbeat, create) would have failed with 401 for good. REST keeps using
3730
+ * the API key; realtime re-mints its own JWT near expiry (see
3731
+ * `getJwtToken`).
3732
+ *
3597
3733
  * Concurrent callers share a single in-flight login so a burst of reconnects
3598
3734
  * (e.g. WebSocket and SSE at once) issues just one request.
3599
3735
  */
@@ -3603,9 +3739,6 @@ var SpooledClient = class _SpooledClient {
3603
3739
  }
3604
3740
  this.jwtLoginPromise = (async () => {
3605
3741
  const response = await this.auth.login({ apiKey: this.config.apiKey });
3606
- this.http.setAuthToken(response.accessToken);
3607
- this.tokenExpiresAt = Date.now() + response.expiresIn * 1e3;
3608
- this.config.refreshToken = response.refreshToken;
3609
3742
  this.cachedJwt = response.accessToken;
3610
3743
  return response.accessToken;
3611
3744
  })();
@@ -3801,6 +3934,9 @@ var DEFAULT_OPTIONS = {
3801
3934
  autoStart: false,
3802
3935
  shutdownTimeout: 3e4
3803
3936
  };
3937
+ var JOB_TIMEOUT_MESSAGE = "Job timeout exceeded";
3938
+ var SHUTDOWN_TIMEOUT_MESSAGE = "Worker shutdown timeout";
3939
+ var MAX_TIMER_MS = 2 ** 31 - 1;
3804
3940
  var SpooledWorker = class {
3805
3941
  client;
3806
3942
  /**
@@ -3817,6 +3953,10 @@ var SpooledWorker = class {
3817
3953
  pollTimer = null;
3818
3954
  workerHeartbeatTimer = null;
3819
3955
  shutdownPromise = null;
3956
+ /** The claim request currently on the wire, so a stop can wait for its jobs. */
3957
+ pollInFlight = null;
3958
+ /** Wakes the shutdown drain when the last active job has been acked. */
3959
+ drainWaiter = null;
3820
3960
  // Event handlers
3821
3961
  eventHandlers = /* @__PURE__ */ new Map();
3822
3962
  constructor(client, options) {
@@ -3833,7 +3973,11 @@ var SpooledWorker = class {
3833
3973
  this.debug = config.debug ?? (() => {
3834
3974
  });
3835
3975
  if (this.options.autoStart) {
3836
- setImmediate(() => this.start());
3976
+ setImmediate(() => {
3977
+ this.start().catch((error) => {
3978
+ this.debug("Auto-start failed", error);
3979
+ });
3980
+ });
3837
3981
  }
3838
3982
  }
3839
3983
  /**
@@ -3904,7 +4048,15 @@ var SpooledWorker = class {
3904
4048
  }
3905
4049
  }
3906
4050
  /**
3907
- * Stop the worker gracefully
4051
+ * Stop the worker gracefully.
4052
+ *
4053
+ * Drains: the worker stops claiming, then lets running handlers finish for
4054
+ * up to `shutdownTimeout`. A handler that returns completes its job and one
4055
+ * that throws fails it, even though a stop was requested meanwhile; its
4056
+ * `signal` is NOT aborted during the grace period. Only handlers still
4057
+ * running when the timeout ends are aborted and failed with "Worker shutdown
4058
+ * timeout". The worker deregisters last, because deregistering returns every
4059
+ * job the worker still holds to the queue.
3908
4060
  */
3909
4061
  async stop() {
3910
4062
  if (this.state !== "running") {
@@ -3917,39 +4069,37 @@ var SpooledWorker = class {
3917
4069
  return this.shutdownPromise;
3918
4070
  }
3919
4071
  async doStop() {
4072
+ const deadline = Date.now() + this.options.shutdownTimeout;
3920
4073
  this.state = "stopping";
3921
4074
  this.debug("Stopping worker...");
3922
4075
  if (this.pollTimer) {
3923
4076
  clearTimeout(this.pollTimer);
3924
4077
  this.pollTimer = null;
3925
4078
  }
3926
- if (this.workerHeartbeatTimer) {
3927
- clearInterval(this.workerHeartbeatTimer);
3928
- this.workerHeartbeatTimer = null;
3929
- }
3930
- for (const active of this.activeJobs.values()) {
3931
- active.abortController.abort();
4079
+ if (this.pollInFlight) {
4080
+ await this.pollInFlight;
3932
4081
  }
3933
4082
  if (this.activeJobs.size > 0) {
3934
4083
  this.debug(
3935
4084
  `Waiting for ${this.activeJobs.size} active jobs to complete...`
3936
4085
  );
3937
- await Promise.race([
3938
- this.waitForActiveJobs(),
3939
- this.sleep(this.options.shutdownTimeout)
3940
- ]);
3941
- const remaining = Array.from(this.activeJobs.values());
3942
- for (const active of remaining) {
3943
- this.debug(
3944
- `Force-failing job ${active.job.id} due to shutdown timeout`
3945
- );
3946
- await this.failJob(active.job, "Worker shutdown timeout");
3947
- this.cleanupJob(active);
3948
- }
4086
+ await this.waitForDrain(Math.max(0, deadline - Date.now()));
4087
+ const stragglers = Array.from(this.activeJobs.values()).filter(
4088
+ (active) => !active.settled
4089
+ );
4090
+ await Promise.all(stragglers.map((active) => this.abandonJob(active)));
4091
+ await Promise.all(
4092
+ Array.from(this.activeJobs.values()).map((active) => active.ack)
4093
+ );
3949
4094
  }
3950
- if (this.workerId) {
4095
+ if (this.workerHeartbeatTimer) {
4096
+ clearInterval(this.workerHeartbeatTimer);
4097
+ this.workerHeartbeatTimer = null;
4098
+ }
4099
+ const workerId = this.workerId;
4100
+ if (workerId) {
3951
4101
  try {
3952
- await this.client.workers.deregister(this.workerId);
4102
+ await this.client.workers.deregister(workerId);
3953
4103
  this.debug("Worker deregistered");
3954
4104
  } catch (error) {
3955
4105
  this.debug("Failed to deregister worker", error);
@@ -3957,7 +4107,7 @@ var SpooledWorker = class {
3957
4107
  }
3958
4108
  this.state = "stopped";
3959
4109
  this.workerId = null;
3960
- this.emit("stopped", { workerId: this.workerId ?? "", reason: "graceful" });
4110
+ this.emit("stopped", { workerId: workerId ?? "", reason: "graceful" });
3961
4111
  this.shutdownPromise = null;
3962
4112
  }
3963
4113
  /**
@@ -3996,7 +4146,17 @@ var SpooledWorker = class {
3996
4146
  if (this.state !== "running") {
3997
4147
  return;
3998
4148
  }
3999
- this.pollTimer = setTimeout(() => this.poll(), this.options.pollInterval);
4149
+ this.pollTimer = setTimeout(() => {
4150
+ this.pollTimer = null;
4151
+ const run = this.poll();
4152
+ this.pollInFlight = run;
4153
+ const done = () => {
4154
+ if (this.pollInFlight === run) {
4155
+ this.pollInFlight = null;
4156
+ }
4157
+ };
4158
+ run.then(done, done);
4159
+ }, this.options.pollInterval);
4000
4160
  }
4001
4161
  async poll() {
4002
4162
  if (this.state !== "running" || !this.workerId) {
@@ -4030,7 +4190,8 @@ var SpooledWorker = class {
4030
4190
  executionId: Symbol(job.id),
4031
4191
  job,
4032
4192
  startedAt: /* @__PURE__ */ new Date(),
4033
- abortController
4193
+ abortController,
4194
+ settled: false
4034
4195
  };
4035
4196
  this.activeJobs.set(activeJob.executionId, activeJob);
4036
4197
  const heartbeatInterval = this.options.leaseDuration * this.options.heartbeatFraction * 1e3;
@@ -4059,23 +4220,72 @@ var SpooledWorker = class {
4059
4220
  }
4060
4221
  };
4061
4222
  try {
4062
- const result = await this.handler(context);
4063
- if (signal.aborted) {
4064
- this.debug(`Job ${job.id} was aborted`);
4223
+ const outcome = await this.runHandler(activeJob, context);
4224
+ if (activeJob.settled) {
4225
+ this.debug(`Job ${job.id} was already settled; dropping late result`);
4065
4226
  return;
4066
4227
  }
4067
- await this.completeJob(job, result ?? void 0);
4068
- } catch (error) {
4069
- if (signal.aborted) {
4070
- this.debug(`Job ${job.id} was aborted`);
4071
- return;
4228
+ activeJob.settled = true;
4229
+ switch (outcome.kind) {
4230
+ case "returned":
4231
+ activeJob.ack = this.completeJob(job, outcome.value ?? void 0);
4232
+ break;
4233
+ case "threw":
4234
+ activeJob.ack = this.failJob(
4235
+ job,
4236
+ outcome.error instanceof Error ? outcome.error.message : String(outcome.error)
4237
+ );
4238
+ break;
4239
+ case "timeout":
4240
+ abortController.abort(new Error(JOB_TIMEOUT_MESSAGE));
4241
+ this.emit("job:timeout", {
4242
+ jobId: job.id,
4243
+ queueName: job.queueName
4244
+ });
4245
+ activeJob.ack = this.failJob(job, JOB_TIMEOUT_MESSAGE);
4246
+ break;
4072
4247
  }
4073
- const errorMessage = error instanceof Error ? error.message : String(error);
4074
- await this.failJob(job, errorMessage);
4248
+ await activeJob.ack;
4075
4249
  } finally {
4076
4250
  this.cleanupJob(activeJob);
4077
4251
  }
4078
4252
  }
4253
+ /**
4254
+ * Run the handler, racing it against the job's `timeoutSeconds`.
4255
+ * Never rejects: a throw (sync or async) is reported as an outcome.
4256
+ */
4257
+ runHandler(activeJob, context) {
4258
+ const run = (async () => {
4259
+ try {
4260
+ return { kind: "returned", value: await this.handler(context) };
4261
+ } catch (error) {
4262
+ return { kind: "threw", error };
4263
+ }
4264
+ })();
4265
+ const { timeoutSeconds } = activeJob.job;
4266
+ if (typeof timeoutSeconds !== "number" || !(timeoutSeconds > 0)) {
4267
+ return run;
4268
+ }
4269
+ return new Promise((resolve) => {
4270
+ activeJob.timeoutTimer = setTimeout(
4271
+ () => resolve({ kind: "timeout" }),
4272
+ Math.min(timeoutSeconds * 1e3, MAX_TIMER_MS)
4273
+ );
4274
+ run.then((outcome) => {
4275
+ this.clearTimeoutTimer(activeJob);
4276
+ resolve(outcome);
4277
+ });
4278
+ });
4279
+ }
4280
+ /** Abort a handler that outlived the shutdown grace period and fail its job. */
4281
+ async abandonJob(activeJob) {
4282
+ this.debug(`Force-failing job ${activeJob.job.id} due to shutdown timeout`);
4283
+ activeJob.settled = true;
4284
+ activeJob.abortController.abort(new Error(SHUTDOWN_TIMEOUT_MESSAGE));
4285
+ activeJob.ack = this.failJob(activeJob.job, SHUTDOWN_TIMEOUT_MESSAGE);
4286
+ await activeJob.ack;
4287
+ this.cleanupJob(activeJob);
4288
+ }
4079
4289
  async completeJob(job, result) {
4080
4290
  if (!this.workerId) return;
4081
4291
  try {
@@ -4091,25 +4301,34 @@ var SpooledWorker = class {
4091
4301
  });
4092
4302
  } catch (error) {
4093
4303
  this.debug(`Failed to complete job ${job.id}`, error);
4304
+ this.emit("error", { error });
4094
4305
  }
4095
4306
  }
4096
4307
  async failJob(job, errorMessage) {
4097
4308
  if (!this.workerId) return;
4098
4309
  const willRetry = job.retryCount < job.maxRetries;
4310
+ const message = coerceFailureMessage(errorMessage);
4099
4311
  try {
4100
4312
  await this.client.jobs.fail(job.id, {
4101
4313
  workerId: this.workerId,
4102
- error: errorMessage,
4314
+ error: message,
4103
4315
  ...job.leaseId != null && { leaseId: job.leaseId }
4104
4316
  });
4105
4317
  this.emit("job:failed", {
4106
4318
  jobId: job.id,
4107
4319
  queueName: job.queueName,
4108
- error: errorMessage,
4320
+ error: message,
4109
4321
  willRetry
4110
4322
  });
4111
4323
  } catch (error) {
4112
4324
  this.debug(`Failed to fail job ${job.id}`, error);
4325
+ this.emit("error", { error });
4326
+ }
4327
+ }
4328
+ clearTimeoutTimer(activeJob) {
4329
+ if (activeJob.timeoutTimer) {
4330
+ clearTimeout(activeJob.timeoutTimer);
4331
+ activeJob.timeoutTimer = void 0;
4113
4332
  }
4114
4333
  }
4115
4334
  cleanupJob(activeJob) {
@@ -4117,8 +4336,12 @@ var SpooledWorker = class {
4117
4336
  clearInterval(activeJob.heartbeatTimer);
4118
4337
  activeJob.heartbeatTimer = void 0;
4119
4338
  }
4339
+ this.clearTimeoutTimer(activeJob);
4120
4340
  if (this.activeJobs.get(activeJob.executionId) === activeJob) {
4121
4341
  this.activeJobs.delete(activeJob.executionId);
4342
+ if (this.activeJobs.size === 0) {
4343
+ this.drainWaiter?.();
4344
+ }
4122
4345
  }
4123
4346
  }
4124
4347
  async sendJobHeartbeat(activeJob) {
@@ -4135,7 +4358,9 @@ var SpooledWorker = class {
4135
4358
  }
4136
4359
  }
4137
4360
  async sendWorkerHeartbeat() {
4138
- if (!this.workerId || this.state !== "running") return;
4361
+ if (!this.workerId || this.state !== "running" && this.state !== "stopping") {
4362
+ return;
4363
+ }
4139
4364
  try {
4140
4365
  await this.client.workers.heartbeat(this.workerId, {
4141
4366
  currentJobs: this.activeJobs.size,
@@ -4145,16 +4370,28 @@ var SpooledWorker = class {
4145
4370
  this.debug("Worker heartbeat failed", error);
4146
4371
  }
4147
4372
  }
4148
- async waitForActiveJobs() {
4149
- while (this.activeJobs.size > 0) {
4150
- await this.sleep(100);
4373
+ /**
4374
+ * Resolve once no job is active, or after `timeoutMs`. The timer is always
4375
+ * cleared, so a clean stop leaves nothing that keeps the process alive.
4376
+ */
4377
+ waitForDrain(timeoutMs) {
4378
+ if (this.activeJobs.size === 0) {
4379
+ return Promise.resolve();
4151
4380
  }
4152
- }
4153
- sleep(ms) {
4154
- return new Promise((resolve) => setTimeout(resolve, ms));
4381
+ return new Promise((resolve) => {
4382
+ const timer = setTimeout(() => {
4383
+ this.drainWaiter = null;
4384
+ resolve();
4385
+ }, timeoutMs);
4386
+ this.drainWaiter = () => {
4387
+ clearTimeout(timer);
4388
+ this.drainWaiter = null;
4389
+ resolve();
4390
+ };
4391
+ });
4155
4392
  }
4156
4393
  };
4157
4394
 
4158
- export { API_VERSION, AdminResource, ApiKeysResource, AuthResource, AuthenticationError, AuthorizationError, BillingResource, CircuitBreaker, CircuitBreakerOpenError, CircuitState, ConflictError, DEFAULT_CONFIG, DashboardResource, GrpcJobStatus, GrpcJobStatusNumber, GrpcQueueOperations, GrpcWorkerOperations, HealthResource, HttpClient, JobsResource, MetricsResource, NetworkError, NotFoundError, OrganizationsResource, PROTO_LOADER_OPTIONS, PayloadTooLargeError, QueuesResource, RateLimitError, SDK_VERSION, SchedulesResource, ServerError, SpooledClient, SpooledError, SpooledGrpcClient, SpooledRealtime, SpooledWorker, SseRealtimeClient, TimeoutError, ValidationError, WebSocketRealtimeClient, WebhookIngestionResource, WebhooksResource, WorkersResource, WorkflowsResource, asyncIterableFromStream, calculateDelay, camelToSnake, clearProtoCache, convertQueryParams, convertRequest, convertResponse, createCircuitBreaker, createClient, createErrorFromResponse, createHttpClient, createJobStream, createPaginatedResponse, createProcessJobsStream, createRetryWrapper, dateToTimestamp, getProtoPath, isRetryableError, isSpooledError, loadProtoDefinition, loadProtoDefinitionAsync, parseRateLimitHeaders, resolveConfig, shouldRetry, sleep, snakeToCamel, timestampToDate, toCamelCase, toSnakeCase, validateConfig, withRetry };
4395
+ export { API_VERSION, AdminResource, ApiKeysResource, AuthResource, AuthenticationError, AuthorizationError, BillingResource, CircuitBreaker, CircuitBreakerOpenError, CircuitState, ConflictError, DEFAULT_CONFIG, DashboardResource, GrpcJobStatus, GrpcJobStatusNumber, GrpcQueueOperations, GrpcWorkerOperations, HealthResource, HttpClient, JobsResource, MetricsResource, NetworkError, NotFoundError, OrganizationsResource, PROTO_LOADER_OPTIONS, PayloadTooLargeError, QueuesResource, RateLimitError, SDK_VERSION, SchedulesResource, ServerError, SpooledClient, SpooledError, SpooledGrpcClient, SpooledRealtime, SpooledWorker, SseRealtimeClient, TimeoutError, UnsupportedOperationError, ValidationError, WebSocketRealtimeClient, WebhookIngestionResource, WebhooksResource, WorkersResource, WorkflowsResource, asyncIterableFromStream, calculateDelay, camelToSnake, clearProtoCache, convertQueryParams, convertRequest, convertResponse, createCircuitBreaker, createClient, createErrorFromResponse, createHttpClient, createJobStream, createPaginatedResponse, createProcessJobsStream, createRetryWrapper, dateToTimestamp, getProtoPath, isRetryableError, isSpooledError, loadProtoDefinition, loadProtoDefinitionAsync, parseRateLimitHeaders, resolveConfig, shouldRetry, sleep, snakeToCamel, timestampToDate, toCamelCase, toSnakeCase, validateConfig, withRetry };
4159
4396
  //# sourceMappingURL=index.js.map
4160
4397
  //# sourceMappingURL=index.js.map