fix(minions): unify JSONB inserts, tighten nullish coalescing

Three non-blocker cleanups from post-ship review of v0.11.0:

- queue.ts add() and completeJob(): pre-stringifying with JSON.stringify
  while other sites pass raw objects with $n::jsonb casts. postgres.js
  double-encodes if you stringify first — works on PGLite (text→JSONB
  auto-cast), fails silently on real PG. Unify on raw object + explicit
  $n::jsonb cast.
- queue.ts readChildCompletions: since clause used sent_at > $2 relying
  on PG's implicit text→TIMESTAMPTZ coercion. Explicit $2::timestamptz
  is safer and clearer.
- types.ts rowToMinionJob: parent_job_id used || which coerces 0 to null.
  Harmless today (SERIAL IDs start at 1) but ?? is semantically correct.

All 110 unit tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-04-18 06:47:53 +08:00
co-authored by Claude Opus 4.7
parent 5382ee0309
commit c502b7ea6d
2 changed files with 7 additions and 7 deletions
+6 -6
View File
@@ -113,13 +113,13 @@ export class MinionQueue {
? `INSERT INTO minion_jobs (name, queue, status, priority, data, max_attempts, backoff_type,
backoff_delay, backoff_jitter, delay_until, parent_job_id, on_child_fail,
depth, max_children, timeout_ms, remove_on_complete, remove_on_fail, idempotency_key)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
ON CONFLICT (idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING
RETURNING *`
: `INSERT INTO minion_jobs (name, queue, status, priority, data, max_attempts, backoff_type,
backoff_delay, backoff_jitter, delay_until, parent_job_id, on_child_fail,
depth, max_children, timeout_ms, remove_on_complete, remove_on_fail, idempotency_key)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
RETURNING *`;
const params = [
@@ -127,7 +127,7 @@ export class MinionQueue {
opts?.queue ?? 'default',
childStatus,
opts?.priority ?? 0,
JSON.stringify(data ?? {}),
data ?? {},
opts?.max_attempts ?? 3,
opts?.backoff_type ?? 'exponential',
opts?.backoff_delay ?? 1000,
@@ -450,11 +450,11 @@ export class MinionQueue {
}
const rows = await tx.executeRaw<Record<string, unknown>>(
`UPDATE minion_jobs SET status = 'completed', result = $1,
`UPDATE minion_jobs SET status = 'completed', result = $1::jsonb,
finished_at = now(), lock_token = NULL, lock_until = NULL, updated_at = now()
WHERE id = $2 AND status = 'active' AND lock_token = $3
RETURNING *`,
[result ? JSON.stringify(result) : null, id, lockToken]
[result ?? null, id, lockToken]
);
if (rows.length === 0) return null;
@@ -841,7 +841,7 @@ export class MinionQueue {
const params: unknown[] = [parentId];
let sinceClause = '';
if (opts?.since) {
sinceClause = ` AND sent_at > $2`;
sinceClause = ` AND sent_at > $2::timestamptz`;
params.push(opts.since.toISOString());
}
+1 -1
View File
@@ -284,7 +284,7 @@ export function rowToMinionJob(row: Record<string, unknown>): MinionJob {
lock_token: (row.lock_token as string) || null,
lock_until: row.lock_until ? new Date(row.lock_until as string) : null,
delay_until: row.delay_until ? new Date(row.delay_until as string) : null,
parent_job_id: (row.parent_job_id as number) || null,
parent_job_id: (row.parent_job_id as number | null) ?? null,
on_child_fail: row.on_child_fail as ChildFailPolicy,
tokens_input: (row.tokens_input as number) ?? 0,
tokens_output: (row.tokens_output as number) ?? 0,