refactor(runtime): derive the group timer on the server

The group timer was derived in the client, which meant every view that wanted it
had to depend on the rundown query. That coupling was already awkward for the PiP
timer, whose separate react root needed its own query client, and it would have
spread to every remaining view.

Moving the derivation to the server removes the coupling. The value is still the
running event timer offset by the content scheduled around it, so the group keeps
behaving as if it were a single event containing all its children.

Because the group timer is the event timer offset by a constant, it changes exactly
when the event timer does and can share its broadcast throttling.

- add RuntimeStore.groupTimer, null unless the running group opted in
- split the group around the loaded event when the group is loaded, so the per tick
  cost is an addition rather than a walk of the rundown
- derive on the getState() projection so it cannot drift from the timer it is built on
- reduce the client to a plain selector, dropping the PiP query client workaround

Also fixes elapsed time, which was calculated from the group duration and so clamped
to zero for as long as time added to an event kept the group in credit. It is now
derived symmetrically with the remaining time, and the two always add up to the total.

Timer and PiP shared eight identical branches for choosing between the two timers.
These now go through a single resolver. A group has no warning or danger thresholds,
so it only reports as running or overtime, and feeding that phase through the existing
modifiers makes the suppression of warning and danger a consequence of what a group is
rather than something each view has to remember.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kniqs443KUNCRABwVJwT7K
This commit is contained in:
Claude
2026-07-26 15:46:58 +00:00
parent 636b2e7c3e
commit 1fad99f505
19 changed files with 395 additions and 267 deletions
@@ -17,6 +17,7 @@ export const runtimeStorePlaceholder: Readonly<RuntimeStore> = {
secondaryTimer: null, // change on every update
startedAt: null, // change can only be initiated by user
},
groupTimer: null, // derived from the timer, null unless the running group opted in
message: {
timer: {
text: '',
@@ -3,12 +3,14 @@ import type { SimpleTimerState } from './AuxTimer.type.js';
import type { MessageState } from './MessageControl.type.js';
import type { Offset } from './Offset.type.js';
import type { RundownState } from './RundownState.type.js';
import type { TimerState } from './TimerState.type.js';
import type { GroupTimerState, TimerState } from './TimerState.type.js';
export type RuntimeStore = {
// timer data
clock: number;
timer: TimerState;
/** shared timer for the running group, null unless the group opted in */
groupTimer: GroupTimerState | null;
// messages service
message: MessageState;
@@ -11,6 +11,22 @@ export enum TimerPhase {
Pending = 'pending',
}
/**
* Timer for the running group, present when the group opted into a shared timer.
*
* The group is treated as a single event containing all its children: the values are
* derived from the running event timer plus the content scheduled around it, so pause,
* added time, overtime and roll are inherited from the event timer rather than recalculated.
*/
export type GroupTimerState = {
/** Time remaining in the group */
current: number;
/** Time elapsed since the group started */
elapsed: number;
/** Total time in the group, includes time added to the running event */
duration: number;
};
/**
* Gathers the current running timer state
*/
+1 -1
View File
@@ -119,7 +119,7 @@ export type { Offset } from './definitions/runtime/Offset.type.js';
export { OffsetMode } from './definitions/runtime/Offset.type.js';
export type { RuntimeStore } from './definitions/runtime/RuntimeStore.type.js';
export { runtimeStorePlaceholder } from './definitions/runtime/RuntimeStore.js';
export { type TimerState, TimerPhase } from './definitions/runtime/TimerState.type.js';
export { type GroupTimerState, type TimerState, TimerPhase } from './definitions/runtime/TimerState.type.js';
// ---> Extra Timer
export { type SimpleTimerState, SimplePlayback, SimpleDirection } from './definitions/runtime/AuxTimer.type.js';