feat: Automatically recover from frozen live streams (#2569)

- Closes: #2099
This commit is contained in:
Dermot Duffy
2026-07-07 21:37:24 -07:00
committed by GitHub
parent 9f956fe89f
commit fb1bbc739e
73 changed files with 3321 additions and 364 deletions
@@ -1,4 +1,4 @@
export type MediaQueryWatcherUnsubscribeCallback = () => void;
import type { UnsubscribeCallback } from '../../types';
// Watches a single CSS media query via `window.matchMedia`. The shared change
// source for the `screen` condition and trigger, whose match state lives
@@ -16,7 +16,7 @@ export class MediaQueryWatcher {
return window.matchMedia(this._query).matches;
}
public subscribe(callback: () => void): MediaQueryWatcherUnsubscribeCallback {
public subscribe(callback: () => void): UnsubscribeCallback {
this._callback = callback;
this._mediaQuery = window.matchMedia(this._query);
this._mediaQuery.addEventListener('change', this._handler);
@@ -1,10 +1,8 @@
import type { TemplateRenderer } from '../../card-controller/templates';
import type { Condition } from '../../config/schema/condition-trigger/conditions/types';
import type { UnsubscribeCallback } from '../../types';
import { isEnabled } from '../common/is-enabled';
import type {
ConditionEvaluator,
ExternalInvalidationUnsubscribeCallback,
} from './conditions/types';
import type { ConditionEvaluator } from './conditions/types';
import { createConditionEvaluator } from './factory';
import type {
ConditionsEvaluationResult,
@@ -32,7 +30,7 @@ export class ConditionsManager implements ConditionsManagerReadonlyInterface {
private _listeners: ConditionsListener[] = [];
private _evaluation: ConditionsEvaluationResult = { result: false };
private _unsubscribeCallbacks: ExternalInvalidationUnsubscribeCallback[] = [];
private _unsubscribeCallbacks: UnsubscribeCallback[] = [];
constructor(
conditions: Condition[],
@@ -1,15 +1,14 @@
import type { TemplateRenderer } from '../../../card-controller/templates';
import type { Condition } from '../../../config/schema/condition-trigger/conditions/types';
import type { UnsubscribeCallback } from '../../../types';
import type { ConditionsEvaluationResult, ConditionState } from '../types';
export type ExternalInvalidationUnsubscribeCallback = () => void;
// A source of change outside the card's `ConditionState` that can invalidate a
// condition's result (currently only `screen`, via `matchMedia`). A condition
// declares its sources so a reactive consumer knows what to watch; a pull
// consumer ignores them.
export interface ExternalInvalidationSource {
subscribe(callback: () => void): ExternalInvalidationUnsubscribeCallback;
subscribe(callback: () => void): UnsubscribeCallback;
}
/**
@@ -1,7 +1,5 @@
import {
MediaQueryWatcher,
type MediaQueryWatcherUnsubscribeCallback,
} from '../../common/media-query-watcher';
import type { UnsubscribeCallback } from '../../../types';
import { MediaQueryWatcher } from '../../common/media-query-watcher';
import { buildCardTriggerData } from '../build-trigger-data';
import type { TriggerCallback, TriggerEvaluator, TriggerOfType } from './types';
@@ -14,7 +12,7 @@ export class ScreenTrigger implements TriggerEvaluator {
private _callback: TriggerCallback | null = null;
private _watcher: MediaQueryWatcher | null = null;
private _unsubscribeCallback: MediaQueryWatcherUnsubscribeCallback | null = null;
private _unsubscribeCallback: UnsubscribeCallback | null = null;
private _matched = false;
constructor(trigger: TriggerOfType<'screen'>) {