5.0 KiB
0003: Timelines are ordered patterns; a source instance is a selection scope
Context
An operator building a Sunday-morning loop wants something like:
Announcements Announcements Announcements Trivia Announcements Announcements Scripture ↻
Five Announcement slots. They must not all show the same announcement, and they must not each start a separate rotation that happens to begin at the same place. They are five turns of one rotation.
Decision
Three concepts, and no more.
A timeline is an ordered, repeating pattern
signage_timelines holds a name, an optional eligibility window, and a revision. It holds no cursor,
no clock, and no channel: a timeline is a program, and running one is what a channel group does.
The same timeline may be activated on two groups at once and they will not interfere.
A slot is a position in that pattern
signage_timeline_slots rows are (timeline_id, position) with position unique and contiguous
from zero. Contiguity is enforced inside the transaction that changes it, never by renumbering on
read: a gap would make "the next slot" ambiguous exactly when playback needs it least.
A slot's identity is its id, which is stable across every reorder. Playback history, resolved
selections, and a display's cursor all reference the id, so dragging a slot up the list never
rewrites what already happened.
Each slot is one of two kinds:
provider-source— "ask this source for the next item when this slot's turn comes".pinned-item— "show exactly this item". A native Signage image is a pinned item of the built-in image provider, which is why the two need no separate code path (ADR 0001).
Every slot may carry an enabled flag, a duration override, its own eligibility window, and — for
native images only — presentation overrides.
A source instance is a selection scope
signage_provider_sources is the piece that makes the five-Announcement pattern work. One row is
"the Announcements module's active-slides source, as used by this timeline", and it carries a stable
opaque selection_scope string.
Every slot referencing that row passes the same scope id to resolveNext. The provider advances
its own cursor within that scope, so the five occurrences walk through five different announcements
and the sixth wraps according to the provider's repeat policy — which Signage neither implements
nor knows about.
Two source rows for the same provider source are two independent rotations, which is exactly how an operator expresses "a fast loop here and a slow one there".
Signage generates the scope id and stores it. It is opaque to the provider in the sense that its shape means nothing; it is stable in the sense that it survives edits, restarts, and reorders. It is not derived from the timeline id or the slot id, because either would silently reset a provider's rotation when an operator renamed or reordered something.
Eligibility is [start, end), in absolute instants
Both a timeline and a slot may carry a window. It is half-open: active exactly at start, inactive
exactly at end, which is the only convention under which two adjacent windows neither overlap nor
leave a gap.
Every stored instant is absolute (timestamptz). The installation's IANA zone is an operator setting
used to write and read those instants for a human, never to interpret an ambiguous local timestamp
at run time. A daylight-saving transition therefore cannot move a slot.
Signage decides whether a source participates; the provider decides what is available. Leaving Trivia out of a service means using a timeline without a Trivia slot, or disabling that slot — and Signage still never learns what Trivia is.
Consequences
- Reordering, renaming, duplicating, and disabling are cheap and safe: none of them touch a scope id or a selection's identity.
- A timeline with every slot disabled, or every slot ineligible, is a legitimate state — it resolves to nothing, and the display shows its slate rather than an error.
- An operator can express "three announcements, then trivia, then two more announcements" without any concept of grouping, nesting, or weighting. The pattern is the expression.
Alternatives
- Slot position as identity. Rejected: every reorder would rewrite history and break a display's cursor mid-service.
- Scope derived from
(timeline, provider, source). Rejected: it silently merges two source rows an operator deliberately created as separate rotations, and changes meaning when a timeline is duplicated. - A
repeat: ncount on one slot instead of repeated slots. Rejected: the pattern is the thing an operator reads, and a count hides where in the loop the repeats actually fall. - Local wall-clock schedules. Rejected: one ambiguous hour every autumn, resolved silently, on the screens a congregation is looking at.