@spooled/sdk 1.4.1 → 1.5.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-Bqgiu-NN.d.cts → index-CSWlguB8.d.cts} +42 -9
- package/dist/{index-Bqgiu-NN.d.ts → index-CSWlguB8.d.ts} +42 -9
- package/dist/index.cjs +248 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -8
- package/dist/index.d.ts +2 -8
- package/dist/index.js +248 -25
- package/dist/index.js.map +1 -1
- package/dist/worker/index.cjs +102 -10
- 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 +102 -10
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
package/dist/worker/index.js
CHANGED
|
@@ -1,7 +1,54 @@
|
|
|
1
1
|
import { hostname } from 'os';
|
|
2
2
|
|
|
3
3
|
// src/config.ts
|
|
4
|
-
var SDK_VERSION = "1.
|
|
4
|
+
var SDK_VERSION = "1.5.0";
|
|
5
|
+
|
|
6
|
+
// src/errors.ts
|
|
7
|
+
var SpooledError = class extends Error {
|
|
8
|
+
/** HTTP status code (if applicable) */
|
|
9
|
+
statusCode;
|
|
10
|
+
/** Error code from API response */
|
|
11
|
+
code;
|
|
12
|
+
/** Additional error details */
|
|
13
|
+
details;
|
|
14
|
+
/** Request ID for debugging */
|
|
15
|
+
requestId;
|
|
16
|
+
/** Original cause of the error */
|
|
17
|
+
cause;
|
|
18
|
+
constructor(message, statusCode = 0, code = "UNKNOWN_ERROR", details, requestId, cause) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "SpooledError";
|
|
21
|
+
this.statusCode = statusCode;
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.details = details;
|
|
24
|
+
this.requestId = requestId;
|
|
25
|
+
this.cause = cause;
|
|
26
|
+
if (Error.captureStackTrace) {
|
|
27
|
+
Error.captureStackTrace(this, this.constructor);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Whether this error is retryable.
|
|
32
|
+
*
|
|
33
|
+
* Only 408 (Request Timeout) is retryable among the plain 4xx statuses: the
|
|
34
|
+
* server gave up waiting for the request, so it never ran. Subclasses
|
|
35
|
+
* override this (5xx, network, timeout, rate limit).
|
|
36
|
+
*/
|
|
37
|
+
isRetryable() {
|
|
38
|
+
return this.statusCode === 408;
|
|
39
|
+
}
|
|
40
|
+
/** Convert to plain object for logging */
|
|
41
|
+
toJSON() {
|
|
42
|
+
return {
|
|
43
|
+
name: this.name,
|
|
44
|
+
message: this.message,
|
|
45
|
+
statusCode: this.statusCode,
|
|
46
|
+
code: this.code,
|
|
47
|
+
details: this.details,
|
|
48
|
+
requestId: this.requestId
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
};
|
|
5
52
|
|
|
6
53
|
// src/utils/failure.ts
|
|
7
54
|
var MAX_FAILURE_MESSAGE_CHARS = 2048;
|
|
@@ -312,6 +359,10 @@ var SpooledWorker = class {
|
|
|
312
359
|
};
|
|
313
360
|
try {
|
|
314
361
|
const outcome = await this.runHandler(activeJob, context);
|
|
362
|
+
if (activeJob.leaseLost) {
|
|
363
|
+
this.debug(`Job ${job.id} lost its lease; dropping late result`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
315
366
|
if (activeJob.settled) {
|
|
316
367
|
this.debug(`Job ${job.id} was already settled; dropping late result`);
|
|
317
368
|
return;
|
|
@@ -319,11 +370,14 @@ var SpooledWorker = class {
|
|
|
319
370
|
activeJob.settled = true;
|
|
320
371
|
switch (outcome.kind) {
|
|
321
372
|
case "returned":
|
|
322
|
-
activeJob.ack = this.completeJob(
|
|
373
|
+
activeJob.ack = this.completeJob(
|
|
374
|
+
activeJob,
|
|
375
|
+
outcome.value ?? void 0
|
|
376
|
+
);
|
|
323
377
|
break;
|
|
324
378
|
case "threw":
|
|
325
379
|
activeJob.ack = this.failJob(
|
|
326
|
-
|
|
380
|
+
activeJob,
|
|
327
381
|
outcome.error instanceof Error ? outcome.error.message : String(outcome.error)
|
|
328
382
|
);
|
|
329
383
|
break;
|
|
@@ -333,7 +387,7 @@ var SpooledWorker = class {
|
|
|
333
387
|
jobId: job.id,
|
|
334
388
|
queueName: job.queueName
|
|
335
389
|
});
|
|
336
|
-
activeJob.ack = this.failJob(
|
|
390
|
+
activeJob.ack = this.failJob(activeJob, JOB_TIMEOUT_MESSAGE);
|
|
337
391
|
break;
|
|
338
392
|
}
|
|
339
393
|
await activeJob.ack;
|
|
@@ -373,12 +427,13 @@ var SpooledWorker = class {
|
|
|
373
427
|
this.debug(`Force-failing job ${activeJob.job.id} due to shutdown timeout`);
|
|
374
428
|
activeJob.settled = true;
|
|
375
429
|
activeJob.abortController.abort(new Error(SHUTDOWN_TIMEOUT_MESSAGE));
|
|
376
|
-
activeJob.ack = this.failJob(activeJob
|
|
430
|
+
activeJob.ack = this.failJob(activeJob, SHUTDOWN_TIMEOUT_MESSAGE);
|
|
377
431
|
await activeJob.ack;
|
|
378
432
|
this.cleanupJob(activeJob);
|
|
379
433
|
}
|
|
380
|
-
async completeJob(
|
|
381
|
-
if (!this.workerId) return;
|
|
434
|
+
async completeJob(activeJob, result) {
|
|
435
|
+
if (!this.workerId || activeJob.leaseLost) return;
|
|
436
|
+
const { job } = activeJob;
|
|
382
437
|
try {
|
|
383
438
|
await this.client.jobs.complete(job.id, {
|
|
384
439
|
workerId: this.workerId,
|
|
@@ -391,12 +446,17 @@ var SpooledWorker = class {
|
|
|
391
446
|
result
|
|
392
447
|
});
|
|
393
448
|
} catch (error) {
|
|
449
|
+
if (isLeaseExpired(error)) {
|
|
450
|
+
this.noteLeaseLost(activeJob, error);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
394
453
|
this.debug(`Failed to complete job ${job.id}`, error);
|
|
395
454
|
this.emit("error", { error });
|
|
396
455
|
}
|
|
397
456
|
}
|
|
398
|
-
async failJob(
|
|
399
|
-
if (!this.workerId) return;
|
|
457
|
+
async failJob(activeJob, errorMessage) {
|
|
458
|
+
if (!this.workerId || activeJob.leaseLost) return;
|
|
459
|
+
const { job } = activeJob;
|
|
400
460
|
const willRetry = job.retryCount < job.maxRetries;
|
|
401
461
|
const message = coerceFailureMessage(errorMessage);
|
|
402
462
|
try {
|
|
@@ -412,6 +472,10 @@ var SpooledWorker = class {
|
|
|
412
472
|
willRetry
|
|
413
473
|
});
|
|
414
474
|
} catch (error) {
|
|
475
|
+
if (isLeaseExpired(error)) {
|
|
476
|
+
this.noteLeaseLost(activeJob, error);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
415
479
|
this.debug(`Failed to fail job ${job.id}`, error);
|
|
416
480
|
this.emit("error", { error });
|
|
417
481
|
}
|
|
@@ -436,7 +500,7 @@ var SpooledWorker = class {
|
|
|
436
500
|
}
|
|
437
501
|
}
|
|
438
502
|
async sendJobHeartbeat(activeJob) {
|
|
439
|
-
if (!this.workerId) return;
|
|
503
|
+
if (!this.workerId || activeJob.leaseLost) return;
|
|
440
504
|
const { job } = activeJob;
|
|
441
505
|
try {
|
|
442
506
|
await this.client.jobs.heartbeat(job.id, {
|
|
@@ -445,9 +509,34 @@ var SpooledWorker = class {
|
|
|
445
509
|
...job.leaseId != null && { leaseId: job.leaseId }
|
|
446
510
|
});
|
|
447
511
|
} catch (error) {
|
|
512
|
+
if (isLeaseExpired(error)) {
|
|
513
|
+
this.noteLeaseLost(activeJob, error);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
448
516
|
this.debug(`Job heartbeat failed for ${job.id}`, error);
|
|
449
517
|
}
|
|
450
518
|
}
|
|
519
|
+
/**
|
|
520
|
+
* The lease is gone. Abort the handler if it listens, stop renewing, and
|
|
521
|
+
* refuse a later complete/fail with the stale lease.
|
|
522
|
+
*/
|
|
523
|
+
noteLeaseLost(activeJob, error) {
|
|
524
|
+
if (activeJob.leaseLost) return;
|
|
525
|
+
activeJob.leaseLost = true;
|
|
526
|
+
if (activeJob.heartbeatTimer) {
|
|
527
|
+
clearInterval(activeJob.heartbeatTimer);
|
|
528
|
+
activeJob.heartbeatTimer = void 0;
|
|
529
|
+
}
|
|
530
|
+
this.clearTimeoutTimer(activeJob);
|
|
531
|
+
const reason = error instanceof Error ? error : new Error("Job lease expired");
|
|
532
|
+
activeJob.abortController.abort(reason);
|
|
533
|
+
this.debug(
|
|
534
|
+
`Job lease lost for ${activeJob.job.id}; stopping handler`,
|
|
535
|
+
error
|
|
536
|
+
);
|
|
537
|
+
activeJob.settled = true;
|
|
538
|
+
this.cleanupJob(activeJob);
|
|
539
|
+
}
|
|
451
540
|
async sendWorkerHeartbeat() {
|
|
452
541
|
if (!this.workerId || this.state !== "running" && this.state !== "stopping") {
|
|
453
542
|
return;
|
|
@@ -482,6 +571,9 @@ var SpooledWorker = class {
|
|
|
482
571
|
});
|
|
483
572
|
}
|
|
484
573
|
};
|
|
574
|
+
function isLeaseExpired(error) {
|
|
575
|
+
return error instanceof SpooledError && error.statusCode === 409 && error.code.toUpperCase() === "LEASE_EXPIRED";
|
|
576
|
+
}
|
|
485
577
|
|
|
486
578
|
export { SpooledWorker };
|
|
487
579
|
//# sourceMappingURL=index.js.map
|
package/dist/worker/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/config.ts","../../src/utils/failure.ts","../../src/worker/worker.ts"],"names":["osHostname"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * SDK Configuration\n *\n * Defines configuration types and defaults for the Spooled SDK.\n */\n\nimport type { CircuitBreakerOptions } from \"./utils/circuit-breaker.js\";\n\n/** Retry configuration options */\nexport interface RetryConfig {\n /** Maximum number of retry attempts (default: 3) */\n maxRetries: number;\n /** Base delay in milliseconds for exponential backoff (default: 1000) */\n baseDelay: number;\n /** Maximum delay in milliseconds (default: 30000) */\n maxDelay: number;\n /** Exponential backoff factor (default: 2) */\n factor: number;\n /** Whether to add jitter to delays (default: true) */\n jitter: boolean;\n /** Custom function to determine if an error is retryable */\n retryOn?: (error: Error, attempt: number) => boolean;\n}\n\n/** Circuit breaker configuration options */\nexport interface CircuitBreakerConfig {\n /** Whether circuit breaker is enabled (default: true) */\n enabled: boolean;\n /** Number of failures before opening the circuit (default: 5) */\n failureThreshold: number;\n /** Number of successes in half-open state to close circuit (default: 3) */\n successThreshold: number;\n /** Time in milliseconds to wait before trying again after opening (default: 30000) */\n timeout: number;\n}\n\n/** Debug logging function type */\nexport type DebugFn = (message: string, meta?: unknown) => void;\n\n/** Main SDK configuration options */\nexport interface SpooledClientConfig {\n /** API key for authentication (starts with sp_live_, sp_test_, sk_live_, or sk_test_) */\n apiKey?: string;\n /** JWT access token (alternative to API key) */\n accessToken?: string;\n /** JWT refresh token for automatic token renewal */\n refreshToken?: string;\n /** Admin API key (for /api/v1/admin/* endpoints; uses X-Admin-Key header) */\n adminKey?: string;\n /** Base URL for the REST API (default: https://api.spooled.cloud) */\n baseUrl?: string;\n /** WebSocket URL for realtime events (default: derived from baseUrl, e.g., wss://api.spooled.cloud) */\n wsUrl?: string;\n /** gRPC server address (default: grpc.spooled.cloud:443) */\n grpcAddress?: string;\n /** Request timeout in milliseconds (default: 30000) */\n timeout?: number;\n /** Number of retry attempts (shorthand for retry.maxRetries) */\n retries?: number;\n /** Base retry delay in milliseconds (shorthand for retry.baseDelay) */\n retryDelay?: number;\n /** Full retry configuration */\n retry?: Partial<RetryConfig>;\n /**\n * Circuit breaker configuration. Besides the thresholds it takes\n * `isFailure` (which errors count as failures) and `onStateChange`.\n */\n circuitBreaker?: Partial<CircuitBreakerOptions>;\n /** Additional headers to include in all requests */\n headers?: Record<string, string>;\n /** Custom fetch implementation (for testing or special runtimes) */\n fetch?: typeof fetch;\n /** Custom user agent string */\n userAgent?: string;\n /** Enable debug logging */\n debug?: boolean | DebugFn;\n /** Whether to automatically refresh tokens when they expire (default: true) */\n autoRefreshToken?: boolean;\n}\n\n/** Resolved configuration with all defaults applied */\nexport interface ResolvedConfig {\n apiKey?: string;\n accessToken?: string;\n refreshToken?: string;\n adminKey?: string;\n baseUrl: string;\n wsUrl: string;\n grpcAddress: string;\n timeout: number;\n retry: RetryConfig;\n circuitBreaker: CircuitBreakerOptions;\n headers: Record<string, string>;\n fetch: typeof fetch;\n userAgent: string;\n debug: DebugFn | null;\n autoRefreshToken: boolean;\n}\n\n/** Default configuration values */\nexport const DEFAULT_CONFIG = {\n baseUrl: \"https://api.spooled.cloud\",\n wsUrl: \"wss://api.spooled.cloud\",\n grpcAddress: \"grpc.spooled.cloud:443\",\n timeout: 30000,\n retry: {\n maxRetries: 3,\n baseDelay: 1000,\n maxDelay: 30000,\n factor: 2,\n jitter: true,\n } satisfies RetryConfig,\n circuitBreaker: {\n enabled: true,\n failureThreshold: 5,\n successThreshold: 3,\n timeout: 30000,\n } satisfies CircuitBreakerConfig,\n userAgent: \"@spooled/sdk-nodejs/1.4.1\",\n autoRefreshToken: true,\n} as const;\n\n/** SDK version */\nexport const SDK_VERSION = \"1.4.1\";\n\n/** API version prefix */\nexport const API_VERSION = \"v1\";\n\n/** API base path */\nexport const API_BASE_PATH = `/api/${API_VERSION}`;\n\n/**\n * Trim surrounding whitespace from a credential string and treat an\n * all-whitespace input as unset. Mirrors the trim policy in the Go, PHP,\n * and Python SDKs so behavior is consistent across languages.\n */\nfunction normalizeCredential(value: string | undefined): string | undefined {\n if (value === undefined) {\n return undefined;\n }\n const trimmed = value.trim();\n return trimmed === \"\" ? undefined : trimmed;\n}\n\n/**\n * Resolve configuration by merging user options with defaults\n */\nexport function resolveConfig(options: SpooledClientConfig): ResolvedConfig {\n // Create debug function\n let debugFn: DebugFn | null = null;\n if (options.debug === true) {\n debugFn = (message: string, meta?: unknown) => {\n // eslint-disable-next-line no-console\n console.log(`[spooled-sdk] ${message}`, meta !== undefined ? meta : \"\");\n };\n } else if (typeof options.debug === \"function\") {\n debugFn = options.debug;\n }\n\n // Merge retry config\n const retry: RetryConfig = {\n ...DEFAULT_CONFIG.retry,\n ...options.retry,\n // Allow shorthand overrides\n ...(options.retries !== undefined && { maxRetries: options.retries }),\n ...(options.retryDelay !== undefined && { baseDelay: options.retryDelay }),\n };\n\n // Merge circuit breaker config\n const circuitBreaker: CircuitBreakerOptions = {\n ...DEFAULT_CONFIG.circuitBreaker,\n ...options.circuitBreaker,\n };\n\n // Build headers\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n ...options.headers,\n };\n\n // Resolve base URL first\n const baseUrl = options.baseUrl ?? DEFAULT_CONFIG.baseUrl;\n\n // Derive WebSocket URL from base URL if not explicitly set\n // Replace http(s):// with ws(s)://\n const derivedWsUrl = baseUrl.replace(/^http/, \"ws\");\n const wsUrl = options.wsUrl ?? derivedWsUrl;\n\n // Trim whitespace (including a stray trailing '\\n') from every credential.\n // Node's `undici` fetch rejects `Authorization: Bearer <key>\\n` with an\n // opaque `TypeError: HeaderValue` and users routinely paste keys from\n // `.env` files that include a newline; normalizing here keeps behavior\n // consistent with the Go, PHP, and Python SDKs.\n return {\n apiKey: normalizeCredential(options.apiKey),\n accessToken: normalizeCredential(options.accessToken),\n refreshToken: normalizeCredential(options.refreshToken),\n adminKey: normalizeCredential(options.adminKey),\n baseUrl,\n wsUrl,\n grpcAddress: options.grpcAddress ?? DEFAULT_CONFIG.grpcAddress,\n timeout: options.timeout ?? DEFAULT_CONFIG.timeout,\n retry,\n circuitBreaker,\n headers,\n fetch: options.fetch ?? globalThis.fetch,\n userAgent: options.userAgent ?? DEFAULT_CONFIG.userAgent,\n debug: debugFn,\n autoRefreshToken:\n options.autoRefreshToken ?? DEFAULT_CONFIG.autoRefreshToken,\n };\n}\n\n/**\n * Validate configuration options\n * @throws Error if configuration is invalid\n */\nexport function validateConfig(config: ResolvedConfig): void {\n // Must have at least one auth method. An admin key alone is enough: the\n // /admin/* endpoints authenticate with X-Admin-Key only, so an admin-only\n // client (`client.admin.*`) is legitimate.\n if (!config.apiKey && !config.accessToken && !config.adminKey) {\n throw new Error(\n \"SpooledClient requires either apiKey or accessToken for authentication (or adminKey for admin-only use)\",\n );\n }\n\n // Validate API key format if provided\n // Accept both sp_ (new standard) and sk_ (legacy) prefixes\n if (\n config.apiKey &&\n !config.apiKey.startsWith(\"sp_\") &&\n !config.apiKey.startsWith(\"sk_\")\n ) {\n throw new Error(\n \"Invalid API key format. API keys should start with sp_live_, sp_test_, sk_live_, or sk_test_\",\n );\n }\n\n // Validate baseUrl\n if (\n !config.baseUrl.startsWith(\"http://\") &&\n !config.baseUrl.startsWith(\"https://\")\n ) {\n throw new Error(\"baseUrl must start with http:// or https://\");\n }\n\n // Validate timeout\n if (config.timeout <= 0) {\n throw new Error(\"timeout must be a positive number\");\n }\n\n // Validate retry config\n if (config.retry.maxRetries < 0) {\n throw new Error(\"retry.maxRetries must be non-negative\");\n }\n if (config.retry.baseDelay <= 0) {\n throw new Error(\"retry.baseDelay must be positive\");\n }\n if (config.retry.maxDelay < config.retry.baseDelay) {\n throw new Error(\"retry.maxDelay must be >= retry.baseDelay\");\n }\n if (config.retry.factor < 1) {\n throw new Error(\"retry.factor must be >= 1\");\n }\n\n // Validate circuit breaker config\n if (config.circuitBreaker.enabled) {\n if (config.circuitBreaker.failureThreshold <= 0) {\n throw new Error(\"circuitBreaker.failureThreshold must be positive\");\n }\n if (config.circuitBreaker.successThreshold <= 0) {\n throw new Error(\"circuitBreaker.successThreshold must be positive\");\n }\n if (config.circuitBreaker.timeout <= 0) {\n throw new Error(\"circuitBreaker.timeout must be positive\");\n }\n }\n}\n","/**\n * Failure message coercion\n *\n * `POST /jobs/{id}/fail` rejects an empty or over-long `error` with a 400.\n * A worker that reported its failure with such text got nothing recorded: the\n * job stayed `processing` until its lease ran out, then ran again. The text of\n * a failure is therefore always made acceptable before it is sent.\n */\n\n/** Longest error text the API accepts (characters, not bytes). */\nexport const MAX_FAILURE_MESSAGE_CHARS = 2048;\n\n/** Text sent when a handler failed without saying why. */\nexport const DEFAULT_FAILURE_MESSAGE = \"Job failed\";\n\n/**\n * Make `message` acceptable to the fail endpoint: empty becomes\n * {@link DEFAULT_FAILURE_MESSAGE}; text over {@link MAX_FAILURE_MESSAGE_CHARS}\n * is cut to that many characters. The cut is by code point, so an emoji or\n * other astral character is never split into a lone surrogate.\n */\nexport function coerceFailureMessage(message: unknown): string {\n const text =\n typeof message === \"string\" ? message : message == null ? \"\" : `${message}`;\n if (text === \"\") {\n return DEFAULT_FAILURE_MESSAGE;\n }\n // UTF-16 length is an upper bound of the code point count: skip the\n // allocation for the common short message.\n if (text.length <= MAX_FAILURE_MESSAGE_CHARS) {\n return text;\n }\n const codePoints = Array.from(text);\n return codePoints.length <= MAX_FAILURE_MESSAGE_CHARS\n ? text\n : codePoints.slice(0, MAX_FAILURE_MESSAGE_CHARS).join(\"\");\n}\n","/**\n * Spooled Worker\n *\n * Worker runtime for processing jobs from a queue.\n */\n\nimport type { SpooledClient } from \"../client.js\";\nimport { SDK_VERSION } from \"../config.js\";\nimport type { JsonValue } from \"../types/common.js\";\nimport type { ClaimedJob } from \"../types/jobs.js\";\nimport { coerceFailureMessage } from \"../utils/failure.js\";\nimport type {\n SpooledWorkerOptions,\n WorkerState,\n JobHandler,\n JobContext,\n WorkerEvent,\n WorkerEventData,\n ActiveJob,\n} from \"./types.js\";\nimport { hostname as osHostname } from \"os\";\n\n/** Default options */\nconst DEFAULT_OPTIONS = {\n concurrency: 5,\n pollInterval: 1000,\n leaseDuration: 30,\n heartbeatFraction: 0.5,\n autoStart: false,\n shutdownTimeout: 30000,\n} as const;\n\n/** Event handler type */\ntype EventHandler<E extends WorkerEvent> = (data: WorkerEventData[E]) => void;\n\n/** Failure text sent when a handler outlives the job's `timeoutSeconds`. */\nconst JOB_TIMEOUT_MESSAGE = \"Job timeout exceeded\";\n\n/** Failure text sent for a handler still running when the shutdown grace ends. */\nconst SHUTDOWN_TIMEOUT_MESSAGE = \"Worker shutdown timeout\";\n\n/** Longest delay `setTimeout` accepts (a larger one fires immediately). */\nconst MAX_TIMER_MS = 2 ** 31 - 1;\n\n/** How a handler run ended. Decided once per execution. */\ntype HandlerOutcome =\n | { kind: \"returned\"; value: JsonValue | void }\n | { kind: \"threw\"; error: unknown }\n | { kind: \"timeout\" };\n\n/**\n * Spooled Worker\n *\n * @example\n * ```typescript\n * const worker = new SpooledWorker(client, {\n * queueName: 'my-queue',\n * concurrency: 10,\n * });\n *\n * worker.process(async (ctx) => {\n * console.log('Processing job:', ctx.jobId);\n * await doSomeWork(ctx.payload);\n * return { success: true };\n * });\n *\n * await worker.start();\n *\n * // Graceful shutdown\n * process.on('SIGTERM', () => worker.stop());\n * ```\n */\nexport class SpooledWorker {\n private readonly client: SpooledClient;\n /**\n * Options with every defaultable field filled in. `workerId` stays optional:\n * there is no sensible default for it, and omitting it is what makes the\n * server mint a UUID.\n */\n private readonly options: Required<Omit<SpooledWorkerOptions, \"workerId\">> &\n Pick<SpooledWorkerOptions, \"workerId\">;\n private readonly debug: (msg: string, meta?: unknown) => void;\n\n private state: WorkerState = \"idle\";\n private workerId: string | null = null;\n private handler: JobHandler | null = null;\n private activeJobs: Map<symbol, ActiveJob> = new Map();\n private pollTimer: ReturnType<typeof setTimeout> | null = null;\n private workerHeartbeatTimer: ReturnType<typeof setInterval> | null = null;\n private shutdownPromise: Promise<void> | null = null;\n /** The claim request currently on the wire, so a stop can wait for its jobs. */\n private pollInFlight: Promise<void> | null = null;\n /** Wakes the shutdown drain when the last active job has been acked. */\n private drainWaiter: (() => void) | null = null;\n\n // Event handlers\n private eventHandlers: Map<WorkerEvent, Set<EventHandler<WorkerEvent>>> =\n new Map();\n\n constructor(client: SpooledClient, options: SpooledWorkerOptions) {\n this.client = client;\n this.options = {\n ...DEFAULT_OPTIONS,\n hostname: osHostname(),\n workerType: \"nodejs\",\n version: SDK_VERSION,\n metadata: {},\n ...options,\n } as Required<Omit<SpooledWorkerOptions, \"workerId\">> &\n Pick<SpooledWorkerOptions, \"workerId\">;\n\n // Get debug from client config\n const config = client.getConfig();\n this.debug = config.debug ?? (() => {});\n\n if (this.options.autoStart) {\n // Defer to next tick to allow process() to be called. start() already\n // reports a failed registration through the \"error\" event; the catch only\n // keeps it from becoming an unhandled rejection that crashes the process.\n setImmediate(() => {\n this.start().catch((error) => {\n this.debug(\"Auto-start failed\", error);\n });\n });\n }\n }\n\n /**\n * Get current worker state\n */\n getState(): WorkerState {\n return this.state;\n }\n\n /**\n * Get worker ID (available after start)\n */\n getWorkerId(): string | null {\n return this.workerId;\n }\n\n /**\n * Get number of active jobs\n */\n getActiveJobCount(): number {\n return this.activeJobs.size;\n }\n\n /**\n * Register job handler\n */\n process(handler: JobHandler): void {\n if (this.state !== \"idle\") {\n throw new Error(\"Cannot set handler after worker has started\");\n }\n this.handler = handler;\n }\n\n /**\n * Start the worker\n */\n async start(): Promise<void> {\n if (this.state !== \"idle\") {\n throw new Error(`Cannot start worker in state: ${this.state}`);\n }\n\n if (!this.handler) {\n throw new Error(\"No job handler registered. Call process() first.\");\n }\n\n this.state = \"starting\";\n this.debug(`Starting worker for queue: ${this.options.queueName}`);\n\n try {\n // Register with the API\n const registration = await this.client.workers.register({\n queueName: this.options.queueName,\n workerId: this.options.workerId,\n hostname: this.options.hostname,\n workerType: this.options.workerType,\n maxConcurrency: this.options.concurrency,\n metadata: this.options.metadata,\n version: this.options.version,\n });\n\n this.workerId = registration.id;\n this.debug(`Worker registered: ${this.workerId}`, registration);\n\n // Start worker heartbeat\n const heartbeatInterval = registration.heartbeatIntervalSecs * 1000;\n this.workerHeartbeatTimer = setInterval(() => {\n this.sendWorkerHeartbeat();\n }, heartbeatInterval);\n\n // Start polling\n this.state = \"running\";\n this.emit(\"started\", {\n workerId: this.workerId,\n queueName: this.options.queueName,\n });\n this.schedulePoll();\n } catch (error) {\n this.state = \"error\";\n this.emit(\"error\", { error: error as Error });\n throw error;\n }\n }\n\n /**\n * Stop the worker gracefully.\n *\n * Drains: the worker stops claiming, then lets running handlers finish for\n * up to `shutdownTimeout`. A handler that returns completes its job and one\n * that throws fails it, even though a stop was requested meanwhile; its\n * `signal` is NOT aborted during the grace period. Only handlers still\n * running when the timeout ends are aborted and failed with \"Worker shutdown\n * timeout\". The worker deregisters last, because deregistering returns every\n * job the worker still holds to the queue.\n */\n async stop(): Promise<void> {\n if (this.state !== \"running\") {\n return;\n }\n\n // Deduplicate concurrent stop calls\n if (this.shutdownPromise) {\n return this.shutdownPromise;\n }\n\n this.shutdownPromise = this.doStop();\n return this.shutdownPromise;\n }\n\n private async doStop(): Promise<void> {\n const deadline = Date.now() + this.options.shutdownTimeout;\n this.state = \"stopping\";\n this.debug(\"Stopping worker...\");\n\n // Stop polling\n if (this.pollTimer) {\n clearTimeout(this.pollTimer);\n this.pollTimer = null;\n }\n\n // A claim already on the wire still returns jobs this worker now holds\n // leases for. Let it land first so they join the drain below instead of\n // starting after the worker has deregistered.\n if (this.pollInFlight) {\n await this.pollInFlight;\n }\n\n // Drain: the worker heartbeat and every job heartbeat keep running while\n // handlers finish, so neither the worker nor a lease expires mid-drain.\n if (this.activeJobs.size > 0) {\n this.debug(\n `Waiting for ${this.activeJobs.size} active jobs to complete...`,\n );\n\n await this.waitForDrain(Math.max(0, deadline - Date.now()));\n\n // Whatever is still running outlived the grace period: abort and fail it.\n // A job whose handler returned just before the deadline is already\n // settled (its ack is in flight) and is awaited below, not failed.\n const stragglers = Array.from(this.activeJobs.values()).filter(\n (active) => !active.settled,\n );\n await Promise.all(stragglers.map((active) => this.abandonJob(active)));\n\n // Acks already on the wire must land before the worker deregisters,\n // otherwise deregistering re-queues a job that was just finished.\n await Promise.all(\n Array.from(this.activeJobs.values()).map((active) => active.ack),\n );\n }\n\n // Stop worker heartbeat\n if (this.workerHeartbeatTimer) {\n clearInterval(this.workerHeartbeatTimer);\n this.workerHeartbeatTimer = null;\n }\n\n // Deregister worker\n const workerId = this.workerId;\n if (workerId) {\n try {\n await this.client.workers.deregister(workerId);\n this.debug(\"Worker deregistered\");\n } catch (error) {\n this.debug(\"Failed to deregister worker\", error);\n }\n }\n\n this.state = \"stopped\";\n this.workerId = null;\n this.emit(\"stopped\", { workerId: workerId ?? \"\", reason: \"graceful\" });\n this.shutdownPromise = null;\n }\n\n /**\n * Add event listener\n */\n on<E extends WorkerEvent>(event: E, handler: EventHandler<E>): () => void {\n if (!this.eventHandlers.has(event)) {\n this.eventHandlers.set(event, new Set());\n }\n this.eventHandlers.get(event)!.add(handler as EventHandler<WorkerEvent>);\n return () => this.off(event, handler);\n }\n\n /**\n * Remove event listener\n */\n off<E extends WorkerEvent>(event: E, handler: EventHandler<E>): void {\n const handlers = this.eventHandlers.get(event);\n if (handlers) {\n handlers.delete(handler as EventHandler<WorkerEvent>);\n }\n }\n\n // Private methods\n\n private emit<E extends WorkerEvent>(\n event: E,\n data: WorkerEventData[E],\n ): void {\n const handlers = this.eventHandlers.get(event);\n if (handlers) {\n handlers.forEach((handler) => {\n try {\n handler(data);\n } catch (error) {\n this.debug(\"Event handler error\", { event, error });\n }\n });\n }\n }\n\n private schedulePoll(): void {\n if (this.state !== \"running\") {\n return;\n }\n\n this.pollTimer = setTimeout(() => {\n this.pollTimer = null;\n const run = this.poll();\n this.pollInFlight = run;\n const done = () => {\n if (this.pollInFlight === run) {\n this.pollInFlight = null;\n }\n };\n run.then(done, done);\n }, this.options.pollInterval);\n }\n\n private async poll(): Promise<void> {\n if (this.state !== \"running\" || !this.workerId) {\n return;\n }\n\n // Check if we have capacity\n const availableSlots = this.options.concurrency - this.activeJobs.size;\n if (availableSlots <= 0) {\n this.schedulePoll();\n return;\n }\n\n try {\n // Claim jobs\n const result = await this.client.jobs.claim({\n queueName: this.options.queueName,\n workerId: this.workerId,\n limit: availableSlots,\n leaseDurationSecs: this.options.leaseDuration,\n });\n\n // Process claimed jobs\n for (const job of result.jobs) {\n this.processJob(job);\n }\n } catch (error) {\n this.debug(\"Poll failed\", error);\n this.emit(\"error\", { error: error as Error });\n }\n\n // Schedule next poll\n this.schedulePoll();\n }\n\n private processJob(job: ClaimedJob): void {\n this.emit(\"job:claimed\", { jobId: job.id, queueName: job.queueName });\n\n const abortController = new AbortController();\n const activeJob: ActiveJob = {\n executionId: Symbol(job.id),\n job,\n startedAt: new Date(),\n abortController,\n settled: false,\n };\n\n this.activeJobs.set(activeJob.executionId, activeJob);\n\n // Capture the immutable execution so a replacement lease cannot be borrowed.\n const heartbeatInterval =\n this.options.leaseDuration * this.options.heartbeatFraction * 1000;\n activeJob.heartbeatTimer = setInterval(() => {\n this.sendJobHeartbeat(activeJob);\n }, heartbeatInterval);\n\n // Execute handler\n this.executeHandler(activeJob);\n }\n\n private async executeHandler(activeJob: ActiveJob): Promise<void> {\n const { job, abortController } = activeJob;\n const { signal } = abortController;\n\n this.emit(\"job:started\", { jobId: job.id, queueName: job.queueName });\n\n const context: JobContext = {\n jobId: job.id,\n queueName: job.queueName,\n payload: job.payload,\n retryCount: job.retryCount,\n maxRetries: job.maxRetries,\n signal,\n progress: async (percent: number, message?: string) => {\n const boundedPercent = Math.max(0, Math.min(100, percent));\n this.debug(`[${job.id}] progress ${boundedPercent}%`, message);\n },\n log: (level, message, meta) => {\n this.debug(`[${job.id}] [${level}] ${message}`, meta);\n },\n };\n\n try {\n const outcome = await this.runHandler(activeJob, context);\n\n // The shutdown drain already failed this job while its handler was still\n // running; the lease is gone, so a late result must not be sent.\n if (activeJob.settled) {\n this.debug(`Job ${job.id} was already settled; dropping late result`);\n return;\n }\n activeJob.settled = true;\n\n // The abort signal is only a hint to the handler. Whatever the handler\n // did decides the ack: a result that arrives after a stop was requested\n // is still sent, never silently dropped.\n switch (outcome.kind) {\n case \"returned\":\n activeJob.ack = this.completeJob(job, outcome.value ?? undefined);\n break;\n case \"threw\":\n activeJob.ack = this.failJob(\n job,\n outcome.error instanceof Error\n ? outcome.error.message\n : String(outcome.error),\n );\n break;\n case \"timeout\":\n // Stop the handler if it listens, then fail the job: extending the\n // lease of a hung handler forever would keep the job and its\n // concurrency slot stuck.\n abortController.abort(new Error(JOB_TIMEOUT_MESSAGE));\n this.emit(\"job:timeout\", {\n jobId: job.id,\n queueName: job.queueName,\n });\n activeJob.ack = this.failJob(job, JOB_TIMEOUT_MESSAGE);\n break;\n }\n await activeJob.ack;\n } finally {\n this.cleanupJob(activeJob);\n }\n }\n\n /**\n * Run the handler, racing it against the job's `timeoutSeconds`.\n * Never rejects: a throw (sync or async) is reported as an outcome.\n */\n private runHandler(\n activeJob: ActiveJob,\n context: JobContext,\n ): Promise<HandlerOutcome> {\n const run = (async (): Promise<HandlerOutcome> => {\n try {\n return { kind: \"returned\", value: await this.handler!(context) };\n } catch (error) {\n return { kind: \"threw\", error };\n }\n })();\n\n const { timeoutSeconds } = activeJob.job;\n if (typeof timeoutSeconds !== \"number\" || !(timeoutSeconds > 0)) {\n return run;\n }\n\n return new Promise<HandlerOutcome>((resolve) => {\n activeJob.timeoutTimer = setTimeout(\n () => resolve({ kind: \"timeout\" }),\n Math.min(timeoutSeconds * 1000, MAX_TIMER_MS),\n );\n run.then((outcome) => {\n this.clearTimeoutTimer(activeJob);\n resolve(outcome);\n });\n });\n }\n\n /** Abort a handler that outlived the shutdown grace period and fail its job. */\n private async abandonJob(activeJob: ActiveJob): Promise<void> {\n this.debug(`Force-failing job ${activeJob.job.id} due to shutdown timeout`);\n // Mark first: the handler may react to the abort, or finish later, and its\n // outcome must not be acked on top of this failure.\n activeJob.settled = true;\n activeJob.abortController.abort(new Error(SHUTDOWN_TIMEOUT_MESSAGE));\n activeJob.ack = this.failJob(activeJob.job, SHUTDOWN_TIMEOUT_MESSAGE);\n await activeJob.ack;\n // The handler's finally may never run if user code ignores the abort\n // signal, so explicitly clear this exact execution's timers.\n this.cleanupJob(activeJob);\n }\n\n private async completeJob(\n job: ClaimedJob,\n result?: JsonValue,\n ): Promise<void> {\n if (!this.workerId) return;\n\n try {\n await this.client.jobs.complete(job.id, {\n workerId: this.workerId,\n result,\n ...(job.leaseId != null && { leaseId: job.leaseId }),\n });\n\n this.emit(\"job:completed\", {\n jobId: job.id,\n queueName: job.queueName,\n result,\n });\n } catch (error) {\n // The ack was already retried by the HTTP layer; what is left is a job\n // that finished but could not be reported.\n this.debug(`Failed to complete job ${job.id}`, error);\n this.emit(\"error\", { error: error as Error });\n }\n }\n\n private async failJob(job: ClaimedJob, errorMessage: string): Promise<void> {\n if (!this.workerId) return;\n\n const willRetry = job.retryCount < job.maxRetries;\n // The API rejects empty or over-long failure text with a 400, which would\n // leave the job processing until its lease expired.\n const message = coerceFailureMessage(errorMessage);\n\n try {\n await this.client.jobs.fail(job.id, {\n workerId: this.workerId,\n error: message,\n ...(job.leaseId != null && { leaseId: job.leaseId }),\n });\n\n this.emit(\"job:failed\", {\n jobId: job.id,\n queueName: job.queueName,\n error: message,\n willRetry,\n });\n } catch (error) {\n this.debug(`Failed to fail job ${job.id}`, error);\n this.emit(\"error\", { error: error as Error });\n }\n }\n\n private clearTimeoutTimer(activeJob: ActiveJob): void {\n if (activeJob.timeoutTimer) {\n clearTimeout(activeJob.timeoutTimer);\n activeJob.timeoutTimer = undefined;\n }\n }\n\n private cleanupJob(activeJob: ActiveJob): void {\n if (activeJob.heartbeatTimer) {\n clearInterval(activeJob.heartbeatTimer);\n activeJob.heartbeatTimer = undefined;\n }\n this.clearTimeoutTimer(activeJob);\n\n if (this.activeJobs.get(activeJob.executionId) === activeJob) {\n this.activeJobs.delete(activeJob.executionId);\n if (this.activeJobs.size === 0) {\n this.drainWaiter?.();\n }\n }\n }\n\n private async sendJobHeartbeat(activeJob: ActiveJob): Promise<void> {\n if (!this.workerId) return;\n\n const { job } = activeJob;\n\n try {\n await this.client.jobs.heartbeat(job.id, {\n workerId: this.workerId,\n leaseDurationSecs: this.options.leaseDuration,\n ...(job.leaseId != null && { leaseId: job.leaseId }),\n });\n } catch (error) {\n this.debug(`Job heartbeat failed for ${job.id}`, error);\n }\n }\n\n private async sendWorkerHeartbeat(): Promise<void> {\n // Keep beating through the shutdown drain: a long `shutdownTimeout` would\n // otherwise let the stale-worker reaper take the worker (and its jobs).\n if (\n !this.workerId ||\n (this.state !== \"running\" && this.state !== \"stopping\")\n ) {\n return;\n }\n\n try {\n await this.client.workers.heartbeat(this.workerId, {\n currentJobs: this.activeJobs.size,\n status: \"healthy\",\n });\n } catch (error) {\n this.debug(\"Worker heartbeat failed\", error);\n }\n }\n\n /**\n * Resolve once no job is active, or after `timeoutMs`. The timer is always\n * cleared, so a clean stop leaves nothing that keeps the process alive.\n */\n private waitForDrain(timeoutMs: number): Promise<void> {\n if (this.activeJobs.size === 0) {\n return Promise.resolve();\n }\n return new Promise<void>((resolve) => {\n const timer = setTimeout(() => {\n this.drainWaiter = null;\n resolve();\n }, timeoutMs);\n this.drainWaiter = () => {\n clearTimeout(timer);\n this.drainWaiter = null;\n resolve();\n };\n });\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/config.ts","../../src/errors.ts","../../src/utils/failure.ts","../../src/worker/worker.ts"],"names":["osHostname"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * SDK Configuration\n *\n * Defines configuration types and defaults for the Spooled SDK.\n */\n\nimport type { CircuitBreakerOptions } from \"./utils/circuit-breaker.js\";\n\n/** Retry configuration options */\nexport interface RetryConfig {\n /** Maximum number of retry attempts (default: 3) */\n maxRetries: number;\n /** Base delay in milliseconds for exponential backoff (default: 1000) */\n baseDelay: number;\n /** Maximum delay in milliseconds (default: 30000) */\n maxDelay: number;\n /** Exponential backoff factor (default: 2) */\n factor: number;\n /** Whether to add jitter to delays (default: true) */\n jitter: boolean;\n /** Custom function to determine if an error is retryable */\n retryOn?: (error: Error, attempt: number) => boolean;\n}\n\n/** Circuit breaker configuration options */\nexport interface CircuitBreakerConfig {\n /** Whether circuit breaker is enabled (default: true) */\n enabled: boolean;\n /** Number of failures before opening the circuit (default: 5) */\n failureThreshold: number;\n /** Number of successes in half-open state to close circuit (default: 3) */\n successThreshold: number;\n /** Time in milliseconds to wait before trying again after opening (default: 30000) */\n timeout: number;\n}\n\n/** Debug logging function type */\nexport type DebugFn = (message: string, meta?: unknown) => void;\n\n/** Main SDK configuration options */\nexport interface SpooledClientConfig {\n /** API key for authentication (starts with sp_live_, sp_test_, sk_live_, or sk_test_) */\n apiKey?: string;\n /** JWT access token (alternative to API key) */\n accessToken?: string;\n /** JWT refresh token for automatic token renewal */\n refreshToken?: string;\n /** Admin API key (for /api/v1/admin/* endpoints; uses X-Admin-Key header) */\n adminKey?: string;\n /** Base URL for the REST API (default: https://api.spooled.cloud) */\n baseUrl?: string;\n /** WebSocket URL for realtime events (default: derived from baseUrl, e.g., wss://api.spooled.cloud) */\n wsUrl?: string;\n /** gRPC server address (default: grpc.spooled.cloud:443) */\n grpcAddress?: string;\n /** Request timeout in milliseconds (default: 30000) */\n timeout?: number;\n /** Number of retry attempts (shorthand for retry.maxRetries) */\n retries?: number;\n /** Base retry delay in milliseconds (shorthand for retry.baseDelay) */\n retryDelay?: number;\n /** Full retry configuration */\n retry?: Partial<RetryConfig>;\n /**\n * Circuit breaker configuration. Besides the thresholds it takes\n * `isFailure` (which errors count as failures) and `onStateChange`.\n */\n circuitBreaker?: Partial<CircuitBreakerOptions>;\n /** Additional headers to include in all requests */\n headers?: Record<string, string>;\n /** Custom fetch implementation (for testing or special runtimes) */\n fetch?: typeof fetch;\n /** Custom user agent string */\n userAgent?: string;\n /** Enable debug logging */\n debug?: boolean | DebugFn;\n /** Whether to automatically refresh tokens when they expire (default: true) */\n autoRefreshToken?: boolean;\n}\n\n/** Resolved configuration with all defaults applied */\nexport interface ResolvedConfig {\n apiKey?: string;\n accessToken?: string;\n refreshToken?: string;\n adminKey?: string;\n baseUrl: string;\n wsUrl: string;\n grpcAddress: string;\n timeout: number;\n retry: RetryConfig;\n circuitBreaker: CircuitBreakerOptions;\n headers: Record<string, string>;\n fetch: typeof fetch;\n userAgent: string;\n debug: DebugFn | null;\n autoRefreshToken: boolean;\n}\n\n/** Default configuration values */\nexport const DEFAULT_CONFIG = {\n baseUrl: \"https://api.spooled.cloud\",\n wsUrl: \"wss://api.spooled.cloud\",\n grpcAddress: \"grpc.spooled.cloud:443\",\n timeout: 30000,\n retry: {\n maxRetries: 3,\n baseDelay: 1000,\n maxDelay: 30000,\n factor: 2,\n jitter: true,\n } satisfies RetryConfig,\n circuitBreaker: {\n enabled: true,\n failureThreshold: 5,\n successThreshold: 3,\n timeout: 30000,\n } satisfies CircuitBreakerConfig,\n userAgent: \"@spooled/sdk-nodejs/1.5.0\",\n autoRefreshToken: true,\n} as const;\n\n/** SDK version */\nexport const SDK_VERSION = \"1.5.0\";\n\n/** API version prefix */\nexport const API_VERSION = \"v1\";\n\n/** API base path */\nexport const API_BASE_PATH = `/api/${API_VERSION}`;\n\n/**\n * Trim surrounding whitespace from a credential string and treat an\n * all-whitespace input as unset. Mirrors the trim policy in the Go, PHP,\n * and Python SDKs so behavior is consistent across languages.\n */\nfunction normalizeCredential(value: string | undefined): string | undefined {\n if (value === undefined) {\n return undefined;\n }\n const trimmed = value.trim();\n return trimmed === \"\" ? undefined : trimmed;\n}\n\n/**\n * Resolve configuration by merging user options with defaults\n */\nexport function resolveConfig(options: SpooledClientConfig): ResolvedConfig {\n // Create debug function\n let debugFn: DebugFn | null = null;\n if (options.debug === true) {\n debugFn = (message: string, meta?: unknown) => {\n // eslint-disable-next-line no-console\n console.log(`[spooled-sdk] ${message}`, meta !== undefined ? meta : \"\");\n };\n } else if (typeof options.debug === \"function\") {\n debugFn = options.debug;\n }\n\n // Merge retry config\n const retry: RetryConfig = {\n ...DEFAULT_CONFIG.retry,\n ...options.retry,\n // Allow shorthand overrides\n ...(options.retries !== undefined && { maxRetries: options.retries }),\n ...(options.retryDelay !== undefined && { baseDelay: options.retryDelay }),\n };\n\n // Merge circuit breaker config\n const circuitBreaker: CircuitBreakerOptions = {\n ...DEFAULT_CONFIG.circuitBreaker,\n ...options.circuitBreaker,\n };\n\n // Build headers\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n ...options.headers,\n };\n\n // Resolve base URL first\n const baseUrl = options.baseUrl ?? DEFAULT_CONFIG.baseUrl;\n\n // Derive WebSocket URL from base URL if not explicitly set\n // Replace http(s):// with ws(s)://\n const derivedWsUrl = baseUrl.replace(/^http/, \"ws\");\n const wsUrl = options.wsUrl ?? derivedWsUrl;\n\n // Trim whitespace (including a stray trailing '\\n') from every credential.\n // Node's `undici` fetch rejects `Authorization: Bearer <key>\\n` with an\n // opaque `TypeError: HeaderValue` and users routinely paste keys from\n // `.env` files that include a newline; normalizing here keeps behavior\n // consistent with the Go, PHP, and Python SDKs.\n return {\n apiKey: normalizeCredential(options.apiKey),\n accessToken: normalizeCredential(options.accessToken),\n refreshToken: normalizeCredential(options.refreshToken),\n adminKey: normalizeCredential(options.adminKey),\n baseUrl,\n wsUrl,\n grpcAddress: options.grpcAddress ?? DEFAULT_CONFIG.grpcAddress,\n timeout: options.timeout ?? DEFAULT_CONFIG.timeout,\n retry,\n circuitBreaker,\n headers,\n fetch: options.fetch ?? globalThis.fetch,\n userAgent: options.userAgent ?? DEFAULT_CONFIG.userAgent,\n debug: debugFn,\n autoRefreshToken:\n options.autoRefreshToken ?? DEFAULT_CONFIG.autoRefreshToken,\n };\n}\n\n/**\n * Validate configuration options\n * @throws Error if configuration is invalid\n */\nexport function validateConfig(config: ResolvedConfig): void {\n // Must have at least one auth method. An admin key alone is enough: the\n // /admin/* endpoints authenticate with X-Admin-Key only, so an admin-only\n // client (`client.admin.*`) is legitimate.\n if (!config.apiKey && !config.accessToken && !config.adminKey) {\n throw new Error(\n \"SpooledClient requires either apiKey or accessToken for authentication (or adminKey for admin-only use)\",\n );\n }\n\n // Validate API key format if provided\n // Accept both sp_ (new standard) and sk_ (legacy) prefixes\n if (\n config.apiKey &&\n !config.apiKey.startsWith(\"sp_\") &&\n !config.apiKey.startsWith(\"sk_\")\n ) {\n throw new Error(\n \"Invalid API key format. API keys should start with sp_live_, sp_test_, sk_live_, or sk_test_\",\n );\n }\n\n // Validate baseUrl\n if (\n !config.baseUrl.startsWith(\"http://\") &&\n !config.baseUrl.startsWith(\"https://\")\n ) {\n throw new Error(\"baseUrl must start with http:// or https://\");\n }\n\n // Validate timeout\n if (config.timeout <= 0) {\n throw new Error(\"timeout must be a positive number\");\n }\n\n // Validate retry config\n if (config.retry.maxRetries < 0) {\n throw new Error(\"retry.maxRetries must be non-negative\");\n }\n if (config.retry.baseDelay <= 0) {\n throw new Error(\"retry.baseDelay must be positive\");\n }\n if (config.retry.maxDelay < config.retry.baseDelay) {\n throw new Error(\"retry.maxDelay must be >= retry.baseDelay\");\n }\n if (config.retry.factor < 1) {\n throw new Error(\"retry.factor must be >= 1\");\n }\n\n // Validate circuit breaker config\n if (config.circuitBreaker.enabled) {\n if (config.circuitBreaker.failureThreshold <= 0) {\n throw new Error(\"circuitBreaker.failureThreshold must be positive\");\n }\n if (config.circuitBreaker.successThreshold <= 0) {\n throw new Error(\"circuitBreaker.successThreshold must be positive\");\n }\n if (config.circuitBreaker.timeout <= 0) {\n throw new Error(\"circuitBreaker.timeout must be positive\");\n }\n }\n}\n","/**\n * Custom Error Classes\n *\n * Provides a hierarchy of error types for the Spooled SDK.\n */\n\n/** Error response body from the API */\nexport interface ApiErrorBody {\n code: string;\n message: string;\n details?: Record<string, unknown>;\n}\n\n/**\n * Rate limit information parsed from headers.\n *\n * Authenticated responses that went through the plan rate limiter carry\n * `X-RateLimit-Limit`, `-Remaining`, `-Reset` and `-Scope` (API 0.1.122+); a\n * 429 adds `Retry-After`. Every field is optional: the headers are left out\n * when no rate-limit check ran (public and admin routes, a server without\n * Redis, older servers).\n */\nexport interface RateLimitInfo {\n /**\n * The limit the other fields describe: the plan burst (organization token\n * bucket) or an API key's own requests per minute.\n */\n limit?: number;\n /** Whole requests left after this one */\n remaining?: number;\n /**\n * When `remaining` next goes up (`X-RateLimit-Reset`, a Unix time in\n * seconds; the current time while the bucket is full).\n */\n reset?: Date;\n /** Seconds to wait before retrying (`Retry-After`, on a 429) */\n retryAfter?: number;\n /**\n * Which limit the headers describe (`X-RateLimit-Scope`): `organization`\n * (the plan's token bucket) or `api_key` (the key's `rateLimit`).\n */\n scope?: \"organization\" | \"api_key\" | (string & {});\n}\n\n/**\n * Base error class for all Spooled SDK errors\n */\nexport class SpooledError extends Error {\n /** HTTP status code (if applicable) */\n readonly statusCode: number;\n /** Error code from API response */\n readonly code: string;\n /** Additional error details */\n readonly details?: Record<string, unknown>;\n /** Request ID for debugging */\n readonly requestId?: string;\n /** Original cause of the error */\n readonly cause?: Error;\n\n constructor(\n message: string,\n statusCode: number = 0,\n code: string = \"UNKNOWN_ERROR\",\n details?: Record<string, unknown>,\n requestId?: string,\n cause?: Error,\n ) {\n super(message);\n this.name = \"SpooledError\";\n this.statusCode = statusCode;\n this.code = code;\n this.details = details;\n this.requestId = requestId;\n this.cause = cause;\n\n // Maintain proper stack trace in V8 environments\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor);\n }\n }\n\n /**\n * Whether this error is retryable.\n *\n * Only 408 (Request Timeout) is retryable among the plain 4xx statuses: the\n * server gave up waiting for the request, so it never ran. Subclasses\n * override this (5xx, network, timeout, rate limit).\n */\n isRetryable(): boolean {\n return this.statusCode === 408;\n }\n\n /** Convert to plain object for logging */\n toJSON(): Record<string, unknown> {\n return {\n name: this.name,\n message: this.message,\n statusCode: this.statusCode,\n code: this.code,\n details: this.details,\n requestId: this.requestId,\n };\n }\n}\n\n/**\n * Authentication error (401)\n * Thrown when authentication fails (invalid/expired token or API key)\n */\nexport class AuthenticationError extends SpooledError {\n constructor(\n message: string = \"Authentication failed\",\n code: string = \"AUTHENTICATION_FAILED\",\n details?: Record<string, unknown>,\n requestId?: string,\n ) {\n super(message, 401, code, details, requestId);\n this.name = \"AuthenticationError\";\n }\n}\n\n/**\n * Authorization error (403)\n * Thrown when the authenticated user lacks permission\n */\nexport class AuthorizationError extends SpooledError {\n constructor(\n message: string = \"Access denied\",\n code: string = \"ACCESS_DENIED\",\n details?: Record<string, unknown>,\n requestId?: string,\n ) {\n super(message, 403, code, details, requestId);\n this.name = \"AuthorizationError\";\n }\n}\n\n/**\n * Not found error (404)\n * Thrown when a requested resource doesn't exist\n */\nexport class NotFoundError extends SpooledError {\n constructor(\n message: string = \"Resource not found\",\n code: string = \"NOT_FOUND\",\n details?: Record<string, unknown>,\n requestId?: string,\n ) {\n super(message, 404, code, details, requestId);\n this.name = \"NotFoundError\";\n }\n}\n\n/**\n * Conflict error (409)\n * Thrown when there's a conflict (e.g., duplicate resource)\n */\nexport class ConflictError extends SpooledError {\n constructor(\n message: string = \"Conflict\",\n code: string = \"CONFLICT\",\n details?: Record<string, unknown>,\n requestId?: string,\n ) {\n super(message, 409, code, details, requestId);\n this.name = \"ConflictError\";\n }\n}\n\n/**\n * Validation error (400 or 422)\n * Thrown when request validation fails.\n *\n * The API answers a field-rule failure (length, range) with 400 but a missing\n * or mistyped JSON field with 422; both are the caller's request being invalid,\n * so both surface as this class. `statusCode` tells them apart.\n */\nexport class ValidationError extends SpooledError {\n constructor(\n message: string = \"Validation failed\",\n code: string = \"VALIDATION_ERROR\",\n details?: Record<string, unknown>,\n requestId?: string,\n statusCode: 400 | 422 = 400,\n ) {\n super(message, statusCode, code, details, requestId);\n this.name = \"ValidationError\";\n }\n}\n\n/**\n * Payload too large error (413)\n * Thrown when request body exceeds size limit\n */\nexport class PayloadTooLargeError extends SpooledError {\n constructor(\n message: string = \"Request payload too large\",\n code: string = \"PAYLOAD_TOO_LARGE\",\n details?: Record<string, unknown>,\n requestId?: string,\n ) {\n super(message, 413, code, details, requestId);\n this.name = \"PayloadTooLargeError\";\n }\n}\n\n/** Plan-limit wording the API uses (\"Daily job limit reached\", \"queues limit reached (5/5)\"). */\nconst QUOTA_MESSAGE = /\\blimit reached\\b/i;\n\n/**\n * Rate limit error (429)\n * Thrown when rate limit is exceeded, or a plan quota is (see `isQuota`)\n */\nexport class RateLimitError extends SpooledError {\n /** Rate limit information */\n readonly rateLimitInfo: RateLimitInfo;\n\n constructor(\n message: string = \"Rate limit exceeded\",\n code: string = \"RATE_LIMIT_EXCEEDED\",\n rateLimitInfo: RateLimitInfo = {},\n details?: Record<string, unknown>,\n requestId?: string,\n ) {\n super(message, 429, code, details, requestId);\n this.name = \"RateLimitError\";\n this.rateLimitInfo = rateLimitInfo;\n }\n\n /**\n * True when this 429 is a plan quota or limit (daily job cap, active jobs,\n * queue/worker/webhook caps) rather than per-second rate limiting.\n *\n * A quota does not clear for minutes or hours, so retrying it only blocks the\n * caller for the whole backoff and, via the circuit breaker, takes every\n * other call on the client down with it. It is never retried and never\n * counted as a circuit breaker failure.\n *\n * Detection: the `QUOTA_EXCEEDED` code, or, for older plan-limit paths that\n * answer with plain text wrapped as `RATE_LIMIT_EXCEEDED`, a \"... limit\n * reached\" message that carries no `Retry-After`.\n */\n get isQuota(): boolean {\n if (this.code === \"QUOTA_EXCEEDED\") {\n return true;\n }\n return (\n this.rateLimitInfo.retryAfter === undefined &&\n QUOTA_MESSAGE.test(this.message)\n );\n }\n\n override isRetryable(): boolean {\n return !this.isQuota;\n }\n\n /** Get the number of seconds to wait before retrying */\n getRetryAfter(): number {\n if (this.rateLimitInfo.retryAfter !== undefined) {\n return this.rateLimitInfo.retryAfter;\n }\n if (this.rateLimitInfo.reset) {\n const now = Date.now();\n const resetTime = this.rateLimitInfo.reset.getTime();\n return Math.max(0, Math.ceil((resetTime - now) / 1000));\n }\n return 60; // Default to 60 seconds\n }\n\n override toJSON(): Record<string, unknown> {\n return {\n ...super.toJSON(),\n rateLimitInfo: this.rateLimitInfo,\n };\n }\n}\n\n/**\n * Server error (5xx)\n * Thrown when the server encounters an error\n */\nexport class ServerError extends SpooledError {\n constructor(\n message: string = \"Server error\",\n statusCode: number = 500,\n code: string = \"SERVER_ERROR\",\n details?: Record<string, unknown>,\n requestId?: string,\n ) {\n super(message, statusCode, code, details, requestId);\n this.name = \"ServerError\";\n }\n\n override isRetryable(): boolean {\n return true;\n }\n}\n\n/**\n * Network error\n * Thrown when a network request fails (no response received)\n */\nexport class NetworkError extends SpooledError {\n constructor(message: string = \"Network request failed\", cause?: Error) {\n super(message, 0, \"NETWORK_ERROR\", undefined, undefined, cause);\n this.name = \"NetworkError\";\n }\n\n override isRetryable(): boolean {\n return true;\n }\n}\n\n/**\n * Timeout error\n * Thrown when a request times out\n */\nexport class TimeoutError extends SpooledError {\n /** Timeout duration in milliseconds */\n readonly timeoutMs: number;\n\n constructor(message: string = \"Request timed out\", timeoutMs: number = 0) {\n super(message, 0, \"TIMEOUT\", { timeoutMs });\n this.name = \"TimeoutError\";\n this.timeoutMs = timeoutMs;\n }\n\n override isRetryable(): boolean {\n return true;\n }\n}\n\n/**\n * Circuit breaker open error\n * Thrown when the circuit breaker is open and rejecting requests\n */\nexport class CircuitBreakerOpenError extends SpooledError {\n constructor(message: string = \"Circuit breaker is open\") {\n super(message, 0, \"CIRCUIT_BREAKER_OPEN\");\n this.name = \"CircuitBreakerOpenError\";\n }\n\n override isRetryable(): boolean {\n return false; // Should wait for circuit to close\n }\n}\n\n/**\n * Unsupported operation error\n * Thrown by SDK methods whose route the Spooled API does not provide (or\n * always rejects). It is raised before any network call, so code that still\n * calls such a method fails loudly with an explanation instead of a 404.\n */\nexport class UnsupportedOperationError extends SpooledError {\n /** The SDK method that is not supported, e.g. `ingest.github` */\n readonly operation: string;\n\n constructor(operation: string, reason: string) {\n super(\n `${operation} is not supported by the Spooled API: ${reason}`,\n 0,\n \"UNSUPPORTED_OPERATION\",\n { operation },\n );\n this.name = \"UnsupportedOperationError\";\n this.operation = operation;\n }\n}\n\n/**\n * Parse rate limit headers from a Response\n */\nexport function parseRateLimitHeaders(headers: Headers): RateLimitInfo {\n const info: RateLimitInfo = {};\n\n const retryAfter = headers.get(\"Retry-After\");\n if (retryAfter) {\n const seconds = parseInt(retryAfter, 10);\n if (!isNaN(seconds)) {\n info.retryAfter = seconds;\n }\n }\n\n const limit = headers.get(\"X-RateLimit-Limit\");\n if (limit) {\n const parsed = parseInt(limit, 10);\n if (!isNaN(parsed)) {\n info.limit = parsed;\n }\n }\n\n const remaining = headers.get(\"X-RateLimit-Remaining\");\n if (remaining) {\n const parsed = parseInt(remaining, 10);\n if (!isNaN(parsed)) {\n info.remaining = parsed;\n }\n }\n\n const reset = headers.get(\"X-RateLimit-Reset\");\n if (reset) {\n const timestamp = parseInt(reset, 10);\n if (!isNaN(timestamp)) {\n info.reset = new Date(timestamp * 1000);\n }\n }\n\n const scope = headers.get(\"X-RateLimit-Scope\");\n if (scope) {\n info.scope = scope.trim();\n }\n\n return info;\n}\n\n/** Cap a raw (non-JSON) error body so a stray HTML page can't become a huge message. */\nconst MAX_RAW_ERROR_BODY_CHARS = 500;\n\n/**\n * Parse an error response body.\n *\n * Reads the body once as text, then:\n * - if it parses as a JSON object, extracts `{code, message}` (falling back to\n * the `error` field when `message` is absent);\n * - otherwise (plain-text/HTML body, or malformed JSON) preserves the raw body\n * text as the message so a server's human-readable error — e.g. a proxy's\n * `502 Bad Gateway` page — is not silently discarded.\n */\nasync function parseErrorBody(\n response: Response,\n): Promise<ApiErrorBody | null> {\n const contentType = response.headers.get(\"Content-Type\") || \"\";\n\n let raw: string;\n try {\n raw = await response.text();\n } catch {\n return null;\n }\n\n if (contentType.includes(\"application/json\")) {\n try {\n const body = JSON.parse(raw) as Record<string, unknown>;\n if (typeof body === \"object\" && body !== null) {\n // Error bodies come in two shapes: {code, message} or {error, code, details}.\n // Fall back to `error` when `message` is absent so the server's\n // human-readable message is not lost.\n const message =\n ((body.message as string) ?? (body.error as string)) ||\n response.statusText;\n return {\n code: (body.code as string) || \"UNKNOWN_ERROR\",\n message,\n details: body.details as Record<string, unknown> | undefined,\n };\n }\n } catch {\n // Content-Type claimed JSON but the body did not parse — fall through and\n // preserve the raw text below instead of throwing it away.\n }\n }\n\n // Non-JSON body (or unparseable JSON): keep the raw text as the message.\n const trimmed = raw.trim();\n if (trimmed) {\n const message =\n trimmed.length > MAX_RAW_ERROR_BODY_CHARS\n ? `${trimmed.slice(0, MAX_RAW_ERROR_BODY_CHARS)}…`\n : trimmed;\n return { code: \"UNKNOWN_ERROR\", message };\n }\n\n return null;\n}\n\n/**\n * Create an appropriate error from an HTTP response\n */\nexport async function createErrorFromResponse(\n response: Response,\n): Promise<SpooledError> {\n const requestId = response.headers.get(\"X-Request-ID\") ?? undefined;\n const body = await parseErrorBody(response);\n\n const message =\n body?.message || response.statusText || `HTTP ${response.status}`;\n const code = body?.code || \"UNKNOWN_ERROR\";\n const details = body?.details;\n\n switch (response.status) {\n case 400:\n return new ValidationError(message, code, details, requestId);\n\n // A missing or mistyped JSON field is a 422 from the API's extractor; it is\n // the same class of caller error as a 400 field-rule failure.\n case 422:\n return new ValidationError(message, code, details, requestId, 422);\n\n case 401:\n return new AuthenticationError(message, code, details, requestId);\n\n case 403:\n return new AuthorizationError(message, code, details, requestId);\n\n case 404:\n return new NotFoundError(message, code, details, requestId);\n\n case 409:\n return new ConflictError(message, code, details, requestId);\n\n case 413:\n return new PayloadTooLargeError(message, code, details, requestId);\n\n case 429: {\n const rateLimitInfo = parseRateLimitHeaders(response.headers);\n return new RateLimitError(\n message,\n code,\n rateLimitInfo,\n details,\n requestId,\n );\n }\n\n default:\n if (response.status >= 500) {\n return new ServerError(\n message,\n response.status,\n code,\n details,\n requestId,\n );\n }\n return new SpooledError(\n message,\n response.status,\n code,\n details,\n requestId,\n );\n }\n}\n\n/**\n * Check if an error is a SpooledError instance\n */\nexport function isSpooledError(error: unknown): error is SpooledError {\n return error instanceof SpooledError;\n}\n\n/**\n * Check if an error is retryable\n */\nexport function isRetryableError(error: unknown): boolean {\n if (error instanceof SpooledError) {\n return error.isRetryable();\n }\n // Network errors from fetch are typically retryable\n if (error instanceof TypeError) {\n return true;\n }\n return false;\n}\n","/**\n * Failure message coercion\n *\n * `POST /jobs/{id}/fail` rejects an empty or over-long `error` with a 400.\n * A worker that reported its failure with such text got nothing recorded: the\n * job stayed `processing` until its lease ran out, then ran again. The text of\n * a failure is therefore always made acceptable before it is sent.\n */\n\n/** Longest error text the API accepts (characters, not bytes). */\nexport const MAX_FAILURE_MESSAGE_CHARS = 2048;\n\n/** Text sent when a handler failed without saying why. */\nexport const DEFAULT_FAILURE_MESSAGE = \"Job failed\";\n\n/**\n * Make `message` acceptable to the fail endpoint: empty becomes\n * {@link DEFAULT_FAILURE_MESSAGE}; text over {@link MAX_FAILURE_MESSAGE_CHARS}\n * is cut to that many characters. The cut is by code point, so an emoji or\n * other astral character is never split into a lone surrogate.\n */\nexport function coerceFailureMessage(message: unknown): string {\n const text =\n typeof message === \"string\" ? message : message == null ? \"\" : `${message}`;\n if (text === \"\") {\n return DEFAULT_FAILURE_MESSAGE;\n }\n // UTF-16 length is an upper bound of the code point count: skip the\n // allocation for the common short message.\n if (text.length <= MAX_FAILURE_MESSAGE_CHARS) {\n return text;\n }\n const codePoints = Array.from(text);\n return codePoints.length <= MAX_FAILURE_MESSAGE_CHARS\n ? text\n : codePoints.slice(0, MAX_FAILURE_MESSAGE_CHARS).join(\"\");\n}\n","/**\n * Spooled Worker\n *\n * Worker runtime for processing jobs from a queue.\n */\n\nimport type { SpooledClient } from \"../client.js\";\nimport { SDK_VERSION } from \"../config.js\";\nimport { SpooledError } from \"../errors.js\";\nimport type { JsonValue } from \"../types/common.js\";\nimport type { ClaimedJob } from \"../types/jobs.js\";\nimport { coerceFailureMessage } from \"../utils/failure.js\";\nimport type {\n SpooledWorkerOptions,\n WorkerState,\n JobHandler,\n JobContext,\n WorkerEvent,\n WorkerEventData,\n ActiveJob,\n} from \"./types.js\";\nimport { hostname as osHostname } from \"os\";\n\n/** Default options */\nconst DEFAULT_OPTIONS = {\n concurrency: 5,\n pollInterval: 1000,\n leaseDuration: 30,\n heartbeatFraction: 0.5,\n autoStart: false,\n shutdownTimeout: 30000,\n} as const;\n\n/** Event handler type */\ntype EventHandler<E extends WorkerEvent> = (data: WorkerEventData[E]) => void;\n\n/** Failure text sent when a handler outlives the job's `timeoutSeconds`. */\nconst JOB_TIMEOUT_MESSAGE = \"Job timeout exceeded\";\n\n/** Failure text sent for a handler still running when the shutdown grace ends. */\nconst SHUTDOWN_TIMEOUT_MESSAGE = \"Worker shutdown timeout\";\n\n/** Longest delay `setTimeout` accepts (a larger one fires immediately). */\nconst MAX_TIMER_MS = 2 ** 31 - 1;\n\n/** How a handler run ended. Decided once per execution. */\ntype HandlerOutcome =\n | { kind: \"returned\"; value: JsonValue | void }\n | { kind: \"threw\"; error: unknown }\n | { kind: \"timeout\" };\n\n/**\n * Spooled Worker\n *\n * @example\n * ```typescript\n * const worker = new SpooledWorker(client, {\n * queueName: 'my-queue',\n * concurrency: 10,\n * });\n *\n * worker.process(async (ctx) => {\n * console.log('Processing job:', ctx.jobId);\n * await doSomeWork(ctx.payload);\n * return { success: true };\n * });\n *\n * await worker.start();\n *\n * // Graceful shutdown\n * process.on('SIGTERM', () => worker.stop());\n * ```\n */\nexport class SpooledWorker {\n private readonly client: SpooledClient;\n /**\n * Options with every defaultable field filled in. `workerId` stays optional:\n * there is no sensible default for it, and omitting it is what makes the\n * server mint a UUID.\n */\n private readonly options: Required<Omit<SpooledWorkerOptions, \"workerId\">> &\n Pick<SpooledWorkerOptions, \"workerId\">;\n private readonly debug: (msg: string, meta?: unknown) => void;\n\n private state: WorkerState = \"idle\";\n private workerId: string | null = null;\n private handler: JobHandler | null = null;\n private activeJobs: Map<symbol, ActiveJob> = new Map();\n private pollTimer: ReturnType<typeof setTimeout> | null = null;\n private workerHeartbeatTimer: ReturnType<typeof setInterval> | null = null;\n private shutdownPromise: Promise<void> | null = null;\n /** The claim request currently on the wire, so a stop can wait for its jobs. */\n private pollInFlight: Promise<void> | null = null;\n /** Wakes the shutdown drain when the last active job has been acked. */\n private drainWaiter: (() => void) | null = null;\n\n // Event handlers\n private eventHandlers: Map<WorkerEvent, Set<EventHandler<WorkerEvent>>> =\n new Map();\n\n constructor(client: SpooledClient, options: SpooledWorkerOptions) {\n this.client = client;\n this.options = {\n ...DEFAULT_OPTIONS,\n hostname: osHostname(),\n workerType: \"nodejs\",\n version: SDK_VERSION,\n metadata: {},\n ...options,\n } as Required<Omit<SpooledWorkerOptions, \"workerId\">> &\n Pick<SpooledWorkerOptions, \"workerId\">;\n\n // Get debug from client config\n const config = client.getConfig();\n this.debug = config.debug ?? (() => {});\n\n if (this.options.autoStart) {\n // Defer to next tick to allow process() to be called. start() already\n // reports a failed registration through the \"error\" event; the catch only\n // keeps it from becoming an unhandled rejection that crashes the process.\n setImmediate(() => {\n this.start().catch((error) => {\n this.debug(\"Auto-start failed\", error);\n });\n });\n }\n }\n\n /**\n * Get current worker state\n */\n getState(): WorkerState {\n return this.state;\n }\n\n /**\n * Get worker ID (available after start)\n */\n getWorkerId(): string | null {\n return this.workerId;\n }\n\n /**\n * Get number of active jobs\n */\n getActiveJobCount(): number {\n return this.activeJobs.size;\n }\n\n /**\n * Register job handler\n */\n process(handler: JobHandler): void {\n if (this.state !== \"idle\") {\n throw new Error(\"Cannot set handler after worker has started\");\n }\n this.handler = handler;\n }\n\n /**\n * Start the worker\n */\n async start(): Promise<void> {\n if (this.state !== \"idle\") {\n throw new Error(`Cannot start worker in state: ${this.state}`);\n }\n\n if (!this.handler) {\n throw new Error(\"No job handler registered. Call process() first.\");\n }\n\n this.state = \"starting\";\n this.debug(`Starting worker for queue: ${this.options.queueName}`);\n\n try {\n // Register with the API\n const registration = await this.client.workers.register({\n queueName: this.options.queueName,\n workerId: this.options.workerId,\n hostname: this.options.hostname,\n workerType: this.options.workerType,\n maxConcurrency: this.options.concurrency,\n metadata: this.options.metadata,\n version: this.options.version,\n });\n\n this.workerId = registration.id;\n this.debug(`Worker registered: ${this.workerId}`, registration);\n\n // Start worker heartbeat\n const heartbeatInterval = registration.heartbeatIntervalSecs * 1000;\n this.workerHeartbeatTimer = setInterval(() => {\n this.sendWorkerHeartbeat();\n }, heartbeatInterval);\n\n // Start polling\n this.state = \"running\";\n this.emit(\"started\", {\n workerId: this.workerId,\n queueName: this.options.queueName,\n });\n this.schedulePoll();\n } catch (error) {\n this.state = \"error\";\n this.emit(\"error\", { error: error as Error });\n throw error;\n }\n }\n\n /**\n * Stop the worker gracefully.\n *\n * Drains: the worker stops claiming, then lets running handlers finish for\n * up to `shutdownTimeout`. A handler that returns completes its job and one\n * that throws fails it, even though a stop was requested meanwhile; its\n * `signal` is NOT aborted during the grace period. Only handlers still\n * running when the timeout ends are aborted and failed with \"Worker shutdown\n * timeout\". The worker deregisters last, because deregistering returns every\n * job the worker still holds to the queue.\n */\n async stop(): Promise<void> {\n if (this.state !== \"running\") {\n return;\n }\n\n // Deduplicate concurrent stop calls\n if (this.shutdownPromise) {\n return this.shutdownPromise;\n }\n\n this.shutdownPromise = this.doStop();\n return this.shutdownPromise;\n }\n\n private async doStop(): Promise<void> {\n const deadline = Date.now() + this.options.shutdownTimeout;\n this.state = \"stopping\";\n this.debug(\"Stopping worker...\");\n\n // Stop polling\n if (this.pollTimer) {\n clearTimeout(this.pollTimer);\n this.pollTimer = null;\n }\n\n // A claim already on the wire still returns jobs this worker now holds\n // leases for. Let it land first so they join the drain below instead of\n // starting after the worker has deregistered.\n if (this.pollInFlight) {\n await this.pollInFlight;\n }\n\n // Drain: the worker heartbeat and every job heartbeat keep running while\n // handlers finish, so neither the worker nor a lease expires mid-drain.\n if (this.activeJobs.size > 0) {\n this.debug(\n `Waiting for ${this.activeJobs.size} active jobs to complete...`,\n );\n\n await this.waitForDrain(Math.max(0, deadline - Date.now()));\n\n // Whatever is still running outlived the grace period: abort and fail it.\n // A job whose handler returned just before the deadline is already\n // settled (its ack is in flight) and is awaited below, not failed.\n const stragglers = Array.from(this.activeJobs.values()).filter(\n (active) => !active.settled,\n );\n await Promise.all(stragglers.map((active) => this.abandonJob(active)));\n\n // Acks already on the wire must land before the worker deregisters,\n // otherwise deregistering re-queues a job that was just finished.\n await Promise.all(\n Array.from(this.activeJobs.values()).map((active) => active.ack),\n );\n }\n\n // Stop worker heartbeat\n if (this.workerHeartbeatTimer) {\n clearInterval(this.workerHeartbeatTimer);\n this.workerHeartbeatTimer = null;\n }\n\n // Deregister worker\n const workerId = this.workerId;\n if (workerId) {\n try {\n await this.client.workers.deregister(workerId);\n this.debug(\"Worker deregistered\");\n } catch (error) {\n this.debug(\"Failed to deregister worker\", error);\n }\n }\n\n this.state = \"stopped\";\n this.workerId = null;\n this.emit(\"stopped\", { workerId: workerId ?? \"\", reason: \"graceful\" });\n this.shutdownPromise = null;\n }\n\n /**\n * Add event listener\n */\n on<E extends WorkerEvent>(event: E, handler: EventHandler<E>): () => void {\n if (!this.eventHandlers.has(event)) {\n this.eventHandlers.set(event, new Set());\n }\n this.eventHandlers.get(event)!.add(handler as EventHandler<WorkerEvent>);\n return () => this.off(event, handler);\n }\n\n /**\n * Remove event listener\n */\n off<E extends WorkerEvent>(event: E, handler: EventHandler<E>): void {\n const handlers = this.eventHandlers.get(event);\n if (handlers) {\n handlers.delete(handler as EventHandler<WorkerEvent>);\n }\n }\n\n // Private methods\n\n private emit<E extends WorkerEvent>(\n event: E,\n data: WorkerEventData[E],\n ): void {\n const handlers = this.eventHandlers.get(event);\n if (handlers) {\n handlers.forEach((handler) => {\n try {\n handler(data);\n } catch (error) {\n this.debug(\"Event handler error\", { event, error });\n }\n });\n }\n }\n\n private schedulePoll(): void {\n if (this.state !== \"running\") {\n return;\n }\n\n this.pollTimer = setTimeout(() => {\n this.pollTimer = null;\n const run = this.poll();\n this.pollInFlight = run;\n const done = () => {\n if (this.pollInFlight === run) {\n this.pollInFlight = null;\n }\n };\n run.then(done, done);\n }, this.options.pollInterval);\n }\n\n private async poll(): Promise<void> {\n if (this.state !== \"running\" || !this.workerId) {\n return;\n }\n\n // Check if we have capacity\n const availableSlots = this.options.concurrency - this.activeJobs.size;\n if (availableSlots <= 0) {\n this.schedulePoll();\n return;\n }\n\n try {\n // Claim jobs\n const result = await this.client.jobs.claim({\n queueName: this.options.queueName,\n workerId: this.workerId,\n limit: availableSlots,\n leaseDurationSecs: this.options.leaseDuration,\n });\n\n // Process claimed jobs\n for (const job of result.jobs) {\n this.processJob(job);\n }\n } catch (error) {\n this.debug(\"Poll failed\", error);\n this.emit(\"error\", { error: error as Error });\n }\n\n // Schedule next poll\n this.schedulePoll();\n }\n\n private processJob(job: ClaimedJob): void {\n this.emit(\"job:claimed\", { jobId: job.id, queueName: job.queueName });\n\n const abortController = new AbortController();\n const activeJob: ActiveJob = {\n executionId: Symbol(job.id),\n job,\n startedAt: new Date(),\n abortController,\n settled: false,\n };\n\n this.activeJobs.set(activeJob.executionId, activeJob);\n\n // Capture the immutable execution so a replacement lease cannot be borrowed.\n const heartbeatInterval =\n this.options.leaseDuration * this.options.heartbeatFraction * 1000;\n activeJob.heartbeatTimer = setInterval(() => {\n this.sendJobHeartbeat(activeJob);\n }, heartbeatInterval);\n\n // Execute handler\n this.executeHandler(activeJob);\n }\n\n private async executeHandler(activeJob: ActiveJob): Promise<void> {\n const { job, abortController } = activeJob;\n const { signal } = abortController;\n\n this.emit(\"job:started\", { jobId: job.id, queueName: job.queueName });\n\n const context: JobContext = {\n jobId: job.id,\n queueName: job.queueName,\n payload: job.payload,\n retryCount: job.retryCount,\n maxRetries: job.maxRetries,\n signal,\n progress: async (percent: number, message?: string) => {\n const boundedPercent = Math.max(0, Math.min(100, percent));\n this.debug(`[${job.id}] progress ${boundedPercent}%`, message);\n },\n log: (level, message, meta) => {\n this.debug(`[${job.id}] [${level}] ${message}`, meta);\n },\n };\n\n try {\n const outcome = await this.runHandler(activeJob, context);\n\n // A lost lease (heartbeat 409) means another worker may already be\n // running this job. Acking it would 409 and must not happen.\n if (activeJob.leaseLost) {\n this.debug(`Job ${job.id} lost its lease; dropping late result`);\n return;\n }\n // The shutdown drain already failed this job while its handler was still\n // running; the lease is gone, so a late result must not be sent.\n if (activeJob.settled) {\n this.debug(`Job ${job.id} was already settled; dropping late result`);\n return;\n }\n activeJob.settled = true;\n\n // The abort signal is only a hint to the handler. Whatever the handler\n // did decides the ack: a result that arrives after a stop was requested\n // is still sent, never silently dropped.\n switch (outcome.kind) {\n case \"returned\":\n activeJob.ack = this.completeJob(\n activeJob,\n outcome.value ?? undefined,\n );\n break;\n case \"threw\":\n activeJob.ack = this.failJob(\n activeJob,\n outcome.error instanceof Error\n ? outcome.error.message\n : String(outcome.error),\n );\n break;\n case \"timeout\":\n // Stop the handler if it listens, then fail the job: extending the\n // lease of a hung handler forever would keep the job and its\n // concurrency slot stuck.\n abortController.abort(new Error(JOB_TIMEOUT_MESSAGE));\n this.emit(\"job:timeout\", {\n jobId: job.id,\n queueName: job.queueName,\n });\n activeJob.ack = this.failJob(activeJob, JOB_TIMEOUT_MESSAGE);\n break;\n }\n await activeJob.ack;\n } finally {\n this.cleanupJob(activeJob);\n }\n }\n\n /**\n * Run the handler, racing it against the job's `timeoutSeconds`.\n * Never rejects: a throw (sync or async) is reported as an outcome.\n */\n private runHandler(\n activeJob: ActiveJob,\n context: JobContext,\n ): Promise<HandlerOutcome> {\n const run = (async (): Promise<HandlerOutcome> => {\n try {\n return { kind: \"returned\", value: await this.handler!(context) };\n } catch (error) {\n return { kind: \"threw\", error };\n }\n })();\n\n const { timeoutSeconds } = activeJob.job;\n if (typeof timeoutSeconds !== \"number\" || !(timeoutSeconds > 0)) {\n return run;\n }\n\n return new Promise<HandlerOutcome>((resolve) => {\n activeJob.timeoutTimer = setTimeout(\n () => resolve({ kind: \"timeout\" }),\n Math.min(timeoutSeconds * 1000, MAX_TIMER_MS),\n );\n run.then((outcome) => {\n this.clearTimeoutTimer(activeJob);\n resolve(outcome);\n });\n });\n }\n\n /** Abort a handler that outlived the shutdown grace period and fail its job. */\n private async abandonJob(activeJob: ActiveJob): Promise<void> {\n this.debug(`Force-failing job ${activeJob.job.id} due to shutdown timeout`);\n // Mark first: the handler may react to the abort, or finish later, and its\n // outcome must not be acked on top of this failure.\n activeJob.settled = true;\n activeJob.abortController.abort(new Error(SHUTDOWN_TIMEOUT_MESSAGE));\n activeJob.ack = this.failJob(activeJob, SHUTDOWN_TIMEOUT_MESSAGE);\n await activeJob.ack;\n // The handler's finally may never run if user code ignores the abort\n // signal, so explicitly clear this exact execution's timers.\n this.cleanupJob(activeJob);\n }\n\n private async completeJob(\n activeJob: ActiveJob,\n result?: JsonValue,\n ): Promise<void> {\n if (!this.workerId || activeJob.leaseLost) return;\n const { job } = activeJob;\n\n try {\n await this.client.jobs.complete(job.id, {\n workerId: this.workerId,\n result,\n ...(job.leaseId != null && { leaseId: job.leaseId }),\n });\n\n this.emit(\"job:completed\", {\n jobId: job.id,\n queueName: job.queueName,\n result,\n });\n } catch (error) {\n // The lease was reclaimed between the handler returning and the ack.\n // The result belongs to whoever holds the job now; this is not a\n // worker failure.\n if (isLeaseExpired(error)) {\n this.noteLeaseLost(activeJob, error);\n return;\n }\n // The ack was already retried by the HTTP layer; what is left is a job\n // that finished but could not be reported.\n this.debug(`Failed to complete job ${job.id}`, error);\n this.emit(\"error\", { error: error as Error });\n }\n }\n\n private async failJob(\n activeJob: ActiveJob,\n errorMessage: string,\n ): Promise<void> {\n if (!this.workerId || activeJob.leaseLost) return;\n const { job } = activeJob;\n\n const willRetry = job.retryCount < job.maxRetries;\n // The API rejects empty or over-long failure text with a 400, which would\n // leave the job processing until its lease expired.\n const message = coerceFailureMessage(errorMessage);\n\n try {\n await this.client.jobs.fail(job.id, {\n workerId: this.workerId,\n error: message,\n ...(job.leaseId != null && { leaseId: job.leaseId }),\n });\n\n this.emit(\"job:failed\", {\n jobId: job.id,\n queueName: job.queueName,\n error: message,\n willRetry,\n });\n } catch (error) {\n if (isLeaseExpired(error)) {\n this.noteLeaseLost(activeJob, error);\n return;\n }\n this.debug(`Failed to fail job ${job.id}`, error);\n this.emit(\"error\", { error: error as Error });\n }\n }\n\n private clearTimeoutTimer(activeJob: ActiveJob): void {\n if (activeJob.timeoutTimer) {\n clearTimeout(activeJob.timeoutTimer);\n activeJob.timeoutTimer = undefined;\n }\n }\n\n private cleanupJob(activeJob: ActiveJob): void {\n if (activeJob.heartbeatTimer) {\n clearInterval(activeJob.heartbeatTimer);\n activeJob.heartbeatTimer = undefined;\n }\n this.clearTimeoutTimer(activeJob);\n\n if (this.activeJobs.get(activeJob.executionId) === activeJob) {\n this.activeJobs.delete(activeJob.executionId);\n if (this.activeJobs.size === 0) {\n this.drainWaiter?.();\n }\n }\n }\n\n private async sendJobHeartbeat(activeJob: ActiveJob): Promise<void> {\n if (!this.workerId || activeJob.leaseLost) return;\n\n const { job } = activeJob;\n\n try {\n await this.client.jobs.heartbeat(job.id, {\n workerId: this.workerId,\n leaseDurationSecs: this.options.leaseDuration,\n ...(job.leaseId != null && { leaseId: job.leaseId }),\n });\n } catch (error) {\n // 409 LEASE_EXPIRED means this worker no longer holds the job: the\n // lease timed out, or the lease_id does not match. The scheduler has\n // already made the job claimable again, so the handler has to stop\n // and this execution must not ack it. Any other failure is only\n // logged; the next beat may still renew the lease in time.\n if (isLeaseExpired(error)) {\n this.noteLeaseLost(activeJob, error);\n return;\n }\n this.debug(`Job heartbeat failed for ${job.id}`, error);\n }\n }\n\n /**\n * The lease is gone. Abort the handler if it listens, stop renewing, and\n * refuse a later complete/fail with the stale lease.\n */\n private noteLeaseLost(activeJob: ActiveJob, error: unknown): void {\n if (activeJob.leaseLost) return;\n activeJob.leaseLost = true;\n if (activeJob.heartbeatTimer) {\n clearInterval(activeJob.heartbeatTimer);\n activeJob.heartbeatTimer = undefined;\n }\n // A timeout must not fail the job after the lease has been reclaimed.\n this.clearTimeoutTimer(activeJob);\n const reason =\n error instanceof Error ? error : new Error(\"Job lease expired\");\n activeJob.abortController.abort(reason);\n this.debug(\n `Job lease lost for ${activeJob.job.id}; stopping handler`,\n error,\n );\n // Free the slot now. The abort is only a hint: a handler that ignores it\n // (or whose I/O never returns) would otherwise hold a concurrency slot\n // forever, since the timeout above no longer settles it. Its late result\n // is dropped by the leaseLost check in executeHandler.\n activeJob.settled = true;\n this.cleanupJob(activeJob);\n }\n\n private async sendWorkerHeartbeat(): Promise<void> {\n // Keep beating through the shutdown drain: a long `shutdownTimeout` would\n // otherwise let the stale-worker reaper take the worker (and its jobs).\n if (\n !this.workerId ||\n (this.state !== \"running\" && this.state !== \"stopping\")\n ) {\n return;\n }\n\n try {\n await this.client.workers.heartbeat(this.workerId, {\n currentJobs: this.activeJobs.size,\n status: \"healthy\",\n });\n } catch (error) {\n this.debug(\"Worker heartbeat failed\", error);\n }\n }\n\n /**\n * Resolve once no job is active, or after `timeoutMs`. The timer is always\n * cleared, so a clean stop leaves nothing that keeps the process alive.\n */\n private waitForDrain(timeoutMs: number): Promise<void> {\n if (this.activeJobs.size === 0) {\n return Promise.resolve();\n }\n return new Promise<void>((resolve) => {\n const timer = setTimeout(() => {\n this.drainWaiter = null;\n resolve();\n }, timeoutMs);\n this.drainWaiter = () => {\n clearTimeout(timer);\n this.drainWaiter = null;\n resolve();\n };\n });\n }\n}\n\n/**\n * Heartbeat, complete and fail all answer a lost or mismatched lease with\n * 409 `LEASE_EXPIRED` (a mismatch uses that same code). Anything else — a\n * dropped connection, a 5xx — may still succeed on the next beat, so it must\n * not abort the handler.\n */\nfunction isLeaseExpired(error: unknown): boolean {\n return (\n error instanceof SpooledError &&\n error.statusCode === 409 &&\n error.code.toUpperCase() === \"LEASE_EXPIRED\"\n );\n}\n"]}
|