Files
Signage/docs/architecture/decisions/0003-timelines-slots-and-selection-scopes.md
T
2026-08-28 16:41:54 -05:00

5.0 KiB

0003: Timelines are ordered patterns; a source instance is a selection scope

  • Status: Accepted
  • Date: 2026-08-27
  • Related: 0002, 0004

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: n count 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.