absurd-sdk 0.0.4 → 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/README.md CHANGED
@@ -13,7 +13,7 @@ Durable execution (or durable workflows) is a way to run long-lived, reliable fu
13
13
  ## Installation
14
14
 
15
15
  ```bash
16
- npm install absurd-sdk
16
+ npm install absurd-sdk pg
17
17
  ```
18
18
 
19
19
  ## Prerequisites
@@ -31,9 +31,7 @@ absurdctl create-queue -d your-database-name default
31
31
  ```typescript
32
32
  import { Absurd } from "absurd-sdk";
33
33
 
34
- const app = new Absurd({
35
- connectionString: "postgresql://localhost/mydb",
36
- });
34
+ const app = new Absurd({ db: "postgresql://localhost/mydb" });
37
35
 
38
36
  // Register a task
39
37
  app.registerTask({ name: "order-fulfillment" }, async (params, ctx) => {
package/dist/cjs/index.js CHANGED
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.Absurd = exports.TaskContext = exports.TimeoutError = exports.SuspendTask = void 0;
36
+ exports.Absurd = exports.TaskContext = exports.TimeoutError = exports.CancelledTask = exports.SuspendTask = void 0;
37
37
  /**
38
38
  * Absurd SDK for TypeScript and JavaScript
39
39
  */
@@ -50,6 +50,17 @@ class SuspendTask extends Error {
50
50
  }
51
51
  }
52
52
  exports.SuspendTask = SuspendTask;
53
+ /**
54
+ * Internal exception that is thrown to cancel a run. As a user
55
+ * you should never see this exception.
56
+ */
57
+ class CancelledTask extends Error {
58
+ constructor() {
59
+ super("Task cancelled");
60
+ this.name = "CancelledTask";
61
+ }
62
+ }
63
+ exports.CancelledTask = CancelledTask;
53
64
  /**
54
65
  * This error is thrown when awaiting an event ran into a timeout.
55
66
  */
@@ -61,36 +72,48 @@ class TimeoutError extends Error {
61
72
  }
62
73
  exports.TimeoutError = TimeoutError;
63
74
  class TaskContext {
75
+ log;
64
76
  taskID;
65
- pool;
77
+ con;
66
78
  queueName;
67
79
  task;
68
80
  checkpointCache;
69
81
  claimTimeout;
70
82
  stepNameCounter = new Map();
71
- constructor(taskID, pool, queueName, task, checkpointCache, claimTimeout) {
83
+ constructor(log, taskID, con, queueName, task, checkpointCache, claimTimeout) {
84
+ this.log = log;
72
85
  this.taskID = taskID;
73
- this.pool = pool;
86
+ this.con = con;
74
87
  this.queueName = queueName;
75
88
  this.task = task;
76
89
  this.checkpointCache = checkpointCache;
77
90
  this.claimTimeout = claimTimeout;
78
91
  }
79
92
  static async create(args) {
80
- const { taskID, pool, queueName, task, claimTimeout } = args;
81
- const result = await pool.query(`SELECT checkpoint_name, state, status, owner_run_id, updated_at
93
+ const { log, taskID, con, queueName, task, claimTimeout } = args;
94
+ const result = await con.query(`SELECT checkpoint_name, state, status, owner_run_id, updated_at
82
95
  FROM absurd.get_task_checkpoint_states($1, $2, $3)`, [queueName, task.task_id, task.run_id]);
83
96
  const cache = new Map();
84
97
  for (const row of result.rows) {
85
98
  cache.set(row.checkpoint_name, row.state);
86
99
  }
87
- return new TaskContext(taskID, pool, queueName, task, cache, claimTimeout);
100
+ return new TaskContext(log, taskID, con, queueName, task, cache, claimTimeout);
101
+ }
102
+ async queryWithCancelCheck(sql, params) {
103
+ try {
104
+ return await this.con.query(sql, params);
105
+ }
106
+ catch (err) {
107
+ if (err?.code === "AB001") {
108
+ throw new CancelledTask();
109
+ }
110
+ throw err;
111
+ }
88
112
  }
89
113
  /**
90
- * Defines a step in the task execution. Steps are idempotent in
91
- * that they are executed exactly once (unless they fail) and their
92
- * results are cached. As a result the return value of this function
93
- * must support `JSON.stringify`.
114
+ * Runs an idempotent step identified by name; caches and reuses its result across retries.
115
+ * @param name Unique checkpoint name for this step.
116
+ * @param fn Async function computing the step result (must be JSON-serializable).
94
117
  */
95
118
  async step(name, fn) {
96
119
  const checkpointName = this.getCheckpointName(name);
@@ -103,16 +126,17 @@ class TaskContext {
103
126
  return rv;
104
127
  }
105
128
  /**
106
- * Sleeps for a given number of seconds. Note that this
107
- * *always* suspends the task, even if you only wait for a very
108
- * short period of time.
129
+ * Suspends the task until the given duration (seconds) elapses.
130
+ * @param stepName Checkpoint name for this wait.
131
+ * @param duration Duration to wait in seconds.
109
132
  */
110
133
  async sleepFor(stepName, duration) {
111
134
  return await this.sleepUntil(stepName, new Date(Date.now() + duration * 1000));
112
135
  }
113
136
  /**
114
- * Like `sleepFor` but with an absolute time when the task should be
115
- * awoken again.
137
+ * Suspends the task until the specified time.
138
+ * @param stepName Checkpoint name for this wait.
139
+ * @param wakeAt Absolute time when the task should resume.
116
140
  */
117
141
  async sleepUntil(stepName, wakeAt) {
118
142
  const checkpointName = this.getCheckpointName(stepName);
@@ -137,7 +161,7 @@ class TaskContext {
137
161
  if (cached !== undefined) {
138
162
  return cached;
139
163
  }
140
- const result = await this.pool.query(`SELECT checkpoint_name, state, status, owner_run_id, updated_at
164
+ const result = await this.con.query(`SELECT checkpoint_name, state, status, owner_run_id, updated_at
141
165
  FROM absurd.get_task_checkpoint_state($1, $2, $3)`, [this.queueName, this.task.task_id, checkpointName]);
142
166
  if (result.rows.length > 0) {
143
167
  const state = result.rows[0].state;
@@ -147,7 +171,7 @@ class TaskContext {
147
171
  return undefined;
148
172
  }
149
173
  async persistCheckpoint(checkpointName, value) {
150
- await this.pool.query(`SELECT absurd.set_task_checkpoint_state($1, $2, $3, $4, $5, $6)`, [
174
+ await this.queryWithCancelCheck(`SELECT absurd.set_task_checkpoint_state($1, $2, $3, $4, $5, $6)`, [
151
175
  this.queueName,
152
176
  this.task.task_id,
153
177
  checkpointName,
@@ -158,15 +182,18 @@ class TaskContext {
158
182
  this.checkpointCache.set(checkpointName, value);
159
183
  }
160
184
  async scheduleRun(wakeAt) {
161
- await this.pool.query(`SELECT absurd.schedule_run($1, $2, $3)`, [
185
+ await this.con.query(`SELECT absurd.schedule_run($1, $2, $3)`, [
162
186
  this.queueName,
163
187
  this.task.run_id,
164
188
  wakeAt,
165
189
  ]);
166
190
  }
167
191
  /**
168
- * Awaits the arrival of an event. Events need to be uniquely
169
- * named so fold in the necessary parameters into the name (eg: customer id).
192
+ * Waits for an event by name and returns its payload; optionally sets a custom step name and timeout (seconds).
193
+ * @param eventName Event identifier to wait for.
194
+ * @param options.stepName Optional checkpoint name (defaults to $awaitEvent:<eventName>).
195
+ * @param options.timeout Optional timeout in seconds.
196
+ * @throws TimeoutError If the event is not received before the timeout.
170
197
  */
171
198
  async awaitEvent(eventName, options) {
172
199
  // the default step name is derived from the event name.
@@ -189,8 +216,8 @@ class TaskContext {
189
216
  this.task.event_payload = null;
190
217
  throw new TimeoutError(`Timed out waiting for event "${eventName}"`);
191
218
  }
192
- const result = await this.pool.query(`SELECT should_suspend, payload
193
- FROM absurd.await_event($1, $2, $3, $4, $5, $6)`, [
219
+ const result = await this.queryWithCancelCheck(`SELECT should_suspend, payload
220
+ FROM absurd.await_event($1, $2, $3, $4, $5, $6)`, [
194
221
  this.queueName,
195
222
  this.task.task_id,
196
223
  this.task.run_id,
@@ -210,61 +237,90 @@ class TaskContext {
210
237
  throw new SuspendTask();
211
238
  }
212
239
  /**
213
- * Emits an event that can be awaited.
240
+ * Extends the current run's lease by the given seconds (defaults to the original claim timeout).
241
+ * @param seconds Lease extension in seconds.
242
+ */
243
+ async heartbeat(seconds) {
244
+ await this.queryWithCancelCheck(`SELECT absurd.extend_claim($1, $2, $3)`, [
245
+ this.queueName,
246
+ this.task.run_id,
247
+ seconds ?? this.claimTimeout,
248
+ ]);
249
+ }
250
+ /**
251
+ * Emits an event to this task's queue with an optional payload.
252
+ * @param eventName Non-empty event name.
253
+ * @param payload Optional JSON-serializable payload.
214
254
  */
215
255
  async emitEvent(eventName, payload) {
216
256
  if (!eventName) {
217
257
  throw new Error("eventName must be a non-empty string");
218
258
  }
219
- await this.pool.query(`SELECT absurd.emit_event($1, $2, $3)`, [
259
+ await this.con.query(`SELECT absurd.emit_event($1, $2, $3)`, [
220
260
  this.queueName,
221
261
  eventName,
222
262
  JSON.stringify(payload ?? null),
223
263
  ]);
224
264
  }
225
- async complete(result) {
226
- await this.pool.query(`SELECT absurd.complete_run($1, $2, $3)`, [
227
- this.queueName,
228
- this.task.run_id,
229
- JSON.stringify(result ?? null),
230
- ]);
231
- }
232
- async fail(err) {
233
- console.error("[absurd] task execution failed:", err);
234
- await this.pool.query(`SELECT absurd.fail_run($1, $2, $3, $4)`, [
235
- this.queueName,
236
- this.task.run_id,
237
- JSON.stringify(serializeError(err)),
238
- null,
239
- ]);
240
- }
241
265
  }
242
266
  exports.TaskContext = TaskContext;
267
+ /**
268
+ * The Absurd SDK Client.
269
+ *
270
+ * Instanciate this class and keep it around to interact with Absurd.
271
+ */
243
272
  class Absurd {
244
- pool;
273
+ con;
245
274
  ownedPool;
246
275
  queueName;
247
276
  defaultMaxAttempts;
248
277
  registry = new Map();
278
+ log;
249
279
  worker = null;
250
- constructor(poolOrUrl, queueName = "default", defaultMaxAttempts = 5) {
251
- if (!poolOrUrl) {
252
- poolOrUrl =
280
+ constructor(options = {}) {
281
+ if (typeof options === "string" || isQueryable(options)) {
282
+ options = { db: options };
283
+ }
284
+ let connectionOrUrl = options.db;
285
+ if (!connectionOrUrl) {
286
+ connectionOrUrl =
253
287
  process.env.ABSURD_DATABASE_URL || "postgresql://localhost/absurd";
254
288
  }
255
- if (typeof poolOrUrl === "string") {
256
- this.pool = new pg.Pool({ connectionString: poolOrUrl });
289
+ if (typeof connectionOrUrl === "string") {
290
+ this.con = new pg.Pool({ connectionString: connectionOrUrl });
257
291
  this.ownedPool = true;
258
292
  }
259
293
  else {
260
- this.pool = poolOrUrl;
294
+ this.con = connectionOrUrl;
261
295
  this.ownedPool = false;
262
296
  }
263
- this.queueName = queueName;
264
- this.defaultMaxAttempts = defaultMaxAttempts;
297
+ this.queueName = options?.queueName ?? "default";
298
+ this.defaultMaxAttempts = options?.defaultMaxAttempts ?? 5;
299
+ this.log = options?.log ?? console;
300
+ }
301
+ /**
302
+ * Returns a new client that uses the provided connection for queries; set owned=true to close it with close().
303
+ * @param con Connection to bind to.
304
+ * @param owned If true, the bound client will close this connection on close().
305
+ */
306
+ bindToConnection(con, owned = false) {
307
+ const bound = new Absurd({
308
+ db: con, // this is okay because we ensure the invariant later
309
+ queueName: this.queueName,
310
+ defaultMaxAttempts: this.defaultMaxAttempts,
311
+ log: this.log,
312
+ });
313
+ bound.registry = this.registry;
314
+ bound.ownedPool = owned;
315
+ return bound;
265
316
  }
266
317
  /**
267
- * This registers a given function as task.
318
+ * Registers a task handler by name (optionally specifying queue, defaultMaxAttempts, and defaultCancellation).
319
+ * @param options.name Task name.
320
+ * @param options.queue Optional queue name (defaults to client queue).
321
+ * @param options.defaultMaxAttempts Optional default max attempts.
322
+ * @param options.defaultCancellation Optional default cancellation policy.
323
+ * @param handler Async task handler.
268
324
  */
269
325
  registerTask(options, handler) {
270
326
  if (!options?.name) {
@@ -289,16 +345,28 @@ class Absurd {
289
345
  handler: handler,
290
346
  });
291
347
  }
348
+ /**
349
+ * Creates a queue (defaults to this client's queue).
350
+ * @param queueName Queue name to create.
351
+ */
292
352
  async createQueue(queueName) {
293
353
  const queue = queueName ?? this.queueName;
294
- await this.pool.query(`SELECT absurd.create_queue($1)`, [queue]);
354
+ await this.con.query(`SELECT absurd.create_queue($1)`, [queue]);
295
355
  }
356
+ /**
357
+ * Drops a queue (defaults to this client's queue).
358
+ * @param queueName Queue name to drop.
359
+ */
296
360
  async dropQueue(queueName) {
297
361
  const queue = queueName ?? this.queueName;
298
- await this.pool.query(`SELECT absurd.drop_queue($1)`, [queue]);
362
+ await this.con.query(`SELECT absurd.drop_queue($1)`, [queue]);
299
363
  }
364
+ /**
365
+ * Lists all queue names.
366
+ * @returns Array of queue names.
367
+ */
300
368
  async listQueues() {
301
- const result = await this.pool.query(`SELECT * FROM absurd.list_queues()`);
369
+ const result = await this.con.query(`SELECT * FROM absurd.list_queues()`);
302
370
  const rv = [];
303
371
  for (const row of result.rows) {
304
372
  rv.push(row.queue_name);
@@ -306,7 +374,17 @@ class Absurd {
306
374
  return rv;
307
375
  }
308
376
  /**
309
- * Spawns a specific task.
377
+ * Spawns a task execution by enqueueing it for processing. The task will be picked up by a worker
378
+ * and executed with the provided parameters. Returns identifiers that can be used to track or cancel the task.
379
+ *
380
+ * For registered tasks, the queue and defaults are inferred from registration. For unregistered tasks,
381
+ * you must provide options.queue.
382
+ *
383
+ * @param taskName Name of the task to spawn (must be registered or provide options.queue).
384
+ * @param params JSON-serializable parameters passed to the task handler.
385
+ * @param options Configure queue, maxAttempts, retryStrategy, headers, and cancellation policies.
386
+ * @returns Object containing taskID (unique task identifier), runID (current attempt identifier), and attempt number.
387
+ * @throws Error If the task is unregistered without a queue, or if the queue mismatches registration.
310
388
  */
311
389
  async spawn(taskName, params, options = {}) {
312
390
  const registration = this.registry.get(taskName);
@@ -334,7 +412,7 @@ class Absurd {
334
412
  maxAttempts: effectiveMaxAttempts,
335
413
  cancellation: effectiveCancellation,
336
414
  });
337
- const result = await this.pool.query(`SELECT task_id, run_id, attempt
415
+ const result = await this.con.query(`SELECT task_id, run_id, attempt
338
416
  FROM absurd.spawn_task($1, $2, $3, $4)`, [
339
417
  queue,
340
418
  taskName,
@@ -352,28 +430,45 @@ class Absurd {
352
430
  };
353
431
  }
354
432
  /**
355
- * Emits an event from outside of a task.
433
+ * Emits an event with an optional payload on the specified or default queue.
434
+ * @param eventName Non-empty event name.
435
+ * @param payload Optional JSON-serializable payload.
436
+ * @param queueName Queue to emit to (defaults to this client's queue).
356
437
  */
357
438
  async emitEvent(eventName, payload, queueName) {
358
439
  if (!eventName) {
359
440
  throw new Error("eventName must be a non-empty string");
360
441
  }
361
- await this.pool.query(`SELECT absurd.emit_event($1, $2, $3)`, [
442
+ await this.con.query(`SELECT absurd.emit_event($1, $2, $3)`, [
362
443
  queueName || this.queueName,
363
444
  eventName,
364
445
  JSON.stringify(payload ?? null),
365
446
  ]);
366
447
  }
448
+ /**
449
+ * Cancels a task by ID on the specified or default queue; running tasks stop at the next checkpoint/heartbeat.
450
+ * @param taskID Task identifier to cancel.
451
+ * @param queueName Queue name (defaults to this client's queue).
452
+ */
453
+ async cancelTask(taskID, queueName) {
454
+ await this.con.query(`SELECT absurd.cancel_task($1, $2)`, [
455
+ queueName || this.queueName,
456
+ taskID,
457
+ ]);
458
+ }
367
459
  async claimTasks(options) {
368
460
  const { batchSize: count = 1, claimTimeout = 120, workerId = "worker", } = options ?? {};
369
- const result = await this.pool.query(`SELECT run_id, task_id, attempt, task_name, params, retry_strategy, max_attempts,
461
+ const result = await this.con.query(`SELECT run_id, task_id, attempt, task_name, params, retry_strategy, max_attempts,
370
462
  headers, wake_event, event_payload
371
463
  FROM absurd.claim_task($1, $2, $3, $4)`, [this.queueName, workerId, claimTimeout, count]);
372
464
  return result.rows;
373
465
  }
374
466
  /**
375
- * Polls and processes a batch of messages sequentially.
376
- * For parallel processing, use startWorker with concurrency option.
467
+ * Claims up to batchSize tasks and processes them sequentially using the given workerId and claimTimeout.
468
+ * @param workerId Worker identifier.
469
+ * @param claimTimeout Lease duration in seconds.
470
+ * @param batchSize Maximum number of tasks to process.
471
+ * Note: For parallel processing, use startWorker().
377
472
  */
378
473
  async workBatch(workerId = "worker", claimTimeout = 120, batchSize = 1) {
379
474
  const tasks = await this.claimTasks({ batchSize, claimTimeout, workerId });
@@ -382,18 +477,37 @@ class Absurd {
382
477
  }
383
478
  }
384
479
  /**
385
- * Starts a worker that continuously polls for tasks and processes them.
386
- * Returns a Worker instance with a close() method for graceful shutdown.
480
+ * Starts a background worker that continuously polls for and processes tasks from the queue.
481
+ * The worker will claim tasks up to the configured concurrency limit and process them in parallel.
482
+ *
483
+ * Tasks are claimed with a lease (claimTimeout) that prevents other workers from processing them.
484
+ * The lease is automatically extended when tasks write checkpoints or call heartbeat(). If a worker
485
+ * crashes or stops making progress, the lease expires and another worker can claim the task.
486
+ *
487
+ * @param options Configure worker behavior:
488
+ * - concurrency: Max parallel tasks (default: 1)
489
+ * - claimTimeout: Task lease duration in seconds (default: 120)
490
+ * - batchSize: Tasks to claim per poll (default: concurrency)
491
+ * - pollInterval: Seconds between polls when idle (default: 0.25)
492
+ * - workerId: Worker identifier for tracking (default: hostname:pid)
493
+ * - onError: Error handler called for execution failures
494
+ * - fatalOnLeaseTimeout: Terminate process if task exceeds 2x claimTimeout (default: true)
495
+ * @returns Worker instance with close() method for graceful shutdown.
387
496
  */
388
497
  async startWorker(options = {}) {
389
- const { workerId = `${os.hostname?.() || "host"}:${process.pid}`, claimTimeout = 120, concurrency = 1, batchSize, pollInterval = 0.25, onError = (err) => console.error("[absurd] Worker error:", err), fatalOnLeaseTimeout = true, } = options;
498
+ 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;
390
499
  const effectiveBatchSize = batchSize ?? concurrency;
391
500
  let running = true;
392
501
  let workerLoopPromise;
393
502
  const executing = new Set();
394
503
  let availabilityPromise = null;
395
504
  let availabilityResolve = null;
505
+ let sleepTimer = null;
396
506
  const notifyAvailability = () => {
507
+ if (sleepTimer) {
508
+ clearTimeout(sleepTimer);
509
+ sleepTimer = null;
510
+ }
397
511
  if (availabilityResolve) {
398
512
  availabilityResolve();
399
513
  availabilityResolve = null;
@@ -401,16 +515,18 @@ class Absurd {
401
515
  }
402
516
  };
403
517
  const waitForAvailability = async () => {
404
- if (executing.size === 0) {
405
- await sleep(pollInterval);
406
- return;
407
- }
408
518
  if (!availabilityPromise) {
409
519
  availabilityPromise = new Promise((resolve) => {
410
520
  availabilityResolve = resolve;
521
+ sleepTimer = setTimeout(() => {
522
+ sleepTimer = null;
523
+ availabilityResolve = null;
524
+ availabilityPromise = null;
525
+ resolve();
526
+ }, pollInterval * 1000);
411
527
  });
412
528
  }
413
- await Promise.race([availabilityPromise, sleep(pollInterval)]);
529
+ await availabilityPromise;
414
530
  };
415
531
  const worker = {
416
532
  close: async () => {
@@ -462,12 +578,15 @@ class Absurd {
462
578
  })();
463
579
  return worker;
464
580
  }
581
+ /**
582
+ * Stops any running worker and closes the underlying pool if owned.
583
+ */
465
584
  async close() {
466
585
  if (this.worker) {
467
586
  await this.worker.close();
468
587
  }
469
588
  if (this.ownedPool) {
470
- await this.pool.end();
589
+ await this.con.end();
471
590
  }
472
591
  }
473
592
  async executeTask(task, claimTimeout, options) {
@@ -475,8 +594,9 @@ class Absurd {
475
594
  let fatalTimer;
476
595
  const registration = this.registry.get(task.task_name);
477
596
  const ctx = await TaskContext.create({
597
+ log: this.log,
478
598
  taskID: task.task_id,
479
- pool: this.pool,
599
+ con: this.con,
480
600
  queueName: registration?.queue ?? "unknown",
481
601
  task: task,
482
602
  claimTimeout,
@@ -485,11 +605,11 @@ class Absurd {
485
605
  if (claimTimeout > 0) {
486
606
  const taskLabel = `${task.task_name} (${task.task_id})`;
487
607
  warnTimer = setTimeout(() => {
488
- console.warn(`[absurd] task ${taskLabel} exceeded claim timeout of ${claimTimeout}s`);
608
+ this.log.warn(`task ${taskLabel} exceeded claim timeout of ${claimTimeout}s`);
489
609
  }, claimTimeout * 1000);
490
610
  if (options?.fatalOnLeaseTimeout) {
491
611
  fatalTimer = setTimeout(() => {
492
- console.error(`[absurd] task ${taskLabel} exceeded claim timeout of ${claimTimeout}s by more than 100%; terminating process`);
612
+ this.log.error(`task ${taskLabel} exceeded claim timeout of ${claimTimeout}s by more than 100%; terminating process`);
493
613
  process.exit(1);
494
614
  }, claimTimeout * 1000 * 2);
495
615
  }
@@ -501,14 +621,15 @@ class Absurd {
501
621
  throw new Error("Misconfigured task (queue mismatch)");
502
622
  }
503
623
  const result = await registration.handler(task.params, ctx);
504
- await ctx.complete(result);
624
+ await completeTaskRun(this.con, this.queueName, task.run_id, result);
505
625
  }
506
626
  catch (err) {
507
- if (err instanceof SuspendTask) {
508
- // Task suspended (sleep or await), don't complete or fail
627
+ if (err instanceof SuspendTask || err instanceof CancelledTask) {
628
+ // Task suspended or cancelled (sleep or await), don't complete or fail
509
629
  return;
510
630
  }
511
- await ctx.fail(err);
631
+ this.log.error("[absurd] task execution failed:", err);
632
+ await failTaskRun(this.con, this.queueName, task.run_id, err);
512
633
  }
513
634
  finally {
514
635
  if (warnTimer) {
@@ -521,6 +642,11 @@ class Absurd {
521
642
  }
522
643
  }
523
644
  exports.Absurd = Absurd;
645
+ function isQueryable(value) {
646
+ return (typeof value === "object" &&
647
+ value !== null &&
648
+ typeof value.query === "function");
649
+ }
524
650
  function serializeError(err) {
525
651
  if (err instanceof Error) {
526
652
  return {
@@ -531,6 +657,21 @@ function serializeError(err) {
531
657
  }
532
658
  return { message: String(err) };
533
659
  }
660
+ async function completeTaskRun(con, queueName, runID, result) {
661
+ await con.query(`SELECT absurd.complete_run($1, $2, $3)`, [
662
+ queueName,
663
+ runID,
664
+ JSON.stringify(result ?? null),
665
+ ]);
666
+ }
667
+ async function failTaskRun(con, queueName, runID, err) {
668
+ await con.query(`SELECT absurd.fail_run($1, $2, $3, $4)`, [
669
+ queueName,
670
+ runID,
671
+ JSON.stringify(serializeError(err)),
672
+ null,
673
+ ]);
674
+ }
534
675
  function normalizeSpawnOptions(options) {
535
676
  const normalized = {};
536
677
  if (options.headers !== undefined) {
@@ -576,6 +717,3 @@ function normalizeCancellation(policy) {
576
717
  }
577
718
  return Object.keys(normalized).length > 0 ? normalized : undefined;
578
719
  }
579
- async function sleep(ms) {
580
- return new Promise((resolve) => setTimeout(resolve, ms * 1000));
581
- }