Created
January 5, 2026 03:41
-
-
Save nashysolutions/6063fed4da4470163294e760c82780b7 to your computer and use it in GitHub Desktop.
A MainActor-isolated, FIFO job queue that deduplicates enqueued jobs and processes them sequentially via an async handler, ensuring only one draining task runs at a time.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /// A simple FIFO job queue that serially processes unique jobs using an async handler. | |
| /// | |
| /// `JobQueue` ensures only one draining task runs at a time. When a job is enqueued, | |
| /// the queue starts a task on the main actor to | |
| /// drain jobs in order. Duplicate jobs (as defined by `Equatable`) are ignored when | |
| /// enqueuing. | |
| /// | |
| /// - Important: This queue is `@MainActor`-isolated. The `handler` runs on the main | |
| /// actor. Offload heavy work from the handler to a background context to avoid UI | |
| /// jank. | |
| @MainActor | |
| final class JobQueue<T: Equatable & Sendable> { | |
| /// Pending jobs to be processed in FIFO order. Duplicates are prevented at enqueue time. | |
| private var jobs: [T] = [] | |
| /// The single draining task responsible for processing jobs. `nil` when idle. | |
| private var task: Task<Void, Never>? | |
| /// The async handler invoked for each job. Runs on the main actor. | |
| private let handler: (T) async -> Void | |
| /// Creates a new job queue. | |
| /// | |
| /// - Parameter handler: The async function to invoke for each job. Because this | |
| /// class is `@MainActor`-isolated, the handler also runs on the main actor. If the | |
| /// work is heavy, dispatch to a background task. | |
| init(handler: @escaping (T) async -> Void) { | |
| self.handler = handler | |
| } | |
| /// Enqueues a job if it isn't already pending and starts the draining task if needed. | |
| /// | |
| /// - Parameter job: The job to add. If an equal job already exists in the queue, the | |
| /// call is ignored. | |
| func enqueue(_ job: T) { | |
| if jobs.contains(job) { | |
| return | |
| } | |
| jobs.append(job) | |
| startIfNeeded() | |
| } | |
| func cancelAll() { | |
| task?.cancel() | |
| /// Clear the task reference so a future enqueue can create a new draining task. | |
| task = nil | |
| jobs.removeAll() | |
| } | |
| /// Starts the draining task if one is not already running. | |
| private func startIfNeeded() { | |
| guard task == nil else { return } | |
| task = Task { [weak self] in | |
| await self?.drain() | |
| } | |
| } | |
| private func drain() async { | |
| defer { | |
| task = nil | |
| } | |
| while !Task.isCancelled { | |
| guard let job = jobs.first else { break } | |
| jobs.removeFirst() | |
| await handler(job) | |
| } | |
| } | |
| deinit { | |
| task?.cancel() | |
| /// Explicitly clear queued jobs during teardown. | |
| /// This is not required for correctness, but it documents intent and | |
| /// helps avoid confusing debugger/logging output that might suggest | |
| /// pending work at the end of the queue’s lifecycle. | |
| jobs.removeAll() | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment