cairnq 0.1.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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +66 -0
  3. package/dist/_protocol/migrations/postgres/0001_init.sql +75 -0
  4. package/dist/_protocol/migrations/sqlite/0001_init.sql +74 -0
  5. package/dist/_protocol/sql/postgres/cancel.sql +16 -0
  6. package/dist/_protocol/sql/postgres/claim.sql +27 -0
  7. package/dist/_protocol/sql/postgres/complete.sql +17 -0
  8. package/dist/_protocol/sql/postgres/fail.sql +21 -0
  9. package/dist/_protocol/sql/postgres/get.sql +2 -0
  10. package/dist/_protocol/sql/postgres/get_by_key.sql +4 -0
  11. package/dist/_protocol/sql/postgres/get_key.sql +3 -0
  12. package/dist/_protocol/sql/postgres/heartbeat.sql +12 -0
  13. package/dist/_protocol/sql/postgres/insert_task.sql +20 -0
  14. package/dist/_protocol/sql/postgres/list.sql +13 -0
  15. package/dist/_protocol/sql/postgres/progress.sql +13 -0
  16. package/dist/_protocol/sql/postgres/recover_leases.sql +20 -0
  17. package/dist/_protocol/sql/postgres/retry.sql +17 -0
  18. package/dist/_protocol/sql/postgres/succeed.sql +16 -0
  19. package/dist/_protocol/sql/postgres/upsert_key.sql +12 -0
  20. package/dist/_protocol/sql/sqlite/cancel.sql +12 -0
  21. package/dist/_protocol/sql/sqlite/claim.sql +20 -0
  22. package/dist/_protocol/sql/sqlite/claimable_probe.sql +14 -0
  23. package/dist/_protocol/sql/sqlite/complete.sql +18 -0
  24. package/dist/_protocol/sql/sqlite/fail.sql +19 -0
  25. package/dist/_protocol/sql/sqlite/get.sql +2 -0
  26. package/dist/_protocol/sql/sqlite/get_by_key.sql +4 -0
  27. package/dist/_protocol/sql/sqlite/get_key.sql +3 -0
  28. package/dist/_protocol/sql/sqlite/heartbeat.sql +10 -0
  29. package/dist/_protocol/sql/sqlite/insert_task.sql +16 -0
  30. package/dist/_protocol/sql/sqlite/list.sql +11 -0
  31. package/dist/_protocol/sql/sqlite/progress.sql +10 -0
  32. package/dist/_protocol/sql/sqlite/recover_leases.sql +17 -0
  33. package/dist/_protocol/sql/sqlite/retry.sql +16 -0
  34. package/dist/_protocol/sql/sqlite/succeed.sql +15 -0
  35. package/dist/_protocol/sql/sqlite/upsert_key.sql +7 -0
  36. package/dist/client.d.ts +46 -0
  37. package/dist/client.js +70 -0
  38. package/dist/context.d.ts +31 -0
  39. package/dist/context.js +78 -0
  40. package/dist/errors.d.ts +60 -0
  41. package/dist/errors.js +100 -0
  42. package/dist/ids.d.ts +3 -0
  43. package/dist/ids.js +19 -0
  44. package/dist/index.d.ts +13 -0
  45. package/dist/index.js +8 -0
  46. package/dist/models.d.ts +36 -0
  47. package/dist/models.js +31 -0
  48. package/dist/sql.d.ts +6 -0
  49. package/dist/sql.js +44 -0
  50. package/dist/store/base.d.ts +77 -0
  51. package/dist/store/base.js +1 -0
  52. package/dist/store/postgres.d.ts +87 -0
  53. package/dist/store/postgres.js +349 -0
  54. package/dist/store/sqlite.d.ts +77 -0
  55. package/dist/store/sqlite.js +297 -0
  56. package/dist/task.d.ts +21 -0
  57. package/dist/task.js +7 -0
  58. package/dist/wait.d.ts +8 -0
  59. package/dist/wait.js +18 -0
  60. package/dist/worker.d.ts +60 -0
  61. package/dist/worker.js +252 -0
  62. package/package.json +57 -0
  63. package/src/client.ts +98 -0
  64. package/src/context.ts +85 -0
  65. package/src/errors.ts +112 -0
  66. package/src/ids.ts +23 -0
  67. package/src/index.ts +31 -0
  68. package/src/models.ts +63 -0
  69. package/src/sql.ts +49 -0
  70. package/src/store/base.ts +67 -0
  71. package/src/store/postgres.ts +409 -0
  72. package/src/store/sqlite.ts +351 -0
  73. package/src/task.ts +27 -0
  74. package/src/wait.ts +23 -0
  75. package/src/worker.ts +284 -0
@@ -0,0 +1,16 @@
1
+ -- Manually re-enqueue a failed/canceled task. :reset_attempt (0/1) controls
2
+ -- whether the attempt counter resets to 0.
3
+ -- params: id, now_ms, reset_attempt
4
+ update cairnq_tasks
5
+ set
6
+ status = 'queued',
7
+ error = null,
8
+ worker_id = null,
9
+ lease_until_ms = null,
10
+ run_at_ms = :now_ms,
11
+ cancel_requested_at_ms = null,
12
+ completed_at_ms = null,
13
+ attempt = case when :reset_attempt = 1 then 0 else attempt end,
14
+ updated_at_ms = :now_ms
15
+ where id = :id and status in ('failed', 'canceled')
16
+ returning *;
@@ -0,0 +1,15 @@
1
+ -- Mark succeeded. Ownership-checked. worker_id kept for audit.
2
+ -- params: id, worker_id, now_ms, result (JSON text), message
3
+ update cairnq_tasks
4
+ set
5
+ status = 'succeeded',
6
+ result = :result,
7
+ progress = 1.0,
8
+ message = coalesce(:message, message),
9
+ completed_at_ms = :now_ms,
10
+ updated_at_ms = :now_ms
11
+ where id = :id
12
+ and status = 'running'
13
+ and worker_id = :worker_id
14
+ and lease_until_ms > :now_ms
15
+ returning *;
@@ -0,0 +1,7 @@
1
+ -- Point a key at a task (initial pointer, or repoint on replace).
2
+ -- params: key, task_id, now_ms
3
+ insert into cairnq_task_keys (key, task_id, created_at_ms, updated_at_ms)
4
+ values (:key, :task_id, :now_ms, :now_ms)
5
+ on conflict(key) do update set
6
+ task_id = excluded.task_id,
7
+ updated_at_ms = excluded.updated_at_ms;
@@ -0,0 +1,46 @@
1
+ import { type Task } from "./models.js";
2
+ import type { ListInput, SubmitInput, TaskStore } from "./store/base.js";
3
+ import { type TaskDef } from "./task.js";
4
+ export type SubmitOptions = Omit<SubmitInput, "name" | "payload">;
5
+ export interface CallOptions extends SubmitOptions {
6
+ waitTimeoutMs?: number;
7
+ pollMs?: number;
8
+ }
9
+ /** API-side handle. Thin wrapper over a TaskStore + SDK-orchestrated wait/call. */
10
+ export declare class CairnQ {
11
+ private readonly _store;
12
+ constructor(_store: TaskStore);
13
+ static sqlite(path: string, opts?: {
14
+ busyTimeoutMs?: number;
15
+ }): CairnQ;
16
+ /** Multi-host backend. `dsn` is a libpq connection string; requires the
17
+ * optional `pg` package. */
18
+ static postgres(dsn: string, opts?: {
19
+ max?: number;
20
+ }): CairnQ;
21
+ get store(): TaskStore;
22
+ connect(): Promise<void>;
23
+ close(): Promise<void>;
24
+ submit(name: string, payload?: unknown, opts?: SubmitOptions): Promise<Task>;
25
+ submit<P, R>(task: TaskDef<P, R>, payload?: P, opts?: SubmitOptions): Promise<Task>;
26
+ get(taskId: string): Promise<Task | null>;
27
+ getByKey(key: string): Promise<Task | null>;
28
+ list(input?: ListInput): Promise<Task[]>;
29
+ cancel(taskId: string): Promise<Task | null>;
30
+ cancelByKey(key: string): Promise<Task | null>;
31
+ retry(taskId: string, opts?: {
32
+ resetAttempt?: boolean;
33
+ }): Promise<Task | null>;
34
+ retryByKey(key: string, opts?: {
35
+ resetAttempt?: boolean;
36
+ }): Promise<Task | null>;
37
+ wait(taskId: string, opts?: {
38
+ timeoutMs?: number;
39
+ pollMs?: number;
40
+ }): Promise<Task>;
41
+ /** submit + wait. Resolves with the result on success; rejects with
42
+ * TaskFailed / TaskCanceled / TaskTimeout otherwise. Pass a TaskDef and the
43
+ * resolved value is typed as its Result. */
44
+ call(name: string, payload?: unknown, opts?: CallOptions): Promise<unknown>;
45
+ call<P, R>(task: TaskDef<P, R>, payload?: P, opts?: CallOptions): Promise<R>;
46
+ }
package/dist/client.js ADDED
@@ -0,0 +1,70 @@
1
+ import { TaskCanceled, TaskFailed } from "./errors.js";
2
+ import { isFailed, isSucceeded } from "./models.js";
3
+ import { SQLiteStore } from "./store/sqlite.js";
4
+ import { PostgresStore } from "./store/postgres.js";
5
+ import { taskName } from "./task.js";
6
+ import { pollWait } from "./wait.js";
7
+ /** API-side handle. Thin wrapper over a TaskStore + SDK-orchestrated wait/call. */
8
+ export class CairnQ {
9
+ _store;
10
+ constructor(_store) {
11
+ this._store = _store;
12
+ }
13
+ static sqlite(path, opts) {
14
+ return new CairnQ(new SQLiteStore(path, opts));
15
+ }
16
+ /** Multi-host backend. `dsn` is a libpq connection string; requires the
17
+ * optional `pg` package. */
18
+ static postgres(dsn, opts) {
19
+ return new CairnQ(new PostgresStore(dsn, opts));
20
+ }
21
+ get store() {
22
+ return this._store;
23
+ }
24
+ connect() {
25
+ return this._store.connect();
26
+ }
27
+ close() {
28
+ return this._store.close();
29
+ }
30
+ submit(task, payload, opts = {}) {
31
+ return this._store.submit({ name: taskName(task), payload, ...opts });
32
+ }
33
+ get(taskId) {
34
+ return this._store.get(taskId);
35
+ }
36
+ getByKey(key) {
37
+ return this._store.getByKey(key);
38
+ }
39
+ list(input) {
40
+ return this._store.list(input);
41
+ }
42
+ cancel(taskId) {
43
+ return this._store.cancel(taskId);
44
+ }
45
+ cancelByKey(key) {
46
+ return this._store.cancelByKey(key);
47
+ }
48
+ retry(taskId, opts) {
49
+ return this._store.retry(taskId, opts);
50
+ }
51
+ retryByKey(key, opts) {
52
+ return this._store.retryByKey(key, opts);
53
+ }
54
+ wait(taskId, opts = {}) {
55
+ return pollWait(this._store, taskId, {
56
+ timeoutMs: opts.timeoutMs ?? 30_000,
57
+ pollMs: opts.pollMs,
58
+ });
59
+ }
60
+ async call(task, payload, opts = {}) {
61
+ const { waitTimeoutMs = 30_000, pollMs, ...submit } = opts;
62
+ const created = await this.submit(taskName(task), payload, submit);
63
+ const final = await pollWait(this._store, created.id, { timeoutMs: waitTimeoutMs, pollMs });
64
+ if (isSucceeded(final))
65
+ return final.result;
66
+ if (isFailed(final))
67
+ throw new TaskFailed(final.error);
68
+ throw new TaskCanceled(final.id);
69
+ }
70
+ }
@@ -0,0 +1,31 @@
1
+ import { type Task } from "./models.js";
2
+ import type { SubmitOptions } from "./client.js";
3
+ import type { TaskStore } from "./store/base.js";
4
+ import { type TaskDef } from "./task.js";
5
+ /** Handed to a task handler. Worker-side capabilities mirror the Python SDK. */
6
+ export declare class TaskContext {
7
+ private readonly store;
8
+ private readonly task;
9
+ readonly workerId: string;
10
+ private readonly leaseMs;
11
+ constructor(store: TaskStore, task: Task, workerId: string, leaseMs: number);
12
+ get taskId(): string;
13
+ get name(): string;
14
+ get queue(): string;
15
+ get attempt(): number;
16
+ get metadata(): any;
17
+ get rootId(): string | null;
18
+ get correlationId(): string | null;
19
+ get payload(): any;
20
+ progress(value: number | null, message?: string | null): Promise<Task>;
21
+ heartbeat(): Promise<Task>;
22
+ /** Cooperative cancel check. */
23
+ canceled(): Promise<boolean>;
24
+ /** Submit a child task; parent/root/correlation are wired automatically. */
25
+ submit(name: string, payload?: unknown, opts?: SubmitOptions): Promise<Task>;
26
+ submit<P, R>(task: TaskDef<P, R>, payload?: P, opts?: SubmitOptions): Promise<Task>;
27
+ wait(taskId: string, opts?: {
28
+ timeoutMs?: number;
29
+ pollMs?: number;
30
+ }): Promise<Task>;
31
+ }
@@ -0,0 +1,78 @@
1
+ import { cancelRequested } from "./models.js";
2
+ import { taskName } from "./task.js";
3
+ import { pollWait } from "./wait.js";
4
+ /** Handed to a task handler. Worker-side capabilities mirror the Python SDK. */
5
+ export class TaskContext {
6
+ store;
7
+ task;
8
+ workerId;
9
+ leaseMs;
10
+ constructor(store, task, workerId, leaseMs) {
11
+ this.store = store;
12
+ this.task = task;
13
+ this.workerId = workerId;
14
+ this.leaseMs = leaseMs;
15
+ }
16
+ get taskId() {
17
+ return this.task.id;
18
+ }
19
+ get name() {
20
+ return this.task.name;
21
+ }
22
+ get queue() {
23
+ return this.task.queue;
24
+ }
25
+ get attempt() {
26
+ return this.task.attempt;
27
+ }
28
+ get metadata() {
29
+ return this.task.metadata;
30
+ }
31
+ get rootId() {
32
+ return this.task.root_id;
33
+ }
34
+ get correlationId() {
35
+ return this.task.correlation_id;
36
+ }
37
+ get payload() {
38
+ return this.task.payload;
39
+ }
40
+ async progress(value, message = null) {
41
+ return this.store.progress({
42
+ taskId: this.task.id,
43
+ workerId: this.workerId,
44
+ progress: value,
45
+ message,
46
+ });
47
+ }
48
+ async heartbeat() {
49
+ return this.store.heartbeat({
50
+ taskId: this.task.id,
51
+ workerId: this.workerId,
52
+ leaseMs: this.leaseMs,
53
+ });
54
+ }
55
+ /** Cooperative cancel check. */
56
+ async canceled() {
57
+ const t = await this.store.get(this.task.id);
58
+ if (!t)
59
+ return true;
60
+ return cancelRequested(t) || t.status === "canceled";
61
+ }
62
+ async submit(task, payload, opts = {}) {
63
+ return this.store.submit({
64
+ name: taskName(task),
65
+ payload,
66
+ parentId: this.task.id,
67
+ rootId: this.task.root_id,
68
+ correlationId: this.task.correlation_id,
69
+ ...opts,
70
+ });
71
+ }
72
+ async wait(taskId, opts = {}) {
73
+ return pollWait(this.store, taskId, {
74
+ timeoutMs: opts.timeoutMs ?? 30_000,
75
+ pollMs: opts.pollMs,
76
+ });
77
+ }
78
+ }
@@ -0,0 +1,60 @@
1
+ /** The single shape of the JSON error envelope (see PROTOCOL.md). Everything that
2
+ * records an error — a handler exception, a missing handler, lease expiry, a thrown
3
+ * TaskError — builds it here, so the contract's fields live in one place. */
4
+ export declare function errorEnvelope(e: {
5
+ type: string;
6
+ code: string;
7
+ message: string;
8
+ retryable: boolean;
9
+ details?: Record<string, unknown>;
10
+ }): Record<string, unknown>;
11
+ export declare class CairnQError extends Error {
12
+ }
13
+ export declare class AlreadyExists extends CairnQError {
14
+ key: string;
15
+ constructor(key: string);
16
+ }
17
+ /** wait/call did not reach a terminal status in time. The task keeps running. */
18
+ export declare class TaskTimeout extends CairnQError {
19
+ taskId: string;
20
+ constructor(taskId: string);
21
+ }
22
+ /** A waited-on task ended in `failed`. The envelope's fields are unpacked onto the
23
+ * error — read `e.code` / `e.message` / `e.retryable` / `e.details` instead of
24
+ * digging into `e.error` (the raw envelope stays available on `e.error`). */
25
+ export declare class TaskFailed extends CairnQError {
26
+ error: unknown;
27
+ readonly type: string;
28
+ readonly code: string;
29
+ readonly retryable: boolean;
30
+ readonly details: Record<string, unknown>;
31
+ constructor(error: unknown);
32
+ }
33
+ export declare class TaskCanceled extends CairnQError {
34
+ taskId: string;
35
+ constructor(taskId: string);
36
+ }
37
+ /** A worker write affected 0 rows: the lease expired and was reclaimed. */
38
+ export declare class LostLease extends CairnQError {
39
+ taskId: string;
40
+ constructor(taskId: string);
41
+ }
42
+ export declare class ProtocolVersionMismatch extends CairnQError {
43
+ constructor(message: string);
44
+ }
45
+ /** Throw inside a handler to control how the failure is recorded. Defaults to
46
+ * non-retryable so deterministic errors fail fast instead of burning retries.
47
+ * Any other thrown value is treated as retryable. */
48
+ export declare class TaskError extends CairnQError {
49
+ code: string;
50
+ retryable: boolean;
51
+ type: string;
52
+ details: Record<string, unknown>;
53
+ constructor(message: string, opts?: {
54
+ code?: string;
55
+ retryable?: boolean;
56
+ type?: string;
57
+ details?: Record<string, unknown>;
58
+ });
59
+ envelope(): Record<string, unknown>;
60
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,100 @@
1
+ /** The single shape of the JSON error envelope (see PROTOCOL.md). Everything that
2
+ * records an error — a handler exception, a missing handler, lease expiry, a thrown
3
+ * TaskError — builds it here, so the contract's fields live in one place. */
4
+ export function errorEnvelope(e) {
5
+ return {
6
+ type: e.type,
7
+ code: e.code,
8
+ message: e.message,
9
+ retryable: e.retryable,
10
+ details: e.details ?? {},
11
+ };
12
+ }
13
+ export class CairnQError extends Error {
14
+ }
15
+ export class AlreadyExists extends CairnQError {
16
+ key;
17
+ constructor(key) {
18
+ super(`task with key ${key} already exists`);
19
+ this.key = key;
20
+ this.name = "AlreadyExists";
21
+ }
22
+ }
23
+ /** wait/call did not reach a terminal status in time. The task keeps running. */
24
+ export class TaskTimeout extends CairnQError {
25
+ taskId;
26
+ constructor(taskId) {
27
+ super(`task ${taskId} did not finish in time`);
28
+ this.taskId = taskId;
29
+ this.name = "TaskTimeout";
30
+ }
31
+ }
32
+ /** A waited-on task ended in `failed`. The envelope's fields are unpacked onto the
33
+ * error — read `e.code` / `e.message` / `e.retryable` / `e.details` instead of
34
+ * digging into `e.error` (the raw envelope stays available on `e.error`). */
35
+ export class TaskFailed extends CairnQError {
36
+ error;
37
+ type;
38
+ code;
39
+ retryable;
40
+ details;
41
+ constructor(error) {
42
+ const env = (error ?? {});
43
+ super(env.message ?? "task failed");
44
+ this.error = error;
45
+ this.name = "TaskFailed";
46
+ this.type = env.type ?? "TaskError";
47
+ this.code = env.code ?? "task_error";
48
+ this.retryable = env.retryable ?? false;
49
+ this.details = env.details ?? {};
50
+ }
51
+ }
52
+ export class TaskCanceled extends CairnQError {
53
+ taskId;
54
+ constructor(taskId) {
55
+ super(`task ${taskId} was canceled`);
56
+ this.taskId = taskId;
57
+ this.name = "TaskCanceled";
58
+ }
59
+ }
60
+ /** A worker write affected 0 rows: the lease expired and was reclaimed. */
61
+ export class LostLease extends CairnQError {
62
+ taskId;
63
+ constructor(taskId) {
64
+ super(`lost lease on task ${taskId}`);
65
+ this.taskId = taskId;
66
+ this.name = "LostLease";
67
+ }
68
+ }
69
+ export class ProtocolVersionMismatch extends CairnQError {
70
+ constructor(message) {
71
+ super(message);
72
+ this.name = "ProtocolVersionMismatch";
73
+ }
74
+ }
75
+ /** Throw inside a handler to control how the failure is recorded. Defaults to
76
+ * non-retryable so deterministic errors fail fast instead of burning retries.
77
+ * Any other thrown value is treated as retryable. */
78
+ export class TaskError extends CairnQError {
79
+ code;
80
+ retryable;
81
+ type;
82
+ details;
83
+ constructor(message, opts = {}) {
84
+ super(message);
85
+ this.name = "TaskError";
86
+ this.code = opts.code ?? "task_error";
87
+ this.retryable = opts.retryable ?? false;
88
+ this.type = opts.type ?? "TaskError";
89
+ this.details = opts.details ?? {};
90
+ }
91
+ envelope() {
92
+ return errorEnvelope({
93
+ type: this.type,
94
+ code: this.code,
95
+ message: this.message,
96
+ retryable: this.retryable,
97
+ details: this.details,
98
+ });
99
+ }
100
+ }
package/dist/ids.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare function newUlid(tsMs?: number): string;
2
+ export declare function newId(prefix?: string): string;
3
+ export declare function nowMs(): number;
package/dist/ids.js ADDED
@@ -0,0 +1,19 @@
1
+ import { randomBytes } from "node:crypto";
2
+ // ULID-style id. Must match the Python SDK byte-for-byte in format (PROTOCOL.md):
3
+ // <prefix>_ + 26-char Crockford base32 of (48-bit ms timestamp << 80 | 80-bit random).
4
+ const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
5
+ export function newUlid(tsMs = Date.now()) {
6
+ let value = (BigInt(tsMs) << 80n) | BigInt("0x" + randomBytes(10).toString("hex"));
7
+ const chars = [];
8
+ for (let i = 0; i < 26; i++) {
9
+ chars.push(CROCKFORD[Number(value & 31n)]);
10
+ value >>= 5n;
11
+ }
12
+ return chars.reverse().join("");
13
+ }
14
+ export function newId(prefix = "task") {
15
+ return `${prefix}_${newUlid()}`;
16
+ }
17
+ export function nowMs() {
18
+ return Date.now();
19
+ }
@@ -0,0 +1,13 @@
1
+ export { CairnQ } from "./client.js";
2
+ export type { CallOptions, SubmitOptions } from "./client.js";
3
+ export { Worker } from "./worker.js";
4
+ export type { Handler, TypedHandler, WorkerOptions } from "./worker.js";
5
+ export { TaskContext } from "./context.js";
6
+ export { defineTask } from "./task.js";
7
+ export type { TaskDef } from "./task.js";
8
+ export { SQLiteStore } from "./store/sqlite.js";
9
+ export { PostgresStore } from "./store/postgres.js";
10
+ export type { ListInput, SubmitInput, TaskStore, Conflict } from "./store/base.js";
11
+ export type { Task, TaskStatus } from "./models.js";
12
+ 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";
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { CairnQ } from "./client.js";
2
+ export { Worker } from "./worker.js";
3
+ export { TaskContext } from "./context.js";
4
+ export { defineTask } from "./task.js";
5
+ export { SQLiteStore } from "./store/sqlite.js";
6
+ export { PostgresStore } from "./store/postgres.js";
7
+ 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";
@@ -0,0 +1,36 @@
1
+ export declare const STATUSES: readonly ["queued", "running", "succeeded", "failed", "canceled"];
2
+ export type TaskStatus = (typeof STATUSES)[number];
3
+ export interface Task {
4
+ id: string;
5
+ name: string;
6
+ queue: string;
7
+ status: TaskStatus;
8
+ payload: any;
9
+ metadata: any;
10
+ result: any | null;
11
+ error: any | null;
12
+ progress: number | null;
13
+ message: string | null;
14
+ attempt: number;
15
+ max_attempts: number;
16
+ priority: number;
17
+ worker_id: string | null;
18
+ lease_until_ms: number | null;
19
+ run_at_ms: number;
20
+ cancel_requested_at_ms: number | null;
21
+ parent_id: string | null;
22
+ root_id: string | null;
23
+ correlation_id: string | null;
24
+ created_at_ms: number;
25
+ updated_at_ms: number;
26
+ completed_at_ms: number | null;
27
+ }
28
+ export declare const TERMINAL: TaskStatus[];
29
+ export declare function rowToTask(row: Record<string, unknown>): Task;
30
+ export declare function isTerminal(task: Task): boolean;
31
+ export declare function cancelRequested(task: Task): boolean;
32
+ export declare const isQueued: (task: Task) => boolean;
33
+ export declare const isRunning: (task: Task) => boolean;
34
+ export declare const isSucceeded: (task: Task) => boolean;
35
+ export declare const isFailed: (task: Task) => boolean;
36
+ export declare const isCanceled: (task: Task) => boolean;
package/dist/models.js ADDED
@@ -0,0 +1,31 @@
1
+ // STATUSES is the canonical declaration within the TS SDK; TaskStatus derives from
2
+ // it so the type and the runtime set can't drift apart. The cross-language source of
3
+ // truth is the status CHECK constraint in cairnq-protocol's migration, which the
4
+ // conformance suite pins this set against.
5
+ export const STATUSES = ["queued", "running", "succeeded", "failed", "canceled"];
6
+ const JSON_COLUMNS = ["payload", "result", "error", "metadata"];
7
+ export const TERMINAL = ["succeeded", "failed", "canceled"];
8
+ export function rowToTask(row) {
9
+ const t = { ...row };
10
+ for (const col of JSON_COLUMNS) {
11
+ const v = row[col];
12
+ // The driver decides a JSON column's wire form: SQLite (TEXT) hands back a
13
+ // string to parse; a jsonb-aware driver (Postgres `pg`) hands back an
14
+ // already-decoded object. Parse only a string — never assume one backend.
15
+ t[col] = typeof v === "string" ? JSON.parse(v) : (v ?? null);
16
+ }
17
+ return t;
18
+ }
19
+ export function isTerminal(task) {
20
+ return TERMINAL.includes(task.status);
21
+ }
22
+ export function cancelRequested(task) {
23
+ return task.cancel_requested_at_ms != null;
24
+ }
25
+ // Status predicates — mirror the Python `task.succeeded` properties so callers
26
+ // don't compare status strings by hand.
27
+ export const isQueued = (task) => task.status === "queued";
28
+ export const isRunning = (task) => task.status === "running";
29
+ export const isSucceeded = (task) => task.status === "succeeded";
30
+ export const isFailed = (task) => task.status === "failed";
31
+ export const isCanceled = (task) => task.status === "canceled";
package/dist/sql.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export declare function findProtocolRoot(): string;
2
+ export declare function loadStatements(dialect?: string, root?: string): Record<string, string>;
3
+ export declare function loadMigrations(dialect?: string, root?: string): {
4
+ name: string;
5
+ sql: string;
6
+ }[];
package/dist/sql.js ADDED
@@ -0,0 +1,44 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
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.
9
+ export function findProtocolRoot() {
10
+ const env = process.env.CAIRNQ_PROTOCOL_DIR;
11
+ if (env)
12
+ 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;
17
+ for (let i = 0; i < 10; i++) {
18
+ const candidate = join(dir, "cairnq-protocol");
19
+ if (existsSync(join(candidate, "sql")))
20
+ return candidate;
21
+ const parent = dirname(dir);
22
+ if (parent === dir)
23
+ break;
24
+ dir = parent;
25
+ }
26
+ throw new Error("cannot locate cairnq-protocol; set CAIRNQ_PROTOCOL_DIR");
27
+ }
28
+ export function loadStatements(dialect = "sqlite", root = findProtocolRoot()) {
29
+ const dir = join(root, "sql", dialect);
30
+ const out = {};
31
+ for (const file of readdirSync(dir).sort()) {
32
+ if (file.endsWith(".sql")) {
33
+ out[file.slice(0, -4)] = readFileSync(join(dir, file), "utf-8");
34
+ }
35
+ }
36
+ return out;
37
+ }
38
+ export function loadMigrations(dialect = "sqlite", root = findProtocolRoot()) {
39
+ const dir = join(root, "migrations", dialect);
40
+ return readdirSync(dir)
41
+ .filter((f) => f.endsWith(".sql"))
42
+ .sort()
43
+ .map((f) => ({ name: f, sql: readFileSync(join(dir, f), "utf-8") }));
44
+ }
@@ -0,0 +1,77 @@
1
+ import type { Task } from "../models.js";
2
+ export type Conflict = "reuse" | "reject" | "replace";
3
+ export interface SubmitInput {
4
+ name: string;
5
+ payload: unknown;
6
+ queue?: string;
7
+ key?: string | null;
8
+ conflict?: Conflict;
9
+ maxAttempts?: number;
10
+ priority?: number;
11
+ metadata?: unknown;
12
+ parentId?: string | null;
13
+ rootId?: string | null;
14
+ correlationId?: string | null;
15
+ runAtDelayMs?: number;
16
+ }
17
+ export interface ListInput {
18
+ status?: string | null;
19
+ queue?: string | null;
20
+ name?: string | null;
21
+ rootId?: string | null;
22
+ correlationId?: string | null;
23
+ limit?: number;
24
+ offset?: number;
25
+ }
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>;
31
+ submit(input: SubmitInput): Promise<Task>;
32
+ get(taskId: string): Promise<Task | null>;
33
+ getByKey(key: string): Promise<Task | null>;
34
+ list(input?: ListInput): Promise<Task[]>;
35
+ cancel(taskId: string): Promise<Task | null>;
36
+ cancelByKey(key: string): Promise<Task | null>;
37
+ retry(taskId: string, opts?: {
38
+ resetAttempt?: boolean;
39
+ }): Promise<Task | null>;
40
+ retryByKey(key: string, opts?: {
41
+ resetAttempt?: boolean;
42
+ }): Promise<Task | null>;
43
+ claim(input: {
44
+ queues: string[];
45
+ workerId: string;
46
+ leaseMs?: number;
47
+ limit?: number;
48
+ }): Promise<Task[]>;
49
+ heartbeat(input: {
50
+ taskId: string;
51
+ workerId: string;
52
+ leaseMs?: number;
53
+ }): Promise<Task>;
54
+ progress(input: {
55
+ taskId: string;
56
+ workerId: string;
57
+ progress: number | null;
58
+ message: string | null;
59
+ }): Promise<Task>;
60
+ succeed(input: {
61
+ taskId: string;
62
+ workerId: string;
63
+ result: unknown;
64
+ }): Promise<Task>;
65
+ complete(input: {
66
+ taskId: string;
67
+ workerId: string;
68
+ result: unknown;
69
+ }): Promise<Task>;
70
+ fail(input: {
71
+ taskId: string;
72
+ workerId: string;
73
+ error: unknown;
74
+ retryable?: boolean;
75
+ delayMs?: number;
76
+ }): Promise<Task>;
77
+ }