@glivion/square-screen-js-sdk 0.1.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/index.cjs +874 -0
  2. package/dist/index.cjs.map +1 -0
  3. package/dist/index.d.cts +522 -0
  4. package/dist/index.d.mts +522 -0
  5. package/dist/index.mjs +870 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/package.json +8 -1
  8. package/.github/workflows/build-js-sdk.yml +0 -70
  9. package/eslint.config.js +0 -3
  10. package/examples/react-app/README.md +0 -73
  11. package/examples/react-app/eslint.config.js +0 -22
  12. package/examples/react-app/index.html +0 -13
  13. package/examples/react-app/package-lock.json +0 -2239
  14. package/examples/react-app/package.json +0 -31
  15. package/examples/react-app/public/favicon.svg +0 -1
  16. package/examples/react-app/public/icons.svg +0 -24
  17. package/examples/react-app/src/App.css +0 -184
  18. package/examples/react-app/src/App.tsx +0 -157
  19. package/examples/react-app/src/EmergencyTicker.tsx +0 -25
  20. package/examples/react-app/src/HeadlessExample.tsx +0 -66
  21. package/examples/react-app/src/RendererExample.tsx +0 -70
  22. package/examples/react-app/src/assets/hero.png +0 -0
  23. package/examples/react-app/src/assets/react.svg +0 -1
  24. package/examples/react-app/src/assets/vite.svg +0 -1
  25. package/examples/react-app/src/index.css +0 -183
  26. package/examples/react-app/src/main.tsx +0 -10
  27. package/examples/react-app/src/mockNetworkDataSource.ts +0 -116
  28. package/examples/react-app/src/usePlayer.ts +0 -71
  29. package/examples/react-app/tsconfig.app.json +0 -25
  30. package/examples/react-app/tsconfig.json +0 -7
  31. package/examples/react-app/tsconfig.node.json +0 -24
  32. package/examples/react-app/vite.config.ts +0 -7
  33. package/examples/react-app/yarn.lock +0 -1089
  34. package/src/__tests__/cache/SquareScreenCache.test.ts +0 -375
  35. package/src/__tests__/network/NetworkClient.test.ts +0 -217
  36. package/src/__tests__/network/mappers.test.ts +0 -163
  37. package/src/__tests__/player/SquareScreenPlayer.test.ts +0 -840
  38. package/src/cache/SquareScreenCache.ts +0 -154
  39. package/src/constants.ts +0 -9
  40. package/src/core/types.ts +0 -251
  41. package/src/env.d.ts +0 -4
  42. package/src/index.ts +0 -34
  43. package/src/network/NetworkClient.ts +0 -234
  44. package/src/network/apiTypes.ts +0 -89
  45. package/src/network/mappers.ts +0 -106
  46. package/src/player/SquareScreenPlayer.ts +0 -414
  47. package/src/renderer/SquareScreenRenderer.ts +0 -282
  48. package/tsconfig.json +0 -12
  49. package/tsdown.config.ts +0 -23
@@ -1,89 +0,0 @@
1
- /**
2
- * Raw API response types — internal to the network layer.
3
- *
4
- * These mirror exactly what the server sends, including snake_case field names.
5
- * Nothing in this file is exported from the SDK's public surface.
6
- * Use the mapper functions in mappers.ts to convert these to core types.
7
- */
8
-
9
- /** Raw emergency object as returned inside the emergency response envelope. */
10
- export interface ApiEmergency {
11
- id: number;
12
- uuid: string;
13
- company_id: number;
14
- title: string;
15
- message: string;
16
- background_color: string;
17
- text_color: string;
18
- target_scope: string;
19
- is_active: boolean;
20
- started_at: string;
21
- ended_at?: string;
22
- }
23
-
24
- /** Top-level shape of the GET /screen/emergency response. */
25
- export interface ApiEmergencyResponse {
26
- emergency: ApiEmergency | null;
27
- }
28
-
29
- /** Raw playlist item as returned by the API. */
30
- export interface ApiPlaylistItem {
31
- uuid: string;
32
- name: string;
33
- type: string;
34
- url: string;
35
- duration_seconds: number;
36
- width?: number;
37
- height?: number;
38
- transition?: string;
39
- title?: string;
40
- thumbnail?: string;
41
- }
42
-
43
- /**
44
- * Raw playback strategy as returned by the API when a playlist is active.
45
- * When no playlist is scheduled the API returns an empty array — use
46
- * Array.isArray() to detect that case before treating this as a strategy.
47
- */
48
- export interface ApiPlaybackStrategy {
49
- loop: boolean;
50
- shuffle: boolean;
51
- preload_count?: number;
52
- }
53
-
54
- /** Raw schedule metadata attached to a playlist response. */
55
- export interface ApiSchedule {
56
- uuid: string;
57
- name: string;
58
- priority: number;
59
- }
60
-
61
- /** Raw playlist container metadata attached to a playlist response. */
62
- export interface ApiPlaylistMeta {
63
- uuid: string;
64
- name: string;
65
- }
66
-
67
- /** Top-level shape of the GET /screen/now-playing response. */
68
- export interface ApiPlaylistResponse {
69
- items: ApiPlaylistItem[];
70
- /** Empty array when no playlist is scheduled — not a real strategy object. */
71
- strategy: ApiPlaybackStrategy | [];
72
- schedule?: ApiSchedule;
73
- playlist?: ApiPlaylistMeta;
74
- }
75
-
76
- /** Top-level shape of the POST /screen/heartbeat response. */
77
- export interface ApiHeartbeatAck {
78
- received: boolean;
79
- }
80
-
81
- /** Top-level shape of the POST /screen/heartbeat request. */
82
- export interface ApiHeartbeatPayload {
83
- cpu_usage?: number;
84
- memory_usage?: number;
85
- disk_usage?: number;
86
- temperature?: number;
87
- os_version?: string;
88
- app_version: string;
89
- }
@@ -1,106 +0,0 @@
1
- import {
2
- EmergencyAlert,
3
- HeartbeatAck,
4
- MediaType,
5
- Playlist,
6
- PlaylistItem,
7
- PlaybackStrategy,
8
- HeartbeatPayload,
9
- TransitionType,
10
- } from "../core/types";
11
- import {
12
- ApiEmergencyResponse,
13
- ApiHeartbeatAck,
14
- ApiHeartbeatPayload,
15
- ApiPlaybackStrategy,
16
- ApiPlaylistItem,
17
- ApiPlaylistResponse,
18
- } from "./apiTypes";
19
-
20
- /** Maps a raw playlist item to the core PlaylistItem type. */
21
- export function mapPlaylistItem(raw: ApiPlaylistItem): PlaylistItem {
22
- return {
23
- uuid: raw.uuid,
24
- name: raw.name,
25
- type: raw.type as MediaType,
26
- url: raw.url,
27
- duration: raw.duration_seconds,
28
- ...(raw.transition !== undefined && {
29
- transition: raw.transition as TransitionType,
30
- }),
31
- ...(raw.width !== undefined && { width: raw.width }),
32
- ...(raw.height !== undefined && { height: raw.height }),
33
- ...(raw.title !== undefined && { title: raw.title }),
34
- ...(raw.thumbnail !== undefined && { thumbnail: raw.thumbnail }),
35
- };
36
- }
37
-
38
- /**
39
- * Maps the raw strategy field to a PlaybackStrategy.
40
- * Returns undefined when the API sends an empty array instead of a real object.
41
- */
42
- export function mapPlaybackStrategy(
43
- raw: ApiPlaybackStrategy | [],
44
- ): PlaybackStrategy | undefined {
45
- if (Array.isArray(raw)) return undefined;
46
- return {
47
- loop: raw.loop,
48
- shuffle: raw.shuffle,
49
- preloadCount: raw.preload_count ?? 0,
50
- };
51
- }
52
-
53
- /** Maps a raw playlist response to the core Playlist type. */
54
- export function mapPlaylistResponse(raw: ApiPlaylistResponse): Playlist {
55
- return {
56
- uuid: raw.playlist!.uuid,
57
- items: raw.items.map(mapPlaylistItem),
58
- strategy: mapPlaybackStrategy(raw.strategy),
59
- schedule: raw.schedule,
60
- playlistMeta: raw.playlist,
61
- cachedAt: Date.now(),
62
- };
63
- }
64
-
65
- /**
66
- * Maps a raw emergency response to an EmergencyAlert.
67
- * Returns null when no emergency is active.
68
- */
69
- export function mapEmergencyResponse(
70
- raw: ApiEmergencyResponse,
71
- ): EmergencyAlert | null {
72
- if (raw.emergency === null) return null;
73
- return {
74
- id: raw.emergency.id,
75
- uuid: raw.emergency.uuid,
76
- companyId: raw.emergency.company_id,
77
- title: raw.emergency.title,
78
- message: raw.emergency.message,
79
- backgroundColor: raw.emergency.background_color,
80
- textColor: raw.emergency.text_color,
81
- targetScope: raw.emergency.target_scope,
82
- isActive: raw.emergency.is_active,
83
- startedAt: raw.emergency.started_at,
84
- ...(raw.emergency.ended_at !== undefined && {
85
- endedAt: raw.emergency.ended_at,
86
- }),
87
- };
88
- }
89
-
90
- /** Maps a raw heartbeat acknowledgement to the core HeartbeatAck type. */
91
- export function mapHeartbeatAck(raw: ApiHeartbeatAck): HeartbeatAck {
92
- return { received: raw.received };
93
- }
94
-
95
- export function mapHeartbeatPayload(
96
- raw: HeartbeatPayload,
97
- ): ApiHeartbeatPayload {
98
- return {
99
- cpu_usage: raw.cpuUsage,
100
- memory_usage: raw.memoryUsage,
101
- disk_usage: raw.diskUsage,
102
- temperature: raw.temperature,
103
- os_version: raw.osVersion,
104
- app_version: raw.playerVersion,
105
- };
106
- }
@@ -1,414 +0,0 @@
1
- import { SQUARESCREEN_API_BASE_URL } from "../constants";
2
- import { NetworkClient } from "../network/NetworkClient";
3
- import {
4
- DeviceStatus,
5
- EmergencyAlert,
6
- NetworkDataSource,
7
- PlaybackEvent,
8
- Playlist,
9
- PlaylistItem,
10
- SquareScreenCacheProvider,
11
- } from "../core/types";
12
- import { SquareScreenCache } from "../cache/SquareScreenCache";
13
-
14
- const LAST_PLAYLIST_KEY = "square-screen:last-playlist-uuid";
15
-
16
- export interface SquareScreenPlayerConfig {
17
- /** Unique identifier for this device. */
18
- deviceId: string;
19
- /** Secret token used to authenticate this device. Never log or expose this value. */
20
- deviceToken: string;
21
- /** SDK version string sent on every heartbeat. */
22
- version: string;
23
- /** How long a cached playlist is considered fresh, in ms. Defaults to 5 minutes. */
24
- ttl?: number;
25
- /** How often to poll the API for playlist updates, in ms. Defaults to 30 seconds. */
26
- pollInterval?: number;
27
- /** How often to poll the API for emergency alerts, in ms. Defaults to 15 seconds. */
28
- emergencyPollInterval?: number;
29
- /** How often to send a heartbeat to the API, in ms. Defaults to 60 seconds. */
30
- heartbeatInterval?: number;
31
- /**
32
- * Override the network layer with a custom implementation — useful for testing
33
- * or mocking without a real API. When provided, `deviceId` and `deviceToken`
34
- * are ignored for network calls. Otherwise requests go to the production API
35
- * URL defined by {@link SQUARESCREEN_API_BASE_URL}.
36
- */
37
- networkDataSource?: NetworkDataSource;
38
- /**
39
- * Override the cache layer with a custom implementation. When omitted the default
40
- * `SquareScreenCache` (IndexedDB + Cache API) is used. Pass your own implementation
41
- * to integrate with an existing media storage layer.
42
- */
43
- cacheProvider?: SquareScreenCacheProvider;
44
- }
45
-
46
- /** Typed detail payloads for each player event. */
47
- export type PlayerEventMap = {
48
- /** Fires when the current playlist item changes. */
49
- itemchange: CustomEvent<{ item: PlaylistItem; index: number; total: number }>;
50
- /** Fires when the device connectivity/sync status changes. */
51
- statuschange: CustomEvent<{ status: DeviceStatus }>;
52
- /** Fires when an emergency alert becomes active or is cleared. */
53
- emergencyalert: CustomEvent<{ alert: EmergencyAlert | null }>;
54
- /** Fires whenever the playlist is refreshed from the network or cache. */
55
- playlistupdate: CustomEvent<{ playlist: Playlist }>;
56
- };
57
-
58
- /**
59
- * Headless, framework-agnostic player that manages playlist fetching, caching,
60
- * item advancement, preloading, polling, and emergency alerts.
61
- *
62
- * Extends `EventTarget` — use `addEventListener` / `removeEventListener` to
63
- * react to player events. No DOM or rendering logic lives here.
64
- *
65
- * @example
66
- * const player = new SquareScreenPlayer({ deviceId, deviceToken, version: "1.0.0" });
67
- * player.addEventListener("itemchange", ({ detail }) => render(detail.item));
68
- * player.addEventListener("emergencyalert", ({ detail }) => showAlert(detail.alert));
69
- * await player.start();
70
- */
71
- export class SquareScreenPlayer extends EventTarget {
72
- private readonly network: NetworkDataSource;
73
- private readonly cache: SquareScreenCacheProvider;
74
- private readonly config: Required<
75
- Omit<SquareScreenPlayerConfig, "networkDataSource" | "cacheProvider">
76
- >;
77
-
78
- private playlist: Playlist | null = null;
79
- private stopped = false;
80
- private currentIndex = 0;
81
- private status: DeviceStatus = "connecting";
82
- private activeAlert: EmergencyAlert | null = null;
83
- private currentItemStartTime: number | null = null;
84
-
85
- private itemTimer: ReturnType<typeof setTimeout> | null = null;
86
- private playlistPollTimer: ReturnType<typeof setInterval> | null = null;
87
- private emergencyPollTimer: ReturnType<typeof setInterval> | null = null;
88
- private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
89
-
90
- constructor(config: SquareScreenPlayerConfig) {
91
- super();
92
-
93
- if (!config.networkDataSource) {
94
- if (!config.deviceId) throw new Error("SquareScreenPlayer: deviceId is required.");
95
- if (!config.deviceToken) throw new Error("SquareScreenPlayer: deviceToken is required.");
96
- }
97
-
98
- this.network =
99
- config.networkDataSource ??
100
- new NetworkClient(SQUARESCREEN_API_BASE_URL, config.deviceId, config.deviceToken);
101
-
102
- this.cache = config.cacheProvider ?? new SquareScreenCache();
103
-
104
- // eslint-disable-next-line @typescript-eslint/no-unused-vars -- omit injected deps from persisted config snapshot
105
- const { networkDataSource: _n, cacheProvider: _c, ...rest } = config;
106
- this.config = {
107
- ttl: 5 * 60 * 1000,
108
- pollInterval: 30_000,
109
- emergencyPollInterval: 15_000,
110
- heartbeatInterval: 60_000,
111
- ...rest,
112
- };
113
- }
114
-
115
- /** The playlist item currently being displayed, or null if no playlist is loaded. */
116
- get currentItem(): PlaylistItem | null {
117
- return this.playlist?.items[this.currentIndex] ?? null;
118
- }
119
-
120
- /**
121
- * Starts the player: fetches the playlist, begins playback, and starts polling
122
- * and heartbeat intervals. Safe to await — resolves once the first playlist load
123
- * attempt completes (whether from network or cache fallback).
124
- */
125
- async start(): Promise<void> {
126
- this.stopped = false;
127
- this.setStatus("connecting");
128
- await this.loadPlaylist();
129
- await this.checkEmergencyAlert();
130
- this.startPolling();
131
- this.startHeartbeat();
132
- }
133
-
134
- /** Stops all timers and clears internal state. Call this when tearing down the player. */
135
- stop(): void {
136
- if (this.itemTimer) clearTimeout(this.itemTimer);
137
- if (this.playlistPollTimer) clearInterval(this.playlistPollTimer);
138
- if (this.emergencyPollTimer) clearInterval(this.emergencyPollTimer);
139
- if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
140
- this.itemTimer = null;
141
- this.playlistPollTimer = null;
142
- this.emergencyPollTimer = null;
143
- this.heartbeatTimer = null;
144
- // Prevent in-flight loadPlaylist/applyPlaylist/scheduleItem continuations
145
- // from dispatching events or setting new timers after teardown.
146
- this.stopped = true;
147
- this.playlist = null;
148
- }
149
-
150
- // ---------------------------------------------------------------------------
151
- // Playlist loading
152
- // ---------------------------------------------------------------------------
153
-
154
- /**
155
- * Network-first playlist load. On success the playlist is saved to cache and
156
- * applied if the UUID changed. On failure the player falls back to the last
157
- * known playlist UUID stored in `localStorage`, loading it from cache with
158
- * `allowStale = true` so it is served even after its TTL has expired.
159
- */
160
- private async loadPlaylist(): Promise<void> {
161
- const result = await this.network.fetchPlaylist();
162
-
163
- if (result.success) {
164
- this.setStatus("online");
165
- const playlist = result.data;
166
- localStorage.setItem(LAST_PLAYLIST_KEY, playlist.uuid);
167
- await this.cache.savePlaylist(playlist, this.config.ttl);
168
-
169
- const isNewPlaylist = !this.playlist || this.playlist.uuid !== playlist.uuid;
170
- if (isNewPlaylist) {
171
- await this.applyPlaylist(playlist);
172
- } else {
173
- this.dispatch("playlistupdate", { playlist });
174
- }
175
- } else {
176
- this.setStatus("offline");
177
- if (!this.playlist) {
178
- const lastUuid = localStorage.getItem(LAST_PLAYLIST_KEY);
179
- if (lastUuid) {
180
- const cached = await this.cache.getPlaylist(lastUuid, true);
181
- if (cached) await this.applyPlaylist(cached);
182
- }
183
- }
184
- }
185
- }
186
-
187
- /**
188
- * Activates a playlist: optionally shuffles the items, resets the index to 0,
189
- * emits `playlistupdate`, and kicks off the first `scheduleItem` call.
190
- * Bails out immediately if `stop()` has been called.
191
- */
192
- private async applyPlaylist(playlist: Playlist): Promise<void> {
193
- if (this.stopped) return;
194
- const items = playlist.strategy?.shuffle
195
- ? [...playlist.items].sort(() => Math.random() - 0.5)
196
- : playlist.items;
197
-
198
- this.playlist = { ...playlist, items };
199
- this.currentIndex = 0;
200
- this.dispatch("playlistupdate", { playlist: this.playlist });
201
- this.scheduleItem();
202
- }
203
-
204
- refreshPlaylist(): void {
205
- this.loadPlaylist();
206
- }
207
-
208
- // ---------------------------------------------------------------------------
209
- // Playback
210
- // ---------------------------------------------------------------------------
211
-
212
- /**
213
- * Emits `itemchange` immediately with either the cached blob URL or the raw
214
- * HTTPS URL, then arms the advancement timer. Never blocks on a network fetch —
215
- * if the media isn't cached yet the browser loads it directly while
216
- * `preloadNext` / `downloadInBackground` cache it for the next cycle.
217
- *
218
- * Guards against `stop()` being called while awaiting the cache lookup.
219
- */
220
- private async scheduleItem(): Promise<void> {
221
- if (this.itemTimer) clearTimeout(this.itemTimer);
222
-
223
- const item = this.currentItem;
224
- if (!item || !this.playlist) return;
225
-
226
- this.currentItemStartTime = Date.now();
227
-
228
- // Cache lookup only — never fetch inline so the timer isn't delayed.
229
- const url = (await this.cache.getMediaUrl(item.url)) ?? item.url;
230
- if (!this.playlist) return; // stopped while awaiting
231
-
232
- this.dispatch("itemchange", {
233
- item: { ...item, url },
234
- index: this.currentIndex,
235
- total: this.playlist.items.length,
236
- });
237
-
238
- // If the current item wasn't in cache, download it now for the next cycle.
239
- if (url === item.url) this.downloadInBackground(item.url);
240
- this.preloadNext();
241
- const elapsed = Date.now() - this.currentItemStartTime!;
242
- const remaining = Math.max(0, item.duration * 1000 - elapsed);
243
- this.itemTimer = setTimeout(() => this.advance(), remaining);
244
- }
245
-
246
- /**
247
- * Schedules background downloads for upcoming items while the current one
248
- * is playing, so subsequent transitions can serve blob URLs immediately.
249
- *
250
- * - `preloadCount: 0` — disabled; nothing is downloaded.
251
- * - `preloadCount: N` — downloads the next N items ahead (wraps at end).
252
- * - `preloadCount` absent — downloads all items in the playlist up front.
253
- */
254
- private preloadNext(): void {
255
- if (!this.playlist) return;
256
- const preloadCount = this.playlist.strategy?.preloadCount;
257
- if (preloadCount === 0) return;
258
- const count = preloadCount ?? this.playlist.items.length;
259
-
260
- for (let i = 1; i <= count; i++) {
261
- const idx = (this.currentIndex + i) % this.playlist.items.length;
262
- this.downloadInBackground(this.playlist.items[idx].url);
263
- }
264
- }
265
-
266
- /**
267
- * Fire-and-forget download. Checks the cache first; if the media is already
268
- * present the call is a no-op. Errors are silently swallowed — a failed
269
- * download is not fatal; the raw URL will be used until the next successful download.
270
- */
271
- private downloadInBackground(url: string): void {
272
- this.cache
273
- .getMediaUrl(url)
274
- .then((cached) => {
275
- if (cached || !this.playlist) return;
276
- return fetch(url, { mode: "cors" })
277
- .then((r) => (r.ok ? r.blob() : Promise.reject(new Error(`HTTP ${r.status}`))))
278
- .then((blob) => this.cache.saveMedia(url, blob));
279
- })
280
- .catch(() => {});
281
- }
282
-
283
- /**
284
- * Moves to the next item. Reports the completed playback event, then either
285
- * wraps back to index 0 (when `loop` is true or absent) or halts when
286
- * `loop: false` and the last item has finished.
287
- */
288
- private advance(): void {
289
- if (!this.playlist) return;
290
- const total = this.playlist.items.length;
291
- const next = this.currentIndex + 1;
292
-
293
- this.reportPlaybackEvent();
294
-
295
- if (next >= total) {
296
- if (this.playlist.strategy?.loop ?? true) {
297
- this.currentIndex = 0;
298
- } else {
299
- return;
300
- }
301
- } else {
302
- this.currentIndex = next;
303
- }
304
-
305
- this.scheduleItem();
306
- }
307
-
308
- /** Sends a proof-of-play event to the server. Fire-and-forget; failures are not surfaced. */
309
- private async reportPlaybackEvent(): Promise<void> {
310
- if (!this.currentItemStartTime) return;
311
- if (!this.currentItem || !this.playlist) return;
312
- const endedAt = Date.now();
313
- const event: PlaybackEvent = {
314
- media_uuid: this.currentItem.uuid,
315
- playlist_uuid: this.playlist.uuid,
316
- schedule_uuid: this.playlist.schedule?.uuid ?? "",
317
- started_at: new Date(this.currentItemStartTime).toISOString(),
318
- ended_at: new Date(endedAt).toISOString(),
319
- duration_seconds: Math.round((endedAt - this.currentItemStartTime) / 1000),
320
- completed: true,
321
- };
322
- this.network.reportPlaybackEvent(event);
323
- }
324
-
325
- // ---------------------------------------------------------------------------
326
- // Polling and heartbeat
327
- // ---------------------------------------------------------------------------
328
-
329
- /** Arms the playlist-refresh and emergency-alert polling intervals. */
330
- private startPolling(): void {
331
- this.playlistPollTimer = setInterval(
332
- () => this.loadPlaylist(),
333
- this.config.pollInterval,
334
- );
335
- this.emergencyPollTimer = setInterval(
336
- () => this.checkEmergencyAlert(),
337
- this.config.emergencyPollInterval,
338
- );
339
- }
340
-
341
- /** Arms the periodic heartbeat that keeps the device registration alive. */
342
- private startHeartbeat(): void {
343
- this.heartbeatTimer = setInterval(async () => {
344
- await this.network.healthCheck({ playerVersion: this.config.version });
345
- }, this.config.heartbeatInterval);
346
- }
347
-
348
- /**
349
- * Polls the server for an active emergency alert. Emits `emergencyalert` only
350
- * when the state actually changes (alert activated, cleared, or replaced by a
351
- * different UUID) to avoid redundant re-renders on the consumer side.
352
- */
353
- private async checkEmergencyAlert(): Promise<void> {
354
- const result = await this.network.checkForEmergencyAlert();
355
- if (!result.success) return;
356
-
357
- const incoming = result.data;
358
- const wasActive = this.activeAlert !== null;
359
- const isActive = incoming !== null;
360
- const alertChanged = isActive && incoming!.uuid !== this.activeAlert?.uuid;
361
-
362
- if (wasActive !== isActive || alertChanged) {
363
- this.activeAlert = incoming;
364
- this.dispatch("emergencyalert", { alert: incoming });
365
- }
366
- }
367
-
368
- // ---------------------------------------------------------------------------
369
- // Helpers
370
- // ---------------------------------------------------------------------------
371
-
372
- private setStatus(status: DeviceStatus): void {
373
- if (this.status === status) return;
374
- this.status = status;
375
- this.dispatch("statuschange", { status });
376
- }
377
-
378
- private dispatch<K extends keyof PlayerEventMap>(
379
- type: K,
380
- detail: PlayerEventMap[K] extends CustomEvent<infer D> ? D : never,
381
- ): void {
382
- this.dispatchEvent(new CustomEvent(type, { detail }));
383
- }
384
-
385
- addEventListener<K extends keyof PlayerEventMap>(
386
- type: K,
387
- listener: (event: PlayerEventMap[K]) => void,
388
- options?: boolean | AddEventListenerOptions,
389
- ): void;
390
- addEventListener(
391
- type: string,
392
- listener: EventListenerOrEventListenerObject,
393
- options?: boolean | AddEventListenerOptions,
394
- ): void;
395
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
396
- addEventListener(type: string, listener: any, options?: any): void {
397
- super.addEventListener(type, listener, options);
398
- }
399
-
400
- removeEventListener<K extends keyof PlayerEventMap>(
401
- type: K,
402
- listener: (event: PlayerEventMap[K]) => void,
403
- options?: boolean | EventListenerOptions,
404
- ): void;
405
- removeEventListener(
406
- type: string,
407
- listener: EventListenerOrEventListenerObject,
408
- options?: boolean | EventListenerOptions,
409
- ): void;
410
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
411
- removeEventListener(type: string, listener: any, options?: any): void {
412
- super.removeEventListener(type, listener, options);
413
- }
414
- }