-
Notifications
You must be signed in to change notification settings - Fork 3k
Event Queue
(11/10/23: Ongoing WIP; parts of this document may change in the future)
RFC: https://docs.google.com/document/d/185jXC9vcoJa6KXitFYLStSMTc2yXHoEmbCUgM-y9SLc
The Event Queue is a new architectural component added to Firefox iOS. It provides a simple API for coordinating and synchronizing code across separate areas of the codebase via dependencies called Events (below). The goal is to make it easy to reason about complex dependencies, while also writing clear and concise code.
Events are similar to notifications and can be configured by adding cases to the AppEvent
enum. You can signal an event on a queue by calling signal(event:)
. Currently, there is a single shared instance of EventQueue available ubiquitously throughout the app, called AppEventQueue. To signal an event called myEvent on this queue, for example, you'd call:
AppEventQueue.signal(event: .myEvent)
Events are generally considered "one shot" and are typically used to indicate whether a particular boolean state has occurred. The startup flow that occurs during app launch is an example.
Where the Event Queue becomes useful is when you declare one or more of these events as dependencies for a particular action. For example, you may have code you need to execute at some point during the startup flow, but you want to ensure that both the full startup process as well as tab restoration have both completed before your code runs. You could write:
AppEventQueue.wait(for: [.startupFlowComplete, .tabRestoration]) {
// Your code
}
In the above example, the code in the closure will be enqueued if needed and automatically executed as soon as all of the dependent events (in this case startup and tab restoration) are completed. If the required events have already completed, the code is executed immediately.
Though the concepts of events and enqueued actions are simple, they can be combined in powerful ways. For example, events can be composed of multiple child events in nested hierarchies. The app startup flow is an example; it is automatically signaled whenever all of its child dependencies are completed.
This is done by establishing a relationship between the children and parent events:
AppEventQueue.establishDependencies(for: .startupFlowComplete, against: [
.profileInitialized,
.preLaunchDependenciesComplete,
.postLaunchDependenciesComplete,
.accountManagerInitialized
])
Once all of the necessary sub-events are completed, the parent event is automatically updated as being completed. Additionally, any enqueued actions that depend on .startupFlowComplete
are also automatically executed once the dependencies are satisfied.
Activities are a type of event that can occur repeatedly, and potentially exist in a few different states. Profile syncing is an example of an activity; it may occur multiple times during the lifetime of the app, and could exist in one of several states (not started, in progress, completed, or failed). The Event Queue supports these types of activities and the API for signalling them is straightforward:
AppEventQueue.started(.myActivity)
and
AppEventQueue.completed(.myActivity)
Activity dependencies are considered resolved as soon as they are moved into the .completed
state.
If needed, enqueued actions can be cancelled (although this will likely be a rare use case). The wait()
function has a @discardableResult
return value which is a UUID
associated with the action. You can cancel an enqueued action by passing in this UUID. Example:
let token = AppEventQueue.wait(for: [.startupFlowComplete]) { // Perform work }
…
AppEventQueue.cancelAction(token: token)
The Event Queue is thread safe. Currently by default all state changes, as well as all enqueued actions, are executed on the main thread. If you wish to explicitly perform background work that avoids tying up the main thread or UI you can simply use a Task or dispatch etc. within your enqueued action as you normally would. (Aspects of this are part of ongoing work and could change in the future.)