Files
boss/src/lib/realtime-refresh.ts
2026-04-10 12:55:43 +08:00

125 lines
3.2 KiB
TypeScript

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;
}