@spooled/sdk 1.0.28 → 1.0.30
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-BKzL6R4h.d.cts → index-Dv2jBkfc.d.cts} +52 -7
- package/dist/{index-BKzL6R4h.d.ts → index-Dv2jBkfc.d.ts} +52 -7
- package/dist/index.cjs +146 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +146 -35
- package/dist/index.js.map +1 -1
- package/dist/worker/index.cjs +3 -1
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +1 -1
- package/dist/worker/index.d.ts +1 -1
- package/dist/worker/index.js +3 -1
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
*/
|
|
@@ -1055,7 +1086,11 @@ interface CreateScheduleParams {
|
|
|
1055
1086
|
timezone?: string;
|
|
1056
1087
|
/** Target queue name */
|
|
1057
1088
|
queueName: string;
|
|
1058
|
-
/**
|
|
1089
|
+
/**
|
|
1090
|
+
* Job payload template. Required: the API rejects a create without it
|
|
1091
|
+
* (HTTP 422), so this is typed as non-optional to surface the omission at
|
|
1092
|
+
* compile time rather than as a runtime 422.
|
|
1093
|
+
*/
|
|
1059
1094
|
payloadTemplate: JsonObject;
|
|
1060
1095
|
/** Job priority (-100 to 100) */
|
|
1061
1096
|
priority?: number;
|
|
@@ -2343,8 +2378,16 @@ interface RealtimeConnectionOptions {
|
|
|
2343
2378
|
baseUrl: string;
|
|
2344
2379
|
/** WebSocket URL for realtime connection (e.g., wss://api.spooled.cloud) - used for WebSocket */
|
|
2345
2380
|
wsUrl: string;
|
|
2346
|
-
/** JWT token for authentication */
|
|
2381
|
+
/** JWT token for authentication (used as a fallback when tokenProvider is not supplied) */
|
|
2347
2382
|
token: string;
|
|
2383
|
+
/**
|
|
2384
|
+
* Async provider that mints a fresh JWT for each (re)connect.
|
|
2385
|
+
*
|
|
2386
|
+
* JWTs are short-lived, so a static token captured once will be stale by
|
|
2387
|
+
* the time a long-running connection needs to reconnect. When provided,
|
|
2388
|
+
* this is invoked before every connect/reconnect to obtain a valid token.
|
|
2389
|
+
*/
|
|
2390
|
+
tokenProvider?: () => Promise<string>;
|
|
2348
2391
|
/** Auto-reconnect on disconnect (default: true) */
|
|
2349
2392
|
autoReconnect?: boolean;
|
|
2350
2393
|
/** Maximum reconnect attempts (default: 10) */
|
|
@@ -2387,6 +2430,8 @@ interface FullRealtimeOptions extends SpooledRealtimeOptions {
|
|
|
2387
2430
|
baseUrl: string;
|
|
2388
2431
|
wsUrl: string;
|
|
2389
2432
|
token: string;
|
|
2433
|
+
/** Async provider that mints a fresh JWT for each (re)connect */
|
|
2434
|
+
tokenProvider?: () => Promise<string>;
|
|
2390
2435
|
debug?: (message: string, meta?: unknown) => void;
|
|
2391
2436
|
}
|
|
2392
2437
|
/** 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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
*/
|
|
@@ -1055,7 +1086,11 @@ interface CreateScheduleParams {
|
|
|
1055
1086
|
timezone?: string;
|
|
1056
1087
|
/** Target queue name */
|
|
1057
1088
|
queueName: string;
|
|
1058
|
-
/**
|
|
1089
|
+
/**
|
|
1090
|
+
* Job payload template. Required: the API rejects a create without it
|
|
1091
|
+
* (HTTP 422), so this is typed as non-optional to surface the omission at
|
|
1092
|
+
* compile time rather than as a runtime 422.
|
|
1093
|
+
*/
|
|
1059
1094
|
payloadTemplate: JsonObject;
|
|
1060
1095
|
/** Job priority (-100 to 100) */
|
|
1061
1096
|
priority?: number;
|
|
@@ -2343,8 +2378,16 @@ interface RealtimeConnectionOptions {
|
|
|
2343
2378
|
baseUrl: string;
|
|
2344
2379
|
/** WebSocket URL for realtime connection (e.g., wss://api.spooled.cloud) - used for WebSocket */
|
|
2345
2380
|
wsUrl: string;
|
|
2346
|
-
/** JWT token for authentication */
|
|
2381
|
+
/** JWT token for authentication (used as a fallback when tokenProvider is not supplied) */
|
|
2347
2382
|
token: string;
|
|
2383
|
+
/**
|
|
2384
|
+
* Async provider that mints a fresh JWT for each (re)connect.
|
|
2385
|
+
*
|
|
2386
|
+
* JWTs are short-lived, so a static token captured once will be stale by
|
|
2387
|
+
* the time a long-running connection needs to reconnect. When provided,
|
|
2388
|
+
* this is invoked before every connect/reconnect to obtain a valid token.
|
|
2389
|
+
*/
|
|
2390
|
+
tokenProvider?: () => Promise<string>;
|
|
2348
2391
|
/** Auto-reconnect on disconnect (default: true) */
|
|
2349
2392
|
autoReconnect?: boolean;
|
|
2350
2393
|
/** Maximum reconnect attempts (default: 10) */
|
|
@@ -2387,6 +2430,8 @@ interface FullRealtimeOptions extends SpooledRealtimeOptions {
|
|
|
2387
2430
|
baseUrl: string;
|
|
2388
2431
|
wsUrl: string;
|
|
2389
2432
|
token: string;
|
|
2433
|
+
/** Async provider that mints a fresh JWT for each (re)connect */
|
|
2434
|
+
tokenProvider?: () => Promise<string>;
|
|
2390
2435
|
debug?: (message: string, meta?: unknown) => void;
|
|
2391
2436
|
}
|
|
2392
2437
|
/** 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
|
|
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
|
|
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(
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
@@ -1790,13 +1857,16 @@ var WebSocketRealtimeClient = class {
|
|
|
1790
1857
|
stateChangeHandlers = /* @__PURE__ */ new Set();
|
|
1791
1858
|
constructor(options) {
|
|
1792
1859
|
this.options = {
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1860
|
+
...options,
|
|
1861
|
+
autoReconnect: options.autoReconnect ?? true,
|
|
1862
|
+
maxReconnectAttempts: options.maxReconnectAttempts ?? 10,
|
|
1863
|
+
reconnectDelay: options.reconnectDelay ?? 1e3,
|
|
1864
|
+
maxReconnectDelay: options.maxReconnectDelay ?? 3e4,
|
|
1865
|
+
// Always resolve to a provider so reconnects can mint a fresh JWT.
|
|
1866
|
+
// Falls back to the static token when no provider is supplied.
|
|
1867
|
+
tokenProvider: options.tokenProvider ?? (async () => options.token),
|
|
1868
|
+
debug: options.debug ?? (() => {
|
|
1869
|
+
})
|
|
1800
1870
|
};
|
|
1801
1871
|
}
|
|
1802
1872
|
/**
|
|
@@ -1823,8 +1893,14 @@ var WebSocketRealtimeClient = class {
|
|
|
1823
1893
|
return;
|
|
1824
1894
|
}
|
|
1825
1895
|
this.setState("connecting");
|
|
1826
|
-
|
|
1827
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
@@ -2074,13 +2155,22 @@ var SseRealtimeClient = class {
|
|
|
2074
2155
|
stateChangeHandlers = /* @__PURE__ */ new Set();
|
|
2075
2156
|
constructor(options) {
|
|
2076
2157
|
this.options = {
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2158
|
+
...options,
|
|
2159
|
+
autoReconnect: options.autoReconnect ?? true,
|
|
2160
|
+
maxReconnectAttempts: options.maxReconnectAttempts ?? 10,
|
|
2161
|
+
reconnectDelay: options.reconnectDelay ?? 1e3,
|
|
2162
|
+
maxReconnectDelay: options.maxReconnectDelay ?? 3e4,
|
|
2163
|
+
// Always resolve to a provider so reconnects can mint a fresh token.
|
|
2164
|
+
// Falls back to the static token when no provider is supplied.
|
|
2165
|
+
tokenProvider: options.tokenProvider ?? (async () => options.token),
|
|
2166
|
+
// Coalesce debug to a no-op AFTER the spread. A caller that omits debug
|
|
2167
|
+
// still passes `debug: undefined` explicitly (e.g. via SpooledRealtime),
|
|
2168
|
+
// and spreading that would overwrite a pre-spread default with undefined,
|
|
2169
|
+
// making connect() call `this.options.debug(...)` on undefined and throw
|
|
2170
|
+
// "this.options.debug is not a function". Guarding here keeps it callable
|
|
2171
|
+
// regardless of what the caller passes.
|
|
2172
|
+
debug: options.debug ?? (() => {
|
|
2173
|
+
})
|
|
2084
2174
|
};
|
|
2085
2175
|
}
|
|
2086
2176
|
/**
|
|
@@ -2112,9 +2202,15 @@ var SseRealtimeClient = class {
|
|
|
2112
2202
|
throw new Error("eventsource package is required for SSE in Node.js. Install it with: npm install eventsource");
|
|
2113
2203
|
}
|
|
2114
2204
|
}
|
|
2205
|
+
let token;
|
|
2206
|
+
try {
|
|
2207
|
+
token = await this.options.tokenProvider();
|
|
2208
|
+
} catch (error) {
|
|
2209
|
+
this.setState("disconnected");
|
|
2210
|
+
throw new Error(`Failed to acquire realtime token: ${error}`);
|
|
2211
|
+
}
|
|
2115
2212
|
return new Promise((resolve, reject) => {
|
|
2116
2213
|
try {
|
|
2117
|
-
const token = this.options.token;
|
|
2118
2214
|
this.eventSource = new EventSourceImpl(sseUrl, {
|
|
2119
2215
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2120
2216
|
fetch: (input, init) => fetch(input, {
|
|
@@ -2138,6 +2234,10 @@ var SseRealtimeClient = class {
|
|
|
2138
2234
|
this.options.debug("SSE error");
|
|
2139
2235
|
if (this.state === "connecting") {
|
|
2140
2236
|
this.setState("disconnected");
|
|
2237
|
+
if (this.eventSource) {
|
|
2238
|
+
this.eventSource.close();
|
|
2239
|
+
this.eventSource = null;
|
|
2240
|
+
}
|
|
2141
2241
|
reject(new Error("SSE connection failed"));
|
|
2142
2242
|
return;
|
|
2143
2243
|
}
|
|
@@ -2222,7 +2322,7 @@ var SseRealtimeClient = class {
|
|
|
2222
2322
|
}
|
|
2223
2323
|
handleMessage(data) {
|
|
2224
2324
|
try {
|
|
2225
|
-
const event = JSON.parse(data);
|
|
2325
|
+
const event = convertResponse(JSON.parse(data));
|
|
2226
2326
|
this.options.debug(`Received SSE event: ${event.type}`, event);
|
|
2227
2327
|
const handlers = this.eventHandlers.get(event.type);
|
|
2228
2328
|
if (handlers) {
|
|
@@ -2260,10 +2360,11 @@ var SseRealtimeClient = class {
|
|
|
2260
2360
|
scheduleReconnect() {
|
|
2261
2361
|
this.setState("reconnecting");
|
|
2262
2362
|
this.reconnectAttempts++;
|
|
2263
|
-
const
|
|
2363
|
+
const baseDelay = Math.min(
|
|
2264
2364
|
this.options.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
|
|
2265
2365
|
this.options.maxReconnectDelay
|
|
2266
2366
|
);
|
|
2367
|
+
const delay = Math.floor(baseDelay + Math.random() * baseDelay * 0.25);
|
|
2267
2368
|
this.options.debug(`Scheduling SSE reconnect attempt ${this.reconnectAttempts} in ${delay}ms`);
|
|
2268
2369
|
this.reconnectTimer = setTimeout(async () => {
|
|
2269
2370
|
try {
|
|
@@ -2284,6 +2385,7 @@ var SpooledRealtime = class {
|
|
|
2284
2385
|
baseUrl: options.baseUrl,
|
|
2285
2386
|
wsUrl: options.wsUrl,
|
|
2286
2387
|
token: options.token,
|
|
2388
|
+
tokenProvider: options.tokenProvider,
|
|
2287
2389
|
autoReconnect: options.autoReconnect,
|
|
2288
2390
|
maxReconnectAttempts: options.maxReconnectAttempts,
|
|
2289
2391
|
reconnectDelay: options.reconnectDelay,
|
|
@@ -2783,7 +2885,7 @@ var SpooledClient = class _SpooledClient {
|
|
|
2783
2885
|
if (this.config.accessToken && this.config.refreshToken && this.config.autoRefreshToken) {
|
|
2784
2886
|
this.http.setRefreshTokenFn(this.refreshAccessToken.bind(this));
|
|
2785
2887
|
}
|
|
2786
|
-
this.auth = new AuthResource(this.http);
|
|
2888
|
+
this.auth = new AuthResource(this.http, () => this.config.refreshToken);
|
|
2787
2889
|
this.jobs = new JobsResource(this.http);
|
|
2788
2890
|
this.queues = new QueuesResource(this.http);
|
|
2789
2891
|
this.workers = new WorkersResource(this.http);
|
|
@@ -2862,8 +2964,15 @@ var SpooledClient = class _SpooledClient {
|
|
|
2862
2964
|
baseUrl: this.config.baseUrl,
|
|
2863
2965
|
wsUrl: this.config.wsUrl,
|
|
2864
2966
|
token,
|
|
2967
|
+
// Provider so each (re)connect mints a fresh JWT via login/refresh,
|
|
2968
|
+
// rather than reusing the short-lived token captured above.
|
|
2969
|
+
tokenProvider: () => this.getJwtToken(),
|
|
2865
2970
|
...options,
|
|
2866
|
-
|
|
2971
|
+
// Only forward debug when the client actually has a logger, so we never
|
|
2972
|
+
// hand the realtime layer an explicit `debug: undefined`. The WS/SSE
|
|
2973
|
+
// constructors also guard against this, but keeping the undefined out of
|
|
2974
|
+
// the option object makes the intent clear at the call site.
|
|
2975
|
+
...this.config.debug ? { debug: this.config.debug } : {}
|
|
2867
2976
|
});
|
|
2868
2977
|
}
|
|
2869
2978
|
/**
|
|
@@ -3169,9 +3278,11 @@ var SpooledWorker = class {
|
|
|
3169
3278
|
this.waitForActiveJobs(),
|
|
3170
3279
|
this.sleep(this.options.shutdownTimeout)
|
|
3171
3280
|
]);
|
|
3172
|
-
|
|
3281
|
+
const remaining = Array.from(this.activeJobs.entries());
|
|
3282
|
+
for (const [jobId, active] of remaining) {
|
|
3173
3283
|
this.debug(`Force-failing job ${jobId} due to shutdown timeout`);
|
|
3174
3284
|
await this.failJob(active.job, "Worker shutdown timeout");
|
|
3285
|
+
this.cleanupJob(jobId);
|
|
3175
3286
|
}
|
|
3176
3287
|
}
|
|
3177
3288
|
if (this.workerId) {
|