Skip to content

Instantly share code, notes, and snippets.

@nashysolutions
Created January 5, 2026 03:41
Show Gist options
  • Select an option

  • Save nashysolutions/6063fed4da4470163294e760c82780b7 to your computer and use it in GitHub Desktop.

Select an option

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.
/// 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