cairnq 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +28 -0
  2. package/dist/_protocol/migrations/postgres/0001_init.sql +3 -1
  3. package/dist/_protocol/migrations/postgres/0002_purge_index.sql +6 -0
  4. package/dist/_protocol/migrations/postgres/0003_notify.sql +38 -0
  5. package/dist/_protocol/migrations/postgres/0004_lease_index.sql +16 -0
  6. package/dist/_protocol/migrations/postgres/0005_clear_terminal_lease.sql +17 -0
  7. package/dist/_protocol/migrations/sqlite/0001_init.sql +3 -1
  8. package/dist/_protocol/migrations/sqlite/0002_purge_index.sql +6 -0
  9. package/dist/_protocol/migrations/sqlite/0004_lease_index.sql +22 -0
  10. package/dist/_protocol/migrations/sqlite/0005_clear_terminal_lease.sql +17 -0
  11. package/dist/_protocol/sql/postgres/claim.sql +18 -5
  12. package/dist/_protocol/sql/postgres/claim_one_queue.sql +35 -0
  13. package/dist/_protocol/sql/postgres/complete.sql +3 -0
  14. package/dist/_protocol/sql/postgres/fail.sql +30 -8
  15. package/dist/_protocol/sql/postgres/insert_task.sql +6 -3
  16. package/dist/_protocol/sql/postgres/list.sql +3 -1
  17. package/dist/_protocol/sql/postgres/lock_key.sql +9 -0
  18. package/dist/_protocol/sql/postgres/progress.sql +4 -3
  19. package/dist/_protocol/sql/postgres/protocol_version.sql +4 -0
  20. package/dist/_protocol/sql/postgres/purge.sql +25 -0
  21. package/dist/_protocol/sql/postgres/recover_leases.sql +49 -14
  22. package/dist/_protocol/sql/postgres/retry.sql +3 -0
  23. package/dist/_protocol/sql/postgres/stats.sql +8 -0
  24. package/dist/_protocol/sql/postgres/succeed.sql +4 -0
  25. package/dist/_protocol/sql/sqlite/claim.sql +13 -2
  26. package/dist/_protocol/sql/sqlite/claim_one_queue.sql +36 -0
  27. package/dist/_protocol/sql/sqlite/claimable_probe.sql +6 -2
  28. package/dist/_protocol/sql/sqlite/complete.sql +3 -0
  29. package/dist/_protocol/sql/sqlite/fail.sql +32 -8
  30. package/dist/_protocol/sql/sqlite/list.sql +3 -1
  31. package/dist/_protocol/sql/sqlite/lock_key.sql +5 -0
  32. package/dist/_protocol/sql/sqlite/progress.sql +6 -2
  33. package/dist/_protocol/sql/sqlite/protocol_version.sql +4 -0
  34. package/dist/_protocol/sql/sqlite/purge.sql +18 -0
  35. package/dist/_protocol/sql/sqlite/recover_leases.sql +25 -7
  36. package/dist/_protocol/sql/sqlite/retry.sql +3 -0
  37. package/dist/_protocol/sql/sqlite/stats.sql +8 -0
  38. package/dist/_protocol/sql/sqlite/succeed.sql +4 -0
  39. package/dist/client.d.ts +10 -2
  40. package/dist/client.js +12 -0
  41. package/dist/context.d.ts +17 -1
  42. package/dist/context.js +60 -6
  43. package/dist/errors.d.ts +18 -2
  44. package/dist/errors.js +49 -3
  45. package/dist/index.d.ts +3 -2
  46. package/dist/index.js +2 -1
  47. package/dist/sql.js +16 -9
  48. package/dist/store/base.d.ts +114 -9
  49. package/dist/store/base.js +376 -1
  50. package/dist/store/postgres.d.ts +62 -63
  51. package/dist/store/postgres.js +245 -222
  52. package/dist/store/sqlite.d.ts +83 -60
  53. package/dist/store/sqlite.js +370 -234
  54. package/dist/wait.d.ts +15 -2
  55. package/dist/wait.js +23 -5
  56. package/dist/worker.d.ts +53 -1
  57. package/dist/worker.js +202 -42
  58. package/package.json +9 -2
  59. package/src/client.ts +16 -2
  60. package/src/context.ts +70 -13
  61. package/src/errors.ts +59 -4
  62. package/src/index.ts +3 -1
  63. package/src/sql.ts +15 -8
  64. package/src/store/base.ts +443 -27
  65. package/src/store/postgres.ts +243 -267
  66. package/src/store/sqlite.ts +378 -263
  67. package/src/wait.ts +28 -5
  68. package/src/worker.ts +242 -42
@@ -1,20 +1,40 @@
1
- import { type Task } from "../models.js";
2
- import type { ListInput, SubmitInput, TaskStore } from "./base.js";
1
+ import { type Fetch, type Params, TaskStore } from "./base.js";
3
2
  /**
4
- * SQLiteStore — better-sqlite3 backend executing the shared cairnq-protocol SQL.
3
+ * SQLiteStore — the SQLite dialect of the shared cairnq-protocol SQL.
5
4
  *
6
- * The driver is synchronous, which suits SQLite's single writer: claim is one
7
- * short transaction, the handler runs outside any transaction, and
8
- * progress/heartbeat/succeed/fail are each their own short write. JS being
9
- * single-threaded means sync DB calls never interleave. Cross-process contention
10
- * (deployment mode B) is absorbed by busy_timeout.
5
+ * Everything protocol-shaped lives in TaskStore; this file is only what SQLite
6
+ * does differently: better-sqlite3's synchronous driver, BEGIN IMMEDIATE
7
+ * transactions, a read-only probe in front of the write lock, and time supplied
8
+ * by the SDK (`:now_ms`) rather than by the database.
9
+ *
10
+ * The driver being synchronous suits SQLite's single writer: claim is one short
11
+ * transaction, the handler runs outside any transaction, and
12
+ * progress/heartbeat/succeed/fail are each their own short write.
13
+ *
14
+ * Cross-process contention is absorbed by retrying in JavaScript, not by
15
+ * busy_timeout. The two cost the same wait but not the same blocking: a nonzero
16
+ * busy_timeout waits *inside* the synchronous driver, so a caller that loses the
17
+ * write lock stalls this process's event loop for up to the whole timeout — the
18
+ * P99 of an HTTP server that submits tasks. Executing a statement takes
19
+ * microseconds; waiting for a lock takes milliseconds to seconds, and only the
20
+ * second part needs to happen off the thread. So busy_timeout goes to 0 (fail
21
+ * immediately) and the wait becomes an awaited backoff, which the event loop runs
22
+ * through. The budget is the same either way — `busyTimeoutMs`.
23
+ *
24
+ * The open path keeps a real busy_timeout: it is synchronous by nature (WAL
25
+ * switch, migrations) and happens once, under the caller's `connect()`.
11
26
  */
12
- export declare class SQLiteStore implements TaskStore {
27
+ export declare class SQLiteStore extends TaskStore {
13
28
  private readonly path;
14
- private readonly opts;
15
29
  private db;
16
30
  private stmts;
17
31
  private readonly statements;
32
+ /** This store's entry in `fileLocks` — see there for why it is per-database. */
33
+ private readonly lockKey;
34
+ /** How long a single operation may keep retrying a lost write lock. */
35
+ private readonly busyBudgetMs;
36
+ /** When this connection may next revisit its planner statistics. */
37
+ private nextStatsRefreshAt;
18
38
  constructor(path: string, opts?: {
19
39
  busyTimeoutMs?: number;
20
40
  });
@@ -22,56 +42,59 @@ export declare class SQLiteStore implements TaskStore {
22
42
  close(): Promise<void>;
23
43
  private ensure;
24
44
  private applyMigrations;
25
- private checkVersion;
26
45
  private readProtocolVersion;
27
46
  protocolVersion(): Promise<number>;
28
- private all;
29
- private run;
30
- private ownedWrite;
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>;
47
+ /**
48
+ * Adapt the dialect-neutral parameters to what this statement binds.
49
+ *
50
+ * SQLite statements carry no DB clock, so every absolute `*_ms` is derived here
51
+ * from one `now`, and booleans cross as 0/1. The result is narrowed to the
52
+ * names the SQL actually uses, which is what makes it safe for a caller to pass
53
+ * one superset of parameters for both dialects.
54
+ *
55
+ * Each derivation writes a name Postgres does not use (`lease_until_ms` from
56
+ * `lease_ms`, and so on), so a statement binds one or the other, never both —
57
+ * which is why the derived values can be computed unconditionally and left for
58
+ * the narrowing step to discard.
59
+ */
60
+ private bind;
61
+ private runNow;
62
+ /** Queue an operation behind every other operation on this database. */
63
+ private enqueue;
64
+ /**
65
+ * Serialize an operation against this database, waiting out a lost write lock on
66
+ * a jittered backoff. Replaces busy_timeout's synchronous wait (see the class
67
+ * comment); on exhausting the budget the original SQLITE_BUSY surfaces, which is
68
+ * what a nonzero busy_timeout would have thrown too.
69
+ *
70
+ * Each attempt re-queues rather than backing off while holding its turn: the
71
+ * contention left to retry is cross-process, and under WAL a *reader* never sees
72
+ * SQLITE_BUSY at all — so sleeping in place would stall this process's reads
73
+ * (including the worker's own poll) on a lock they were never waiting for.
74
+ *
75
+ * Retrying is safe because an attempt is one statement, or one transaction that
76
+ * has already rolled back: nothing partially applied survives it. `fn` may
77
+ * therefore run more than once and must not carry effects of its own — the
78
+ * callers in TaskStore build their ids and payloads before opening one.
79
+ */
80
+ private withLock;
81
+ /**
82
+ * Revisit this connection's planner statistics, at most once per
83
+ * STATS_REFRESH_INTERVAL_MS.
84
+ *
85
+ * A connection lives for days, and the statements were prepared against whatever
86
+ * the table looked like when it opened — a worker started against an empty
87
+ * database plans as if it were still empty however large the backlog grows. The
88
+ * prepared statements do pick the refreshed plans up: ANALYZE bumps the schema
89
+ * cookie, so SQLite silently re-prepares them on next use. That is what makes
90
+ * this worth doing rather than a restart-only concern.
91
+ *
92
+ * Queued rather than run under `withLock`: statistics are best-effort, so losing
93
+ * the write lock to another process should cost nothing — skip and let the next
94
+ * interval try, instead of spending an operation's whole retry budget on them.
95
+ */
96
+ private maybeRefreshStatistics;
97
+ protected fetch(name: string, params: Params): Promise<any[]>;
98
+ protected tx<T>(fn: (fetch: Fetch) => Promise<T>): Promise<T>;
99
+ protected hasClaimableWork(params: Params): Promise<boolean>;
77
100
  }