All files / src/services liveTimingService.ts

0% Statements 0/16
0% Branches 0/15
0% Functions 0/4
0% Lines 0/15

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85                                                                                                                                                                         
/**
 * Live Timing Service
 * Story 15.1: REST polling replacement for the STOMP live-agenda surface.
 *
 * - getLiveTiming polls GET /events/{eventCode}/live-timing with If-None-Match so an
 *   unchanged snapshot costs a 304 (no body). Returns the ETag for the next poll.
 * - postLiveTimingAction POSTs an END/EXTEND/DELAY action and returns the post-cascade
 *   snapshot with the bumped version.
 *
 * The organizer live-control hook calls these authenticated (apiClient adds the bearer
 * token), which also refreshes server-side presence. The public presenter passes
 * skipAuth so it polls anonymously.
 */
 
import apiClient from './api/apiClient';
import type { components } from '@/types/generated/event-watch-api.types';
 
export type LiveTimingResponse = components['schemas']['LiveTimingResponse'];
export type LiveTimingActionRequest = components['schemas']['LiveTimingActionRequest'];
 
export interface LiveTimingPollResult {
  /** 200 when the snapshot changed (data present), 304 when unchanged (data null). */
  status: 200 | 304;
  data: LiveTimingResponse | null;
  /** ETag to send back as If-None-Match on the next poll. */
  etag: string | null;
}
 
export interface LiveTimingActionResult {
  data: LiveTimingResponse;
  etag: string | null;
}
 
const LIVE_TIMING_PATH = (eventCode: string) => `/events/${eventCode}/live-timing`;
 
export const liveTimingService = {
  /**
   * Polls the live-timing snapshot. When `etag` is supplied it is sent as If-None-Match;
   * an unchanged version yields a 304 (status 304, data null).
   *
   * @param eventCode the event to poll
   * @param etag      ETag from the previous poll, or null on first poll
   * @param skipAuth  true for the public presenter (anonymous poll; no presence heartbeat)
   */
  async getLiveTiming(
    eventCode: string,
    etag?: string | null,
    skipAuth = false
  ): Promise<LiveTimingPollResult> {
    const headers: Record<string, string> = {};
    if (etag) {
      headers['If-None-Match'] = etag;
    }
    if (skipAuth) {
      headers['Skip-Auth'] = 'true';
    }
    const res = await apiClient.get<LiveTimingResponse>(LIVE_TIMING_PATH(eventCode), {
      headers,
      validateStatus: (s) => s === 200 || s === 304,
    });
    const responseEtag = (res.headers?.etag as string | undefined) ?? null;
    if (res.status === 304) {
      return { status: 304, data: null, etag: etag ?? responseEtag };
    }
    return { status: 200, data: res.data, etag: responseEtag };
  },
 
  /**
   * Applies a timing action and returns the post-cascade snapshot with the bumped version.
   *
   * @param eventCode the event
   * @param request   the END/EXTEND/DELAY action
   */
  async postLiveTimingAction(
    eventCode: string,
    request: LiveTimingActionRequest
  ): Promise<LiveTimingActionResult> {
    const res = await apiClient.post<LiveTimingResponse>(
      `${LIVE_TIMING_PATH(eventCode)}/actions`,
      request
    );
    return { data: res.data, etag: (res.headers?.etag as string | undefined) ?? null };
  },
};