cairnq 0.1.0 → 0.2.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.
Files changed (56) hide show
  1. package/README.md +28 -0
  2. package/dist/_protocol/migrations/postgres/0002_purge_index.sql +6 -0
  3. package/dist/_protocol/migrations/postgres/0003_notify.sql +38 -0
  4. package/dist/_protocol/migrations/sqlite/0002_purge_index.sql +6 -0
  5. package/dist/_protocol/sql/postgres/claim.sql +18 -5
  6. package/dist/_protocol/sql/postgres/fail.sql +28 -8
  7. package/dist/_protocol/sql/postgres/insert_task.sql +6 -3
  8. package/dist/_protocol/sql/postgres/list.sql +3 -1
  9. package/dist/_protocol/sql/postgres/lock_key.sql +9 -0
  10. package/dist/_protocol/sql/postgres/progress.sql +4 -3
  11. package/dist/_protocol/sql/postgres/protocol_version.sql +4 -0
  12. package/dist/_protocol/sql/postgres/purge.sql +25 -0
  13. package/dist/_protocol/sql/postgres/recover_leases.sql +38 -14
  14. package/dist/_protocol/sql/postgres/retry.sql +3 -0
  15. package/dist/_protocol/sql/postgres/stats.sql +8 -0
  16. package/dist/_protocol/sql/sqlite/claim.sql +13 -2
  17. package/dist/_protocol/sql/sqlite/claimable_probe.sql +6 -2
  18. package/dist/_protocol/sql/sqlite/fail.sql +30 -8
  19. package/dist/_protocol/sql/sqlite/list.sql +3 -1
  20. package/dist/_protocol/sql/sqlite/lock_key.sql +5 -0
  21. package/dist/_protocol/sql/sqlite/progress.sql +6 -2
  22. package/dist/_protocol/sql/sqlite/protocol_version.sql +4 -0
  23. package/dist/_protocol/sql/sqlite/purge.sql +18 -0
  24. package/dist/_protocol/sql/sqlite/recover_leases.sql +25 -7
  25. package/dist/_protocol/sql/sqlite/retry.sql +3 -0
  26. package/dist/_protocol/sql/sqlite/stats.sql +8 -0
  27. package/dist/client.d.ts +10 -2
  28. package/dist/client.js +12 -0
  29. package/dist/context.d.ts +17 -1
  30. package/dist/context.js +60 -6
  31. package/dist/errors.d.ts +18 -2
  32. package/dist/errors.js +49 -3
  33. package/dist/index.d.ts +3 -2
  34. package/dist/index.js +2 -1
  35. package/dist/sql.js +16 -9
  36. package/dist/store/base.d.ts +107 -9
  37. package/dist/store/base.js +370 -1
  38. package/dist/store/postgres.d.ts +62 -63
  39. package/dist/store/postgres.js +245 -222
  40. package/dist/store/sqlite.d.ts +34 -59
  41. package/dist/store/sqlite.js +200 -232
  42. package/dist/wait.d.ts +15 -2
  43. package/dist/wait.js +23 -5
  44. package/dist/worker.d.ts +53 -1
  45. package/dist/worker.js +202 -42
  46. package/package.json +9 -2
  47. package/src/client.ts +16 -2
  48. package/src/context.ts +70 -13
  49. package/src/errors.ts +59 -4
  50. package/src/index.ts +3 -1
  51. package/src/sql.ts +15 -8
  52. package/src/store/base.ts +430 -27
  53. package/src/store/postgres.ts +243 -267
  54. package/src/store/sqlite.ts +211 -265
  55. package/src/wait.ts +28 -5
  56. package/src/worker.ts +242 -42
package/dist/context.d.ts CHANGED
@@ -8,6 +8,9 @@ export declare class TaskContext {
8
8
  private readonly task;
9
9
  readonly workerId: string;
10
10
  private readonly leaseMs;
11
+ private readonly abort;
12
+ private leaseLost;
13
+ private cancelSeen;
11
14
  constructor(store: TaskStore, task: Task, workerId: string, leaseMs: number);
12
15
  get taskId(): string;
13
16
  get name(): string;
@@ -17,9 +20,22 @@ export declare class TaskContext {
17
20
  get rootId(): string | null;
18
21
  get correlationId(): string | null;
19
22
  get payload(): any;
23
+ /**
24
+ * True once this worker has lost the task's lease — it expired and another
25
+ * worker reclaimed it. Nothing this handler writes will be recorded any more
26
+ * and the task is already running elsewhere, so a long handler should check
27
+ * this (or `signal`) and bail out instead of continuing to do side effects.
28
+ */
29
+ get lostLease(): boolean;
30
+ /** Aborts when the lease is lost. Pass it to fetch / any AbortSignal-aware API. */
31
+ get signal(): AbortSignal;
32
+ /** @internal Called by the worker when an owned write reports a lost lease. */
33
+ markLeaseLost(): void;
34
+ private observe;
35
+ private owned;
20
36
  progress(value: number | null, message?: string | null): Promise<Task>;
21
37
  heartbeat(): Promise<Task>;
22
- /** Cooperative cancel check. */
38
+ /** Cooperative cancel check. Free once a heartbeat has already seen the flag. */
23
39
  canceled(): Promise<boolean>;
24
40
  /** Submit a child task; parent/root/correlation are wired automatically. */
25
41
  submit(name: string, payload?: unknown, opts?: SubmitOptions): Promise<Task>;
package/dist/context.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { LostLease } from "./errors.js";
1
2
  import { cancelRequested } from "./models.js";
2
3
  import { taskName } from "./task.js";
3
4
  import { pollWait } from "./wait.js";
@@ -7,6 +8,11 @@ export class TaskContext {
7
8
  task;
8
9
  workerId;
9
10
  leaseMs;
11
+ abort = new AbortController();
12
+ leaseLost = false;
13
+ // Cancellation is monotonic: once the DB has told us a cancel was requested it
14
+ // can't be taken back, so canceled() can answer from this without a re-read.
15
+ cancelSeen = false;
10
16
  constructor(store, task, workerId, leaseMs) {
11
17
  this.store = store;
12
18
  this.task = task;
@@ -37,27 +43,75 @@ export class TaskContext {
37
43
  get payload() {
38
44
  return this.task.payload;
39
45
  }
46
+ /**
47
+ * True once this worker has lost the task's lease — it expired and another
48
+ * worker reclaimed it. Nothing this handler writes will be recorded any more
49
+ * and the task is already running elsewhere, so a long handler should check
50
+ * this (or `signal`) and bail out instead of continuing to do side effects.
51
+ */
52
+ get lostLease() {
53
+ return this.leaseLost;
54
+ }
55
+ /** Aborts when the lease is lost. Pass it to fetch / any AbortSignal-aware API. */
56
+ get signal() {
57
+ return this.abort.signal;
58
+ }
59
+ /** @internal Called by the worker when an owned write reports a lost lease. */
60
+ markLeaseLost() {
61
+ if (this.leaseLost)
62
+ return;
63
+ this.leaseLost = true;
64
+ this.abort.abort(new LostLease(this.task.id));
65
+ }
66
+ // Every owned write returns the current row, so cancellation and lease loss
67
+ // ride along on writes the handler was making anyway.
68
+ observe(task) {
69
+ if (cancelRequested(task))
70
+ this.cancelSeen = true;
71
+ return task;
72
+ }
73
+ async owned(write) {
74
+ // Short-circuit once the lease is known lost: nothing this context writes
75
+ // may be recorded any more. Locally, not just via the store's ownership
76
+ // check — after an abandoned (timed-out) attempt the same worker may
77
+ // re-claim this task under the same workerId, and a zombie handler's write
78
+ // would then pass ownership against the NEW attempt.
79
+ if (this.leaseLost)
80
+ throw new LostLease(this.task.id);
81
+ try {
82
+ return this.observe(await write());
83
+ }
84
+ catch (err) {
85
+ if (err instanceof LostLease)
86
+ this.markLeaseLost();
87
+ throw err;
88
+ }
89
+ }
40
90
  async progress(value, message = null) {
41
- return this.store.progress({
91
+ return this.owned(() => this.store.progress({
42
92
  taskId: this.task.id,
43
93
  workerId: this.workerId,
44
94
  progress: value,
45
95
  message,
46
- });
96
+ }));
47
97
  }
48
98
  async heartbeat() {
49
- return this.store.heartbeat({
99
+ return this.owned(() => this.store.heartbeat({
50
100
  taskId: this.task.id,
51
101
  workerId: this.workerId,
52
102
  leaseMs: this.leaseMs,
53
- });
103
+ }));
54
104
  }
55
- /** Cooperative cancel check. */
105
+ /** Cooperative cancel check. Free once a heartbeat has already seen the flag. */
56
106
  async canceled() {
107
+ if (this.cancelSeen)
108
+ return true;
57
109
  const t = await this.store.get(this.task.id);
58
110
  if (!t)
59
111
  return true;
60
- return cancelRequested(t) || t.status === "canceled";
112
+ if (cancelRequested(t))
113
+ this.cancelSeen = true;
114
+ return this.cancelSeen || t.status === "canceled";
61
115
  }
62
116
  async submit(task, payload, opts = {}) {
63
117
  return this.store.submit({
package/dist/errors.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type Task } from "./models.js";
1
2
  /** The single shape of the JSON error envelope (see PROTOCOL.md). Everything that
2
3
  * records an error — a handler exception, a missing handler, lease expiry, a thrown
3
4
  * TaskError — builds it here, so the contract's fields live in one place. */
@@ -9,15 +10,23 @@ export declare function errorEnvelope(e: {
9
10
  details?: Record<string, unknown>;
10
11
  }): Record<string, unknown>;
11
12
  export declare class CairnQError extends Error {
13
+ constructor(message?: string);
12
14
  }
13
15
  export declare class AlreadyExists extends CairnQError {
14
16
  key: string;
15
17
  constructor(key: string);
16
18
  }
17
- /** wait/call did not reach a terminal status in time. The task keeps running. */
19
+ /** wait/call did not reach a terminal status in time. The task keeps running.
20
+ * `task` is the last snapshot wait() observed (null if get() found nothing), and
21
+ * the message says what state it was stuck in — a queued-never-claimed task is
22
+ * the classic first-run failure (no worker, no handler, wrong queue or file). */
18
23
  export declare class TaskTimeout extends CairnQError {
19
24
  taskId: string;
20
- constructor(taskId: string);
25
+ readonly task: Task | null;
26
+ constructor(taskId: string, opts?: {
27
+ timeoutMs?: number;
28
+ task?: Task | null;
29
+ });
21
30
  }
22
31
  /** A waited-on task ended in `failed`. The envelope's fields are unpacked onto the
23
32
  * error — read `e.code` / `e.message` / `e.retryable` / `e.details` instead of
@@ -42,6 +51,13 @@ export declare class LostLease extends CairnQError {
42
51
  export declare class ProtocolVersionMismatch extends CairnQError {
43
52
  constructor(message: string);
44
53
  }
54
+ /** A value could not be encoded for a protocol JSON column (non-finite number,
55
+ * BigInt, circular structure, …). Raised at the boundary — submit rejects with
56
+ * it, and a worker records a handler result that triggers it as a permanent
57
+ * `unserializable_result` failure. The Python SDK raises the same named error. */
58
+ export declare class SerializationError extends CairnQError {
59
+ constructor(message: string);
60
+ }
45
61
  /** Throw inside a handler to control how the failure is recorded. Defaults to
46
62
  * non-retryable so deterministic errors fail fast instead of burning retries.
47
63
  * Any other thrown value is treated as retryable. */
package/dist/errors.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { nowMs } from "./ids.js";
2
+ import { cancelRequested, isQueued } from "./models.js";
1
3
  /** The single shape of the JSON error envelope (see PROTOCOL.md). Everything that
2
4
  * records an error — a handler exception, a missing handler, lease expiry, a thrown
3
5
  * TaskError — builds it here, so the contract's fields live in one place. */
@@ -11,6 +13,13 @@ export function errorEnvelope(e) {
11
13
  };
12
14
  }
13
15
  export class CairnQError extends Error {
16
+ constructor(message) {
17
+ super(message);
18
+ // Subclasses each set their own; without this a bare CairnQError reports
19
+ // "Error", and `err.name` is how callers (and the conformance runner) tell
20
+ // one apart from another.
21
+ this.name = "CairnQError";
22
+ }
14
23
  }
15
24
  export class AlreadyExists extends CairnQError {
16
25
  key;
@@ -20,13 +29,40 @@ export class AlreadyExists extends CairnQError {
20
29
  this.name = "AlreadyExists";
21
30
  }
22
31
  }
23
- /** wait/call did not reach a terminal status in time. The task keeps running. */
32
+ /** One line of "why hasn't this finished" from the last snapshot wait()
33
+ * observed. No worker running, no handler for the name, wrong queue, and two
34
+ * processes on different database files all look identical from the API side —
35
+ * queued, never claimed — so that case names the likely causes. */
36
+ function timeoutDetail(task) {
37
+ if (!task)
38
+ return "task not found — wrong database file, or already purged?";
39
+ if (isQueued(task)) {
40
+ const delayMs = task.run_at_ms - nowMs();
41
+ if (task.attempt === 0 && delayMs <= 0) {
42
+ return (`never claimed by a worker — is a worker running with a handler for ` +
43
+ `'${task.name}' on queue '${task.queue}', against this same database?`);
44
+ }
45
+ const next = delayMs > 0 ? `, next run in ~${delayMs}ms` : "";
46
+ return `still queued (attempt ${task.attempt}/${task.max_attempts})${next}`;
47
+ }
48
+ if (cancelRequested(task))
49
+ return "cancel requested, waiting for the handler to observe it";
50
+ return `still running (attempt ${task.attempt}/${task.max_attempts})`;
51
+ }
52
+ /** wait/call did not reach a terminal status in time. The task keeps running.
53
+ * `task` is the last snapshot wait() observed (null if get() found nothing), and
54
+ * the message says what state it was stuck in — a queued-never-claimed task is
55
+ * the classic first-run failure (no worker, no handler, wrong queue or file). */
24
56
  export class TaskTimeout extends CairnQError {
25
57
  taskId;
26
- constructor(taskId) {
27
- super(`task ${taskId} did not finish in time`);
58
+ task;
59
+ constructor(taskId, opts = {}) {
60
+ super(opts.timeoutMs == null
61
+ ? `task ${taskId} did not finish in time`
62
+ : `task ${taskId} did not finish within ${opts.timeoutMs}ms: ${timeoutDetail(opts.task ?? null)}`);
28
63
  this.taskId = taskId;
29
64
  this.name = "TaskTimeout";
65
+ this.task = opts.task ?? null;
30
66
  }
31
67
  }
32
68
  /** A waited-on task ended in `failed`. The envelope's fields are unpacked onto the
@@ -72,6 +108,16 @@ export class ProtocolVersionMismatch extends CairnQError {
72
108
  this.name = "ProtocolVersionMismatch";
73
109
  }
74
110
  }
111
+ /** A value could not be encoded for a protocol JSON column (non-finite number,
112
+ * BigInt, circular structure, …). Raised at the boundary — submit rejects with
113
+ * it, and a worker records a handler result that triggers it as a permanent
114
+ * `unserializable_result` failure. The Python SDK raises the same named error. */
115
+ export class SerializationError extends CairnQError {
116
+ constructor(message) {
117
+ super(message);
118
+ this.name = "SerializationError";
119
+ }
120
+ }
75
121
  /** Throw inside a handler to control how the failure is recorded. Defaults to
76
122
  * non-retryable so deterministic errors fail fast instead of burning retries.
77
123
  * Any other thrown value is treated as retryable. */
package/dist/index.d.ts CHANGED
@@ -7,7 +7,8 @@ export { defineTask } from "./task.js";
7
7
  export type { TaskDef } from "./task.js";
8
8
  export { SQLiteStore } from "./store/sqlite.js";
9
9
  export { PostgresStore } from "./store/postgres.js";
10
- export type { ListInput, SubmitInput, TaskStore, Conflict } from "./store/base.js";
10
+ export { TaskStore } from "./store/base.js";
11
+ export type { ListInput, PurgeInput, SubmitInput, Conflict } from "./store/base.js";
11
12
  export type { Task, TaskStatus } from "./models.js";
12
13
  export { STATUSES, isTerminal, cancelRequested, isQueued, isRunning, isSucceeded, isFailed, isCanceled, } from "./models.js";
13
- export { CairnQError, AlreadyExists, TaskTimeout, TaskFailed, TaskCanceled, TaskError, LostLease, ProtocolVersionMismatch, } from "./errors.js";
14
+ export { CairnQError, AlreadyExists, TaskTimeout, TaskFailed, TaskCanceled, TaskError, LostLease, ProtocolVersionMismatch, SerializationError, } from "./errors.js";
package/dist/index.js CHANGED
@@ -4,5 +4,6 @@ export { TaskContext } from "./context.js";
4
4
  export { defineTask } from "./task.js";
5
5
  export { SQLiteStore } from "./store/sqlite.js";
6
6
  export { PostgresStore } from "./store/postgres.js";
7
+ export { TaskStore } from "./store/base.js";
7
8
  export { STATUSES, isTerminal, cancelRequested, isQueued, isRunning, isSucceeded, isFailed, isCanceled, } from "./models.js";
8
- export { CairnQError, AlreadyExists, TaskTimeout, TaskFailed, TaskCanceled, TaskError, LostLease, ProtocolVersionMismatch, } from "./errors.js";
9
+ export { CairnQError, AlreadyExists, TaskTimeout, TaskFailed, TaskCanceled, TaskError, LostLease, ProtocolVersionMismatch, SerializationError, } from "./errors.js";
package/dist/sql.js CHANGED
@@ -1,19 +1,23 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- // Locate the shared cairnq-protocol dir. Resolution: $CAIRNQ_PROTOCOL_DIR ->
5
- // vendored `_protocol/` next to this module -> walk up to `cairnq-protocol/`
6
- // (monorepo dev). Both SDKs load the SAME .sql strings (zero-drift guarantee).
7
- // The dir is laid out per-dialect (sql/<dialect>/*.sql, migrations/<dialect>/*.sql)
8
- // so a second backend (Postgres) slots in beside sqlite; `dialect` picks the subtree.
4
+ // Locate the shared cairnq-protocol dir. Resolution: $CAIRNQ_PROTOCOL_DIR -> walk
5
+ // up to `cairnq-protocol/` (monorepo dev) -> vendored `_protocol/` next to this
6
+ // module (written at publish time). Both SDKs load the SAME .sql strings
7
+ // (zero-drift guarantee). The dir is laid out per-dialect (sql/<dialect>/*.sql,
8
+ // migrations/<dialect>/*.sql) so a second backend (Postgres) slots in beside
9
+ // sqlite; `dialect` picks the subtree.
10
+ //
11
+ // The source tree wins over the vendored copy on purpose: vendoring is a publish
12
+ // step that also runs locally, and a stale `_protocol/` shadowing the canonical
13
+ // SQL means edits to cairnq-protocol/ are silently not under test. An installed
14
+ // package has no repo above it, so it falls through to the vendored copy.
9
15
  export function findProtocolRoot() {
10
16
  const env = process.env.CAIRNQ_PROTOCOL_DIR;
11
17
  if (env)
12
18
  return env;
13
- let dir = dirname(fileURLToPath(import.meta.url));
14
- const vendored = join(dir, "_protocol");
15
- if (existsSync(join(vendored, "sql")))
16
- return vendored;
19
+ const start = dirname(fileURLToPath(import.meta.url));
20
+ let dir = start;
17
21
  for (let i = 0; i < 10; i++) {
18
22
  const candidate = join(dir, "cairnq-protocol");
19
23
  if (existsSync(join(candidate, "sql")))
@@ -23,6 +27,9 @@ export function findProtocolRoot() {
23
27
  break;
24
28
  dir = parent;
25
29
  }
30
+ const vendored = join(start, "_protocol");
31
+ if (existsSync(join(vendored, "sql")))
32
+ return vendored;
26
33
  throw new Error("cannot locate cairnq-protocol; set CAIRNQ_PROTOCOL_DIR");
27
34
  }
28
35
  export function loadStatements(dialect = "sqlite", root = findProtocolRoot()) {
@@ -1,5 +1,17 @@
1
- import type { Task } from "../models.js";
2
- export type Conflict = "reuse" | "reject" | "replace";
1
+ import { type Task, type TaskStatus } from "../models.js";
2
+ /** Encode a value for a protocol JSON column, raising SerializationError on
3
+ * anything JSON cannot represent. Refuses what JSON.stringify would silently
4
+ * mangle into `null`: NaN/Infinity anywhere, undefined/function/symbol inside an
5
+ * array, and a top-level undefined that disappears entirely — either way the
6
+ * twin SDK reads back something other than what the caller meant (the Python
7
+ * SDK rejects the same values, via allow_nan=False). */
8
+ export declare function dumpJson(value: unknown): string;
9
+ /** Refuse to run against a store whose protocol major this SDK does not speak.
10
+ * The supported major is a protocol fact, not a dialect one — every backend
11
+ * checks it here so the constant can't fork per store. */
12
+ export declare function checkProtocolVersion(version: number): void;
13
+ declare const CONFLICTS: readonly ["reuse", "reject", "replace"];
14
+ export type Conflict = (typeof CONFLICTS)[number];
3
15
  export interface SubmitInput {
4
16
  name: string;
5
17
  payload: unknown;
@@ -15,7 +27,7 @@ export interface SubmitInput {
15
27
  runAtDelayMs?: number;
16
28
  }
17
29
  export interface ListInput {
18
- status?: string | null;
30
+ status?: TaskStatus | null;
19
31
  queue?: string | null;
20
32
  name?: string | null;
21
33
  rootId?: string | null;
@@ -23,28 +35,113 @@ export interface ListInput {
23
35
  limit?: number;
24
36
  offset?: number;
25
37
  }
26
- /** The storage seam. SQLiteStore is the only MVP implementation. */
27
- export interface TaskStore {
28
- connect(): Promise<void>;
29
- close(): Promise<void>;
30
- protocolVersion(): Promise<number>;
38
+ export interface PurgeInput {
39
+ olderThanMs?: number;
40
+ limit?: number;
41
+ }
42
+ export type Params = Record<string, unknown>;
43
+ /** Runs one named protocol statement and returns its rows. */
44
+ export type Fetch = (name: string, params: Params) => Promise<any[]>;
45
+ export declare const LEASE_EXPIRED_ERROR_JSON: string;
46
+ /** Strips SQL line comments, so a `:name` in a header comment isn't a parameter. */
47
+ export declare const COMMENT: RegExp;
48
+ /** A `:name` placeholder. The lookbehind spares Postgres `::type` casts. */
49
+ export declare const NAMED: RegExp;
50
+ /**
51
+ * The parameter names a statement binds, in first-appearance order.
52
+ *
53
+ * Callers pass a superset of parameters and each dialect takes what its own SQL
54
+ * asks for — that is what lets one call site serve both dialects even though e.g.
55
+ * SQLite binds `:lease_until_ms` where Postgres binds `:lease_ms`. This is the one
56
+ * place that decides what counts as a parameter; both dialects' binding goes
57
+ * through it.
58
+ */
59
+ export declare function statementParams(sql: string): readonly string[];
60
+ /**
61
+ * The storage seam.
62
+ *
63
+ * A backend supplies three things: how to run one protocol statement, how to run
64
+ * several inside a transaction, and how its dialect binds parameters. Everything
65
+ * above that — the submit conflict branches, the *_by_key lookups, the
66
+ * recover-then-claim sequence, the ownership-checked writes — lives here once,
67
+ * because those are protocol decisions rather than storage decisions. Keeping
68
+ * them in one place is what stops SQLite and Postgres from drifting apart in
69
+ * behavior; the shared SQL already stops them from drifting in wording.
70
+ */
71
+ export declare abstract class TaskStore {
72
+ abstract connect(): Promise<void>;
73
+ abstract close(): Promise<void>;
74
+ abstract protocolVersion(): Promise<number>;
75
+ /** Run one protocol statement outside a transaction, connecting if needed. */
76
+ protected abstract fetch(name: string, params: Params): Promise<any[]>;
77
+ /** Run several statements atomically; `fn` receives a Fetch bound to the txn. */
78
+ protected abstract tx<T>(fn: (fetch: Fetch) => Promise<T>): Promise<T>;
79
+ /**
80
+ * Whether it is worth opening the claim transaction at all. SQLite gates its
81
+ * single write lock behind a read-only probe; Postgres readers don't block
82
+ * writers, so it just says yes.
83
+ */
84
+ protected hasClaimableWork(_params: Params): Promise<boolean>;
85
+ /** Resolves when a task may have become claimable on one of `queues`. The
86
+ * timer is unref'd: the worker races this against its own stop-aware, ref'd
87
+ * sleep, so it must neither hold the process open nor need clearing. */
88
+ claimWake(_queues: string[], timeoutMs: number): Promise<void>;
89
+ /** Resolves when `taskId` may have gone terminal. Plain ref'd sleep —
90
+ * pollWait awaits it directly, so it is what keeps the process alive. */
91
+ taskDoneWake(_taskId: string, timeoutMs: number): Promise<void>;
92
+ /**
93
+ * An ownership-checked worker write (heartbeat/progress/succeed/complete/fail).
94
+ * Each statement's WHERE pins worker_id + a live lease, so 0 rows back means
95
+ * the lease was lost — every such write reports it the same way.
96
+ */
97
+ private ownedWrite;
98
+ private static one;
31
99
  submit(input: SubmitInput): Promise<Task>;
32
100
  get(taskId: string): Promise<Task | null>;
33
101
  getByKey(key: string): Promise<Task | null>;
34
102
  list(input?: ListInput): Promise<Task[]>;
35
103
  cancel(taskId: string): Promise<Task | null>;
36
- cancelByKey(key: string): Promise<Task | null>;
37
104
  retry(taskId: string, opts?: {
38
105
  resetAttempt?: boolean;
39
106
  }): Promise<Task | null>;
107
+ cancelByKey(key: string): Promise<Task | null>;
40
108
  retryByKey(key: string, opts?: {
41
109
  resetAttempt?: boolean;
42
110
  }): Promise<Task | null>;
111
+ /**
112
+ * Resolve a key to the task it currently points at, then act on that task —
113
+ * under the key's lock, so a concurrent `replace` can't repoint the key
114
+ * between the lookup and the write (the transaction alone is not enough on
115
+ * Postgres; see lock_key.sql).
116
+ */
117
+ private byKey;
118
+ /**
119
+ * Delete terminal tasks that completed more than `olderThanMs` ago and return
120
+ * their ids. Nothing else removes rows, so a long-lived database needs this
121
+ * called periodically. Bounded by `limit` to keep each sweep a short write;
122
+ * call it in a loop until it returns fewer than `limit`.
123
+ */
124
+ purge(input?: PurgeInput): Promise<string[]>;
125
+ /**
126
+ * Task counts per queue, keyed by status and zero-filled across all statuses —
127
+ * `(await stats()).default.queued` is the backlog of a queue. A queue appears
128
+ * only while it has rows; terminal tasks keep counting until `purge` removes
129
+ * them.
130
+ */
131
+ stats(): Promise<Record<string, Record<TaskStatus, number>>>;
132
+ /**
133
+ * Take up to `limit` claimable tasks. `names` restricts the claim to task names
134
+ * this caller can actually run — a worker passes its registered handlers.
135
+ * Queues alone do not partition work, so without it a worker claims a task it
136
+ * cannot run and fails it permanently. Undefined means no filter; an empty
137
+ * array claims nothing.
138
+ */
43
139
  claim(input: {
44
140
  queues: string[];
45
141
  workerId: string;
46
142
  leaseMs?: number;
47
143
  limit?: number;
144
+ names?: string[];
48
145
  }): Promise<Task[]>;
49
146
  heartbeat(input: {
50
147
  taskId: string;
@@ -75,3 +172,4 @@ export interface TaskStore {
75
172
  delayMs?: number;
76
173
  }): Promise<Task>;
77
174
  }
175
+ export {};