Throttle realtime refresh bursts
This commit is contained in:
102
src/lib/realtime-refresh.ts
Normal file
102
src/lib/realtime-refresh.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user