PageSourceSearch

https://15half.com/assets/channel-show-priority.js?v=20260820-experience-first-prefetch-v1

js 15half.com collected 2026-10-09 22:39:17 UTC 4,817 bytes, 159 lines download raw bytes

1export const CHANNEL_SHOW_PRELOAD_LEAD_MS = 180_000;
2export const CHANNEL_SHOW_ACTIVE_WINDOW_MS = 15 * 60_000;
3
4const SHOW_KIND_BY_PROGRAM = new Map([
5  ["hamoach", "quiz"],
6  ["quiz", "quiz"],
7  ["marketplace-price", "marketplace"],
8  ["price", "marketplace"],
9  ["daily-news", "news"],
10  ["news", "news"],
11  ["music-battle", "music"],
12  ["music", "music"],
13  ["songs-quiz", "songs"],
14  ["songs", "songs"],
15  ["film-ghosts", "ghosts"],
16  ["ghosts", "ghosts"],
17]);
18
19const INACTIVE_QUIZ_STATES = new Set(["off_air", "ended"]);
20
21function timestamp(value) {
22  const parsed = Date.parse(String(value ?? ""));
23  return Number.isFinite(parsed) ? parsed : NaN;
24}
25
26function showKind(slot) {
27  return SHOW_KIND_BY_PROGRAM.get(
28    String(slot?.programKey ?? slot?.program_key ?? "").trim().toLowerCase(),
29  ) ?? null;
30}
31
32export function normalizeChannelSchedule(schedule = []) {
33  const unique = new Map();
34  for (const slot of Array.isArray(schedule) ? schedule : []) {
35    const kind = showKind(slot);
36    const startsAtMs = timestamp(slot?.startsAt ?? slot?.starts_at);
37    if (!kind || !Number.isFinite(startsAtMs)) continue;
38    const declaredEndsAtMs = timestamp(slot?.endsAt ?? slot?.ends_at);
39    const key = `${kind}:${startsAtMs}`;
40    const candidate = {
41      id: String(slot?.id ?? key),
42      kind,
43      programKey: String(slot?.programKey ?? slot?.program_key ?? "")
44        .trim()
45        .toLowerCase(),
46      startsAtMs,
47      declaredEndsAtMs,
48    };
49    const existing = unique.get(key);
50    if (
51      !existing ||
52      (Number.isFinite(declaredEndsAtMs) &&
53        (!Number.isFinite(existing.declaredEndsAtMs) ||
54          declaredEndsAtMs < existing.declaredEndsAtMs))
55    ) {
56      unique.set(key, candidate);
57    }
58  }
59  return [...unique.values()].sort((left, right) =>
60    left.startsAtMs - right.startsAtMs || left.kind.localeCompare(right.kind)
61  );
62}
63
64function activeUntil(slots, index) {
65  const slot = slots[index];
66  const nextStart = slots.find((candidate, candidateIndex) =>
67    candidateIndex > index && candidate.startsAtMs > slot.startsAtMs
68  )?.startsAtMs;
69  const declaredOrFallback = Number.isFinite(slot.declaredEndsAtMs) &&
70      slot.declaredEndsAtMs > slot.startsAtMs
71    ? slot.declaredEndsAtMs
72    : slot.startsAtMs + CHANNEL_SHOW_ACTIVE_WINDOW_MS;
73  return Math.min(
74    declaredOrFallback,
75    Number.isFinite(nextStart) ? nextStart : Number.POSITIVE_INFINITY,
76  );
77}
78
79function authoritativeQuiz(bootstrap, nowMs) {
80  const show = bootstrap?.show ?? null;
81  const state = String(show?.state ?? "").trim().toLowerCase();
82  if (
83    !show ||
84    !state ||
85    INACTIVE_QUIZ_STATES.has(state) ||
86    String(show?.runId ?? show?.run_id ?? "") === "off-air"
87  ) return null;
88
89  const startsAtMs = timestamp(
90    show?.startsAt ?? show?.starts_at ?? show?.scheduledAt ??
91      show?.scheduled_at,
92  );
93  if (
94    state === "scheduled" && Number.isFinite(startsAtMs) && startsAtMs > nowMs
95  ) {
96    return null;
97  }
98  return {
99    id: String(show?.runId ?? show?.run_id ?? "live-quiz"),
100    kind: "quiz",
101    programKey: String(
102      show?.programKey ?? show?.program_key ?? show?.publicPayload?.programKey ?? "hamoach",
103    ).trim().toLowerCase(),
104    startsAtMs: Number.isFinite(startsAtMs) ? startsAtMs : nowMs,
105    activeUntilMs: nowMs + CHANNEL_SHOW_ACTIVE_WINDOW_MS,
106    source: "authoritative-live-state",
107  };
108}
109
110export function buildChannelShowLoadPlan(
111  bootstrap,
112  nowMs = Date.now(),
113  preloadLeadMs = CHANNEL_SHOW_PRELOAD_LEAD_MS,
114) {
115  const slots = normalizeChannelSchedule(bootstrap?.schedule);
116  let active = authoritativeQuiz(bootstrap, nowMs);
117
118  if (!active) {
119    for (let index = slots.length - 1; index >= 0; index -= 1) {
120      const slot = slots[index];
121      const activeUntilMs = activeUntil(slots, index);
122      if (slot.startsAtMs <= nowMs && nowMs < activeUntilMs) {
123        active = { ...slot, activeUntilMs, source: "schedule" };
124        break;
125      }
126    }
127  }
128
129  const next = slots.find((slot) => slot.startsAtMs > nowMs) ?? null;
130  const upcoming = next && next.startsAtMs - nowMs <= preloadLeadMs
131    ? { ...next, source: "upcoming" }
132    : null;
133  const immediate = [];
134  if (active) immediate.push({ ...active, phase: "active" });
135  if (
136    upcoming && !immediate.some((candidate) => candidate.kind === upcoming.kind)
137  ) {
138    immediate.push({ ...upcoming, phase: "upcoming" });
139  }
140
141  const reevaluationCandidates = [
142    active?.activeUntilMs,
143    next && next.startsAtMs - preloadLeadMs > nowMs
144      ? next.startsAtMs - preloadLeadMs
145      : null,
146    upcoming?.startsAtMs,
147  ].filter((value) => Number.isFinite(value) && value > nowMs + 250);
148
149  return {
150    nowMs,
151    active,
152    next,
153    upcoming,
154    immediate,
155    reevaluateAtMs: reevaluationCandidates.length
156      ? Math.min(...reevaluationCandidates)
157      : null,
158  };
159}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.