@spooled/sdk 1.0.28 → 1.0.29

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.
@@ -285,15 +285,22 @@ declare class HttpClient {
285
285
  private readonly debug;
286
286
  /** Current authentication token (API key or JWT) */
287
287
  private authToken;
288
+ /** Optional function to mint a fresh access token (set when auto-refresh is enabled) */
289
+ private refreshTokenFn;
290
+ /** Guard to prevent a refresh request from recursively triggering another refresh */
291
+ private isRefreshing;
288
292
  constructor(config: ResolvedConfig, circuitBreaker: CircuitBreaker);
289
293
  /**
290
294
  * Set the authentication token
291
295
  */
292
296
  setAuthToken(token: string): void;
293
297
  /**
294
- * Set the token refresh function (for future use)
298
+ * Set the token refresh function.
299
+ *
300
+ * When set, an expired-token 401 on the data plane triggers a single
301
+ * refresh + retry of the original request with the new token.
295
302
  */
296
- setRefreshTokenFn(_fn: () => Promise<string>): void;
303
+ setRefreshTokenFn(fn: () => Promise<string>): void;
297
304
  /**
298
305
  * Build full URL with query parameters
299
306
  */
@@ -314,6 +321,22 @@ declare class HttpClient {
314
321
  * Make an HTTP request with retry and circuit breaker
315
322
  */
316
323
  request<T>(path: string, options?: HttpRequestOptions): Promise<HttpResponse<T>>;
324
+ /**
325
+ * Determine whether a request carries an idempotency key (making a
326
+ * non-idempotent method safe to retry).
327
+ */
328
+ private hasIdempotencyKey;
329
+ /**
330
+ * Resolve the retry configuration for a request.
331
+ *
332
+ * Idempotent methods (GET/PUT/DELETE) and any request carrying an
333
+ * idempotency key retry normally. Non-idempotent methods (POST/PATCH)
334
+ * without an idempotency key must NOT be retried on ambiguous failures
335
+ * (network/timeout/5xx) — doing so risks duplicate jobs, double claims,
336
+ * etc. They are still retried on 429, which is rejected before the
337
+ * server processes the request and is therefore side-effect-free.
338
+ */
339
+ private resolveRetryConfig;
317
340
  /**
318
341
  * Make a GET request
319
342
  */
@@ -401,7 +424,11 @@ interface ValidateTokenResponse {
401
424
 
402
425
  declare class AuthResource {
403
426
  private readonly http;
404
- constructor(http: HttpClient);
427
+ /** Optional accessor for the client's current refresh token (used by logout) */
428
+ private readonly getRefreshToken?;
429
+ constructor(http: HttpClient,
430
+ /** Optional accessor for the client's current refresh token (used by logout) */
431
+ getRefreshToken?: (() => string | undefined) | undefined);
405
432
  /**
406
433
  * Exchange API key for JWT tokens
407
434
  */
@@ -411,9 +438,13 @@ declare class AuthResource {
411
438
  */
412
439
  refresh(params: RefreshTokenParams): Promise<RefreshTokenResponse>;
413
440
  /**
414
- * Logout and invalidate current token
441
+ * Logout and revoke the refresh token server-side.
442
+ *
443
+ * The backend requires the refresh token in the body to invalidate the
444
+ * session. When omitted, the client's current refresh token (if any) is
445
+ * used automatically.
415
446
  */
416
- logout(): Promise<void>;
447
+ logout(refreshToken?: string): Promise<void>;
417
448
  /**
418
449
  * Get current user/session info
419
450
  */
@@ -2343,8 +2374,16 @@ interface RealtimeConnectionOptions {
2343
2374
  baseUrl: string;
2344
2375
  /** WebSocket URL for realtime connection (e.g., wss://api.spooled.cloud) - used for WebSocket */
2345
2376
  wsUrl: string;
2346
- /** JWT token for authentication */
2377
+ /** JWT token for authentication (used as a fallback when tokenProvider is not supplied) */
2347
2378
  token: string;
2379
+ /**
2380
+ * Async provider that mints a fresh JWT for each (re)connect.
2381
+ *
2382
+ * JWTs are short-lived, so a static token captured once will be stale by
2383
+ * the time a long-running connection needs to reconnect. When provided,
2384
+ * this is invoked before every connect/reconnect to obtain a valid token.
2385
+ */
2386
+ tokenProvider?: () => Promise<string>;
2348
2387
  /** Auto-reconnect on disconnect (default: true) */
2349
2388
  autoReconnect?: boolean;
2350
2389
  /** Maximum reconnect attempts (default: 10) */
@@ -2387,6 +2426,8 @@ interface FullRealtimeOptions extends SpooledRealtimeOptions {
2387
2426
  baseUrl: string;
2388
2427
  wsUrl: string;
2389
2428
  token: string;
2429
+ /** Async provider that mints a fresh JWT for each (re)connect */
2430
+ tokenProvider?: () => Promise<string>;
2390
2431
  debug?: (message: string, meta?: unknown) => void;
2391
2432
  }
2392
2433
  /** Event handler type - simplified for internal use */
@@ -285,15 +285,22 @@ declare class HttpClient {
285
285
  private readonly debug;
286
286
  /** Current authentication token (API key or JWT) */
287
287
  private authToken;
288
+ /** Optional function to mint a fresh access token (set when auto-refresh is enabled) */
289
+ private refreshTokenFn;
290
+ /** Guard to prevent a refresh request from recursively triggering another refresh */
291
+ private isRefreshing;
288
292
  constructor(config: ResolvedConfig, circuitBreaker: CircuitBreaker);
289
293
  /**
290
294
  * Set the authentication token
291
295
  */
292
296
  setAuthToken(token: string): void;
293
297
  /**
294
- * Set the token refresh function (for future use)
298
+ * Set the token refresh function.
299
+ *
300
+ * When set, an expired-token 401 on the data plane triggers a single
301
+ * refresh + retry of the original request with the new token.
295
302
  */
296
- setRefreshTokenFn(_fn: () => Promise<string>): void;
303
+ setRefreshTokenFn(fn: () => Promise<string>): void;
297
304
  /**
298
305
  * Build full URL with query parameters
299
306
  */
@@ -314,6 +321,22 @@ declare class HttpClient {
314
321
  * Make an HTTP request with retry and circuit breaker
315
322
  */
316
323
  request<T>(path: string, options?: HttpRequestOptions): Promise<HttpResponse<T>>;
324
+ /**
325
+ * Determine whether a request carries an idempotency key (making a
326
+ * non-idempotent method safe to retry).
327
+ */
328
+ private hasIdempotencyKey;
329
+ /**
330
+ * Resolve the retry configuration for a request.
331
+ *
332
+ * Idempotent methods (GET/PUT/DELETE) and any request carrying an
333
+ * idempotency key retry normally. Non-idempotent methods (POST/PATCH)
334
+ * without an idempotency key must NOT be retried on ambiguous failures
335
+ * (network/timeout/5xx) — doing so risks duplicate jobs, double claims,
336
+ * etc. They are still retried on 429, which is rejected before the
337
+ * server processes the request and is therefore side-effect-free.
338
+ */
339
+ private resolveRetryConfig;
317
340
  /**
318
341
  * Make a GET request
319
342
  */
@@ -401,7 +424,11 @@ interface ValidateTokenResponse {
401
424
 
402
425
  declare class AuthResource {
403
426
  private readonly http;
404
- constructor(http: HttpClient);
427
+ /** Optional accessor for the client's current refresh token (used by logout) */
428
+ private readonly getRefreshToken?;
429
+ constructor(http: HttpClient,
430
+ /** Optional accessor for the client's current refresh token (used by logout) */
431
+ getRefreshToken?: (() => string | undefined) | undefined);
405
432
  /**
406
433
  * Exchange API key for JWT tokens
407
434
  */
@@ -411,9 +438,13 @@ declare class AuthResource {
411
438
  */
412
439
  refresh(params: RefreshTokenParams): Promise<RefreshTokenResponse>;
413
440
  /**
414
- * Logout and invalidate current token
441
+ * Logout and revoke the refresh token server-side.
442
+ *
443
+ * The backend requires the refresh token in the body to invalidate the
444
+ * session. When omitted, the client's current refresh token (if any) is
445
+ * used automatically.
415
446
  */
416
- logout(): Promise<void>;
447
+ logout(refreshToken?: string): Promise<void>;
417
448
  /**
418
449
  * Get current user/session info
419
450
  */
@@ -2343,8 +2374,16 @@ interface RealtimeConnectionOptions {
2343
2374
  baseUrl: string;
2344
2375
  /** WebSocket URL for realtime connection (e.g., wss://api.spooled.cloud) - used for WebSocket */
2345
2376
  wsUrl: string;
2346
- /** JWT token for authentication */
2377
+ /** JWT token for authentication (used as a fallback when tokenProvider is not supplied) */
2347
2378
  token: string;
2379
+ /**
2380
+ * Async provider that mints a fresh JWT for each (re)connect.
2381
+ *
2382
+ * JWTs are short-lived, so a static token captured once will be stale by
2383
+ * the time a long-running connection needs to reconnect. When provided,
2384
+ * this is invoked before every connect/reconnect to obtain a valid token.
2385
+ */
2386
+ tokenProvider?: () => Promise<string>;
2348
2387
  /** Auto-reconnect on disconnect (default: true) */
2349
2388
  autoReconnect?: boolean;
2350
2389
  /** Maximum reconnect attempts (default: 10) */
@@ -2387,6 +2426,8 @@ interface FullRealtimeOptions extends SpooledRealtimeOptions {
2387
2426
  baseUrl: string;
2388
2427
  wsUrl: string;
2389
2428
  token: string;
2429
+ /** Async provider that mints a fresh JWT for each (re)connect */
2430
+ tokenProvider?: () => Promise<string>;
2390
2431
  debug?: (message: string, meta?: unknown) => void;
2391
2432
  }
2392
2433
  /** Event handler type - simplified for internal use */
package/dist/index.cjs CHANGED
@@ -328,9 +328,10 @@ async function parseErrorBody(response) {
328
328
  try {
329
329
  const body = await response.json();
330
330
  if (typeof body === "object" && body !== null) {
331
+ const message = (body.message ?? body.error) || response.statusText;
331
332
  return {
332
333
  code: body.code || "UNKNOWN_ERROR",
333
- message: body.message || response.statusText,
334
+ message,
334
335
  details: body.details
335
336
  };
336
337
  }
@@ -548,12 +549,17 @@ function createRetryWrapper(config) {
548
549
  }
549
550
 
550
551
  // src/utils/http.ts
552
+ var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "PUT", "DELETE"]);
551
553
  var HttpClient = class {
552
554
  config;
553
555
  circuitBreaker;
554
556
  debug;
555
557
  /** Current authentication token (API key or JWT) */
556
558
  authToken;
559
+ /** Optional function to mint a fresh access token (set when auto-refresh is enabled) */
560
+ refreshTokenFn = null;
561
+ /** Guard to prevent a refresh request from recursively triggering another refresh */
562
+ isRefreshing = false;
557
563
  constructor(config, circuitBreaker) {
558
564
  this.config = config;
559
565
  this.circuitBreaker = circuitBreaker;
@@ -567,9 +573,13 @@ var HttpClient = class {
567
573
  this.authToken = token;
568
574
  }
569
575
  /**
570
- * Set the token refresh function (for future use)
576
+ * Set the token refresh function.
577
+ *
578
+ * When set, an expired-token 401 on the data plane triggers a single
579
+ * refresh + retry of the original request with the new token.
571
580
  */
572
- setRefreshTokenFn(_fn) {
581
+ setRefreshTokenFn(fn) {
582
+ this.refreshTokenFn = fn;
573
583
  }
574
584
  /**
575
585
  * Build full URL with query parameters
@@ -639,7 +649,7 @@ var HttpClient = class {
639
649
  /**
640
650
  * Execute a single HTTP request (no retry)
641
651
  */
642
- async executeRequest(url, options) {
652
+ async executeRequest(url, options, hasRefreshed = false) {
643
653
  const method = options.method ?? "GET";
644
654
  const headers = this.buildHeaders(options.headers);
645
655
  const timeout = options.timeout ?? this.config.timeout;
@@ -668,6 +678,23 @@ var HttpClient = class {
668
678
  }
669
679
  this.debug?.(`Response: ${response.status}`, { url });
670
680
  if (!response.ok) {
681
+ if (response.status === 401 && this.refreshTokenFn && !hasRefreshed && !this.isRefreshing) {
682
+ let refreshed = false;
683
+ this.isRefreshing = true;
684
+ try {
685
+ const newToken = await this.refreshTokenFn();
686
+ this.setAuthToken(newToken);
687
+ refreshed = true;
688
+ } catch (refreshError) {
689
+ const message = refreshError instanceof Error ? refreshError.message : "unknown";
690
+ this.debug?.("Token refresh failed", { error: message });
691
+ } finally {
692
+ this.isRefreshing = false;
693
+ }
694
+ if (refreshed) {
695
+ return this.executeRequest(url, options, true);
696
+ }
697
+ }
671
698
  const apiError = await createErrorFromResponse(response);
672
699
  throw apiError;
673
700
  }
@@ -692,6 +719,7 @@ var HttpClient = class {
692
719
  */
693
720
  async request(path, options = {}) {
694
721
  const url = this.buildUrl(path, options.params, options.skipApiPrefix);
722
+ const method = options.method ?? "GET";
695
723
  const execute = async () => {
696
724
  return this.circuitBreaker.execute(() => this.executeRequest(url, options));
697
725
  };
@@ -699,13 +727,46 @@ var HttpClient = class {
699
727
  return execute();
700
728
  }
701
729
  return withRetry(execute, {
702
- config: this.config.retry,
730
+ config: this.resolveRetryConfig(method, options),
703
731
  signal: options.signal,
704
732
  onRetry: (attempt, error, delayMs) => {
705
733
  this.debug?.(`Retry attempt ${attempt} after ${delayMs}ms`, { error: error.message });
706
734
  }
707
735
  });
708
736
  }
737
+ /**
738
+ * Determine whether a request carries an idempotency key (making a
739
+ * non-idempotent method safe to retry).
740
+ */
741
+ hasIdempotencyKey(options) {
742
+ if (!options.headers) {
743
+ return false;
744
+ }
745
+ return Object.keys(options.headers).some((key) => key.toLowerCase() === "idempotency-key");
746
+ }
747
+ /**
748
+ * Resolve the retry configuration for a request.
749
+ *
750
+ * Idempotent methods (GET/PUT/DELETE) and any request carrying an
751
+ * idempotency key retry normally. Non-idempotent methods (POST/PATCH)
752
+ * without an idempotency key must NOT be retried on ambiguous failures
753
+ * (network/timeout/5xx) — doing so risks duplicate jobs, double claims,
754
+ * etc. They are still retried on 429, which is rejected before the
755
+ * server processes the request and is therefore side-effect-free.
756
+ */
757
+ resolveRetryConfig(method, options) {
758
+ const base = this.config.retry;
759
+ if (base.retryOn) {
760
+ return base;
761
+ }
762
+ if (IDEMPOTENT_METHODS.has(method) || this.hasIdempotencyKey(options)) {
763
+ return base;
764
+ }
765
+ return {
766
+ ...base,
767
+ retryOn: (error) => error instanceof RateLimitError
768
+ };
769
+ }
709
770
  /**
710
771
  * Make a GET request
711
772
  */
@@ -907,8 +968,9 @@ function createCircuitBreaker(config) {
907
968
 
908
969
  // src/resources/auth.ts
909
970
  var AuthResource = class {
910
- constructor(http) {
971
+ constructor(http, getRefreshToken) {
911
972
  this.http = http;
973
+ this.getRefreshToken = getRefreshToken;
912
974
  }
913
975
  /**
914
976
  * Exchange API key for JWT tokens
@@ -923,10 +985,15 @@ var AuthResource = class {
923
985
  return this.http.post("/auth/refresh", params);
924
986
  }
925
987
  /**
926
- * Logout and invalidate current token
988
+ * Logout and revoke the refresh token server-side.
989
+ *
990
+ * The backend requires the refresh token in the body to invalidate the
991
+ * session. When omitted, the client's current refresh token (if any) is
992
+ * used automatically.
927
993
  */
928
- async logout() {
929
- await this.http.post("/auth/logout");
994
+ async logout(refreshToken) {
995
+ const token = refreshToken ?? this.getRefreshToken?.();
996
+ await this.http.post("/auth/logout", token ? { refreshToken: token } : void 0);
930
997
  }
931
998
  /**
932
999
  * Get current user/session info
@@ -1796,7 +1863,10 @@ var WebSocketRealtimeClient = class {
1796
1863
  maxReconnectDelay: 3e4,
1797
1864
  debug: () => {
1798
1865
  },
1799
- ...options
1866
+ ...options,
1867
+ // Always resolve to a provider so reconnects can mint a fresh JWT.
1868
+ // Falls back to the static token when no provider is supplied.
1869
+ tokenProvider: options.tokenProvider ?? (async () => options.token)
1800
1870
  };
1801
1871
  }
1802
1872
  /**
@@ -1823,8 +1893,14 @@ var WebSocketRealtimeClient = class {
1823
1893
  return;
1824
1894
  }
1825
1895
  this.setState("connecting");
1826
- const wsUrl = this.buildWsUrl();
1827
- this.options.debug(`Connecting to ${wsUrl}`);
1896
+ let wsUrl;
1897
+ try {
1898
+ wsUrl = await this.buildWsUrl();
1899
+ } catch (error) {
1900
+ this.setState("disconnected");
1901
+ throw new Error(`Failed to acquire realtime token: ${error}`);
1902
+ }
1903
+ this.options.debug(`Connecting to ${redactToken(wsUrl)}`);
1828
1904
  let WebSocketImpl;
1829
1905
  try {
1830
1906
  WebSocketImpl = await this.getWebSocketImpl();
@@ -1938,9 +2014,10 @@ var WebSocketRealtimeClient = class {
1938
2014
  return () => this.stateChangeHandlers.delete(handler);
1939
2015
  }
1940
2016
  // Private methods
1941
- buildWsUrl() {
2017
+ async buildWsUrl() {
1942
2018
  const wsBase = this.options.wsUrl;
1943
- return `${wsBase}/api/v1/ws?token=${encodeURIComponent(this.options.token)}`;
2019
+ const token = await this.options.tokenProvider();
2020
+ return `${wsBase}/api/v1/ws?token=${encodeURIComponent(token)}`;
1944
2021
  }
1945
2022
  setState(state) {
1946
2023
  if (this.state !== state) {
@@ -1950,7 +2027,7 @@ var WebSocketRealtimeClient = class {
1950
2027
  }
1951
2028
  handleMessage(data) {
1952
2029
  try {
1953
- const message = JSON.parse(data);
2030
+ const message = convertResponse(JSON.parse(data));
1954
2031
  if (message.type === "subscribed" || message.type === "unsubscribed" || message.type === "error") {
1955
2032
  this.handleCommandResponse(message);
1956
2033
  return;
@@ -2007,10 +2084,11 @@ var WebSocketRealtimeClient = class {
2007
2084
  scheduleReconnect() {
2008
2085
  this.setState("reconnecting");
2009
2086
  this.reconnectAttempts++;
2010
- const delay = Math.min(
2087
+ const baseDelay = Math.min(
2011
2088
  this.options.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
2012
2089
  this.options.maxReconnectDelay
2013
2090
  );
2091
+ const delay = Math.floor(baseDelay + Math.random() * baseDelay * 0.25);
2014
2092
  this.options.debug(`Scheduling reconnect attempt ${this.reconnectAttempts} in ${delay}ms`);
2015
2093
  this.reconnectTimer = setTimeout(async () => {
2016
2094
  try {
@@ -2058,6 +2136,9 @@ var WebSocketRealtimeClient = class {
2058
2136
  return `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
2059
2137
  }
2060
2138
  };
2139
+ function redactToken(url) {
2140
+ return url.replace(/([?&](?:token|api_key)=)[^&]*/gi, "$1***");
2141
+ }
2061
2142
 
2062
2143
  // src/realtime/sse.ts
2063
2144
  var SseRealtimeClient = class {
@@ -2080,7 +2161,10 @@ var SseRealtimeClient = class {
2080
2161
  maxReconnectDelay: 3e4,
2081
2162
  debug: () => {
2082
2163
  },
2083
- ...options
2164
+ ...options,
2165
+ // Always resolve to a provider so reconnects can mint a fresh token.
2166
+ // Falls back to the static token when no provider is supplied.
2167
+ tokenProvider: options.tokenProvider ?? (async () => options.token)
2084
2168
  };
2085
2169
  }
2086
2170
  /**
@@ -2112,9 +2196,15 @@ var SseRealtimeClient = class {
2112
2196
  throw new Error("eventsource package is required for SSE in Node.js. Install it with: npm install eventsource");
2113
2197
  }
2114
2198
  }
2199
+ let token;
2200
+ try {
2201
+ token = await this.options.tokenProvider();
2202
+ } catch (error) {
2203
+ this.setState("disconnected");
2204
+ throw new Error(`Failed to acquire realtime token: ${error}`);
2205
+ }
2115
2206
  return new Promise((resolve, reject) => {
2116
2207
  try {
2117
- const token = this.options.token;
2118
2208
  this.eventSource = new EventSourceImpl(sseUrl, {
2119
2209
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
2120
2210
  fetch: (input, init) => fetch(input, {
@@ -2138,6 +2228,10 @@ var SseRealtimeClient = class {
2138
2228
  this.options.debug("SSE error");
2139
2229
  if (this.state === "connecting") {
2140
2230
  this.setState("disconnected");
2231
+ if (this.eventSource) {
2232
+ this.eventSource.close();
2233
+ this.eventSource = null;
2234
+ }
2141
2235
  reject(new Error("SSE connection failed"));
2142
2236
  return;
2143
2237
  }
@@ -2222,7 +2316,7 @@ var SseRealtimeClient = class {
2222
2316
  }
2223
2317
  handleMessage(data) {
2224
2318
  try {
2225
- const event = JSON.parse(data);
2319
+ const event = convertResponse(JSON.parse(data));
2226
2320
  this.options.debug(`Received SSE event: ${event.type}`, event);
2227
2321
  const handlers = this.eventHandlers.get(event.type);
2228
2322
  if (handlers) {
@@ -2260,10 +2354,11 @@ var SseRealtimeClient = class {
2260
2354
  scheduleReconnect() {
2261
2355
  this.setState("reconnecting");
2262
2356
  this.reconnectAttempts++;
2263
- const delay = Math.min(
2357
+ const baseDelay = Math.min(
2264
2358
  this.options.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
2265
2359
  this.options.maxReconnectDelay
2266
2360
  );
2361
+ const delay = Math.floor(baseDelay + Math.random() * baseDelay * 0.25);
2267
2362
  this.options.debug(`Scheduling SSE reconnect attempt ${this.reconnectAttempts} in ${delay}ms`);
2268
2363
  this.reconnectTimer = setTimeout(async () => {
2269
2364
  try {
@@ -2284,6 +2379,7 @@ var SpooledRealtime = class {
2284
2379
  baseUrl: options.baseUrl,
2285
2380
  wsUrl: options.wsUrl,
2286
2381
  token: options.token,
2382
+ tokenProvider: options.tokenProvider,
2287
2383
  autoReconnect: options.autoReconnect,
2288
2384
  maxReconnectAttempts: options.maxReconnectAttempts,
2289
2385
  reconnectDelay: options.reconnectDelay,
@@ -2783,7 +2879,7 @@ var SpooledClient = class _SpooledClient {
2783
2879
  if (this.config.accessToken && this.config.refreshToken && this.config.autoRefreshToken) {
2784
2880
  this.http.setRefreshTokenFn(this.refreshAccessToken.bind(this));
2785
2881
  }
2786
- this.auth = new AuthResource(this.http);
2882
+ this.auth = new AuthResource(this.http, () => this.config.refreshToken);
2787
2883
  this.jobs = new JobsResource(this.http);
2788
2884
  this.queues = new QueuesResource(this.http);
2789
2885
  this.workers = new WorkersResource(this.http);
@@ -2862,6 +2958,9 @@ var SpooledClient = class _SpooledClient {
2862
2958
  baseUrl: this.config.baseUrl,
2863
2959
  wsUrl: this.config.wsUrl,
2864
2960
  token,
2961
+ // Provider so each (re)connect mints a fresh JWT via login/refresh,
2962
+ // rather than reusing the short-lived token captured above.
2963
+ tokenProvider: () => this.getJwtToken(),
2865
2964
  ...options,
2866
2965
  debug: this.config.debug ?? void 0
2867
2966
  });
@@ -3169,9 +3268,11 @@ var SpooledWorker = class {
3169
3268
  this.waitForActiveJobs(),
3170
3269
  this.sleep(this.options.shutdownTimeout)
3171
3270
  ]);
3172
- for (const [jobId, active] of this.activeJobs.entries()) {
3271
+ const remaining = Array.from(this.activeJobs.entries());
3272
+ for (const [jobId, active] of remaining) {
3173
3273
  this.debug(`Force-failing job ${jobId} due to shutdown timeout`);
3174
3274
  await this.failJob(active.job, "Worker shutdown timeout");
3275
+ this.cleanupJob(jobId);
3175
3276
  }
3176
3277
  }
3177
3278
  if (this.workerId) {