Scopes
The Scopes API provides concurrency control for event processing. Scopes determine how multiple events are queued, debounced, blocked, or serialized to ensure correctness and predictable user-facing behavior.
Concept
- The
Scope
field on a hook attribute accepts a slice of scopes (a scope pipeline).
- Simple usage: one-scope pipeline via helpers like
doors.ScopeOnlyBlocking()
.
- Advanced usage: construct reusable scope objects (e.g.
&doors.ScopeBlocking{}
) and combine them into a pipeline.
- Each scope can hold, promote, or cancel an event.
- Event processing is considered complete when the request is executed or canceled.
- If an event clears all scopes, it is sent to the backend.
- Scopes can be shared between handlers to coordinate behavior across them.
Scope Types
Blocking
Cancels new events while one is processing. Prevents double-clicks or repeated submissions.
Simple usage:
@doors.AClick{
Scope: doors.ScopeOnlyBlocking(),
}
Advanced usage:
{{ block := &doors.ScopeBlocking{} }}
@doors.AClick{
Scope: []doors.Scope{block},
}
Serial
Queues events and processes them sequentially in arrival order.
Simple usage:
@doors.AClick{
Scope: doors.ScopeOnlySerial(),
}
Advanced usage:
{{ serial := &doors.ScopeSerial{} }}
@doors.AClick{
Scope: []doors.Scope{serial},
}
Latest
Cancels previous events and processes only the most recent one. Useful for search-as-you-type.
Simple usage:
@doors.AInput{
Scope: doors.ScopeOnlyLatest(),
}
Advanced usage:
{{ latest := &doors.ScopeLatest{} }}
@doors.AInput{
Scope: []doors.Scope{latest},
}
Debounce
Delays handling of rapid bursts of events.
New events reset the delay timer; execution is guaranteed after the limit even if activity continues.
Simple usage:
@doors.AClick{
Scope: doors.ScopeOnlyDebounce(300*time.Millisecond, time.Second),
}
Advanced usage:
{{ debounce := &doors.ScopeDebounce{} }}
@doors.AClick{
Scope: []doors.Scope{debounce.Scope(300*time.Millisecond, time.Second)},
}
Frame
Separates immediate and frame events.
frame=false
: event runs immediately.
frame=true
: waits for all previous events, blocks new ones, then runs exclusively.
Advanced usage:
{{ frame := &doors.ScopeFrame{} }}
@doors.AInput{
Scope: []doors.Scope{frame.Scope(false)}, // immediate
}
@doors.AClick{
Scope: []doors.Scope{frame.Scope(true)}, // frame
}
Priority
Cancels lower-priority events (pending or running) when a higher-priority event is triggered.
Advanced usage:
{{ prio := &doors.ScopePriority{} }}
@doors.AClick{
Scope: []doors.Scope{prio.Scope(10)}, // higher number = higher priority
}
Scope Pipelining
Combine multiple scopes to form a pipeline. Each scope is applied in sequence.
{{ frame := doors.FrameScope{} }}
{{ debounce = doors.DebounceScope{} }}
@doors.AInput {
// pass throught frame scope, then apply debounce (300 milliseconds, no limit)
Scope: []doors.Scope{ frame.Scope(false), debounce.Scope(300 * time.Millisecond, 0) }
/* setup */
}
<input type="text" name="name">
@doors.AClick {
// Termination frame, to ensure that input value will be passed to server
Scope: []doors.Scope{ frame.Scope(true) }
}
<button>Submut</button>
In this example:
- Input is debounced.
- Click event waits for input to finish (frame termination) and then blocks everything until completion.
This guarantees that input is processed before submission.
⚠️ Scope pipelining is powerful but easy to misuse. Conflicting scopes may lead to unexpected behavior.