export interface RealtimeRefreshScope { projectId?: string; projectIds?: string[]; deviceId?: string; deviceIds?: string[]; conversationUpdatedNotes?: string[]; } export interface RefreshThrottleState { lastRefreshAt: number | null; pending: boolean; } export type RefreshThrottleDecision = | { type: "refresh_now" } | { type: "schedule_refresh"; delayMs: number } | { type: "skip" }; interface RealtimeEventPayload { projectId?: string; deviceId?: string; note?: string; } function parseRealtimeEventPayload(eventData: unknown): RealtimeEventPayload | null { if (typeof eventData !== "string" || !eventData.trim()) { return null; } try { const parsed = JSON.parse(eventData) as RealtimeEventPayload; if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { return null; } return parsed; } catch { return null; } } export function shouldRefreshRealtimeEvent(input: { eventType: string; eventData?: unknown; } & RealtimeRefreshScope) { const payload = parseRealtimeEventPayload(input.eventData); const projectScopeIds = new Set( [input.projectId, ...(input.projectIds ?? [])] .filter((value): value is string => Boolean(value?.trim())) .map((value) => value.trim()), ); const deviceScopeIds = new Set( [input.deviceId, ...(input.deviceIds ?? [])] .filter((value): value is string => Boolean(value?.trim())) .map((value) => value.trim()), ); let matchedScopedIdentifier = false; if (projectScopeIds.size > 0) { if (payload && typeof payload.projectId === "string" && payload.projectId.trim()) { if (!projectScopeIds.has(payload.projectId.trim())) { return false; } matchedScopedIdentifier = true; } } if (deviceScopeIds.size > 0) { if (payload && typeof payload.deviceId === "string" && payload.deviceId.trim()) { if (!deviceScopeIds.has(payload.deviceId.trim())) { return false; } matchedScopedIdentifier = true; } } if ((projectScopeIds.size > 0 || deviceScopeIds.size > 0) && !matchedScopedIdentifier) { return false; } if (input.eventType === "conversation.updated" && input.conversationUpdatedNotes?.length) { if (!payload || typeof payload.note !== "string" || !payload.note.trim()) { return false; } if (!input.conversationUpdatedNotes.includes(payload.note)) { return false; } } return true; } export function createRefreshThrottleState(): RefreshThrottleState { return { lastRefreshAt: null, pending: false, }; } export function planThrottledRefresh( state: RefreshThrottleState, now: number, throttleMs: number, ): RefreshThrottleDecision { if (state.pending) { return { type: "skip" }; } if (state.lastRefreshAt === null || now - state.lastRefreshAt >= throttleMs) { state.lastRefreshAt = now; return { type: "refresh_now" }; } state.pending = true; return { type: "schedule_refresh", delayMs: Math.max(0, throttleMs - (now - state.lastRefreshAt)), }; } export function markScheduledRefreshExecuted(state: RefreshThrottleState, now: number) { state.lastRefreshAt = now; state.pending = false; } export function cancelScheduledRefresh(state: RefreshThrottleState) { state.pending = false; }