Skip to content

Instantly share code, notes, and snippets.

@garand
Created November 11, 2025 20:20
Show Gist options
  • Select an option

  • Save garand/c6b006ca0650384bdf543bd4cc850d59 to your computer and use it in GitHub Desktop.

Select an option

Save garand/c6b006ca0650384bdf543bd4cc850d59 to your computer and use it in GitHub Desktop.
Event Queue
import * as React from "react";
import { Button, Badge, Switch, Popover, Icon } from "@towbook/flatbed";
import {
faListUl,
faXmark,
faCircle,
faTrash,
faChevronDown,
} from "@fortawesome/pro-solid-svg-icons";
import { useEventQueueDevtools } from "~/hooks/useEventQueueDevtools";
import { setDevtoolsPause } from "~/utils/eventQueueManager";
const EventQueueDevTools =
process.env.NODE_ENV === "development"
? () => {
const [isOpen, setIsOpen] = React.useState(() => {
// Read from localStorage on mount (client-side only)
if (typeof window !== "undefined") {
const saved = localStorage.getItem("eventQueueDevToolsOpen");
return saved === "true";
}
return false;
});
const { state, controls } = useEventQueueDevtools();
const [showClearConfirm, setShowClearConfirm] = React.useState(false);
const [selectedItemKey, setSelectedItemKey] = React.useState<string | null>(
null,
);
const [isHoveringQueue, setIsHoveringQueue] = React.useState(false);
// Update body padding when panel opens/closes - use CSS class
React.useEffect(() => {
document.body.classList.toggle("event-queue-devtools-open", isOpen);
// Persist to localStorage (client-side only)
if (typeof window !== "undefined") {
localStorage.setItem("eventQueueDevToolsOpen", String(isOpen));
}
return () => {
document.body.classList.remove("event-queue-devtools-open");
};
}, [isOpen]);
// Cleanup devtools pause on unmount
React.useEffect(() => {
return () => {
setDevtoolsPause(false);
};
}, []);
// Create stable reference to queue item keys for dependency tracking
const queueItemKeys = React.useMemo(
() => state?.queueItems.map((i) => i.key).join(",") || "",
[state?.queueItems],
);
// Auto-clear selection if selected item no longer exists in queue
React.useEffect(() => {
if (
selectedItemKey &&
queueItemKeys &&
!queueItemKeys.includes(selectedItemKey)
) {
setSelectedItemKey(null);
setDevtoolsPause(false);
}
}, [selectedItemKey, queueItemKeys]);
// Event handlers for hover - set devtools pause directly
const handleMouseEnter = () => {
setIsHoveringQueue(true);
setDevtoolsPause(true);
};
const handleMouseLeave = () => {
setIsHoveringQueue(false);
// Only unpause if nothing is selected
if (!selectedItemKey) {
setDevtoolsPause(false);
}
};
const handleLogQueue = () => {
console.log("[EventQueueDevTools] Queue state:", state);
};
const handleClearQueue = () => {
if (showClearConfirm) {
controls.clearQueue();
setShowClearConfirm(false);
setSelectedItemKey(null);
setDevtoolsPause(isHoveringQueue);
} else {
setShowClearConfirm(true);
setTimeout(() => setShowClearConfirm(false), 3000);
}
};
// Event handler for item click - set devtools pause directly
const handleItemClick = (key: string) => {
const newSelection = selectedItemKey === key ? null : key;
setSelectedItemKey(newSelection);
setDevtoolsPause(!!newSelection || isHoveringQueue);
};
if (!state) {
// Queue not initialized yet
return (
<div className="pointer-events-none fixed right-0 bottom-0 left-0 z-50">
{!isOpen && (
<div className="pointer-events-none pl-16">
<button
className="bg-slate-3 border-slate-5 text-slate-8 pointer-events-auto mb-2.5 rounded-full border p-2.5 opacity-50"
disabled
>
<Icon icon={faListUl} className="size-6" />
</button>
</div>
)}
</div>
);
}
const {
queueSize,
queueItems,
isPaused,
isProcessing,
missedInterval,
pauseConditions,
processInterval,
nextFlushCountdown,
isManuallyPaused,
manualPauseReason,
} = state;
const selectedItem = selectedItemKey
? queueItems.find((item) => item.key === selectedItemKey)
: null;
return (
<>
<div className="">
{isOpen && (
<div className="border-slate-7 pointer-events-auto fixed right-0 bottom-0 left-0 z-999999 h-[512px] border-t bg-white shadow-lg">
{/* Header with All Controls */}
<div className="border-slate-6 flex items-center justify-between gap-4 border-b px-4 py-2">
<h2 className="text-sm font-semibold">
EventQueue DevTools
</h2>
{/* Inline Controls */}
<div className="flex flex-1 items-center gap-3">
{/* Manual Pause Switch */}
<div className="flex items-center gap-2">
<span className="text-slate-11 text-xs">Enabled:</span>
<Switch
size="small"
checked={!isManuallyPaused}
onCheckedChange={(checked) =>
controls.setManualPause(
!checked,
checked ? undefined : "Manual pause via DevTools",
)
}
/>
</div>
{/* Config Display */}
<div className="flex items-center gap-2 text-xs">
<span className="text-slate-11">
Debounce: <span className="font-semibold">0.5s</span>
</span>
<span className="text-slate-11">|</span>
<span className="text-slate-11">
Max: <span className="font-semibold">5s</span>
</span>
</div>
{/* Metrics */}
<div className="flex items-center gap-3 text-xs">
<span className="text-slate-11">
Size:{" "}
<span className="font-semibold">{queueSize}</span>
</span>
<span className="text-slate-11">
Next:{" "}
<span className="font-semibold">
{nextFlushCountdown.toFixed(1)}s
</span>
</span>
{isProcessing && (
<Badge color="blue">
<span className="animate-pulse">Processing</span>
</Badge>
)}
{isManuallyPaused && (
<Badge color="purple">Manual Pause</Badge>
)}
{missedInterval && <Badge color="amber">Missed</Badge>}
</div>
</div>
{/* Right Side Controls */}
<div className="flex items-center gap-2">
{/* Quick Actions */}
<Button
size="small"
variant="light"
onClick={() => {
controls.forceFlush();
setSelectedItemKey(null);
}}
>
Force Flush
</Button>
<Button
size="small"
variant={showClearConfirm ? "default" : "light"}
onClick={handleClearQueue}
>
<Icon icon={faTrash} className="mr-1" />
{showClearConfirm ? "Confirm?" : "Clear"}
</Button>
{/* Pause Status with Popover */}
<Popover.Root>
<Popover.Trigger asChild>
<button className="hover:bg-slate-3 flex items-center gap-1.5 rounded px-2 py-1 text-xs">
<Icon
icon={faCircle}
className={`h-2 w-2 ${
isPaused ? "text-red-10" : "text-green-10"
}`}
/>
<span className="font-medium">
{isPaused ? "Paused" : "Active"}
</span>
<Icon
icon={faChevronDown}
className="text-slate-10 h-3 w-3"
/>
</button>
</Popover.Trigger>
<Popover.Content
side="top"
align="end"
className="w-64 p-3"
>
<div className="mb-2 text-xs font-semibold">
Pause Status
</div>
{/* Manual Pause */}
{isManuallyPaused && (
<div className="border-purple-6 bg-purple-3 mb-2 rounded border p-2">
<div className="flex items-center gap-2">
<Icon
icon={faCircle}
className="text-purple-10 h-2 w-2"
/>
<span className="text-purple-12 text-xs font-medium">
Manual Pause
</span>
</div>
{manualPauseReason && (
<div className="text-purple-11 mt-1 text-xs">
{manualPauseReason}
</div>
)}
</div>
)}
{/* Pause Conditions */}
<div className="text-slate-11 mb-1.5 text-xs">
Conditions: {pauseConditions.length}
</div>
{pauseConditions.length === 0 ? (
<div className="text-slate-9 text-xs italic">
No conditions
</div>
) : (
<div className="space-y-1.5">
{pauseConditions.map((condition) => (
<div
key={condition.id}
className="flex items-center gap-2"
>
<Icon
icon={faCircle}
className={`h-2 w-2 ${
condition.active
? "text-red-10"
: "text-slate-7"
}`}
/>
<span
className={`text-xs ${
condition.active
? "text-slate-12 font-medium"
: "text-slate-11"
}`}
>
{condition.id}
</span>
</div>
))}
</div>
)}
</Popover.Content>
</Popover.Root>
{/* Close Button */}
<Button
variant="text"
size="small"
square
onClick={() => {
setIsOpen(false);
setSelectedItemKey(null);
}}
>
<Icon icon={faXmark} />
</Button>
</div>
</div>
{/* Content - Single Column for Queue Items */}
<div className="h-[calc(512px-48px)] overflow-hidden px-4 py-3">
<div className="grid h-full grid-cols-2 gap-4">
{/* Left Column - Queue Items List */}
<div
className="flex h-full flex-col overflow-auto"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div className="text-slate-11 mb-1.5 flex items-center justify-between text-xs font-medium">
<span>Queued Items ({queueItems.length})</span>
{(isHoveringQueue || selectedItemKey) && (
<Badge size="small" color="amber">
Paused for review
</Badge>
)}
</div>
{queueItems.length === 0 ? (
<div className="text-slate-9 text-xs italic">
Queue is empty
</div>
) : (
<div className="min-h-0 flex-1 overflow-y-auto">
<table className="w-full table-fixed text-xs">
<thead className="border-slate-6 sticky top-0 border-b bg-white">
<tr>
<th className="text-slate-11 w-[45%] px-2 py-1.5 text-left font-medium">
Channel
</th>
<th className="text-slate-11 w-[45%] px-2 py-1.5 text-left font-medium">
Event
</th>
<th className="text-slate-11 w-[10%] px-2 py-1.5 text-right font-medium">
Age
</th>
</tr>
</thead>
<tbody>
{queueItems.map((item) => (
<tr
key={item.key}
onClick={() => handleItemClick(item.key)}
className={`cursor-pointer transition-colors ${
selectedItemKey === item.key
? "border-blue-6 bg-blue-3"
: "hover:bg-slate-3"
}`}
>
<td className="text-slate-11 w-[45%] truncate px-2 py-1.5 font-mono">
{item.eventData?.channel || "—"}
</td>
<td className="text-slate-11 w-[45%] truncate px-2 py-1.5 font-mono">
{item.eventData?.event || item.key}
</td>
<td className="text-slate-9 w-[10%] px-2 py-1.5 text-right font-mono">
{item.age}s
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* Right Column - Item Detail View */}
{selectedItem && (
<div className="border-slate-6 flex h-full flex-col border-l pl-4">
<div className="mb-2 flex items-center justify-between">
<h4 className="text-xs font-semibold">
Item Details
</h4>
<Button
size="small"
variant="text"
square
onClick={() => setSelectedItemKey(null)}
>
<Icon icon={faXmark} />
</Button>
</div>
<div className="min-h-0 flex-1 space-y-2 overflow-y-auto text-xs">
{/* Queue Key */}
<div>
<div className="text-slate-11 mb-1">
Queue Key
</div>
<div className="border-slate-6 bg-slate-2 rounded border p-2 font-mono">
{selectedItem.key}
</div>
</div>
{/* Timing Info */}
<div className="grid grid-cols-2 gap-2">
<div>
<div className="text-slate-11 mb-1">
Added At
</div>
<div className="font-mono">
{new Date(
selectedItem.timestamp,
).toLocaleTimeString()}
</div>
</div>
<div>
<div className="text-slate-11 mb-1">Age</div>
<div className="font-mono">
{selectedItem.age}s
</div>
</div>
</div>
{/* Pusher Event Data */}
{selectedItem.eventData ? (
<>
<div>
<div className="text-slate-11 mb-1">
Pusher Channel
</div>
<div className="font-mono">
{selectedItem.eventData.channel}
</div>
</div>
<div>
<div className="text-slate-11 mb-1">
Event Name
</div>
<div className="font-mono">
{selectedItem.eventData.event}
</div>
</div>
<div>
<div className="text-slate-11 mb-1">
Event Data
</div>
<pre className="border-slate-6 bg-slate-2 max-h-96 overflow-auto rounded border p-2 font-mono text-[10px] leading-relaxed">
{JSON.stringify(
selectedItem.eventData.data,
null,
2,
)}
</pre>
</div>
</>
) : (
<div className="text-slate-9 text-xs italic">
No Pusher event data available
</div>
)}
</div>
</div>
)}
</div>
</div>
</div>
)}
{/* FAB Button - Hide when panel is open */}
{!isOpen && (
<div className="pointer-events-none fixed right-33 bottom-3">
<button
onClick={() => setIsOpen(true)}
className={`bg-background shadow-soft-sm pointer-events-auto grid size-12 cursor-pointer place-items-center rounded-full transition-transform hover:scale-105 ${
isPaused ? "text-red-10" : "text-green-10"
}`}
>
<div className="relative grid aspect-square place-items-center">
<Icon icon={faListUl} className="size-5" />
{queueSize > 0 && (
<span className="bg-red-9 text-background absolute -top-4 -right-4 flex items-center justify-center rounded-full px-1.5 py-0.5 text-[10px] font-medium tabular-nums">
{queueSize > 9 ? "9+" : queueSize}
</span>
)}
</div>
</button>
</div>
)}
</div>
</>
);
}
: () => null;
export default EventQueueDevTools;
import * as React from "react";
import { destroyEventQueue, initEventQueue } from "./eventQueueManager";
/**
* EventQueueInitializer Component
* Initializes the global event queue on mount
* This ensures the queue is available for batching updates
*/
export function EventQueueInitializer() {
React.useEffect(() => {
// Initialize event queue with 5-second processing interval
initEventQueue(5000);
// console.log("[EventQueueInitializer] Event queue initialized");
return () => {
// Cleanup on unmount
destroyEventQueue();
// console.log("[EventQueueInitializer] Event queue destroyed");
};
}, []);
return null; // This component doesn't render anything
}
import ms from "ms";
import { startTransition } from "react";
interface QueuedEvent {
key: string;
handler: () => void | Promise<void>;
timestamp: number;
eventData?: {
channel: string;
event: string;
data: unknown;
};
}
/**
* Pause condition interface
* Allows pages to register custom pause logic
*/
export interface PauseCondition {
id: string;
shouldPause: () => boolean;
}
/**
* Queue manager for batching events
* Collects updates and processes them at regular intervals to reduce re-renders
* Supports configurable pause conditions (e.g., during map interaction)
* Catches up immediately when all pause conditions clear
*/
class EventQueueManager {
private queue: Map<string, QueuedEvent> = new Map(); // Map for deduplication by ID
private debounceTimer: NodeJS.Timeout | null = null; // Timer for debounced flush
private maxWaitTimer: NodeJS.Timeout | null = null; // Timer for max wait flush
private firstEventTime: number | null = null; // Timestamp of first event in current batch
private readonly DEBOUNCE_DELAY = ms("250ms"); // 0.5s - flush if no events for this long
private readonly MAX_WAIT_TIME = ms("2.5s"); // 5s - force flush after this long
private isProcessing: boolean = false;
private missedInterval: boolean = false; // Track if we skipped a flush due to pause
private lastPausedState: boolean = false;
private pauseConditions: Map<string, PauseCondition> = new Map();
private pauseCheckInterval: NodeJS.Timeout | null = null;
private pauseEndDebounce: NodeJS.Timeout | null = null; // Debounce timer for pause end
private readonly PAUSE_END_DELAY = 1000; // Wait 1 second after pause clears
private lastFlushTime: number = Date.now(); // Track when last flush occurred
private isManuallyPaused: boolean = false; // Manual pause state
private manualPauseReason: string | null = null; // Optional reason for manual pause
private devtoolsPaused: boolean = false; // DevTools-specific pause (for hover/selection)
private processingKeys: Set<string> = new Set(); // Keys currently in processing batch
private completedKeys: Set<string> = new Set(); // Keys that have started processing in current batch
constructor() {
this.startPauseMonitoring();
}
/**
* Register a pause condition
* When any condition returns true, queue processing is paused
*/
registerPauseCondition(condition: PauseCondition) {
this.pauseConditions.set(condition.id, condition);
// console.log(`[EventQueue] Registered pause condition: ${condition.id}`);
}
/**
* Unregister a pause condition
*/
unregisterPauseCondition(conditionId: string) {
this.pauseConditions.delete(conditionId);
// console.log(`[EventQueue] Unregistered pause condition: ${conditionId}`);
}
/**
* Check if any pause condition is active
*/
private isPaused(): boolean {
// DevTools pause takes highest priority (for hover/selection review)
if (this.devtoolsPaused) {
return true;
}
// Manual pause takes second priority
if (this.isManuallyPaused) {
return true;
}
// Check if any registered condition is active
return Array.from(this.pauseConditions.values()).some((condition) =>
condition.shouldPause(),
);
}
/**
* Monitor pause state changes
* Polls conditions every 100ms to detect state transitions
*/
private startPauseMonitoring() {
this.lastPausedState = this.isPaused();
// Poll every 100ms to detect pause state changes
this.pauseCheckInterval = setInterval(() => {
const isPaused = this.isPaused();
// Detect transition from paused -> not paused
if (this.lastPausedState && !isPaused) {
// console.log(
// "[EventQueue] Pause cleared - waiting 1s to confirm",
// );
// Clear any existing debounce timer
if (this.pauseEndDebounce) {
clearTimeout(this.pauseEndDebounce);
}
// Wait 1 second before flushing to ensure pause is truly cleared
this.pauseEndDebounce = setTimeout(() => {
// console.log("[EventQueue] Confirmed pause cleared");
if (this.missedInterval) {
// console.log(
// "[EventQueue] Processing missed interval immediately",
// );
this.forceFlush();
}
this.pauseEndDebounce = null;
}, this.PAUSE_END_DELAY);
}
// Detect transition from not paused -> paused
else if (!this.lastPausedState && isPaused) {
// console.log("[EventQueue] Pause activated");
// Cancel pending flush if pause activates again
if (this.pauseEndDebounce) {
// console.log(
// "[EventQueue] Cancelling pending flush - pause reactivated",
// );
clearTimeout(this.pauseEndDebounce);
this.pauseEndDebounce = null;
}
}
this.lastPausedState = isPaused;
}, 100); // Check every 100ms
}
/**
* Add event to queue
* If an event for the same key already exists, it will be replaced (deduplication)
*/
enqueue(
key: string,
handler: () => void | Promise<void>,
eventData?: { channel: string; event: string; data: unknown },
) {
// Check if event is in processing batch but hasn't started yet
if (this.processingKeys.has(key) && !this.completedKeys.has(key)) {
console.log(
`[EventQueue] Event ${key} is pending in current batch, skipping duplicate`,
);
return; // Don't queue - will execute very soon
}
// Queue the event (either not processing, or already started and this is new data)
const now = Date.now();
this.queue.set(key, {
key,
handler,
timestamp: now,
eventData,
});
// Start max wait timer if this is the first event in batch
if (this.firstEventTime === null) {
this.firstEventTime = now;
// Force flush after max wait time
this.maxWaitTimer = setTimeout(() => {
console.log("[EventQueue] Max wait time reached, forcing flush");
this.flush();
}, this.MAX_WAIT_TIME);
}
// Reset/start debounce timer
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = setTimeout(() => {
console.log("[EventQueue] Debounce timeout reached, flushing");
this.flush();
}, this.DEBOUNCE_DELAY);
// console.log(
// `[EventQueue] Queued event for key ${key}, Queue size: ${this.queue.size}`,
// );
}
/**
* Process all queued updates in parallel
* @param ignorePause - If true, flush even when paused (used by forceFlush)
*/
private async flush(ignorePause: boolean = false) {
if (this.queue.size === 0) return;
if (this.isProcessing) {
console.warn("[EventQueue] Already processing, skipping flush");
return;
}
// Check if any pause condition is active (unless we're forcing)
if (!ignorePause && this.isPaused()) {
// console.log(
// `[EventQueue] Queue is paused, skipping flush (${this.queue.size} updates queued)`,
// );
this.missedInterval = true; // Mark that we skipped an interval
return; // Skip this flush, updates stay queued for next interval
}
// Calculate how long queue was open (first event to flush)
const queueOpenDuration = this.firstEventTime
? Date.now() - this.firstEventTime
: 0;
// Clear both timers since we're flushing now
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
this.debounceTimer = null;
}
if (this.maxWaitTimer) {
clearTimeout(this.maxWaitTimer);
this.maxWaitTimer = null;
}
this.firstEventTime = null;
this.isProcessing = true;
const updatesToProcess = Array.from(this.queue.values());
// Mark all keys as processing
updatesToProcess.forEach((item) => {
this.processingKeys.add(item.key);
});
this.queue.clear(); // Clear queue immediately
console.log(
`[EventQueue] Processing ${updatesToProcess.length} events | Queue open: ${queueOpenDuration}ms (${(queueOpenDuration / 1000).toFixed(2)}s)`,
);
// Performance measurement using Performance API
const markName = `event-queue-process-${Date.now()}`;
const measureName = `event-queue-measure-${Date.now()}`;
performance.mark(markName);
// Process updates in parallel
startTransition(async () => {
await Promise.allSettled(
updatesToProcess.map(async (item) => {
// Mark as completed RIGHT BEFORE processing starts
this.completedKeys.add(item.key);
try {
await item.handler();
} catch (error) {
console.error(
`[EventQueue] Error processing event for key ${item.key}:`,
error,
);
}
}),
);
});
// End performance measurement
performance.measure(measureName, markName);
const measures = performance.getEntriesByName(measureName);
if (measures.length > 0) {
const duration = measures[0].duration;
console.log(
`[EventQueue] Processed ${updatesToProcess.length} updates in ${duration.toFixed(2)}ms`,
{
queueSize: updatesToProcess.length,
duration: `${duration.toFixed(2)}ms`,
timestamp: new Date().toISOString(),
},
);
// Clean up performance entries
performance.clearMarks(markName);
performance.clearMeasures(measureName);
}
// Clear both tracking sets after batch completes
updatesToProcess.forEach((item) => {
this.processingKeys.delete(item.key);
this.completedKeys.delete(item.key);
});
this.isProcessing = false;
this.missedInterval = false; // Reset after successful flush
this.lastFlushTime = Date.now(); // Track flush time for devtools
}
/**
* Force immediate flush
* Bypasses pause conditions and processes immediately
* Useful for cleanup or manual testing
*/
forceFlush() {
console.log("[EventQueue] Force flushing queue (bypassing pause)");
this.flush(true); // Pass true to bypass pause check
}
/**
* Get current queue size
*/
getQueueSize(): number {
return this.queue.size;
}
/**
* Get queue items for devtools inspection
* Returns array of items with keys, timestamps, and event data
*/
getQueue(): Array<{
key: string;
timestamp: number;
eventData?: { channel: string; event: string; data: unknown };
}> {
return Array.from(this.queue.values()).map(
({ key, timestamp, eventData }) => ({
key,
timestamp,
eventData,
}),
);
}
/**
* Check if queue is currently paused
*/
getIsPaused(): boolean {
return this.isPaused();
}
/**
* Check if queue is currently processing
*/
getIsProcessing(): boolean {
return this.isProcessing;
}
/**
* Check if intervals were missed due to pause
*/
getMissedInterval(): boolean {
return this.missedInterval;
}
/**
* Get all registered pause condition IDs
*/
getPauseConditions(): Array<{ id: string }> {
return Array.from(this.pauseConditions.keys()).map((id) => ({ id }));
}
/**
* Get currently active pause condition IDs
*/
getActivePauseConditions(): string[] {
return Array.from(this.pauseConditions.entries())
.filter(([_, condition]) => condition.shouldPause())
.map(([id]) => id);
}
/**
* Get the debounce delay in milliseconds
*/
getDebounceDelay(): number {
return this.DEBOUNCE_DELAY;
}
/**
* Get the max wait time in milliseconds
*/
getMaxWaitTime(): number {
return this.MAX_WAIT_TIME;
}
/**
* Get the timestamp of the last flush
*/
getLastFlushTime(): number {
return this.lastFlushTime;
}
/**
* Get timestamp of first event in current batch (for countdown calculation)
*/
getFirstEventTime(): number | null {
return this.firstEventTime;
}
/**
* Check if queue is manually paused
*/
getIsManuallyPaused(): boolean {
return this.isManuallyPaused;
}
/**
* Get manual pause reason
*/
getManualPauseReason(): string | null {
return this.manualPauseReason;
}
/**
* Manually pause or resume the queue
* Manual pause takes priority over all pause conditions
*/
setManualPause(paused: boolean, reason?: string) {
this.isManuallyPaused = paused;
this.manualPauseReason = paused ? reason || null : null;
console.log(
`[EventQueue] Manual pause ${paused ? "enabled" : "disabled"}${
reason ? `: ${reason}` : ""
}`,
);
}
/**
* Set devtools pause state (for hover/selection review)
* DevTools pause takes highest priority
*/
setDevtoolsPause(paused: boolean) {
this.devtoolsPaused = paused;
}
/**
* Check if queue is paused by devtools
*/
getIsDevtoolsPaused(): boolean {
return this.devtoolsPaused;
}
/**
* Clear all queued items
* Useful for testing or manual cleanup
*/
clearQueue() {
const size = this.queue.size;
this.queue.clear();
console.log(`[EventQueue] Cleared ${size} items from queue`);
}
/**
* Cleanup and stop processing
*/
destroy() {
// console.log("[EventQueue] Destroying queue manager");
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
this.debounceTimer = null;
}
if (this.maxWaitTimer) {
clearTimeout(this.maxWaitTimer);
this.maxWaitTimer = null;
}
if (this.pauseCheckInterval) {
clearInterval(this.pauseCheckInterval);
this.pauseCheckInterval = null;
}
if (this.pauseEndDebounce) {
clearTimeout(this.pauseEndDebounce);
this.pauseEndDebounce = null;
}
// Clear all pause conditions
this.pauseConditions.clear();
// Process any remaining updates before destroying (bypass pause)
if (this.queue.size > 0) {
// console.log(
// `[EventQueue] Processing ${this.queue.size} remaining updates before destroy`,
// );
this.flush(true); // Bypass pause on destroy
}
this.queue.clear();
}
}
// Global singleton instance
let queueManager: EventQueueManager | null = null;
/**
* Initialize the event queue manager
* @returns Queue manager instance
*/
export function initEventQueue() {
if (queueManager) {
// console.log(
// "[EventQueue] Manager already initialized, destroying old instance",
// );
queueManager.destroy();
}
queueManager = new EventQueueManager();
return queueManager;
}
/**
* Get the global queue manager instance
* @throws Error if not initialized
*/
export function getEventQueue() {
if (!queueManager) {
throw new Error("EventQueue not initialized. Call initEventQueue() first.");
}
return queueManager;
}
/**
* Register a pause condition
* When the condition returns true, queue processing is paused
*/
export function registerPauseCondition(condition: PauseCondition) {
const queue = getEventQueue();
queue.registerPauseCondition(condition);
}
/**
* Unregister a pause condition
*/
export function unregisterPauseCondition(conditionId: string) {
const queue = getEventQueue();
queue.unregisterPauseCondition(conditionId);
}
/**
* Queue a event update for batched processing
* @param key - Event key
* @param handler - Function to execute when processing
* @param eventData - Optional Pusher event data for debugging
*/
export function queueEvent(
key: string,
handler: () => void | Promise<void>,
eventData?: { channel: string; event: string; data: unknown },
) {
try {
const queue = getEventQueue();
queue.enqueue(key, handler, eventData);
} catch (error) {
console.error("[EventQueue] Failed to queue event:", error);
// Fallback: Execute immediately if queue not available
console.warn("[EventQueue] Executing update immediately as fallback");
handler();
}
}
/**
* Set devtools pause state
* Used by devtools to pause queue during hover/selection review
*/
export function setDevtoolsPause(paused: boolean) {
try {
const queue = getEventQueue();
queue.setDevtoolsPause(paused);
} catch (error) {
console.error("[EventQueue] Failed to set devtools pause:", error);
}
}
/**
* Destroy the global queue manager instance
*/
export function destroyEventQueue() {
if (queueManager) {
queueManager.destroy();
queueManager = null;
}
}
import * as React from "react";
import { getEventQueue } from "~/utils/eventQueueManager";
export interface EventQueueDevtoolsState {
queueSize: number;
queueItems: Array<{
key: string;
timestamp: number;
age: number;
eventData?: { channel: string; event: string; data: unknown };
}>;
isPaused: boolean;
isProcessing: boolean;
missedInterval: boolean;
pauseConditions: Array<{ id: string; active: boolean }>;
processInterval: number;
nextFlushCountdown: number; // seconds until next flush
lastFlushTime: number;
isManuallyPaused: boolean;
manualPauseReason: string | null;
}
export interface EventQueueDevtoolsControls {
setManualPause: (paused: boolean, reason?: string) => void;
clearQueue: () => void;
forceFlush: () => void;
}
/**
* Hook to access EventQueue state for devtools
* Polls every 100ms for real-time updates
* Returns null if queue is not initialized
*/
export function useEventQueueDevtools(): {
state: EventQueueDevtoolsState | null;
controls: EventQueueDevtoolsControls;
} {
const [state, setState] = React.useState<EventQueueDevtoolsState | null>(null);
React.useEffect(() => {
const updateState = () => {
try {
const queue = getEventQueue();
const now = Date.now();
const debounceDelay = queue.getDebounceDelay();
const maxWaitTime = queue.getMaxWaitTime();
const firstEventTime = queue.getFirstEventTime();
// Calculate next flush time
let nextFlushCountdown = 0;
if (firstEventTime !== null) {
const timeSinceFirst = now - firstEventTime;
const maxWaitRemaining = (maxWaitTime - timeSinceFirst) / 1000;
const debounceRemaining = debounceDelay / 1000;
// Next flush is whichever comes first
nextFlushCountdown = Math.max(0, Math.min(debounceRemaining, maxWaitRemaining));
}
const queueItems = queue
.getQueue()
.map((item) => ({
...item,
age: Math.floor((now - item.timestamp) / 1000), // age in seconds
}))
.sort((a, b) => b.timestamp - a.timestamp); // Newest first
const allPauseConditions = queue.getPauseConditions();
const activePauseConditionIds = queue.getActivePauseConditions();
const pauseConditions = allPauseConditions.map((condition) => ({
...condition,
active: activePauseConditionIds.includes(condition.id),
}));
setState({
queueSize: queue.getQueueSize(),
queueItems,
isPaused: queue.getIsPaused(),
isProcessing: queue.getIsProcessing(),
missedInterval: queue.getMissedInterval(),
pauseConditions,
processInterval: debounceDelay, // Use debounce delay for now (DevTools may need update)
nextFlushCountdown,
lastFlushTime: queue.getLastFlushTime(),
isManuallyPaused: queue.getIsManuallyPaused(),
manualPauseReason: queue.getManualPauseReason(),
});
} catch (error) {
// Queue not initialized yet
setState(null);
}
};
// Initial update
updateState();
// Poll every 100ms for real-time updates
const interval = setInterval(updateState, 100);
return () => clearInterval(interval);
}, []);
// Control functions
const controls: EventQueueDevtoolsControls = {
setManualPause: React.useCallback((paused: boolean, reason?: string) => {
try {
const queue = getEventQueue();
queue.setManualPause(paused, reason);
} catch (error) {
console.error("[EventQueueDevtools] Failed to set manual pause:", error);
}
}, []),
clearQueue: React.useCallback(() => {
try {
const queue = getEventQueue();
queue.clearQueue();
} catch (error) {
console.error("[EventQueueDevtools] Failed to clear queue:", error);
}
}, []),
forceFlush: React.useCallback(() => {
try {
const queue = getEventQueue();
queue.forceFlush();
} catch (error) {
console.error("[EventQueueDevtools] Failed to force flush:", error);
}
}, []),
};
return { state, controls };
}
import * as React from "react";
import {
registerPauseCondition,
unregisterPauseCondition,
type PauseCondition,
} from "~/utils/eventQueueManager";
/**
* Hook to register a pause condition for the event queue
* When the condition returns true, queue processing is paused
* Automatically unregisters on unmount
*
* @param conditionId - Unique identifier for this pause condition
* @param shouldPause - Function that returns true when queue should be paused
*
* @example
* // On map page - pause during map interactions
* useEventQueuePause('map-interaction', () => useMapStore.getState().isMapInteracting)
*
* @example
* // On dispatching page - no pause needed, don't call the hook
*/
export function useEventQueuePause(
conditionId: string,
shouldPause: () => boolean,
) {
React.useEffect(() => {
const condition: PauseCondition = {
id: conditionId,
shouldPause,
};
// Register the pause condition
registerPauseCondition(condition);
// Cleanup: unregister on unmount
return () => {
unregisterPauseCondition(conditionId);
};
}, [conditionId, shouldPause]);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment