@spooled/sdk 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,22 @@
3
3
  var os = require('os');
4
4
 
5
5
  // src/config.ts
6
- var SDK_VERSION = "1.2.0";
6
+ var SDK_VERSION = "1.3.0";
7
+
8
+ // src/utils/failure.ts
9
+ var MAX_FAILURE_MESSAGE_CHARS = 2048;
10
+ var DEFAULT_FAILURE_MESSAGE = "Job failed";
11
+ function coerceFailureMessage(message) {
12
+ const text = typeof message === "string" ? message : message == null ? "" : `${message}`;
13
+ if (text === "") {
14
+ return DEFAULT_FAILURE_MESSAGE;
15
+ }
16
+ if (text.length <= MAX_FAILURE_MESSAGE_CHARS) {
17
+ return text;
18
+ }
19
+ const codePoints = Array.from(text);
20
+ return codePoints.length <= MAX_FAILURE_MESSAGE_CHARS ? text : codePoints.slice(0, MAX_FAILURE_MESSAGE_CHARS).join("");
21
+ }
7
22
  var DEFAULT_OPTIONS = {
8
23
  concurrency: 5,
9
24
  pollInterval: 1e3,
@@ -12,6 +27,9 @@ var DEFAULT_OPTIONS = {
12
27
  autoStart: false,
13
28
  shutdownTimeout: 3e4
14
29
  };
30
+ var JOB_TIMEOUT_MESSAGE = "Job timeout exceeded";
31
+ var SHUTDOWN_TIMEOUT_MESSAGE = "Worker shutdown timeout";
32
+ var MAX_TIMER_MS = 2 ** 31 - 1;
15
33
  var SpooledWorker = class {
16
34
  client;
17
35
  /**
@@ -28,6 +46,10 @@ var SpooledWorker = class {
28
46
  pollTimer = null;
29
47
  workerHeartbeatTimer = null;
30
48
  shutdownPromise = null;
49
+ /** The claim request currently on the wire, so a stop can wait for its jobs. */
50
+ pollInFlight = null;
51
+ /** Wakes the shutdown drain when the last active job has been acked. */
52
+ drainWaiter = null;
31
53
  // Event handlers
32
54
  eventHandlers = /* @__PURE__ */ new Map();
33
55
  constructor(client, options) {
@@ -44,7 +66,11 @@ var SpooledWorker = class {
44
66
  this.debug = config.debug ?? (() => {
45
67
  });
46
68
  if (this.options.autoStart) {
47
- setImmediate(() => this.start());
69
+ setImmediate(() => {
70
+ this.start().catch((error) => {
71
+ this.debug("Auto-start failed", error);
72
+ });
73
+ });
48
74
  }
49
75
  }
50
76
  /**
@@ -115,7 +141,15 @@ var SpooledWorker = class {
115
141
  }
116
142
  }
117
143
  /**
118
- * Stop the worker gracefully
144
+ * Stop the worker gracefully.
145
+ *
146
+ * Drains: the worker stops claiming, then lets running handlers finish for
147
+ * up to `shutdownTimeout`. A handler that returns completes its job and one
148
+ * that throws fails it, even though a stop was requested meanwhile; its
149
+ * `signal` is NOT aborted during the grace period. Only handlers still
150
+ * running when the timeout ends are aborted and failed with "Worker shutdown
151
+ * timeout". The worker deregisters last, because deregistering returns every
152
+ * job the worker still holds to the queue.
119
153
  */
120
154
  async stop() {
121
155
  if (this.state !== "running") {
@@ -128,39 +162,37 @@ var SpooledWorker = class {
128
162
  return this.shutdownPromise;
129
163
  }
130
164
  async doStop() {
165
+ const deadline = Date.now() + this.options.shutdownTimeout;
131
166
  this.state = "stopping";
132
167
  this.debug("Stopping worker...");
133
168
  if (this.pollTimer) {
134
169
  clearTimeout(this.pollTimer);
135
170
  this.pollTimer = null;
136
171
  }
137
- if (this.workerHeartbeatTimer) {
138
- clearInterval(this.workerHeartbeatTimer);
139
- this.workerHeartbeatTimer = null;
140
- }
141
- for (const active of this.activeJobs.values()) {
142
- active.abortController.abort();
172
+ if (this.pollInFlight) {
173
+ await this.pollInFlight;
143
174
  }
144
175
  if (this.activeJobs.size > 0) {
145
176
  this.debug(
146
177
  `Waiting for ${this.activeJobs.size} active jobs to complete...`
147
178
  );
148
- await Promise.race([
149
- this.waitForActiveJobs(),
150
- this.sleep(this.options.shutdownTimeout)
151
- ]);
152
- const remaining = Array.from(this.activeJobs.values());
153
- for (const active of remaining) {
154
- this.debug(
155
- `Force-failing job ${active.job.id} due to shutdown timeout`
156
- );
157
- await this.failJob(active.job, "Worker shutdown timeout");
158
- this.cleanupJob(active);
159
- }
179
+ await this.waitForDrain(Math.max(0, deadline - Date.now()));
180
+ const stragglers = Array.from(this.activeJobs.values()).filter(
181
+ (active) => !active.settled
182
+ );
183
+ await Promise.all(stragglers.map((active) => this.abandonJob(active)));
184
+ await Promise.all(
185
+ Array.from(this.activeJobs.values()).map((active) => active.ack)
186
+ );
160
187
  }
161
- if (this.workerId) {
188
+ if (this.workerHeartbeatTimer) {
189
+ clearInterval(this.workerHeartbeatTimer);
190
+ this.workerHeartbeatTimer = null;
191
+ }
192
+ const workerId = this.workerId;
193
+ if (workerId) {
162
194
  try {
163
- await this.client.workers.deregister(this.workerId);
195
+ await this.client.workers.deregister(workerId);
164
196
  this.debug("Worker deregistered");
165
197
  } catch (error) {
166
198
  this.debug("Failed to deregister worker", error);
@@ -168,7 +200,7 @@ var SpooledWorker = class {
168
200
  }
169
201
  this.state = "stopped";
170
202
  this.workerId = null;
171
- this.emit("stopped", { workerId: this.workerId ?? "", reason: "graceful" });
203
+ this.emit("stopped", { workerId: workerId ?? "", reason: "graceful" });
172
204
  this.shutdownPromise = null;
173
205
  }
174
206
  /**
@@ -207,7 +239,17 @@ var SpooledWorker = class {
207
239
  if (this.state !== "running") {
208
240
  return;
209
241
  }
210
- this.pollTimer = setTimeout(() => this.poll(), this.options.pollInterval);
242
+ this.pollTimer = setTimeout(() => {
243
+ this.pollTimer = null;
244
+ const run = this.poll();
245
+ this.pollInFlight = run;
246
+ const done = () => {
247
+ if (this.pollInFlight === run) {
248
+ this.pollInFlight = null;
249
+ }
250
+ };
251
+ run.then(done, done);
252
+ }, this.options.pollInterval);
211
253
  }
212
254
  async poll() {
213
255
  if (this.state !== "running" || !this.workerId) {
@@ -241,7 +283,8 @@ var SpooledWorker = class {
241
283
  executionId: Symbol(job.id),
242
284
  job,
243
285
  startedAt: /* @__PURE__ */ new Date(),
244
- abortController
286
+ abortController,
287
+ settled: false
245
288
  };
246
289
  this.activeJobs.set(activeJob.executionId, activeJob);
247
290
  const heartbeatInterval = this.options.leaseDuration * this.options.heartbeatFraction * 1e3;
@@ -270,23 +313,72 @@ var SpooledWorker = class {
270
313
  }
271
314
  };
272
315
  try {
273
- const result = await this.handler(context);
274
- if (signal.aborted) {
275
- this.debug(`Job ${job.id} was aborted`);
316
+ const outcome = await this.runHandler(activeJob, context);
317
+ if (activeJob.settled) {
318
+ this.debug(`Job ${job.id} was already settled; dropping late result`);
276
319
  return;
277
320
  }
278
- await this.completeJob(job, result ?? void 0);
279
- } catch (error) {
280
- if (signal.aborted) {
281
- this.debug(`Job ${job.id} was aborted`);
282
- return;
321
+ activeJob.settled = true;
322
+ switch (outcome.kind) {
323
+ case "returned":
324
+ activeJob.ack = this.completeJob(job, outcome.value ?? void 0);
325
+ break;
326
+ case "threw":
327
+ activeJob.ack = this.failJob(
328
+ job,
329
+ outcome.error instanceof Error ? outcome.error.message : String(outcome.error)
330
+ );
331
+ break;
332
+ case "timeout":
333
+ abortController.abort(new Error(JOB_TIMEOUT_MESSAGE));
334
+ this.emit("job:timeout", {
335
+ jobId: job.id,
336
+ queueName: job.queueName
337
+ });
338
+ activeJob.ack = this.failJob(job, JOB_TIMEOUT_MESSAGE);
339
+ break;
283
340
  }
284
- const errorMessage = error instanceof Error ? error.message : String(error);
285
- await this.failJob(job, errorMessage);
341
+ await activeJob.ack;
286
342
  } finally {
287
343
  this.cleanupJob(activeJob);
288
344
  }
289
345
  }
346
+ /**
347
+ * Run the handler, racing it against the job's `timeoutSeconds`.
348
+ * Never rejects: a throw (sync or async) is reported as an outcome.
349
+ */
350
+ runHandler(activeJob, context) {
351
+ const run = (async () => {
352
+ try {
353
+ return { kind: "returned", value: await this.handler(context) };
354
+ } catch (error) {
355
+ return { kind: "threw", error };
356
+ }
357
+ })();
358
+ const { timeoutSeconds } = activeJob.job;
359
+ if (typeof timeoutSeconds !== "number" || !(timeoutSeconds > 0)) {
360
+ return run;
361
+ }
362
+ return new Promise((resolve) => {
363
+ activeJob.timeoutTimer = setTimeout(
364
+ () => resolve({ kind: "timeout" }),
365
+ Math.min(timeoutSeconds * 1e3, MAX_TIMER_MS)
366
+ );
367
+ run.then((outcome) => {
368
+ this.clearTimeoutTimer(activeJob);
369
+ resolve(outcome);
370
+ });
371
+ });
372
+ }
373
+ /** Abort a handler that outlived the shutdown grace period and fail its job. */
374
+ async abandonJob(activeJob) {
375
+ this.debug(`Force-failing job ${activeJob.job.id} due to shutdown timeout`);
376
+ activeJob.settled = true;
377
+ activeJob.abortController.abort(new Error(SHUTDOWN_TIMEOUT_MESSAGE));
378
+ activeJob.ack = this.failJob(activeJob.job, SHUTDOWN_TIMEOUT_MESSAGE);
379
+ await activeJob.ack;
380
+ this.cleanupJob(activeJob);
381
+ }
290
382
  async completeJob(job, result) {
291
383
  if (!this.workerId) return;
292
384
  try {
@@ -302,25 +394,34 @@ var SpooledWorker = class {
302
394
  });
303
395
  } catch (error) {
304
396
  this.debug(`Failed to complete job ${job.id}`, error);
397
+ this.emit("error", { error });
305
398
  }
306
399
  }
307
400
  async failJob(job, errorMessage) {
308
401
  if (!this.workerId) return;
309
402
  const willRetry = job.retryCount < job.maxRetries;
403
+ const message = coerceFailureMessage(errorMessage);
310
404
  try {
311
405
  await this.client.jobs.fail(job.id, {
312
406
  workerId: this.workerId,
313
- error: errorMessage,
407
+ error: message,
314
408
  ...job.leaseId != null && { leaseId: job.leaseId }
315
409
  });
316
410
  this.emit("job:failed", {
317
411
  jobId: job.id,
318
412
  queueName: job.queueName,
319
- error: errorMessage,
413
+ error: message,
320
414
  willRetry
321
415
  });
322
416
  } catch (error) {
323
417
  this.debug(`Failed to fail job ${job.id}`, error);
418
+ this.emit("error", { error });
419
+ }
420
+ }
421
+ clearTimeoutTimer(activeJob) {
422
+ if (activeJob.timeoutTimer) {
423
+ clearTimeout(activeJob.timeoutTimer);
424
+ activeJob.timeoutTimer = void 0;
324
425
  }
325
426
  }
326
427
  cleanupJob(activeJob) {
@@ -328,8 +429,12 @@ var SpooledWorker = class {
328
429
  clearInterval(activeJob.heartbeatTimer);
329
430
  activeJob.heartbeatTimer = void 0;
330
431
  }
432
+ this.clearTimeoutTimer(activeJob);
331
433
  if (this.activeJobs.get(activeJob.executionId) === activeJob) {
332
434
  this.activeJobs.delete(activeJob.executionId);
435
+ if (this.activeJobs.size === 0) {
436
+ this.drainWaiter?.();
437
+ }
333
438
  }
334
439
  }
335
440
  async sendJobHeartbeat(activeJob) {
@@ -346,7 +451,9 @@ var SpooledWorker = class {
346
451
  }
347
452
  }
348
453
  async sendWorkerHeartbeat() {
349
- if (!this.workerId || this.state !== "running") return;
454
+ if (!this.workerId || this.state !== "running" && this.state !== "stopping") {
455
+ return;
456
+ }
350
457
  try {
351
458
  await this.client.workers.heartbeat(this.workerId, {
352
459
  currentJobs: this.activeJobs.size,
@@ -356,13 +463,25 @@ var SpooledWorker = class {
356
463
  this.debug("Worker heartbeat failed", error);
357
464
  }
358
465
  }
359
- async waitForActiveJobs() {
360
- while (this.activeJobs.size > 0) {
361
- await this.sleep(100);
466
+ /**
467
+ * Resolve once no job is active, or after `timeoutMs`. The timer is always
468
+ * cleared, so a clean stop leaves nothing that keeps the process alive.
469
+ */
470
+ waitForDrain(timeoutMs) {
471
+ if (this.activeJobs.size === 0) {
472
+ return Promise.resolve();
362
473
  }
363
- }
364
- sleep(ms) {
365
- return new Promise((resolve) => setTimeout(resolve, ms));
474
+ return new Promise((resolve) => {
475
+ const timer = setTimeout(() => {
476
+ this.drainWaiter = null;
477
+ resolve();
478
+ }, timeoutMs);
479
+ this.drainWaiter = () => {
480
+ clearTimeout(timer);
481
+ this.drainWaiter = null;
482
+ resolve();
483
+ };
484
+ });
366
485
  }
367
486
  };
368
487
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/config.ts","../../src/worker/worker.ts"],"names":["osHostname"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * SDK Configuration\n *\n * Defines configuration types and defaults for the Spooled SDK.\n */\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 /** Circuit breaker configuration */\n circuitBreaker?: Partial<CircuitBreakerConfig>;\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: CircuitBreakerConfig;\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.2.0\",\n autoRefreshToken: true,\n} as const;\n\n/** SDK version */\nexport const SDK_VERSION = \"1.2.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: CircuitBreakerConfig = {\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\n if (!config.apiKey && !config.accessToken) {\n throw new Error(\n \"SpooledClient requires either apiKey or accessToken for authentication\",\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 * 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 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/**\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\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\n setImmediate(() => this.start());\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 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 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 // Stop worker heartbeat\n if (this.workerHeartbeatTimer) {\n clearInterval(this.workerHeartbeatTimer);\n this.workerHeartbeatTimer = null;\n }\n\n // Signal all active jobs to stop\n for (const active of this.activeJobs.values()) {\n active.abortController.abort();\n }\n\n // Wait for active jobs to complete (with timeout)\n if (this.activeJobs.size > 0) {\n this.debug(\n `Waiting for ${this.activeJobs.size} active jobs to complete...`,\n );\n\n await Promise.race([\n this.waitForActiveJobs(),\n this.sleep(this.options.shutdownTimeout),\n ]);\n\n // Force-fail any remaining jobs. Snapshot the entries first because\n // cleanupJob() mutates the activeJobs map as we go.\n const remaining = Array.from(this.activeJobs.values());\n for (const active of remaining) {\n this.debug(\n `Force-failing job ${active.job.id} due to shutdown timeout`,\n );\n await this.failJob(active.job, \"Worker shutdown timeout\");\n // The handler's finally may never run if user code ignores the abort\n // signal, so explicitly clear this exact execution's timer.\n this.cleanupJob(active);\n }\n }\n\n // Deregister worker\n if (this.workerId) {\n try {\n await this.client.workers.deregister(this.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: this.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(() => this.poll(), 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 };\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 result = await this.handler!(context);\n\n // Check if aborted\n if (signal.aborted) {\n this.debug(`Job ${job.id} was aborted`);\n return;\n }\n\n // Complete the job\n await this.completeJob(job, result ?? undefined);\n } catch (error) {\n // Check if aborted\n if (signal.aborted) {\n this.debug(`Job ${job.id} was aborted`);\n return;\n }\n\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n await this.failJob(job, errorMessage);\n } finally {\n this.cleanupJob(activeJob);\n }\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 this.debug(`Failed to complete job ${job.id}`, 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\n try {\n await this.client.jobs.fail(job.id, {\n workerId: this.workerId,\n error: errorMessage,\n ...(job.leaseId != null && { leaseId: job.leaseId }),\n });\n\n this.emit(\"job:failed\", {\n jobId: job.id,\n queueName: job.queueName,\n error: errorMessage,\n willRetry,\n });\n } catch (error) {\n this.debug(`Failed to fail job ${job.id}`, error);\n }\n }\n\n private cleanupJob(activeJob: ActiveJob): void {\n if (activeJob.heartbeatTimer) {\n clearInterval(activeJob.heartbeatTimer);\n activeJob.heartbeatTimer = undefined;\n }\n\n if (this.activeJobs.get(activeJob.executionId) === activeJob) {\n this.activeJobs.delete(activeJob.executionId);\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 if (!this.workerId || this.state !== \"running\") return;\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 private async waitForActiveJobs(): Promise<void> {\n while (this.activeJobs.size > 0) {\n await this.sleep(100);\n }\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/config.ts","../../src/utils/failure.ts","../../src/worker/worker.ts"],"names":["osHostname"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * SDK Configuration\n *\n * Defines configuration types and defaults for the Spooled SDK.\n */\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 /** Circuit breaker configuration */\n circuitBreaker?: Partial<CircuitBreakerConfig>;\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: CircuitBreakerConfig;\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.3.0\",\n autoRefreshToken: true,\n} as const;\n\n/** SDK version */\nexport const SDK_VERSION = \"1.3.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: CircuitBreakerConfig = {\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,2 +1,2 @@
1
- export { c_ as ActiveJob, b4 as JobContext, b7 as JobHandler, b9 as JobResult, cc as SpooledWorker, cd as SpooledWorkerOptions, cA as WorkerEvent, cB as WorkerEventData, cE as WorkerState } from '../index-BzfyWvfc.cjs';
1
+ export { c$ as ActiveJob, b5 as JobContext, b8 as JobHandler, ba as JobResult, cd as SpooledWorker, ce as SpooledWorkerOptions, cB as WorkerEvent, cC as WorkerEventData, cF as WorkerState } from '../index-BVWKEr6p.cjs';
2
2
  import '@grpc/grpc-js';
@@ -1,2 +1,2 @@
1
- export { c_ as ActiveJob, b4 as JobContext, b7 as JobHandler, b9 as JobResult, cc as SpooledWorker, cd as SpooledWorkerOptions, cA as WorkerEvent, cB as WorkerEventData, cE as WorkerState } from '../index-BzfyWvfc.js';
1
+ export { c$ as ActiveJob, b5 as JobContext, b8 as JobHandler, ba as JobResult, cd as SpooledWorker, ce as SpooledWorkerOptions, cB as WorkerEvent, cC as WorkerEventData, cF as WorkerState } from '../index-BVWKEr6p.js';
2
2
  import '@grpc/grpc-js';