Skip to content

Commit 6d20f9e

Browse files
committed
cache
1 parent 3bbf387 commit 6d20f9e

5 files changed

Lines changed: 108 additions & 37 deletions

File tree

docs/architecture.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ that its outcome is still being scanned; it never treats missing coverage as
5454
proof that no `Slashed` log exists. A stronger user-supplied RPC can complete
5555
the same bounded scan faster without changing case semantics.
5656

57+
Switching to PINGME pauses Monitor without making background RPC requests. Its
58+
in-memory scanner, coverage cursor, and projected evidence remain available for
59+
10 minutes. Returning within that window refreshes the L1 head and continues
60+
incrementally. Expiry, an RPC change, a network change, or a deployment change
61+
starts a clean scanner session. This cache is never persisted to browser
62+
storage.
63+
5764
Its unavoidable limitations are visible:
5865

5966
- an L1-only case starts at voting; it cannot see duty misses or node

docs/v3-plan.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ newest-to-oldest. Successful RPC ranges grow, rejected ranges shrink, rate
181181
limits pause without discarding progress, and later polls cover only new blocks
182182
plus a reorg overlap. The UI reports scan coverage and keeps an uninspected
183183
receipt distinct from an inspected receipt with no `Slashed` log.
184+
Switching to PINGME pauses Monitor RPC work and retains its scanner session in
185+
memory for 10 minutes. Returning refreshes the head and resumes incrementally;
186+
the cache expires or is discarded when its RPC, network, or deployment changes.
184187
Visible sequencer addresses link to the matching Dashtec network.
185188
Watchlist cases appear before the public case feed. PINGME places source health
186189
last because it describes the reliability of the whole page.

src/App.test.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,13 @@ describe('top-level view isolation', () => {
6060

6161
renderToStaticMarkup(<App />);
6262

63-
expect(scannerSpy).toHaveBeenCalledWith(expect.objectContaining({
64-
chainId: 1,
65-
l1RpcUrl: 'https://rpc.example/mainnet',
66-
}));
63+
expect(scannerSpy).toHaveBeenCalledWith(
64+
expect.objectContaining({
65+
chainId: 1,
66+
l1RpcUrl: 'https://rpc.example/mainnet',
67+
}),
68+
true,
69+
);
6770
});
6871

6972
});

src/App.tsx

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type { ProtocolSnapshot } from '../shared/protocol/index.ts';
1919

2020
const MAINNET_REGISTRY_ADDRESS = '0x35b22e09Ee0390539439E24f06Da43D83f90e298' as Address;
2121
const TESTNET_REGISTRY_ADDRESS = '0xA0BFb1B494FB49041e5c6e8c2C1BE09cD171c6Ba' as Address;
22+
const MONITOR_SESSION_CACHE_MS = 10 * 60 * 1_000;
2223

2324
const createConfig = (isTestnet: boolean, rpcOverride: string | null): MonitorConfigInput => {
2425
const chainId = isTestnet ? 11155111 : 1;
@@ -44,6 +45,9 @@ export function App() {
4445
testnet: getRpcOverride(11_155_111),
4546
}));
4647
const [scannerGeneration, setScannerGeneration] = useState(0);
48+
const [isScannerMounted, setIsScannerMounted] = useState(
49+
() => location.view === 'monitor',
50+
);
4751
const [protocolGuide, setProtocolGuide] = useState<{
4852
isOpen: boolean;
4953
protocol: ProtocolSnapshot | null;
@@ -67,12 +71,10 @@ export function App() {
6771
const next = urlForView(window.location.href, view);
6872
window.history.pushState({}, '', next);
6973
if (location.view !== view) setCurrentProtocol(null);
70-
if (location.view === 'pingme' && view !== 'pingme') {
71-
restartScanner();
72-
}
74+
if (view === 'monitor') setIsScannerMounted(true);
7375
setLocation(parseAppSearch(next.search));
7476
window.scrollTo({ top: 0, behavior: 'smooth' });
75-
}, [location.view, restartScanner]);
77+
}, [location.view]);
7678

7779
const toggleNetwork = useCallback(() => {
7880
const network = isTestnet ? 'mainnet' : 'testnet';
@@ -129,14 +131,24 @@ export function App() {
129131
useEffect(() => {
130132
const handlePopState = () => {
131133
const next = parseAppSearch(window.location.search);
132-
if (next.network !== location.network || (location.view === 'pingme' && next.view === 'monitor')) {
134+
if (next.network !== location.network) {
133135
restartScanner();
134136
}
137+
if (next.view === 'monitor') setIsScannerMounted(true);
135138
setLocation(next);
136139
};
137140
window.addEventListener('popstate', handlePopState);
138141
return () => window.removeEventListener('popstate', handlePopState);
139-
}, [location.network, location.view, restartScanner]);
142+
}, [location.network, restartScanner]);
143+
144+
useEffect(() => {
145+
if (location.view === 'monitor' || !isScannerMounted) return;
146+
const timeout = window.setTimeout(() => {
147+
resetMonitor();
148+
setIsScannerMounted(false);
149+
}, MONITOR_SESSION_CACHE_MS);
150+
return () => window.clearTimeout(timeout);
151+
}, [isScannerMounted, location.view, resetMonitor]);
140152

141153
return (
142154
<div className="min-h-screen bg-brand-black text-white">
@@ -150,6 +162,13 @@ export function App() {
150162
onNavigate={navigateTo}
151163
onOpenProtocolGuide={() => openProtocolGuide(currentProtocol)}
152164
/>
165+
{isScannerMounted && (
166+
<ScannerRuntime
167+
key={`${location.network}:${scannerGeneration}`}
168+
config={config}
169+
active={location.view === 'monitor'}
170+
/>
171+
)}
153172
{location.view === 'pingme' ? (
154173
<main className="mx-auto max-w-7xl px-4 py-8">
155174
<BackendOverview
@@ -164,28 +183,31 @@ export function App() {
164183
/>
165184
</main>
166185
) : (
167-
<>
168-
<ScannerRuntime key={`${location.network}:${scannerGeneration}`} config={config} />
169-
<Dashboard
170-
key={`${location.network}:${location.watchlistAddresses.join(',')}`}
171-
configInput={config}
172-
network={location.network}
173-
linkedAddresses={location.watchlistAddresses}
174-
selectedCaseId={location.selectedCaseId}
175-
onResetRpc={resetRpc}
176-
onToggleNetwork={toggleNetwork}
177-
onUpdateRpc={updateRpc}
178-
onWatchlistChange={updateWatchlist}
179-
onOpenProtocolGuide={openProtocolGuide}
180-
onProtocolChange={updateCurrentProtocol}
181-
/>
182-
</>
186+
<Dashboard
187+
key={`${location.network}:${location.watchlistAddresses.join(',')}`}
188+
configInput={config}
189+
network={location.network}
190+
linkedAddresses={location.watchlistAddresses}
191+
selectedCaseId={location.selectedCaseId}
192+
onResetRpc={resetRpc}
193+
onToggleNetwork={toggleNetwork}
194+
onUpdateRpc={updateRpc}
195+
onWatchlistChange={updateWatchlist}
196+
onOpenProtocolGuide={openProtocolGuide}
197+
onProtocolChange={updateCurrentProtocol}
198+
/>
183199
)}
184200
</div>
185201
);
186202
}
187203

188-
function ScannerRuntime({ config }: { config: MonitorConfigInput }) {
189-
useSlashingMonitor(config);
204+
function ScannerRuntime({
205+
config,
206+
active,
207+
}: {
208+
config: MonitorConfigInput;
209+
active: boolean;
210+
}) {
211+
useSlashingMonitor(config, active);
190212
return null;
191213
}

src/hooks/useSlashingMonitor.ts

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ const MAX_EXECUTION_SCAN_MS = 15_000;
2424
const ETHEREUM_BLOCK_TIME_SECONDS = 12n;
2525
const EXECUTION_LOOKBACK_SAFETY_BLOCKS = 5_000n;
2626

27-
export function useSlashingMonitor(config: MonitorConfigInput) {
27+
export function useSlashingMonitor(
28+
config: MonitorConfigInput,
29+
active = true,
30+
) {
2831
const {
2932
initialize,
3033
setIsScanning,
@@ -37,7 +40,7 @@ export function useSlashingMonitor(config: MonitorConfigInput) {
3740
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
3841
const isFirstScanRef = useRef(true);
3942
const isPollingRef = useRef(false);
40-
const isActiveRef = useRef(true);
43+
const isActiveRef = useRef(active);
4144
const runGenerationRef = useRef(0);
4245

4346
const initializeMonitor = useCallback(async (generation: number) => {
@@ -231,6 +234,15 @@ export function useSlashingMonitor(config: MonitorConfigInput) {
231234
}, [applySnapshot, initializeMonitor, setIsScanning, setMonitorFailure]);
232235

233236
useEffect(() => {
237+
if (!active) {
238+
isActiveRef.current = false;
239+
if (timeoutRef.current) {
240+
clearTimeout(timeoutRef.current);
241+
timeoutRef.current = null;
242+
}
243+
return;
244+
}
245+
234246
let cancelled = false;
235247
const generation = runGenerationRef.current + 1;
236248
runGenerationRef.current = generation;
@@ -255,12 +267,26 @@ export function useSlashingMonitor(config: MonitorConfigInput) {
255267

256268
const start = async () => {
257269
try {
258-
const initialState = await initializeMonitor(generation);
259-
if (cancelled || generation !== runGenerationRef.current) {
260-
return;
270+
while (isPollingRef.current) {
271+
if (cancelled || generation !== runGenerationRef.current) {
272+
return;
273+
}
274+
await waitForPollToStop();
275+
}
276+
if (
277+
l1MonitorRef.current &&
278+
detectorRef.current &&
279+
useSlashingStore.getState().isInitialized
280+
) {
281+
await poll(undefined, generation);
282+
}
283+
else {
284+
const initialState = await initializeMonitor(generation);
285+
if (cancelled || generation !== runGenerationRef.current) {
286+
return;
287+
}
288+
await poll(initialState, generation);
261289
}
262-
263-
await poll(initialState, generation);
264290
if (!cancelled && generation === runGenerationRef.current) {
265291
scheduleNextPoll();
266292
}
@@ -288,13 +314,19 @@ export function useSlashingMonitor(config: MonitorConfigInput) {
288314
if (runGenerationRef.current === generation) {
289315
runGenerationRef.current += 1;
290316
}
291-
isPollingRef.current = false;
292317
if (timeoutRef.current) {
293318
clearTimeout(timeoutRef.current);
294319
timeoutRef.current = null;
295320
}
296321
};
297-
}, [initializeMonitor, poll, setInitializationError, setIsScanning, setMonitorFailure]);
322+
}, [
323+
active,
324+
initializeMonitor,
325+
poll,
326+
setInitializationError,
327+
setIsScanning,
328+
setMonitorFailure,
329+
]);
298330
}
299331

300332
function buildSnapshot(
@@ -428,3 +460,7 @@ function assertCurrentRun(expected: number, actual: number): void {
428460
function yieldToBrowser(): Promise<void> {
429461
return new Promise((resolve) => setTimeout(resolve, 0));
430462
}
463+
464+
function waitForPollToStop(): Promise<void> {
465+
return new Promise((resolve) => setTimeout(resolve, 50));
466+
}

0 commit comments

Comments
 (0)