import type { QueuedJob } from "./job-queue.types.js"; /** * Array-backed binary min-heap keyed by `QueuedJob.runAt`. * * - `enqueue` / `dequeue`: O(log n). * - `peek` / `size`: O(1). * - `remove(jobId)`: O(n) — linear scan, swap-with-last, sift in the * appropriate direction. * * The heap is intentionally minimal — no external dependency, no GC * churn beyond the single backing array, no comparator allocation per * operation. It exists for the scheduler's tick loop, which calls * `peek` once per tick and `dequeue` at most `concurrency` times. */ export class JobQueue { private readonly heap: QueuedJob[] = []; /** Add `job` to the heap. Ties on `runAt` are broken by insertion order. */ public enqueue(job: QueuedJob): void { this.heap.push(job); this.siftUp(this.heap.length - 1); } /** Remove and return the earliest job, or `undefined` if empty. */ public dequeue(): QueuedJob | undefined { const n = this.heap.length; if (n === 0) return undefined; const root = this.heap[0] as QueuedJob; const last = this.heap.pop() as QueuedJob; if (n > 1) { this.heap[0] = last; this.siftDown(0); } return root; } /** Earliest job without removing it, or `undefined` if empty. */ public peek(): QueuedJob | undefined { return this.heap[0]; } /** Current heap size. */ public size(): number { return this.heap.length; } /** * Remove the first entry whose `jobId` matches and rebalance. * Returns `true` if an entry was found and removed. */ public remove(jobId: string): boolean { const idx = this.heap.findIndex((j) => j.jobId === jobId); if (idx === -1) return false; const lastIdx = this.heap.length - 1; if (idx === lastIdx) { this.heap.pop(); return true; } const last = this.heap.pop() as QueuedJob; this.heap[idx] = last; // The replacement may belong higher OR lower; try both. Only one // direction will do work, the other no-ops. this.siftUp(idx); this.siftDown(idx); return true; } /** Drain all entries into a snapshot array in heap-order (NOT sorted). */ public snapshot(): readonly QueuedJob[] { return Object.freeze([...this.heap]); } private siftUp(from: number): void { let i = from; while (i > 0) { const parent = (i - 1) >>> 1; const a = this.heap[i] as QueuedJob; const b = this.heap[parent] as QueuedJob; if (a.runAt >= b.runAt) break; this.heap[i] = b; this.heap[parent] = a; i = parent; } } private siftDown(from: number): void { const n = this.heap.length; let i = from; for (;;) { const left = i * 2 + 1; const right = left + 1; let smallest = i; if ( left < n && (this.heap[left] as QueuedJob).runAt < (this.heap[smallest] as QueuedJob).runAt ) { smallest = left; } if ( right < n && (this.heap[right] as QueuedJob).runAt < (this.heap[smallest] as QueuedJob).runAt ) { smallest = right; } if (smallest === i) break; const tmp = this.heap[i] as QueuedJob; this.heap[i] = this.heap[smallest] as QueuedJob; this.heap[smallest] = tmp; i = smallest; } } }