@@ -392,38 +392,38 @@ export default function Home() {
392392 const seenSignalIds = useRef < Set < string > > ( new Set ( ) ) ;
393393 const seenMarketIds = useRef < Set < string > > ( new Set ( ) ) ;
394394 const isFirstLoad = useRef ( true ) ;
395+ const sessionStartTime = useRef ( Date . now ( ) ) ;
395396 const lbCacheRef = useRef < Record < string , import ( "@/types" ) . SmartWallet [ ] > > ( { } ) ;
396397
397- // Quick fingerprint to detect if market data actually changed
398- const dataFingerprintRef = useRef ( "" ) ;
399- const marketFingerprint = ( arr : ProcessedMarket [ ] ) => {
400- // count + first/last ID + sum of probs (fast, no allocation)
401- if ( arr . length === 0 ) return "0" ;
402- let probSum = 0 ;
403- for ( let i = 0 ; i < arr . length ; i ++ ) probSum += ( arr [ i ] . prob ?? 0 ) ;
404- return `${ arr . length } :${ arr [ 0 ] . id } :${ arr [ arr . length - 1 ] . id } :${ probSum . toFixed ( 2 ) } ` ;
405- } ;
398+ // ETag for /api/markets — skip JSON parse when data unchanged (saves ~1-3MB per poll on mobile)
399+ const marketsEtagRef = useRef < string | null > ( null ) ;
406400
407401 const fetchData = useCallback ( async ( signal ?: AbortSignal ) => {
408402 const { setMapped, setUnmapped, setLoading, setDataMode, setLastSyncTime, setSignals, setNewMarkets, setLastRefresh } = useMarketStore . getState ( ) ;
409403 setLoading ( true ) ;
410404 setNewMarkets ( [ ] ) ;
411405 try {
412- const res = await fetch ( "/api/markets" , { signal } ) ;
406+ const headers : HeadersInit = { } ;
407+ if ( marketsEtagRef . current ) headers [ "If-None-Match" ] = marketsEtagRef . current ;
408+ const res = await fetch ( "/api/markets" , { signal, headers } ) ;
409+
410+ // 304 Not Modified — data unchanged, skip parse entirely
411+ if ( res . status === 304 ) {
412+ setLoading ( false ) ;
413+ setLastRefresh ( new Date ( ) . toLocaleTimeString ( ) ) ;
414+ return ;
415+ }
416+
413417 if ( ! res . ok ) throw new Error ( "API error" ) ;
418+ const etag = res . headers . get ( "etag" ) ;
419+ if ( etag ) marketsEtagRef . current = etag ;
414420 const data = await res . json ( ) ;
415421 const m : ProcessedMarket [ ] = data . mapped || [ ] ;
416422 const u : ProcessedMarket [ ] = data . unmapped || [ ] ;
417423
418424 if ( m . length > 0 || u . length > 0 ) {
419- // Skip store update if data hasn't changed — avoids re-render + GeoJSON rebuild
420- const fp = marketFingerprint ( m ) + "|" + marketFingerprint ( u ) ;
421- const changed = fp !== dataFingerprintRef . current ;
422- dataFingerprintRef . current = fp ;
423- if ( changed ) {
424- setMapped ( m ) ;
425- setUnmapped ( u ) ;
426- }
425+ setMapped ( m ) ;
426+ setUnmapped ( u ) ;
427427 setDataMode ( "live" ) ;
428428 setRefreshError ( false ) ;
429429 if ( data . lastSync ) setLastSyncTime ( data . lastSync ) ;
@@ -444,11 +444,16 @@ export default function Home() {
444444 for ( const item of all ) seenMarketIds . current . add ( item . id ) ;
445445 isFirstLoad . current = false ;
446446 } else {
447- const fresh = all . filter (
448- ( item ) => ! seenMarketIds . current . has ( item . id )
449- ) ;
447+ // Only treat as "new" if: 1) not seen before AND 2) created after session start
448+ // This prevents old markets that re-enter the active set from triggering alerts
449+ const fresh = all . filter ( ( item ) => {
450+ if ( seenMarketIds . current . has ( item . id ) ) return false ;
451+ seenMarketIds . current . add ( item . id ) ;
452+ if ( ! item . createdAt ) return false ;
453+ const created = new Date ( item . createdAt ) . getTime ( ) ;
454+ return created > sessionStartTime . current ;
455+ } ) ;
450456 if ( fresh . length > 0 ) {
451- for ( const item of fresh ) seenMarketIds . current . add ( item . id ) ;
452457 setNewMarkets ( fresh ) ;
453458 }
454459 }
@@ -469,6 +474,7 @@ export default function Home() {
469474 setLoading ( false ) ;
470475 } , [ ] ) ;
471476
477+ const smartMoneyEtagRef = useRef < string | null > ( null ) ;
472478 const fetchSmartMoney = useCallback ( async ( periodOrSignal ?: LeaderboardPeriod | AbortSignal , leaderboardOnly ?: boolean ) => {
473479 // Support being called from useVisibilityPolling (signal) or directly (period)
474480 let period : LeaderboardPeriod | undefined ;
@@ -482,8 +488,13 @@ export default function Home() {
482488 const sm = useSmartMoneyStore . getState ( ) ;
483489 const p = period ?? sm . leaderboardPeriod ;
484490 const params = `period=${ p } ${ leaderboardOnly ? "&leaderboardOnly=1" : "" } ` ;
485- const res = await fetch ( `/api/smart-money?${ params } ` , { signal } ) ;
491+ const headers : HeadersInit = { } ;
492+ if ( ! leaderboardOnly && smartMoneyEtagRef . current ) headers [ "If-None-Match" ] = smartMoneyEtagRef . current ;
493+ const res = await fetch ( `/api/smart-money?${ params } ` , { signal, headers } ) ;
494+ if ( res . status === 304 ) return ; // data unchanged
486495 if ( ! res . ok ) return ;
496+ const etag = res . headers . get ( "etag" ) ;
497+ if ( etag && ! leaderboardOnly ) smartMoneyEtagRef . current = etag ;
487498 const data = await res . json ( ) ;
488499 const lb = data . leaderboard || [ ] ;
489500 lbCacheRef . current [ p ] = lb ;
0 commit comments