absurd-sdk 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -13,6 +13,16 @@ export class SuspendTask extends Error {
13
13
  this.name = "SuspendTask";
14
14
  }
15
15
  }
16
+ /**
17
+ * Internal exception that is thrown to cancel a run. As a user
18
+ * you should never see this exception.
19
+ */
20
+ export class CancelledTask extends Error {
21
+ constructor() {
22
+ super("Task cancelled");
23
+ this.name = "CancelledTask";
24
+ }
25
+ }
16
26
  /**
17
27
  * This error is thrown when awaiting an event ran into a timeout.
18
28
  */
@@ -23,36 +33,48 @@ export class TimeoutError extends Error {
23
33
  }
24
34
  }
25
35
  export class TaskContext {
36
+ log;
26
37
  taskID;
27
- pool;
38
+ con;
28
39
  queueName;
29
40
  task;
30
41
  checkpointCache;
31
42
  claimTimeout;
32
43
  stepNameCounter = new Map();
33
- constructor(taskID, pool, queueName, task, checkpointCache, claimTimeout) {
44
+ constructor(log, taskID, con, queueName, task, checkpointCache, claimTimeout) {
45
+ this.log = log;
34
46
  this.taskID = taskID;
35
- this.pool = pool;
47
+ this.con = con;
36
48
  this.queueName = queueName;
37
49
  this.task = task;
38
50
  this.checkpointCache = checkpointCache;
39
51
  this.claimTimeout = claimTimeout;
40
52
  }
41
53
  static async create(args) {
42
- const { taskID, pool, queueName, task, claimTimeout } = args;
43
- const result = await pool.query(`SELECT checkpoint_name, state, status, owner_run_id, updated_at
54
+ const { log, taskID, con, queueName, task, claimTimeout } = args;
55
+ const result = await con.query(`SELECT checkpoint_name, state, status, owner_run_id, updated_at
44
56
  FROM absurd.get_task_checkpoint_states($1, $2, $3)`, [queueName, task.task_id, task.run_id]);
45
57
  const cache = new Map();
46
58
  for (const row of result.rows) {
47
59
  cache.set(row.checkpoint_name, row.state);
48
60
  }
49
- return new TaskContext(taskID, pool, queueName, task, cache, claimTimeout);
61
+ return new TaskContext(log, taskID, con, queueName, task, cache, claimTimeout);
62
+ }
63
+ async queryWithCancelCheck(sql, params) {
64
+ try {
65
+ return await this.con.query(sql, params);
66
+ }
67
+ catch (err) {
68
+ if (err?.code === "AB001") {
69
+ throw new CancelledTask();
70
+ }
71
+ throw err;
72
+ }
50
73
  }
51
74
  /**
52
- * Defines a step in the task execution. Steps are idempotent in
53
- * that they are executed exactly once (unless they fail) and their
54
- * results are cached. As a result the return value of this function
55
- * must support `JSON.stringify`.
75
+ * Runs an idempotent step identified by name; caches and reuses its result across retries.
76
+ * @param name Unique checkpoint name for this step.
77
+ * @param fn Async function computing the step result (must be JSON-serializable).
56
78
  */
57
79
  async step(name, fn) {
58
80
  const checkpointName = this.getCheckpointName(name);
@@ -65,16 +87,17 @@ export class TaskContext {
65
87
  return rv;
66
88
  }
67
89
  /**
68
- * Sleeps for a given number of seconds. Note that this
69
- * *always* suspends the task, even if you only wait for a very
70
- * short period of time.
90
+ * Suspends the task until the given duration (seconds) elapses.
91
+ * @param stepName Checkpoint name for this wait.
92
+ * @param duration Duration to wait in seconds.
71
93
  */
72
94
  async sleepFor(stepName, duration) {
73
95
  return await this.sleepUntil(stepName, new Date(Date.now() + duration * 1000));
74
96
  }
75
97
  /**
76
- * Like `sleepFor` but with an absolute time when the task should be
77
- * awoken again.
98
+ * Suspends the task until the specified time.
99
+ * @param stepName Checkpoint name for this wait.
100
+ * @param wakeAt Absolute time when the task should resume.
78
101
  */
79
102
  async sleepUntil(stepName, wakeAt) {
80
103
  const checkpointName = this.getCheckpointName(stepName);
@@ -99,7 +122,7 @@ export class TaskContext {
99
122
  if (cached !== undefined) {
100
123
  return cached;
101
124
  }
102
- const result = await this.pool.query(`SELECT checkpoint_name, state, status, owner_run_id, updated_at
125
+ const result = await this.con.query(`SELECT checkpoint_name, state, status, owner_run_id, updated_at
103
126
  FROM absurd.get_task_checkpoint_state($1, $2, $3)`, [this.queueName, this.task.task_id, checkpointName]);
104
127
  if (result.rows.length > 0) {
105
128
  const state = result.rows[0].state;
@@ -109,7 +132,7 @@ export class TaskContext {
109
132
  return undefined;
110
133
  }
111
134
  async persistCheckpoint(checkpointName, value) {
112
- await this.pool.query(`SELECT absurd.set_task_checkpoint_state($1, $2, $3, $4, $5, $6)`, [
135
+ await this.queryWithCancelCheck(`SELECT absurd.set_task_checkpoint_state($1, $2, $3, $4, $5, $6)`, [
113
136
  this.queueName,
114
137
  this.task.task_id,
115
138
  checkpointName,
@@ -120,15 +143,18 @@ export class TaskContext {
120
143
  this.checkpointCache.set(checkpointName, value);
121
144
  }
122
145
  async scheduleRun(wakeAt) {
123
- await this.pool.query(`SELECT absurd.schedule_run($1, $2, $3)`, [
146
+ await this.con.query(`SELECT absurd.schedule_run($1, $2, $3)`, [
124
147
  this.queueName,
125
148
  this.task.run_id,
126
149
  wakeAt,
127
150
  ]);
128
151
  }
129
152
  /**
130
- * Awaits the arrival of an event. Events need to be uniquely
131
- * named so fold in the necessary parameters into the name (eg: customer id).
153
+ * Waits for an event by name and returns its payload; optionally sets a custom step name and timeout (seconds).
154
+ * @param eventName Event identifier to wait for.
155
+ * @param options.stepName Optional checkpoint name (defaults to $awaitEvent:<eventName>).
156
+ * @param options.timeout Optional timeout in seconds.
157
+ * @throws TimeoutError If the event is not received before the timeout.
132
158
  */
133
159
  async awaitEvent(eventName, options) {
134
160
  // the default step name is derived from the event name.
@@ -151,8 +177,8 @@ export class TaskContext {
151
177
  this.task.event_payload = null;
152
178
  throw new TimeoutError(`Timed out waiting for event "${eventName}"`);
153
179
  }
154
- const result = await this.pool.query(`SELECT should_suspend, payload
155
- FROM absurd.await_event($1, $2, $3, $4, $5, $6)`, [
180
+ const result = await this.queryWithCancelCheck(`SELECT should_suspend, payload
181
+ FROM absurd.await_event($1, $2, $3, $4, $5, $6)`, [
156
182
  this.queueName,
157
183
  this.task.task_id,
158
184
  this.task.run_id,
@@ -172,60 +198,89 @@ export class TaskContext {
172
198
  throw new SuspendTask();
173
199
  }
174
200
  /**
175
- * Emits an event that can be awaited.
201
+ * Extends the current run's lease by the given seconds (defaults to the original claim timeout).
202
+ * @param seconds Lease extension in seconds.
203
+ */
204
+ async heartbeat(seconds) {
205
+ await this.queryWithCancelCheck(`SELECT absurd.extend_claim($1, $2, $3)`, [
206
+ this.queueName,
207
+ this.task.run_id,
208
+ seconds ?? this.claimTimeout,
209
+ ]);
210
+ }
211
+ /**
212
+ * Emits an event to this task's queue with an optional payload.
213
+ * @param eventName Non-empty event name.
214
+ * @param payload Optional JSON-serializable payload.
176
215
  */
177
216
  async emitEvent(eventName, payload) {
178
217
  if (!eventName) {
179
218
  throw new Error("eventName must be a non-empty string");
180
219
  }
181
- await this.pool.query(`SELECT absurd.emit_event($1, $2, $3)`, [
220
+ await this.con.query(`SELECT absurd.emit_event($1, $2, $3)`, [
182
221
  this.queueName,
183
222
  eventName,
184
223
  JSON.stringify(payload ?? null),
185
224
  ]);
186
225
  }
187
- async complete(result) {
188
- await this.pool.query(`SELECT absurd.complete_run($1, $2, $3)`, [
189
- this.queueName,
190
- this.task.run_id,
191
- JSON.stringify(result ?? null),
192
- ]);
193
- }
194
- async fail(err) {
195
- console.error("[absurd] task execution failed:", err);
196
- await this.pool.query(`SELECT absurd.fail_run($1, $2, $3, $4)`, [
197
- this.queueName,
198
- this.task.run_id,
199
- JSON.stringify(serializeError(err)),
200
- null,
201
- ]);
202
- }
203
226
  }
227
+ /**
228
+ * The Absurd SDK Client.
229
+ *
230
+ * Instanciate this class and keep it around to interact with Absurd.
231
+ */
204
232
  export class Absurd {
205
- pool;
233
+ con;
206
234
  ownedPool;
207
235
  queueName;
208
236
  defaultMaxAttempts;
209
237
  registry = new Map();
238
+ log;
210
239
  worker = null;
211
- constructor(poolOrUrl, queueName = "default", defaultMaxAttempts = 5) {
212
- if (!poolOrUrl) {
213
- poolOrUrl =
240
+ constructor(options = {}) {
241
+ if (typeof options === "string" || isQueryable(options)) {
242
+ options = { db: options };
243
+ }
244
+ let connectionOrUrl = options.db;
245
+ if (!connectionOrUrl) {
246
+ connectionOrUrl =
214
247
  process.env.ABSURD_DATABASE_URL || "postgresql://localhost/absurd";
215
248
  }
216
- if (typeof poolOrUrl === "string") {
217
- this.pool = new pg.Pool({ connectionString: poolOrUrl });
249
+ if (typeof connectionOrUrl === "string") {
250
+ this.con = new pg.Pool({ connectionString: connectionOrUrl });
218
251
  this.ownedPool = true;
219
252
  }
220
253
  else {
221
- this.pool = poolOrUrl;
254
+ this.con = connectionOrUrl;
222
255
  this.ownedPool = false;
223
256
  }
224
- this.queueName = queueName;
225
- this.defaultMaxAttempts = defaultMaxAttempts;
257
+ this.queueName = options?.queueName ?? "default";
258
+ this.defaultMaxAttempts = options?.defaultMaxAttempts ?? 5;
259
+ this.log = options?.log ?? console;
226
260
  }
227
261
  /**
228
- * This registers a given function as task.
262
+ * Returns a new client that uses the provided connection for queries; set owned=true to close it with close().
263
+ * @param con Connection to bind to.
264
+ * @param owned If true, the bound client will close this connection on close().
265
+ */
266
+ bindToConnection(con, owned = false) {
267
+ const bound = new Absurd({
268
+ db: con, // this is okay because we ensure the invariant later
269
+ queueName: this.queueName,
270
+ defaultMaxAttempts: this.defaultMaxAttempts,
271
+ log: this.log,
272
+ });
273
+ bound.registry = this.registry;
274
+ bound.ownedPool = owned;
275
+ return bound;
276
+ }
277
+ /**
278
+ * Registers a task handler by name (optionally specifying queue, defaultMaxAttempts, and defaultCancellation).
279
+ * @param options.name Task name.
280
+ * @param options.queue Optional queue name (defaults to client queue).
281
+ * @param options.defaultMaxAttempts Optional default max attempts.
282
+ * @param options.defaultCancellation Optional default cancellation policy.
283
+ * @param handler Async task handler.
229
284
  */
230
285
  registerTask(options, handler) {
231
286
  if (!options?.name) {
@@ -250,25 +305,46 @@ export class Absurd {
250
305
  handler: handler,
251
306
  });
252
307
  }
308
+ /**
309
+ * Creates a queue (defaults to this client's queue).
310
+ * @param queueName Queue name to create.
311
+ */
253
312
  async createQueue(queueName) {
254
313
  const queue = queueName ?? this.queueName;
255
- await this.pool.query(`SELECT absurd.create_queue($1)`, [queue]);
314
+ await this.con.query(`SELECT absurd.create_queue($1)`, [queue]);
256
315
  }
316
+ /**
317
+ * Drops a queue (defaults to this client's queue).
318
+ * @param queueName Queue name to drop.
319
+ */
257
320
  async dropQueue(queueName) {
258
321
  const queue = queueName ?? this.queueName;
259
- await this.pool.query(`SELECT absurd.drop_queue($1)`, [queue]);
322
+ await this.con.query(`SELECT absurd.drop_queue($1)`, [queue]);
260
323
  }
324
+ /**
325
+ * Lists all queue names.
326
+ * @returns Array of queue names.
327
+ */
261
328
  async listQueues() {
262
- const result = await this.pool.query(`SELECT * FROM absurd.list_queues()`);
329
+ const result = await this.con.query(`SELECT * FROM absurd.list_queues()`);
263
330
  const rv = [];
264
- console.log(result);
265
331
  for (const row of result.rows) {
266
332
  rv.push(row.queue_name);
267
333
  }
268
334
  return rv;
269
335
  }
270
336
  /**
271
- * Spawns a specific task.
337
+ * Spawns a task execution by enqueueing it for processing. The task will be picked up by a worker
338
+ * and executed with the provided parameters. Returns identifiers that can be used to track or cancel the task.
339
+ *
340
+ * For registered tasks, the queue and defaults are inferred from registration. For unregistered tasks,
341
+ * you must provide options.queue.
342
+ *
343
+ * @param taskName Name of the task to spawn (must be registered or provide options.queue).
344
+ * @param params JSON-serializable parameters passed to the task handler.
345
+ * @param options Configure queue, maxAttempts, retryStrategy, headers, and cancellation policies.
346
+ * @returns Object containing taskID (unique task identifier), runID (current attempt identifier), and attempt number.
347
+ * @throws Error If the task is unregistered without a queue, or if the queue mismatches registration.
272
348
  */
273
349
  async spawn(taskName, params, options = {}) {
274
350
  const registration = this.registry.get(taskName);
@@ -296,7 +372,7 @@ export class Absurd {
296
372
  maxAttempts: effectiveMaxAttempts,
297
373
  cancellation: effectiveCancellation,
298
374
  });
299
- const result = await this.pool.query(`SELECT task_id, run_id, attempt
375
+ const result = await this.con.query(`SELECT task_id, run_id, attempt
300
376
  FROM absurd.spawn_task($1, $2, $3, $4)`, [
301
377
  queue,
302
378
  taskName,
@@ -314,28 +390,45 @@ export class Absurd {
314
390
  };
315
391
  }
316
392
  /**
317
- * Emits an event from outside of a task.
393
+ * Emits an event with an optional payload on the specified or default queue.
394
+ * @param eventName Non-empty event name.
395
+ * @param payload Optional JSON-serializable payload.
396
+ * @param queueName Queue to emit to (defaults to this client's queue).
318
397
  */
319
398
  async emitEvent(eventName, payload, queueName) {
320
399
  if (!eventName) {
321
400
  throw new Error("eventName must be a non-empty string");
322
401
  }
323
- await this.pool.query(`SELECT absurd.emit_event($1, $2, $3)`, [
402
+ await this.con.query(`SELECT absurd.emit_event($1, $2, $3)`, [
324
403
  queueName || this.queueName,
325
404
  eventName,
326
405
  JSON.stringify(payload ?? null),
327
406
  ]);
328
407
  }
408
+ /**
409
+ * Cancels a task by ID on the specified or default queue; running tasks stop at the next checkpoint/heartbeat.
410
+ * @param taskID Task identifier to cancel.
411
+ * @param queueName Queue name (defaults to this client's queue).
412
+ */
413
+ async cancelTask(taskID, queueName) {
414
+ await this.con.query(`SELECT absurd.cancel_task($1, $2)`, [
415
+ queueName || this.queueName,
416
+ taskID,
417
+ ]);
418
+ }
329
419
  async claimTasks(options) {
330
420
  const { batchSize: count = 1, claimTimeout = 120, workerId = "worker", } = options ?? {};
331
- const result = await this.pool.query(`SELECT run_id, task_id, attempt, task_name, params, retry_strategy, max_attempts,
421
+ const result = await this.con.query(`SELECT run_id, task_id, attempt, task_name, params, retry_strategy, max_attempts,
332
422
  headers, wake_event, event_payload
333
423
  FROM absurd.claim_task($1, $2, $3, $4)`, [this.queueName, workerId, claimTimeout, count]);
334
424
  return result.rows;
335
425
  }
336
426
  /**
337
- * Polls and processes a batch of messages sequentially.
338
- * For parallel processing, use startWorker with concurrency option.
427
+ * Claims up to batchSize tasks and processes them sequentially using the given workerId and claimTimeout.
428
+ * @param workerId Worker identifier.
429
+ * @param claimTimeout Lease duration in seconds.
430
+ * @param batchSize Maximum number of tasks to process.
431
+ * Note: For parallel processing, use startWorker().
339
432
  */
340
433
  async workBatch(workerId = "worker", claimTimeout = 120, batchSize = 1) {
341
434
  const tasks = await this.claimTasks({ batchSize, claimTimeout, workerId });
@@ -344,14 +437,57 @@ export class Absurd {
344
437
  }
345
438
  }
346
439
  /**
347
- * Starts a worker that continuously polls for tasks and processes them.
348
- * Returns a Worker instance with a close() method for graceful shutdown.
440
+ * Starts a background worker that continuously polls for and processes tasks from the queue.
441
+ * The worker will claim tasks up to the configured concurrency limit and process them in parallel.
442
+ *
443
+ * Tasks are claimed with a lease (claimTimeout) that prevents other workers from processing them.
444
+ * The lease is automatically extended when tasks write checkpoints or call heartbeat(). If a worker
445
+ * crashes or stops making progress, the lease expires and another worker can claim the task.
446
+ *
447
+ * @param options Configure worker behavior:
448
+ * - concurrency: Max parallel tasks (default: 1)
449
+ * - claimTimeout: Task lease duration in seconds (default: 120)
450
+ * - batchSize: Tasks to claim per poll (default: concurrency)
451
+ * - pollInterval: Seconds between polls when idle (default: 0.25)
452
+ * - workerId: Worker identifier for tracking (default: hostname:pid)
453
+ * - onError: Error handler called for execution failures
454
+ * - fatalOnLeaseTimeout: Terminate process if task exceeds 2x claimTimeout (default: true)
455
+ * @returns Worker instance with close() method for graceful shutdown.
349
456
  */
350
457
  async startWorker(options = {}) {
351
- const { workerId = `${os.hostname?.() || "host"}:${process.pid}`, claimTimeout = 120, concurrency = 1, batchSize, pollInterval = 0.25, onError = (err) => console.error("Worker error:", err), } = options;
458
+ const { workerId = `${os.hostname?.() || "host"}:${process.pid}`, claimTimeout = 120, concurrency = 1, batchSize, pollInterval = 0.25, onError = (err) => console.error("Worker error:", err), fatalOnLeaseTimeout = true, } = options;
352
459
  const effectiveBatchSize = batchSize ?? concurrency;
353
460
  let running = true;
354
461
  let workerLoopPromise;
462
+ const executing = new Set();
463
+ let availabilityPromise = null;
464
+ let availabilityResolve = null;
465
+ let sleepTimer = null;
466
+ const notifyAvailability = () => {
467
+ if (sleepTimer) {
468
+ clearTimeout(sleepTimer);
469
+ sleepTimer = null;
470
+ }
471
+ if (availabilityResolve) {
472
+ availabilityResolve();
473
+ availabilityResolve = null;
474
+ availabilityPromise = null;
475
+ }
476
+ };
477
+ const waitForAvailability = async () => {
478
+ if (!availabilityPromise) {
479
+ availabilityPromise = new Promise((resolve) => {
480
+ availabilityResolve = resolve;
481
+ sleepTimer = setTimeout(() => {
482
+ sleepTimer = null;
483
+ availabilityResolve = null;
484
+ availabilityPromise = null;
485
+ resolve();
486
+ }, pollInterval * 1000);
487
+ });
488
+ }
489
+ await availabilityPromise;
490
+ };
355
491
  const worker = {
356
492
  close: async () => {
357
493
  running = false;
@@ -362,53 +498,82 @@ export class Absurd {
362
498
  workerLoopPromise = (async () => {
363
499
  while (running) {
364
500
  try {
501
+ if (executing.size >= concurrency) {
502
+ await waitForAvailability();
503
+ continue;
504
+ }
505
+ const availableCapacity = Math.max(concurrency - executing.size, 0);
506
+ const toClaim = Math.min(effectiveBatchSize, availableCapacity);
507
+ if (toClaim <= 0) {
508
+ await waitForAvailability();
509
+ continue;
510
+ }
365
511
  const messages = await this.claimTasks({
366
- batchSize: effectiveBatchSize,
512
+ batchSize: toClaim,
367
513
  claimTimeout: claimTimeout,
368
514
  workerId,
369
515
  });
370
516
  if (messages.length === 0) {
371
- await sleep(pollInterval);
517
+ await waitForAvailability();
372
518
  continue;
373
519
  }
374
- const executing = new Set();
375
520
  for (const task of messages) {
376
- const promise = this.executeTask(task, claimTimeout)
521
+ const promise = this.executeTask(task, claimTimeout, {
522
+ fatalOnLeaseTimeout,
523
+ })
377
524
  .catch((err) => onError(err))
378
- .finally(() => executing.delete(promise));
525
+ .finally(() => {
526
+ executing.delete(promise);
527
+ notifyAvailability();
528
+ });
379
529
  executing.add(promise);
380
- if (executing.size >= concurrency) {
381
- await Promise.race(executing);
382
- }
383
530
  }
384
- await Promise.all(executing);
385
531
  }
386
532
  catch (err) {
387
533
  onError(err);
388
- await sleep(pollInterval);
534
+ await waitForAvailability();
389
535
  }
390
536
  }
537
+ await Promise.allSettled(executing);
391
538
  })();
392
539
  return worker;
393
540
  }
541
+ /**
542
+ * Stops any running worker and closes the underlying pool if owned.
543
+ */
394
544
  async close() {
395
545
  if (this.worker) {
396
546
  await this.worker.close();
397
547
  }
398
548
  if (this.ownedPool) {
399
- await this.pool.end();
549
+ await this.con.end();
400
550
  }
401
551
  }
402
- async executeTask(task, claimTimeout) {
552
+ async executeTask(task, claimTimeout, options) {
553
+ let warnTimer;
554
+ let fatalTimer;
403
555
  const registration = this.registry.get(task.task_name);
404
556
  const ctx = await TaskContext.create({
557
+ log: this.log,
405
558
  taskID: task.task_id,
406
- pool: this.pool,
559
+ con: this.con,
407
560
  queueName: registration?.queue ?? "unknown",
408
561
  task: task,
409
562
  claimTimeout,
410
563
  });
411
564
  try {
565
+ if (claimTimeout > 0) {
566
+ const taskLabel = `${task.task_name} (${task.task_id})`;
567
+ warnTimer = setTimeout(() => {
568
+ this.log.warn(`task ${taskLabel} exceeded claim timeout of ${claimTimeout}s`);
569
+ }, claimTimeout * 1000);
570
+ if (options?.fatalOnLeaseTimeout) {
571
+ fatalTimer = setTimeout(() => {
572
+ this.log.error(`task ${taskLabel} exceeded claim timeout of ${claimTimeout}s by more than 100%; terminating process`);
573
+ process.exit(1);
574
+ }, claimTimeout * 1000 * 2);
575
+ }
576
+ }
412
577
  if (!registration) {
413
578
  throw new Error("Unknown task");
414
579
  }
@@ -416,17 +581,31 @@ export class Absurd {
416
581
  throw new Error("Misconfigured task (queue mismatch)");
417
582
  }
418
583
  const result = await registration.handler(task.params, ctx);
419
- await ctx.complete(result);
584
+ await completeTaskRun(this.con, this.queueName, task.run_id, result);
420
585
  }
421
586
  catch (err) {
422
- if (err instanceof SuspendTask) {
423
- // Task suspended (sleep or await), don't complete or fail
587
+ if (err instanceof SuspendTask || err instanceof CancelledTask) {
588
+ // Task suspended or cancelled (sleep or await), don't complete or fail
424
589
  return;
425
590
  }
426
- await ctx.fail(err);
591
+ this.log.error("[absurd] task execution failed:", err);
592
+ await failTaskRun(this.con, this.queueName, task.run_id, err);
593
+ }
594
+ finally {
595
+ if (warnTimer) {
596
+ clearTimeout(warnTimer);
597
+ }
598
+ if (fatalTimer) {
599
+ clearTimeout(fatalTimer);
600
+ }
427
601
  }
428
602
  }
429
603
  }
604
+ function isQueryable(value) {
605
+ return (typeof value === "object" &&
606
+ value !== null &&
607
+ typeof value.query === "function");
608
+ }
430
609
  function serializeError(err) {
431
610
  if (err instanceof Error) {
432
611
  return {
@@ -437,6 +616,21 @@ function serializeError(err) {
437
616
  }
438
617
  return { message: String(err) };
439
618
  }
619
+ async function completeTaskRun(con, queueName, runID, result) {
620
+ await con.query(`SELECT absurd.complete_run($1, $2, $3)`, [
621
+ queueName,
622
+ runID,
623
+ JSON.stringify(result ?? null),
624
+ ]);
625
+ }
626
+ async function failTaskRun(con, queueName, runID, err) {
627
+ await con.query(`SELECT absurd.fail_run($1, $2, $3, $4)`, [
628
+ queueName,
629
+ runID,
630
+ JSON.stringify(serializeError(err)),
631
+ null,
632
+ ]);
633
+ }
440
634
  function normalizeSpawnOptions(options) {
441
635
  const normalized = {};
442
636
  if (options.headers !== undefined) {
@@ -482,7 +676,4 @@ function normalizeCancellation(policy) {
482
676
  }
483
677
  return Object.keys(normalized).length > 0 ? normalized : undefined;
484
678
  }
485
- async function sleep(ms) {
486
- return new Promise((resolve) => setTimeout(resolve, ms * 1000));
487
- }
488
679
  //# sourceMappingURL=index.js.map