Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"eslint": "^9",
"eslint-config-next": "16.1.6",
"tailwindcss": "^4",
"typescript": "^5",
"typescript": "^5.9.3",
"webpack-bundle-analyzer": "^4.9.0"
}
}
15 changes: 15 additions & 0 deletions src/app/components/providers/HistoryProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use client'

import { ReactNode } from 'react'
import { useHistoricalSync } from '@/app/hooks/useHistoricalSync'

interface HistoryProviderProps {
children: ReactNode
enabled?: boolean
}

export function HistoryProvider({ children, enabled = true }: HistoryProviderProps) {
useHistoricalSync(enabled)

return <>{children}</>
}
122 changes: 122 additions & 0 deletions src/app/hooks/useHistoricalData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
'use client'

import { useState, useEffect, useCallback } from 'react'
import { getDataPoints, getLatestDataPoints, getAllAssetPairs } from '@/lib/indexeddb'
import type { HistoricalDataPoint } from '@/types'

interface UseHistoricalDataOptions {
fromTimestamp?: number
toTimestamp?: number
limit?: number
}

export function useHistoricalData(
assetPair: string | null,
options?: UseHistoricalDataOptions,
) {
const [data, setData] = useState<HistoricalDataPoint[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [refreshKey, setRefreshKey] = useState(0)

useEffect(() => {
let cancelled = false

async function load() {
const pair = assetPair
if (!pair) {
setData([])
setLoading(false)
setError(null)
return
}

setLoading(true)
setError(null)

try {
const points = await getDataPoints(pair, {
fromTimestamp: options?.fromTimestamp,
toTimestamp: options?.toTimestamp,
limit: options?.limit ?? 500,
})
if (!cancelled) {
setData(points)
setLoading(false)
}
} catch (err) {
if (!cancelled) {
setError(err instanceof Error ? err.message : 'Failed to load historical data')
setLoading(false)
}
}
}

load()
return () => { cancelled = true }
}, [assetPair, options?.fromTimestamp, options?.toTimestamp, options?.limit, refreshKey])

const refresh = useCallback(() => setRefreshKey(k => k + 1), [])

return { data, loading, error, refresh }
}

export function useLatestHistoricalData(
assetPair: string | null,
count: number = 50,
) {
const [data, setData] = useState<HistoricalDataPoint[]>([])
const [loading, setLoading] = useState(true)

useEffect(() => {
let cancelled = false

async function load() {
const pair = assetPair
if (!pair) {
setData([])
setLoading(false)
return
}

try {
const points = await getLatestDataPoints(pair, count)
if (!cancelled) setData(points)
} catch {
if (!cancelled) setData([])
} finally {
if (!cancelled) setLoading(false)
}
}

load()
return () => { cancelled = true }
}, [assetPair, count])

return { data, loading }
}

export function useHistoricalAssetPairs() {
const [pairs, setPairs] = useState<string[]>([])
const [loading, setLoading] = useState(true)

useEffect(() => {
let cancelled = false

async function load() {
try {
const result = await getAllAssetPairs()
if (!cancelled) setPairs(result)
} catch {
if (!cancelled) setPairs([])
} finally {
if (!cancelled) setLoading(false)
}
}

load()
return () => { cancelled = true }
}, [])

return { pairs, loading }
}
72 changes: 72 additions & 0 deletions src/app/hooks/useHistoricalSync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use client'

import { useEffect, useRef } from 'react'
import { storeDataPoints, pruneDataPoints } from '@/lib/indexeddb'
import { useSocketData } from '@/app/components/providers/SocketProvider'

const FLUSH_INTERVAL_MS = 10_000
const PRUNE_INTERVAL_MS = 300_000
const MAX_BATCH_SIZE = 50

export function useHistoricalSync(enabled: boolean = true) {
const { lastUpdate } = useSocketData()
const batchRef = useRef<Array<{
assetPair: string
timestamp: number
price: number
decimals: number
source: string
confidenceScore: number
}>>([])
const lastPruneRef = useRef(0)
const lastDataRef = useRef<string | null>(null)

useEffect(() => {
if (!enabled || !lastUpdate) return

const key = `${lastUpdate.assetPair}_${lastUpdate.timestamp}`
if (key === lastDataRef.current) return
lastDataRef.current = key

batchRef.current.push({
assetPair: lastUpdate.assetPair,
timestamp: lastUpdate.timestamp,
price: lastUpdate.price,
decimals: lastUpdate.decimals,
source: lastUpdate.source,
confidenceScore: lastUpdate.confidenceScore,
})

if (batchRef.current.length >= MAX_BATCH_SIZE) {
const batch = batchRef.current.splice(0, MAX_BATCH_SIZE)
storeDataPoints(batch).catch(() => {})
}
}, [lastUpdate, enabled])

useEffect(() => {
if (!enabled) return

const flushTimer = setInterval(() => {
if (batchRef.current.length > 0) {
const batch = batchRef.current.splice(0)
storeDataPoints(batch).catch(() => {})
}
}, FLUSH_INTERVAL_MS)

return () => clearInterval(flushTimer)
}, [enabled])

useEffect(() => {
if (!enabled) return

const pruneTimer = setInterval(() => {
const now = Date.now()
if (now - lastPruneRef.current >= PRUNE_INTERVAL_MS) {
lastPruneRef.current = now
pruneDataPoints().catch(() => {})
}
}, PRUNE_INTERVAL_MS)

return () => clearInterval(pruneTimer)
}, [enabled])
}
11 changes: 8 additions & 3 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { UserProvider } from "./components/providers/UserProvider";
import { QueryProvider } from "./components/providers/QueryProvider";
import Script from "next/script";
import {SocketProvider} from "./components/providers/SocketProvider";
import { HistoryProvider } from "./components/providers/HistoryProvider";

const geistSans = Geist({
variable: "--font-geist-sans",
Expand Down Expand Up @@ -74,9 +75,13 @@ export default function RootLayout({
>
<UserProvider>
<QueryProvider>
<ProgressBarProvider>
{children}
</ProgressBarProvider>
<SocketProvider>
<HistoryProvider>
<ProgressBarProvider>
{children}
</ProgressBarProvider>
</HistoryProvider>
</SocketProvider>
</QueryProvider>
</UserProvider>
</ThemeProvider>
Expand Down
Loading