@antcdn/live-signaling 0.1.0 → 0.1.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.
package/dist/client.d.ts CHANGED
@@ -38,6 +38,17 @@ export interface LiveRoomEvents {
38
38
  connected: boolean;
39
39
  reconnecting: boolean;
40
40
  };
41
+ /**
42
+ * A cued question arrived and is being held until the video reports its cue.
43
+ *
44
+ * Emitted so that waiting is visible. A question held forever because no cue ever
45
+ * came looks exactly like a question that was never asked, and that is the single
46
+ * hardest state to diagnose from outside.
47
+ */
48
+ pending: {
49
+ question: LiveQuestion;
50
+ cueTimeoutMs: number;
51
+ };
41
52
  }
42
53
  type Listener<K extends keyof LiveRoomEvents> = (payload: LiveRoomEvents[K]) => void;
43
54
  export interface LiveRoomOptions {
@@ -54,6 +65,18 @@ export interface LiveRoomOptions {
54
65
  * reintroduces exactly the early-reveal problem cues exist to prevent.
55
66
  */
56
67
  revealImmediately?: boolean;
68
+ /**
69
+ * How long to wait for a question's in-band cue before revealing it anyway, in
70
+ * milliseconds. Zero waits forever.
71
+ *
72
+ * The cue is what makes a question appear at the moment it was asked, so waiting is
73
+ * the whole point. But some players never surface in-band metadata at all, and there
74
+ * the choice is between a question that arrives slightly early and one that never
75
+ * arrives. Late and visible beats perfectly timed and invisible.
76
+ *
77
+ * Defaults to 30 seconds, which is beyond any plausible live delay.
78
+ */
79
+ cueTimeoutMs?: number;
57
80
  /** Injectable for tests and non-browser runtimes. */
58
81
  webSocketFactory?: (url: string) => WebSocket;
59
82
  }
@@ -68,6 +91,8 @@ export declare class LiveRoom {
68
91
  private earlyCues;
69
92
  /** Revealed already, so a duplicate cue cannot fire the event twice. */
70
93
  private revealed;
94
+ /** Fallback reveals for held questions, cancelled when the real cue arrives. */
95
+ private cueTimers;
71
96
  private closedByCaller;
72
97
  private reconnectAttempts;
73
98
  private reconnectTimer;
package/dist/client.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { PROTOCOL_VERSION, } from './protocol.js';
2
2
  import { resolveSessionId } from './session.js';
3
3
  const RECONNECT_BASE_MS = 500;
4
+ /** See LiveRoomOptions.cueTimeoutMs. */
5
+ const DEFAULT_CUE_TIMEOUT_MS = 30_000;
4
6
  const RECONNECT_MAX_MS = 15000;
5
7
  export class LiveRoom {
6
8
  options;
@@ -13,6 +15,8 @@ export class LiveRoom {
13
15
  earlyCues = new Set();
14
16
  /** Revealed already, so a duplicate cue cannot fire the event twice. */
15
17
  revealed = new Set();
18
+ /** Fallback reveals for held questions, cancelled when the real cue arrives. */
19
+ cueTimers = new Map();
16
20
  closedByCaller = false;
17
21
  reconnectAttempts = 0;
18
22
  reconnectTimer = null;
@@ -76,6 +80,9 @@ export class LiveRoom {
76
80
  }
77
81
  close() {
78
82
  this.closedByCaller = true;
83
+ for (const timer of this.cueTimers.values())
84
+ clearTimeout(timer);
85
+ this.cueTimers.clear();
79
86
  if (this.reconnectTimer)
80
87
  clearTimeout(this.reconnectTimer);
81
88
  this.reconnectTimer = null;
@@ -205,9 +212,32 @@ export class LiveRoom {
205
212
  return;
206
213
  }
207
214
  this.pending.set(question.id, question);
215
+ const timeout = this.options.cueTimeoutMs ?? DEFAULT_CUE_TIMEOUT_MS;
216
+ this.emit('pending', { question, cueTimeoutMs: timeout });
217
+ if (timeout > 0) {
218
+ const timer = setTimeout(() => {
219
+ this.cueTimers.delete(question.id);
220
+ // Still held, so no cue ever arrived for it.
221
+ if (!this.pending.has(question.id))
222
+ return;
223
+ this.pending.delete(question.id);
224
+ this.emit('error', {
225
+ code: 'cue_timeout',
226
+ message: `No in-band cue arrived for ${question.id} within ${timeout}ms, so it is ` +
227
+ 'being shown now. It will be slightly ahead of the moment it was asked.',
228
+ });
229
+ this.reveal(question);
230
+ }, timeout);
231
+ this.cueTimers.set(question.id, timer);
232
+ }
208
233
  }
209
234
  reveal(question) {
210
235
  this.revealed.add(question.id);
236
+ const timer = this.cueTimers.get(question.id);
237
+ if (timer) {
238
+ clearTimeout(timer);
239
+ this.cueTimers.delete(question.id);
240
+ }
211
241
  this.emit('question', question);
212
242
  }
213
243
  emit(event, payload) {
@@ -42,7 +42,7 @@ export interface LiveTally {
42
42
  */
43
43
  textCount?: number;
44
44
  }
45
- export type ServerErrorCode = 'bad_message' | 'no_active_question' | 'already_voted' | 'unknown_option' | 'text_not_allowed' | 'text_too_long';
45
+ export type ServerErrorCode = 'bad_message' | 'no_active_question' | 'already_voted' | 'unknown_option' | 'text_not_allowed' | 'text_too_long' | 'cue_timeout';
46
46
  export interface WelcomeMessage {
47
47
  v: number;
48
48
  type: 'welcome';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antcdn/live-signaling",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Interactivity for AntCDN live streams and recordings: viewer count, questions, and voting, independent of any video player.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",