MonitorOrchestrator
Re-evaluates actions continuously. Use for long-running checks that can trigger mid-session.
When to Use
Use MonitorOrchestrator when an action needs to fire after the app is already running — expired terms that require re-acceptance, a session timeout, a cached content refresh. The monitor re-checks shouldRun() on every tick; whenever it returns true, the action executes.
Interval Mode
Pass an interval at init time to run automatic re-evaluation on a timer. Call start() to begin and stop() to halt.
import ForgeOrchestrator
// Re-evaluate every 5 minutes
let monitor = MonitorOrchestrator(interval: 300)
monitor.register(TermsExpiredAction())
monitor.register(SessionExpiredAction())
monitor.start()
// Later, when the app goes background
monitor.stop() On-Demand Mode
Pass nil (or omit the interval) to run purely on-demand. Call reevaluate() yourself whenever relevant state changes.
// No interval — manual re-evaluation only
let monitor = MonitorOrchestrator()
monitor.register(UnreadBadgeAction())
// Call reevaluate() whenever relevant state changes
await monitor.reevaluate() Both modes can coexist: even with an interval configured, you can still call reevaluate() manually to force an immediate check.
Screen Exclusion
Monitor actions can interrupt the user — a modal, a forced sign-out, a terms sheet. You rarely want that to happen during critical user flows like checkout. Use screen exclusion to suppress re-evaluation on specific screens.
// Don't re-evaluate while the user is in critical flows
monitor.setExcludedScreens(["Checkout", "Payment", "VideoCall"])
// Tell the monitor which screen is currently visible
monitor.updateCurrentScreen("Feed") // evaluates
monitor.updateCurrentScreen("Checkout") // skipped
monitor.updateCurrentScreen("Feed") // evaluates again Exclusion is tick-level: the monitor checks the current screen at the start of each cycle. If the screen is excluded, the entire cycle is skipped — no shouldRun() calls, no executions. The next cycle checks again.
Defining a Monitor Action
MonitorAction has the same shape as SequenceAction — id, priority, shouldRun(), execute(). The difference is operational: the monitor keeps re-evaluating it.
struct TermsExpiredAction: MonitorAction {
let id: ActionID = "terms-expired"
let priority: ActionPriority = .critical
func shouldRun() async -> Bool {
// Cheap check — called on every cycle
TermsService.hasExpiredSinceLastCheck()
}
func execute() async {
let signal = CompletionSignal()
TermsPresenter.show { signal.complete() }
await signal.wait()
}
} shouldRun() runs on every tick. Keep it cheap and non-allocating. Expensive work here multiplies by (app runtime) / (interval) — and runs on the main actor.
Reentrancy
If reevaluate() is called while an evaluation is already in progress, the second call returns immediately without doing anything. This means you don't need to debounce your triggers.
Observable State
Same as the other orchestrators: isProcessing and currentActionId are @Observable. Useful for surfacing a subtle "refreshing…" indicator when the monitor is checking.
Lifecycle
start()— begins the interval loop. No-op if no interval was configured.stop()— cancels the interval loop. Safe to call multiple times.reevaluate()— runs one evaluation cycle synchronously. Safe to call from any context.removeAll()— clears registered actions. Does not stop the timer; callstop()separately if needed.
Best Practices
- Pick an interval that matches your need. Too short wastes power; too long misses events. 5 minutes is reasonable for most policy checks.
- Stop the monitor in background. Call
stop()when the app enters background to avoid battery drain. - Exclude screens where interruption is unacceptable. Checkout, video calls, in-progress forms — any user task that loses data on dismissal.
- Pair interval with notifications. Don't rely on the monitor alone — critical state changes should also post notifications that trigger
reevaluate()directly. - Keep monitor actions rare and important. If you find yourself adding many monitor actions, reconsider — most one-shot gates belong in
SequenceOrchestrator.