summaryrefslogtreecommitdiffhomepage
path: root/packages/cache-warming/src/pure.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-11 13:08:38 +0900
committerAdam Malczewski <[email protected]>2026-06-11 13:08:38 +0900
commitffbbcf692a97ec8648af39353b49f32896367207 (patch)
tree2e2ddfd03d4a868f4a4ba12e20586cc03c37f90a /packages/cache-warming/src/pure.ts
parent27fd0be36b2f6395249de5aacc86e41fe4e0207f (diff)
downloaddispatch-ffbbcf692a97ec8648af39353b49f32896367207.tar.gz
dispatch-ffbbcf692a97ec8648af39353b49f32896367207.zip
feat(surfaces): NumberField + per-conversation surface scoping; cache-warming controls
Extend the surface framework so cache-warming exposes per-conversation controls: - ui-contract: add NumberField (settable free-value numeric) to SurfaceField; add optional conversationId to subscribe/unsubscribe/invoke + surface/update - surface-registry: SurfaceContext { conversationId? } on getSpec/invoke (backward-compatible) - transport-ws: thread conversationId; key subscriptions by (surfaceId, conversationId); tag surface/update replies with conversationId - cache-warming: per-conversation surface — Toggle(enabled) + Number(interval seconds, cache-warming/set-interval) + Stat(last cache %); drop the currentConversationId closure Global surfaces (surface-loaded-extensions) unchanged. 784 vitest + 109 bun = 893 tests; tsc -b EXIT 0; biome clean.
Diffstat (limited to 'packages/cache-warming/src/pure.ts')
-rw-r--r--packages/cache-warming/src/pure.ts96
1 files changed, 96 insertions, 0 deletions
diff --git a/packages/cache-warming/src/pure.ts b/packages/cache-warming/src/pure.ts
index 2b00dab..7b91b11 100644
--- a/packages/cache-warming/src/pure.ts
+++ b/packages/cache-warming/src/pure.ts
@@ -3,6 +3,8 @@
* Every function is input → output; testable without mocks.
*/
+import type { NumberField, StatField, SurfaceSpec, ToggleField } from "@dispatch/ui-contract";
+
// --- Types ---
/** Persisted per-conversation settings (storage-facing). */
@@ -93,3 +95,97 @@ export function serializeSettings(settings: ConversationSettings): string {
export function settingsKey(conversationId: string): string {
return `${SETTINGS_KEY}:${conversationId}`;
}
+
+// --- Surface spec builders (pure) ---
+
+/** Convert intervalMs to display seconds (rounded). */
+export function msToSeconds(intervalMs: number): number {
+ return Math.round(intervalMs / 1000);
+}
+
+/**
+ * Convert seconds (from the UI) to intervalMs, flooring at MIN_INTERVAL_MS.
+ * Returns null for NaN / non-positive (caller should ignore).
+ */
+export function secondsToMs(seconds: number): number | null {
+ if (!Number.isFinite(seconds) || seconds <= 0) return null;
+ return Math.max(MIN_INTERVAL_MS, Math.round(seconds * 1000));
+}
+
+/**
+ * Build a per-conversation surface spec with toggle + number(interval) + stat fields.
+ * Pure — no I/O.
+ */
+export function buildConversationSpec(
+ enabled: boolean,
+ intervalMs: number,
+ lastPct: number | null,
+): SurfaceSpec {
+ const pctDisplay = lastPct === null ? "—" : `${lastPct}%`;
+ const toggle: ToggleField = {
+ kind: "toggle",
+ label: "Enabled",
+ value: enabled,
+ action: { actionId: "cache-warming/toggle" },
+ };
+ const interval: NumberField = {
+ kind: "number",
+ label: "Refresh Interval",
+ value: msToSeconds(intervalMs),
+ min: 1,
+ step: 1,
+ unit: "s",
+ action: { actionId: "cache-warming/set-interval" },
+ };
+ const stat: StatField = {
+ kind: "stat",
+ label: "Last Cache %",
+ value: pctDisplay,
+ };
+ return {
+ id: "cache-warming",
+ region: "side",
+ title: "Cache Warming",
+ fields: [toggle, interval, stat],
+ };
+}
+
+/**
+ * Build a default surface spec when no conversation is in focus.
+ * Pure — no I/O.
+ */
+export function buildDefaultSpec(): SurfaceSpec {
+ return {
+ id: "cache-warming",
+ region: "side",
+ title: "Cache Warming",
+ fields: [
+ {
+ kind: "stat",
+ label: "Status",
+ value: "No conversation focused",
+ },
+ ],
+ };
+}
+
+/**
+ * Parse the payload for a set-interval action.
+ * Accepts a bare number OR { value: number }. Returns the seconds value, or
+ * null if the payload is invalid (NaN / non-positive / wrong shape).
+ */
+export function parseIntervalPayload(payload: unknown): number | null {
+ if (typeof payload === "number" && Number.isFinite(payload) && payload > 0) {
+ return payload;
+ }
+ if (
+ typeof payload === "object" &&
+ payload !== null &&
+ "value" in payload &&
+ typeof (payload as Record<string, unknown>).value === "number"
+ ) {
+ const v = (payload as Record<string, unknown>).value as number;
+ if (Number.isFinite(v) && v > 0) return v;
+ }
+ return null;
+}