MobiusCore/Source/WorkBag.swift (41 lines of code) (raw):
// Copyright Spotify AB.
// SPDX-License-Identifier: Apache-2.0
import Foundation
/// Like a work queue, but aggressively avoids doing things sequentially.
///
/// We don’t want to commit to any sequencing guarantees in Mobius loops. Since we’re cognizant of Hyrum’s Law, we don’t
/// want to provide _unguaranteed_ sequencing either, so this implementation currently randomizes execution order. We
/// can change this later to give any specific guarantees we want to commit to.
///
/// Submitted blocks are executed in random order by calling `service`. Nested calls to `service` are ignored, so that
/// work is only processed in the outermost invocation. This avoids surprising reentrancy in effect handlers (for
/// example, if the recursion check is removed, the effect handler in `SequencingTests` will enqueue events 2 and 3
/// multiple times).
final class WorkBag {
typealias WorkItem = () -> Void
private enum State {
case notStarted
case idle
case servicing
}
private var queue = [WorkItem]()
private var state = State.notStarted
private var access: ConcurrentAccessDetector
init(accessGuard: ConcurrentAccessDetector = ConcurrentAccessDetector()) {
access = accessGuard
}
/// Start the workbag. Must be called once and once only in order to process events.
func start() {
access.guard {
precondition(state == .notStarted)
state = .idle
}
service()
}
/// Submit an action to be executed.
func submit(_ action: @escaping WorkItem) {
access.guard {
queue.append(action)
}
}
/// Execute all pending work, if we’re not being called recursively from an invocation of `service`.
///
/// If we _are_ being invoked recursively, new work submitted via `submit` will be executed by the ongoing `service`
/// call, until there is no more pending work.
func service() {
access.guard {
guard state == .idle else { return }
state = .servicing
defer { state = .idle }
while let action = next() {
action()
}
}
}
private func next() -> WorkItem? {
guard !queue.isEmpty else { return nil }
return queue.remove(at: Int.random(in: 0..<queue.endIndex))
}
}