diff options
| author | Adam Malczewski <[email protected]> | 2026-06-11 13:08:38 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-11 13:08:38 +0900 |
| commit | ffbbcf692a97ec8648af39353b49f32896367207 (patch) | |
| tree | 2e2ddfd03d4a868f4a4ba12e20586cc03c37f90a /packages/cache-warming/src/pure.ts | |
| parent | 27fd0be36b2f6395249de5aacc86e41fe4e0207f (diff) | |
| download | dispatch-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.ts | 96 |
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; +} |
