summaryrefslogtreecommitdiffhomepage
path: root/src/features/cache-warming/logic/view-model.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/cache-warming/logic/view-model.ts')
-rw-r--r--src/features/cache-warming/logic/view-model.ts244
1 files changed, 122 insertions, 122 deletions
diff --git a/src/features/cache-warming/logic/view-model.ts b/src/features/cache-warming/logic/view-model.ts
index eb105f6..bc8cf2e 100644
--- a/src/features/cache-warming/logic/view-model.ts
+++ b/src/features/cache-warming/logic/view-model.ts
@@ -15,37 +15,37 @@ import type { SurfaceSpec } from "@dispatch/ui-contract";
// ── Manual-warm port (consumer-defines-port; the composition root adapts the
// store's `POST /chat/warm` result to this shape). ──────────────────────────
export type WarmFeedback =
- | { readonly ok: true; readonly cachePct: number; readonly expectedCacheRate: number }
- | { readonly ok: false; readonly error: string };
+ | { readonly ok: true; readonly cachePct: number; readonly expectedCacheRate: number }
+ | { readonly ok: false; readonly error: string };
export type WarmNow = () => Promise<WarmFeedback | null>;
// ── Parsed surface controls ───────────────────────────────────────────────────
export interface ParsedControls {
- readonly enabled: boolean;
- readonly toggleActionId: string | null;
- readonly intervalSeconds: number;
- readonly setIntervalActionId: string | null;
- /** Most recent warm's cache-hit %, from the "last cache rate" stat (`null` when "—"/absent). */
- readonly lastPct: number | null;
- /** Cross-turn retention %, from the "cache retention" stat (`null` when "—"/absent). */
- readonly retentionPct: number | null;
- /** Authoritative epoch-ms the next AUTOMATIC warm fires, or `null` when not scheduled. */
- readonly nextWarmAt: number | null;
- /** Authoritative epoch-ms of the most recent completed warm, or `null` if none. */
- readonly lastWarmAt: number | null;
+ readonly enabled: boolean;
+ readonly toggleActionId: string | null;
+ readonly intervalSeconds: number;
+ readonly setIntervalActionId: string | null;
+ /** Most recent warm's cache-hit %, from the "last cache rate" stat (`null` when "—"/absent). */
+ readonly lastPct: number | null;
+ /** Cross-turn retention %, from the "cache retention" stat (`null` when "—"/absent). */
+ readonly retentionPct: number | null;
+ /** Authoritative epoch-ms the next AUTOMATIC warm fires, or `null` when not scheduled. */
+ readonly nextWarmAt: number | null;
+ /** Authoritative epoch-ms of the most recent completed warm, or `null` if none. */
+ readonly lastWarmAt: number | null;
}
const EMPTY_CONTROLS: ParsedControls = {
- enabled: false,
- toggleActionId: null,
- intervalSeconds: 0,
- setIntervalActionId: null,
- lastPct: null,
- retentionPct: null,
- nextWarmAt: null,
- lastWarmAt: null,
+ enabled: false,
+ toggleActionId: null,
+ intervalSeconds: 0,
+ setIntervalActionId: null,
+ lastPct: null,
+ retentionPct: null,
+ nextWarmAt: null,
+ lastWarmAt: null,
};
/** The `cache-warming-timer` custom field's renderer id (this feature owns it). */
@@ -53,24 +53,24 @@ const TIMER_RENDERER_ID = "cache-warming-timer";
/** Parse a stat's display string (e.g. "100%", "93 %", "—") into a number or null. */
export function parsePct(value: string): number | null {
- const match = value.match(/-?\d+(?:\.\d+)?/);
- if (match === null) return null;
- const n = Number(match[0]);
- return Number.isFinite(n) ? n : null;
+ const match = value.match(/-?\d+(?:\.\d+)?/);
+ if (match === null) return null;
+ const n = Number(match[0]);
+ return Number.isFinite(n) ? n : null;
}
/** A finite number, else null. */
function numOrNull(v: unknown): number | null {
- return typeof v === "number" && Number.isFinite(v) ? v : null;
+ return typeof v === "number" && Number.isFinite(v) ? v : null;
}
/** Pull the authoritative `nextWarmAt`/`lastWarmAt` out of the timer custom payload. */
function parseTimer(payload: unknown): { nextWarmAt: number | null; lastWarmAt: number | null } {
- if (typeof payload !== "object" || payload === null) {
- return { nextWarmAt: null, lastWarmAt: null };
- }
- const p = payload as Record<string, unknown>;
- return { nextWarmAt: numOrNull(p.nextWarmAt), lastWarmAt: numOrNull(p.lastWarmAt) };
+ if (typeof payload !== "object" || payload === null) {
+ return { nextWarmAt: null, lastWarmAt: null };
+ }
+ const p = payload as Record<string, unknown>;
+ return { nextWarmAt: numOrNull(p.nextWarmAt), lastWarmAt: numOrNull(p.lastWarmAt) };
}
/**
@@ -80,79 +80,79 @@ function parseTimer(payload: unknown): { nextWarmAt: number | null; lastWarmAt:
* absent.
*/
export function parseControls(spec: SurfaceSpec | null): ParsedControls {
- if (spec === null) return EMPTY_CONTROLS;
- let enabled = false;
- let toggleActionId: string | null = null;
- let intervalSeconds = 0;
- let setIntervalActionId: string | null = null;
- let lastPct: number | null = null;
- let retentionPct: number | null = null;
- let nextWarmAt: number | null = null;
- let lastWarmAt: number | null = null;
- let seenToggle = false;
- let seenNumber = false;
- let seenRateStat = false;
- for (const field of spec.fields) {
- if (field.kind === "toggle" && !seenToggle) {
- enabled = field.value;
- toggleActionId = field.action.actionId;
- seenToggle = true;
- } else if (field.kind === "number" && !seenNumber) {
- intervalSeconds = field.value;
- setIntervalActionId = field.action.actionId;
- seenNumber = true;
- } else if (field.kind === "stat") {
- // Retention is told apart by its label; everything else is the cache rate
- // (first one wins, so a stray later stat can't clobber it).
- if (/retention/i.test(field.label)) {
- retentionPct = parsePct(field.value);
- } else if (!seenRateStat) {
- lastPct = parsePct(field.value);
- seenRateStat = true;
- }
- } else if (field.kind === "custom" && field.rendererId === TIMER_RENDERER_ID) {
- const timer = parseTimer(field.payload);
- nextWarmAt = timer.nextWarmAt;
- lastWarmAt = timer.lastWarmAt;
- }
- }
- return {
- enabled,
- toggleActionId,
- intervalSeconds,
- setIntervalActionId,
- lastPct,
- retentionPct,
- nextWarmAt,
- lastWarmAt,
- };
+ if (spec === null) return EMPTY_CONTROLS;
+ let enabled = false;
+ let toggleActionId: string | null = null;
+ let intervalSeconds = 0;
+ let setIntervalActionId: string | null = null;
+ let lastPct: number | null = null;
+ let retentionPct: number | null = null;
+ let nextWarmAt: number | null = null;
+ let lastWarmAt: number | null = null;
+ let seenToggle = false;
+ let seenNumber = false;
+ let seenRateStat = false;
+ for (const field of spec.fields) {
+ if (field.kind === "toggle" && !seenToggle) {
+ enabled = field.value;
+ toggleActionId = field.action.actionId;
+ seenToggle = true;
+ } else if (field.kind === "number" && !seenNumber) {
+ intervalSeconds = field.value;
+ setIntervalActionId = field.action.actionId;
+ seenNumber = true;
+ } else if (field.kind === "stat") {
+ // Retention is told apart by its label; everything else is the cache rate
+ // (first one wins, so a stray later stat can't clobber it).
+ if (/retention/i.test(field.label)) {
+ retentionPct = parsePct(field.value);
+ } else if (!seenRateStat) {
+ lastPct = parsePct(field.value);
+ seenRateStat = true;
+ }
+ } else if (field.kind === "custom" && field.rendererId === TIMER_RENDERER_ID) {
+ const timer = parseTimer(field.payload);
+ nextWarmAt = timer.nextWarmAt;
+ lastWarmAt = timer.lastWarmAt;
+ }
+ }
+ return {
+ enabled,
+ toggleActionId,
+ intervalSeconds,
+ setIntervalActionId,
+ lastPct,
+ retentionPct,
+ nextWarmAt,
+ lastWarmAt,
+ };
}
// ── Interval ↔ minutes/seconds (seconds capped at 59) ─────────────────────────
export interface MinSec {
- readonly minutes: number;
- readonly seconds: number;
+ readonly minutes: number;
+ readonly seconds: number;
}
export function clampSeconds(n: number): number {
- if (!Number.isFinite(n)) return 0;
- return Math.min(59, Math.max(0, Math.floor(n)));
+ if (!Number.isFinite(n)) return 0;
+ return Math.min(59, Math.max(0, Math.floor(n)));
}
export function clampMinutes(n: number): number {
- if (!Number.isFinite(n)) return 0;
- return Math.max(0, Math.floor(n));
+ if (!Number.isFinite(n)) return 0;
+ return Math.max(0, Math.floor(n));
}
export function toMinSec(totalSeconds: number): MinSec {
- const total = Math.max(0, Math.floor(totalSeconds));
- return { minutes: Math.floor(total / 60), seconds: total % 60 };
+ const total = Math.max(0, Math.floor(totalSeconds));
+ return { minutes: Math.floor(total / 60), seconds: total % 60 };
}
/** Combine a minutes + seconds pair (each clamped) into total seconds. */
export function fromMinSec(minutes: number, seconds: number): number {
- return clampMinutes(minutes) * 60 + clampSeconds(seconds);
+ return clampMinutes(minutes) * 60 + clampSeconds(seconds);
}
// ── Status + formatting ───────────────────────────────────────────────────────
@@ -161,56 +161,56 @@ export type WarmStatus = "success" | "warning" | "error";
/** Cache-hit % → semantic status (green high, yellow mid, red low). */
export function statusForPct(pct: number): WarmStatus {
- if (pct >= 80) return "success";
- if (pct >= 40) return "warning";
- return "error";
+ if (pct >= 80) return "success";
+ if (pct >= 40) return "warning";
+ return "error";
}
/** A status → its DaisyUI text-colour class (full literal so Tailwind keeps it). */
export function colorClass(status: WarmStatus): string {
- switch (status) {
- case "success":
- return "text-success";
- case "warning":
- return "text-warning";
- case "error":
- return "text-error";
- }
+ switch (status) {
+ case "success":
+ return "text-success";
+ case "warning":
+ return "text-warning";
+ case "error":
+ return "text-error";
+ }
}
/** The status line for a warm, matching the manual-warm feedback phrasing. */
export function formatWarmLabel(pct: number): string {
- return `Warmed — ${Math.round(pct)}% cache hit`;
+ return `Warmed — ${Math.round(pct)}% cache hit`;
}
/** Seconds → a short countdown string (e.g. "3:05", "9s"). */
export function formatCountdown(seconds: number): string {
- const s = Math.max(0, Math.floor(seconds));
- if (s < 60) return `${s}s`;
- const m = Math.floor(s / 60);
- const rem = s % 60;
- return `${m}:${String(rem).padStart(2, "0")}`;
+ const s = Math.max(0, Math.floor(seconds));
+ if (s < 60) return `${s}s`;
+ const m = Math.floor(s / 60);
+ const rem = s % 60;
+ return `${m}:${String(rem).padStart(2, "0")}`;
}
// ── Warming history reducer (keyed off the authoritative `lastWarmAt`) ─────────
export interface WarmEntry {
- readonly pct: number;
- /** Authoritative epoch-ms of this warm (the surface's `lastWarmAt`). */
- readonly at: number;
+ readonly pct: number;
+ /** Authoritative epoch-ms of this warm (the surface's `lastWarmAt`). */
+ readonly at: number;
}
export interface WarmingViewState {
- /** Warmings, MOST RECENT FIRST. */
- readonly history: readonly WarmEntry[];
- /** The last authoritative `lastWarmAt` recorded, for change-detection (de-dup key). */
- readonly lastWarmAt: number | null;
+ /** Warmings, MOST RECENT FIRST. */
+ readonly history: readonly WarmEntry[];
+ /** The last authoritative `lastWarmAt` recorded, for change-detection (de-dup key). */
+ readonly lastWarmAt: number | null;
}
const MAX_HISTORY = 50;
export function initialWarmingState(): WarmingViewState {
- return { history: [], lastWarmAt: null };
+ return { history: [], lastWarmAt: null };
}
/**
@@ -221,14 +221,14 @@ export function initialWarmingState(): WarmingViewState {
* ignored; a null pct advances the de-dup key without adding an entry.
*/
export function observeWarm(
- state: WarmingViewState,
- lastWarmAt: number | null,
- pct: number | null,
+ state: WarmingViewState,
+ lastWarmAt: number | null,
+ pct: number | null,
): WarmingViewState {
- if (lastWarmAt === null || lastWarmAt === state.lastWarmAt) return state;
- if (pct === null) return { ...state, lastWarmAt };
- const history = [{ pct, at: lastWarmAt }, ...state.history].slice(0, MAX_HISTORY);
- return { history, lastWarmAt };
+ if (lastWarmAt === null || lastWarmAt === state.lastWarmAt) return state;
+ if (pct === null) return { ...state, lastWarmAt };
+ const history = [{ pct, at: lastWarmAt }, ...state.history].slice(0, MAX_HISTORY);
+ return { history, lastWarmAt };
}
/**
@@ -248,7 +248,7 @@ const STALE_NEXT_WARM_MS = 3000;
* when `nextWarmAt` is stale (further than the grace into the past).
*/
export function secondsUntilNext(nextWarmAt: number | null, now: number): number | null {
- if (nextWarmAt === null) return null;
- if (now - nextWarmAt > STALE_NEXT_WARM_MS) return null;
- return Math.max(0, Math.ceil((nextWarmAt - now) / 1000));
+ if (nextWarmAt === null) return null;
+ if (now - nextWarmAt > STALE_NEXT_WARM_MS) return null;
+ return Math.max(0, Math.ceil((nextWarmAt - now) / 1000));
}