Throttle realtime refresh bursts

This commit is contained in:
kris
2026-04-07 17:50:30 +08:00
parent 4093c41949
commit 4c31dd7e98
5 changed files with 226 additions and 47 deletions

102
src/lib/realtime-refresh.ts Normal file
View File

@@ -0,0 +1,102 @@
export interface RealtimeRefreshScope {
projectId?: string;
projectIds?: 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;
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()),
);
if (projectScopeIds.size > 0) {
if (!payload || typeof payload.projectId !== "string" || !payload.projectId.trim()) {
return true;
}
if (!projectScopeIds.has(payload.projectId)) {
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;
}