diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6033a68 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,75 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + path: ~/.cache/biome + key: biome-${{ runner.os }}-${{ hashFiles('biome.json') }} + restore-keys: biome-${{ runner.os }}- + - uses: biomejs/setup-biome@v2 + with: + version: 2.1.2 + - run: biome ci --linter-enabled=true --formatter-enabled=true + + typecheck: + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: frontend/pnpm-lock.yaml + - run: pnpm install --frozen-lockfile + - run: pnpm typecheck + + build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: frontend/pnpm-lock.yaml + - run: pnpm install --frozen-lockfile + - run: pnpm build + + rust: + runs-on: ubuntu-latest + # Match the backend Dockerfile's build image so bindgen sees a libclang + # new enough for the upstream C23 (`constexpr`) headers from monad-bft. + container: rust:1.91-slim + steps: + - name: Install build dependencies + run: | + apt-get update + apt-get install -y git curl gcc g++ cmake pkg-config libssl-dev libclang-dev libzstd-dev libhugetlbfs-dev + - uses: actions/checkout@v4 + - run: rustup component add rustfmt clippy + - uses: Swatinem/rust-cache@v2 + - run: cargo fmt --all --check + - run: cargo clippy --all-targets --all-features -- -D warnings + - run: cargo build --all-targets diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index a45e8ad..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Lint - -on: - pull_request: - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Cache Biome - uses: actions/cache@v4 - with: - path: ~/.cache/biome - key: biome-${{ runner.os }}-${{ hashFiles('biome.json') }} - restore-keys: | - biome-${{ runner.os }}- - - name: Setup Biome - uses: biomejs/setup-biome@v2 - with: - version: 2.1.2 - - name: Run Biome - run: biome ci --linter-enabled=true --formatter-enabled=true diff --git a/backend/src/bin/client.rs b/backend/src/bin/client.rs index 4dbb548..85309ef 100644 --- a/backend/src/bin/client.rs +++ b/backend/src/bin/client.rs @@ -110,7 +110,7 @@ async fn main() -> Result<(), Box> { }, SerializableExecEvent::TxnEvmOutput { txn_index, .. } => { if let Some((txn_hash, txn_start_ns)) = client_state.txs_start_ns.remove(&txn_index) { - let txn_duration = std::time::Duration::from_nanos((event.timestamp_ns - txn_start_ns) as u64); + let txn_duration = std::time::Duration::from_nanos(event.timestamp_ns - txn_start_ns); client_state.block_txns_total_duration += txn_duration; log_event!("TxnEvmOutput", txn_index = txn_index, txn_hash = txn_hash, duration = txn_duration); @@ -120,7 +120,7 @@ async fn main() -> Result<(), Box> { }, SerializableExecEvent::BlockPerfEvmExit => { log_event!("BlockPerfEvmExit"); - let block_duration = std::time::Duration::from_nanos((event.timestamp_ns - client_state.block_start_ns) as u64); + let block_duration = std::time::Duration::from_nanos(event.timestamp_ns - client_state.block_start_ns); let parallel_execution_savings = client_state.block_txns_total_duration.checked_sub(block_duration); let savings_pct = if parallel_execution_savings.is_none() { // This only happens with really small/empty blocks error!("Parallel execution savings is negative: txs={:?} block={:?} height={}", client_state.block_txns_total_duration, block_duration, client_state.current_block_number); diff --git a/backend/src/lib/event_filter.rs b/backend/src/lib/event_filter.rs index 41f8373..81f5311 100644 --- a/backend/src/lib/event_filter.rs +++ b/backend/src/lib/event_filter.rs @@ -63,7 +63,7 @@ pub struct ArrayPrefixFilter { impl ArrayPrefixFilter { /// Checks if input array starts with filter values (prefix match) - pub fn matches(&self, value: &Vec) -> bool { + pub fn matches(&self, value: &[T]) -> bool { self.values.is_empty() || value.starts_with(&self.values) } @@ -192,7 +192,7 @@ impl EventFilter { return true; } - if self.includes_native_transfers() && is_native_transfer(&event) { + if self.includes_native_transfers() && is_native_transfer(event) { return true; } diff --git a/backend/src/lib/event_listener.rs b/backend/src/lib/event_listener.rs index 27a8522..6d311fc 100644 --- a/backend/src/lib/event_listener.rs +++ b/backend/src/lib/event_listener.rs @@ -85,7 +85,7 @@ impl EventName { } } - pub fn from_str(s: &str) -> Option { + pub fn from_name(s: &str) -> Option { match s { "RECORD_ERROR" => Some(EventName::RecordError), "BLOCK_START" => Some(EventName::BlockStart), @@ -157,7 +157,7 @@ fn event_to_data(event: &EventDescriptor) -> Option } = event.info(); // Convert event_type to EventName enum for type safety - let event_name = EventName::from_str(EXEC_EVENT_NAMES[event_type as usize])?; + let event_name = EventName::from_name(EXEC_EVENT_NAMES[event_type as usize])?; // Get block number if present let block_number = if flow_info.block_seqno != 0 { @@ -284,7 +284,7 @@ pub fn run_event_listener( last_event_timestamp_ns = Some(event.info().record_epoch_nanos); event_count += 1; - if event_count % 100 == 0 { + if event_count.is_multiple_of(100) { debug!("Processed {} events", event_count); } diff --git a/backend/src/lib/serializable_event.rs b/backend/src/lib/serializable_event.rs index 2005903..f5faba6 100644 --- a/backend/src/lib/serializable_event.rs +++ b/backend/src/lib/serializable_event.rs @@ -326,7 +326,7 @@ impl From<&EventData> for SerializableEventData { txn_hash: data.txn_hash.map(B256::from), payload: SerializableExecEvent::from(&data.payload), seqno: data.seqno, - timestamp_ns: data.timestamp_ns.clone(), + timestamp_ns: data.timestamp_ns, } } } diff --git a/backend/src/lib/server.rs b/backend/src/lib/server.rs index a2c470c..57766bd 100644 --- a/backend/src/lib/server.rs +++ b/backend/src/lib/server.rs @@ -43,7 +43,7 @@ pub struct TopAccessesData { #[derive(Debug, Clone)] pub enum EventDataOrMetrics { - Event(EventData), + Event(Box), TopAccesses(TopAccessesData), TPS(usize) } @@ -80,7 +80,7 @@ impl TPSTracker { self.block_2_txs = self.block_3_txs; self.block_3_txs = self.current_tx_count; self.current_tx_count = 0; - return self.block_1_txs + self.block_2_txs + (self.block_3_txs / 2); + self.block_1_txs + self.block_2_txs + (self.block_3_txs / 2) } } @@ -95,7 +95,7 @@ fn process_event( ) { match event { EventDataOrMetrics::Event(event_data) => { - let serializable = SerializableEventData::from(&event_data); + let serializable = SerializableEventData::from(&*event_data); if filter.matches_event(&serializable) { events_buf.push(serializable); } @@ -289,13 +289,9 @@ async fn run_event_forwarder_task( } // Send accesses update on BlockEnd events (after all access events are processed) - let send_accesses_update = if let EventName::BlockEnd = event_data.event_name { - true - } else { - false - }; + let send_accesses_update = matches!(event_data.event_name, EventName::BlockEnd); - let _ = event_broadcast_sender.send(EventDataOrMetrics::Event(event_data)); + let _ = event_broadcast_sender.send(EventDataOrMetrics::Event(Box::new(event_data))); if send_accesses_update { let top_accesses_data = TopAccessesData { diff --git a/frontend/components/network-activity-tracker/tps-chart.tsx b/frontend/components/network-activity-tracker/tps-chart.tsx index 1394b5a..05906ec 100644 --- a/frontend/components/network-activity-tracker/tps-chart.tsx +++ b/frontend/components/network-activity-tracker/tps-chart.tsx @@ -1,6 +1,7 @@ 'use client' import Image from 'next/image' +import { useEffect, useState } from 'react' import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts' import { type ChartConfig, @@ -9,7 +10,7 @@ import { ChartTooltipContent, } from '@/components/ui/chart' import { useTotalTransactions } from '@/hooks/use-total-transactions' -import { useTps } from '@/hooks/use-tps' +import { type TpsDataPoint, useTps } from '@/hooks/use-tps' import { formatRelativeTime, formatTimeHMS } from '@/lib/timestamp' import { formatIntNumber } from '@/lib/ui' import { NetworkActivityStats } from './network-activity-stats' @@ -21,10 +22,96 @@ const chartConfig = { }, } satisfies ChartConfig +/** Maximum visible time window of the chart, matching the TPS history retained. */ +const CHART_WINDOW_MS = 5 * 60 * 1000 + +/** Smallest window to show early on, so the chart starts zoomed in rather than mostly empty. */ +const MIN_WINDOW_MS = 3 * 1000 + +/** + * Drives a smoothly advancing "now" so the chart's x-domain slides + * continuously instead of jumping by one slot as each point arrives. + * Updates every animation frame for the smoothest motion; rAF auto-pauses + * when the tab is hidden. + */ +function useSlidingNow(): number { + const [now, setNow] = useState(() => Date.now()) + + useEffect(() => { + let raf: number + const tick = () => { + setNow(Date.now()) + raf = requestAnimationFrame(tick) + } + raf = requestAnimationFrame(tick) + return () => cancelAnimationFrame(raf) + }, []) + + return now +} + +/** + * Returns the history with its newest segment progressively "drawn": instead of + * the last point appearing fully-formed, a head vertex travels from the previous + * point to the newest one over the inter-arrival interval. `now` advances every + * frame, so the head moves smoothly. Once it reaches the newest point the segment + * is complete and the next arrival starts drawing the following one. + */ +function drawHistory(history: TpsDataPoint[], now: number): TpsDataPoint[] { + if (history.length < 2) return history + + const target = history[history.length - 1] + const from = history[history.length - 2] + const duration = target.timestamp - from.timestamp + if (duration <= 0) return history + + const progress = Math.min(1, Math.max(0, (now - target.timestamp) / duration)) + const head: TpsDataPoint = { + timestamp: from.timestamp + (target.timestamp - from.timestamp) * progress, + tps: from.tps + (target.tps - from.tps) * progress, + } + + return [...history.slice(0, -1), head] +} + +/** Nice, human-friendly tick steps in ms for the relative-time x-axis. */ +const TICK_STEPS_MS = [1, 2, 5, 10, 15, 30, 60, 120, 300].map((s) => s * 1000) + +/** + * Builds evenly-spaced ticks anchored at the sliding edge (`end`) and stepping + * backwards. Anchoring at `end` keeps a single "now" pinned to the far right; + * supplying ticks explicitly also avoids recharts' auto-generated ticks landing + * both at the edge and at the current second (which both format as "now"). + */ +function buildTicks(start: number, end: number): number[] { + const step = + TICK_STEPS_MS.find((s) => s >= (end - start) / 6) ?? + TICK_STEPS_MS[TICK_STEPS_MS.length - 1] + + const ticks: number[] = [] + for (let t = end; t >= start; t -= step) { + ticks.push(t) + } + return ticks.reverse() +} + export function TpsChart() { const { currentTps, peakTps, history } = useTps() const totalTransactions = useTotalTransactions() const hasData = history.length > 0 + const now = useSlidingNow() + + // Start zoomed in to the earliest data point and expand the window as data + // accumulates, capping at CHART_WINDOW_MS once we have 5 minutes of history. + const earliest = history[0]?.timestamp ?? now + const windowStart = Math.max( + now - CHART_WINDOW_MS, + Math.min(earliest, now - MIN_WINDOW_MS), + ) + + // Progressively draw the newest segment rather than snapping it into place. + const chartData = drawHistory(history, now) + const ticks = buildTicks(windowStart, now) return (
@@ -59,7 +146,7 @@ export function TpsChart() { className="h-full min-w-2xl w-full p-0" > @@ -77,6 +164,11 @@ export function TpsChart() { { key: T @@ -27,11 +28,19 @@ interface ServerMessage { TPS?: number } +export interface SubscribeOptions { + // Restrict delivery to these event types. Omit to receive all events. + eventTypes?: readonly EventName[] +} + interface EventsContextValue { accountAccesses: AccessEntry[] storageAccesses: AccessEntry<[string, string]>[] isConnected: boolean - subscribe: (callback: (event: SerializableEventData) => void) => () => void + subscribe: ( + callback: (event: SerializableEventData) => void, + options?: SubscribeOptions, + ) => () => void subscribeToTps: (callback: (tps: number) => void) => () => void } @@ -39,6 +48,11 @@ interface EventsProviderProps { children: ReactNode } +interface Subscriber { + callback: (event: SerializableEventData) => void + eventTypes: ReadonlySet | null +} + const EventsContext = createContext(null) const RECONNECT_DELAY = 3000 @@ -57,9 +71,7 @@ export function EventsProvider({ children }: EventsProviderProps) { >([]) const [isConnected, setIsConnected] = useState(false) const wsRef = useRef(null) - const subscribersRef = useRef< - Map void> - >(new Map()) + const subscribersRef = useRef>(new Map()) const tpsSubscribersRef = useRef void>>( new Map(), ) @@ -84,6 +96,17 @@ export function EventsProvider({ children }: EventsProviderProps) { } ws.onmessage = (event) => { + // Browsers don't throttle WebSocket onmessage in background tabs, but + // the heavy downstream work (setState fan-out, viem.decodeEventLog) + // still allocates aggressively. Dropping messages while hidden keeps + // the JS heap from blowing up during long backgrounded sessions. + if ( + typeof document !== 'undefined' && + document.visibilityState === 'hidden' + ) { + return + } + try { const message: ServerMessage = JSON.parse(event.data) @@ -101,13 +124,19 @@ export function EventsProvider({ children }: EventsProviderProps) { if (message.Events && message.Events.length > 0) { const newEvents = message.Events - - // Notify all subscribers - newEvents.forEach((evt) => { - subscribersRef.current.forEach((callback) => { - callback(evt) + const subscribers = subscribersRef.current + + for (const evt of newEvents) { + const eventType = evt.payload.type as EventName + subscribers.forEach((sub) => { + if ( + sub.eventTypes === null || + sub.eventTypes.has(eventType) + ) { + sub.callback(evt) + } }) - }) + } } } catch (error) { console.error('Failed to parse message:', error) @@ -147,11 +176,17 @@ export function EventsProvider({ children }: EventsProviderProps) { }, []) const subscribe = useCallback( - (callback: (event: SerializableEventData) => void): (() => void) => { + ( + callback: (event: SerializableEventData) => void, + options?: SubscribeOptions, + ): (() => void) => { const subscriberId = Math.random().toString(36).slice(2) - subscribersRef.current.set(subscriberId, callback) + const eventTypes = + options?.eventTypes && options.eventTypes.length > 0 + ? new Set(options.eventTypes) + : null + subscribersRef.current.set(subscriberId, { callback, eventTypes }) - // Return unsubscribe function return () => { subscribersRef.current.delete(subscriberId) } @@ -168,13 +203,18 @@ export function EventsProvider({ children }: EventsProviderProps) { } }, []) - const value: EventsContextValue = { - accountAccesses, - storageAccesses, - isConnected, - subscribe, - subscribeToTps, - } + // Without memoization, every TopAccesses update re-renders every consumer of + // useEventsContext, even ones that only need subscribe/subscribeToTps. + const value = useMemo( + () => ({ + accountAccesses, + storageAccesses, + isConnected, + subscribe, + subscribeToTps, + }), + [accountAccesses, storageAccesses, isConnected, subscribe, subscribeToTps], + ) return ( {children} diff --git a/frontend/hooks/use-block-execution-tracker.ts b/frontend/hooks/use-block-execution-tracker.ts index 97188b7..0511c8e 100644 --- a/frontend/hooks/use-block-execution-tracker.ts +++ b/frontend/hooks/use-block-execution-tracker.ts @@ -1,267 +1,246 @@ -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useMemo, useRef, useState } from 'react' import { fromNsToMsPrecise, getBlockWallTimeMs, getTotalTransactionTimeMs, } from '@/lib/block-metrics' -import type { Block } from '@/types/block' -import type { SerializableEventData } from '@/types/events' +import type { Block, BlockState } from '@/types/block' +import type { EventName, SerializableEventData } from '@/types/events' +import type { Transaction } from '@/types/transaction' import { useEvents } from './use-events' const MAX_BLOCKS = 200 +const MAX_INFLIGHT_BLOCKS = MAX_BLOCKS // Highlight when total tx execution time exceeds block execution time. // Keep this as a single constant so UI/copy can stay consistent. export const PARALLEL_EXECUTION_RATIO_THRESHOLD = 1 +const EXECUTION_EVENT_TYPES: readonly EventName[] = [ + 'BlockStart', + 'TxnHeaderStart', + 'TxnEnd', + 'TxnEvmOutput', + 'BlockEnd', + 'BlockQC', + 'BlockFinalized', + 'BlockVerified', +] + +interface InflightBlock { + block: Omit + // Keyed by txnIndex so per-event updates are O(1) instead of O(N). + transactions: Map +} + +/** + * Build the immutable Block snapshot that gets pushed into React state. + */ +function freezeInflightBlock(entry: InflightBlock, state: BlockState): Block { + const txs = Array.from(entry.transactions.values()).sort( + (a, b) => a.txnIndex - b.txnIndex, + ) + return { + ...entry.block, + state, + transactions: txs, + } +} + /** * Hook to track block execution events and derive timing metrics. + * + * In-flight blocks (and their transactions) live in refs and are mutated in + * place as events arrive. Only when a block finalizes do we materialize it + * into React state, which avoids the O(N²) array allocations the previous + * implementation did on every txn event during a block. */ export function useBlockExecutionTracker() { - const [blocks, setBlocks] = useState([]) + const [finalizedBlocks, setFinalizedBlocks] = useState([]) + const inflightRef = useRef>(new Map()) + const currentBlockNumberRef = useRef(null) - /** - * Replace the last element of an array with a new value. - * Returns a new array (shallow copy) so React detects the change, - * but reuses all object references except the replaced element. - */ - const replaceLastBlock = (prev: Block[], updated: Block): Block[] => { - const next = prev.slice() - next[next.length - 1] = updated - return next - } - - /** - * Replace a single block by index. - * Returns a new array (shallow copy) with only that one element changed. - */ - const replaceBlockAt = ( - prev: Block[], - index: number, - updated: Block, - ): Block[] => { - const next = prev.slice() - next[index] = updated - return next - } + const updateFinalizedState = useCallback( + (blockNumber: number, state: BlockState) => { + setFinalizedBlocks((prev) => { + const index = prev.findIndex((b) => b.number === blockNumber) + if (index === -1) return prev + if (prev[index].state === state) return prev + const next = prev.slice() + next[index] = { ...prev[index], state } + return next + }) + }, + [], + ) - // Handle real-time events from the backend - const handleEvent = useCallback((event: SerializableEventData) => { - switch (event.payload.type) { - case 'BlockStart': { - const payload = event.payload - const blockNumber = event.block_number || payload.block_number - if (blockNumber === undefined) { - console.warn('BlockStart event missing block_number:', event) - break + const promoteInflightToFinalized = useCallback( + (blockNumber: number, state: BlockState) => { + const entry = inflightRef.current.get(blockNumber) + if (!entry) { + updateFinalizedState(blockNumber, state) + return + } + inflightRef.current.delete(blockNumber) + if (currentBlockNumberRef.current === blockNumber) { + currentBlockNumberRef.current = null + } + const frozen = freezeInflightBlock(entry, state) + setFinalizedBlocks((prev) => { + const next = [...prev, frozen] + if (next.length > MAX_BLOCKS) { + return next.slice(-Math.ceil(MAX_BLOCKS / 3)) } - setBlocks((prev) => { - const existingBlock = prev.find((b) => b.id === payload.block_id) + return next + }) + }, + [updateFinalizedState], + ) - // Should never happen - if (existingBlock) { - console.warn( - '2 BlockStart events received on block:', - payload.block_number, - ) - const lastBlock = prev[prev.length - 1] - return replaceLastBlock(prev, { - ...lastBlock, - state: 'proposed', - startTimestamp: BigInt(event.timestamp_ns), - }) + const handleEvent = useCallback( + (event: SerializableEventData) => { + switch (event.payload.type) { + case 'BlockStart': { + const payload = event.payload + const blockNumber = event.block_number || payload.block_number + if (blockNumber === undefined) { + console.warn('BlockStart event missing block_number:', event) + return } - - // Create new block — this is the only case that grows the array - const newBlocks = [ - ...prev, - { + const existing = inflightRef.current.get(blockNumber) + if (existing && existing.block.id === payload.block_id) { + // Duplicate BlockStart for the same block id — refresh timing. + existing.block.state = 'proposed' + existing.block.startTimestamp = BigInt(event.timestamp_ns) + currentBlockNumberRef.current = blockNumber + return + } + inflightRef.current.set(blockNumber, { + block: { id: payload.block_id, number: blockNumber, - state: 'proposed' as const, + state: 'proposed', startTimestamp: BigInt(event.timestamp_ns), - transactions: [], }, - ] + transactions: new Map(), + }) + currentBlockNumberRef.current = blockNumber - if (newBlocks.length > MAX_BLOCKS) { - return newBlocks.slice(-Math.ceil(MAX_BLOCKS / 3)) + // Defensive cap in case finalization events never arrive for some + // blocks — without this the inflight map could grow without bound. + if (inflightRef.current.size > MAX_INFLIGHT_BLOCKS) { + const oldest = Math.min(...inflightRef.current.keys()) + inflightRef.current.delete(oldest) } - return newBlocks - }) - break - } + return + } - case 'TxnHeaderStart': { - const payload = event.payload - setBlocks((prev) => { - if (prev.length === 0) { + case 'TxnHeaderStart': { + const payload = event.payload + const blockNumber = currentBlockNumberRef.current + if (blockNumber === null) { console.warn( - 'TxnHeaderStart event received but no blocks exist yet:', + 'TxnHeaderStart event received but no inflight block:', event, ) - return prev + return } - const lastBlock = prev[prev.length - 1] - return replaceLastBlock(prev, { - ...lastBlock, - transactions: [ - ...(lastBlock.transactions ?? []), - { - id: payload.txn_index, - txnIndex: payload.txn_index, - txnHash: payload.txn_hash, - startTimestamp: BigInt(event.timestamp_ns), - transactionTime: undefined, - gasLimit: payload.gas_limit, - sender: payload.sender, - to: payload.to, - }, - ], + const entry = inflightRef.current.get(blockNumber) + if (!entry) return + entry.transactions.set(payload.txn_index, { + id: payload.txn_index, + txnIndex: payload.txn_index, + txnHash: payload.txn_hash, + startTimestamp: BigInt(event.timestamp_ns), + transactionTime: undefined, + gasLimit: payload.gas_limit, + sender: payload.sender, + to: payload.to, }) - }) - break - } - - case 'TxnEnd': { - if (event.txn_idx === undefined) { - console.warn('TxnEnd event missing txn_idx:', event) - break + return } - setBlocks((prev) => { - if (prev.length === 0) { - console.warn( - 'TxnEnd event received but no blocks exist yet:', - event, - ) - return prev - } - const lastBlock = prev[prev.length - 1] - return replaceLastBlock(prev, { - ...lastBlock, - transactions: (lastBlock.transactions ?? []).map((tx) => - tx.txnIndex === event.txn_idx && tx.startTimestamp - ? { - ...tx, - endTimestamp: BigInt(event.timestamp_ns), - transactionTime: - BigInt(event.timestamp_ns) - tx.startTimestamp, - } - : tx, - ), - }) - }) - break - } - case 'TxnEvmOutput': { - const payload = event.payload - setBlocks((prev) => { - if (prev.length === 0) { - console.warn( - 'TxnEvmOutput event received but no blocks exist yet:', - event, - ) - return prev + case 'TxnEnd': { + if (event.txn_idx === undefined) { + console.warn('TxnEnd event missing txn_idx:', event) + return } - const lastBlock = prev[prev.length - 1] - return replaceLastBlock(prev, { - ...lastBlock, - transactions: (lastBlock.transactions ?? []).map((tx) => - tx.txnIndex === payload.txn_index - ? { - ...tx, - status: payload.status, - gasUsed: payload.gas_used, - } - : tx, - ), - }) - }) - break - } + const blockNumber = currentBlockNumberRef.current + if (blockNumber === null) return + const entry = inflightRef.current.get(blockNumber) + if (!entry) return + const tx = entry.transactions.get(event.txn_idx) + if (!tx || tx.startTimestamp === undefined) return + const endTs = BigInt(event.timestamp_ns) + tx.endTimestamp = endTs + tx.transactionTime = endTs - tx.startTimestamp + return + } - case 'BlockQC': { - const payload = event.payload - const blockNumber = event.block_number || payload.block_number - if (blockNumber === undefined) { - break + case 'TxnEvmOutput': { + const payload = event.payload + const blockNumber = currentBlockNumberRef.current + if (blockNumber === null) return + const entry = inflightRef.current.get(blockNumber) + if (!entry) return + const tx = entry.transactions.get(payload.txn_index) + if (!tx) return + tx.status = payload.status + tx.gasUsed = payload.gas_used + return } - setBlocks((prev) => { - const index = prev.findIndex((b) => b.number === blockNumber) - if (index === -1) return prev - return replaceBlockAt(prev, index, { - ...prev[index], - state: 'voted', - }) - }) - break - } - case 'BlockFinalized': { - const payload = event.payload - const blockNumber = event.block_number || payload.block_number - if (blockNumber === undefined) { - break + case 'BlockEnd': { + const blockNumber = event.block_number + if (blockNumber === undefined) return + const entry = inflightRef.current.get(blockNumber) + if (!entry || entry.block.startTimestamp === undefined) return + const endTs = BigInt(event.timestamp_ns) + entry.block.endTimestamp = endTs + entry.block.executionTime = endTs - entry.block.startTimestamp + return } - setBlocks((prev) => { - const index = prev.findIndex((b) => b.number === blockNumber) - if (index === -1) return prev - return replaceBlockAt(prev, index, { - ...prev[index], - state: 'finalized', - }) - }) - break - } - case 'BlockVerified': { - const payload = event.payload - const blockNumber = event.block_number || payload.block_number - if (blockNumber === undefined) { - break + case 'BlockQC': { + const payload = event.payload + const blockNumber = event.block_number || payload.block_number + if (blockNumber === undefined) return + const entry = inflightRef.current.get(blockNumber) + if (entry) { + entry.block.state = 'voted' + } else { + updateFinalizedState(blockNumber, 'voted') + } + return } - setBlocks((prev) => { - const index = prev.findIndex((b) => b.number === blockNumber) - if (index === -1) return prev - return replaceBlockAt(prev, index, { - ...prev[index], - state: 'verified', - }) - }) - break - } - case 'BlockEnd': - setBlocks((prev) => { - const index = prev.findIndex( - (b) => b.number === event?.block_number && b.startTimestamp, - ) - if (index === -1) return prev - const block = prev[index] - return replaceBlockAt(prev, index, { - ...block, - endTimestamp: BigInt(event.timestamp_ns), - executionTime: BigInt(event.timestamp_ns) - block.startTimestamp!, - }) - }) - break + case 'BlockFinalized': { + const payload = event.payload + const blockNumber = event.block_number || payload.block_number + if (blockNumber === undefined) return + promoteInflightToFinalized(blockNumber, 'finalized') + return + } + + case 'BlockVerified': { + const payload = event.payload + const blockNumber = event.block_number || payload.block_number + if (blockNumber === undefined) return + promoteInflightToFinalized(blockNumber, 'verified') + return + } - default: - break - } - }, []) + default: + return + } + }, + [promoteInflightToFinalized, updateFinalizedState], + ) - // Subscribe to real-time events useEvents({ onEvent: handleEvent, + eventTypes: EXECUTION_EVENT_TYPES, }) - // Memoize computed values to avoid unnecessary recalculations - const finalizedBlocks = useMemo( - () => - blocks.filter((b) => b.state === 'finalized' || b.state === 'verified'), - [blocks], - ) const maxBlockExecutionTime = useMemo(() => { return fromNsToMsPrecise( @@ -301,7 +280,6 @@ export function useBlockExecutionTracker() { }, [finalizedBlocks]) return { - blocks, finalizedBlocks, maxBlockExecutionTime, normalizedTimeScaleMs, diff --git a/frontend/hooks/use-block-state-tracker.ts b/frontend/hooks/use-block-state-tracker.ts index 01ba61e..f0067dc 100644 --- a/frontend/hooks/use-block-state-tracker.ts +++ b/frontend/hooks/use-block-state-tracker.ts @@ -5,9 +5,16 @@ import { useBlockchainSlowMotion } from '@/hooks/use-blockchain-slow-motion' import { useEvents } from '@/hooks/use-events' import { formatTimestamp } from '@/lib/timestamp' import type { Block, BlockState } from '@/types/block' -import type { SerializableEventData } from '@/types/events' +import type { EventName, SerializableEventData } from '@/types/events' const MAX_BLOCKS = 200 +const BLOCK_STATE_EVENT_TYPES: readonly EventName[] = [ + 'BlockStart', + 'BlockQC', + 'BlockFinalized', + 'BlockVerified', + 'BlockReject', +] interface UseBlockStateTrackerReturn { blocks: Block[] @@ -102,6 +109,7 @@ export function useBlockStateTracker(): UseBlockStateTrackerReturn { useEvents({ onEvent: queueEvent, + eventTypes: BLOCK_STATE_EVENT_TYPES, }) return { diff --git a/frontend/hooks/use-events.ts b/frontend/hooks/use-events.ts index 15e944c..65d4842 100644 --- a/frontend/hooks/use-events.ts +++ b/frontend/hooks/use-events.ts @@ -2,10 +2,13 @@ import { useEffect, useRef } from 'react' import { useEventsContext } from '@/contexts/events-context' -import type { SerializableEventData } from '@/types/events' +import type { EventName, SerializableEventData } from '@/types/events' interface UseEventsOptions { onEvent?: (event: SerializableEventData) => void + // Restrict delivery to these event types. Pass a stable reference (e.g. a + // module-level constant) so the subscription isn't torn down on every render. + eventTypes?: readonly EventName[] } /** @@ -22,9 +25,15 @@ interface UseEventsOptions { * onEvent: (event) => console.log('New event:', event) * }) * ``` + * + * @example Restrict to specific event types + * ```tsx + * const EVENT_TYPES = ['TxnLog'] as const + * useEvents({ onEvent: handleLog, eventTypes: EVENT_TYPES }) + * ``` */ export function useEvents(options: UseEventsOptions = {}) { - const { onEvent } = options + const { onEvent, eventTypes } = options const { accountAccesses, storageAccesses, isConnected, subscribe } = useEventsContext() const onEventRef = useRef(onEvent) @@ -38,12 +47,15 @@ export function useEvents(options: UseEventsOptions = {}) { return } - const unsubscribe = subscribe((event) => { - onEventRef.current?.(event) - }) + const unsubscribe = subscribe( + (event) => { + onEventRef.current?.(event) + }, + eventTypes ? { eventTypes } : undefined, + ) return unsubscribe - }, [onEvent, subscribe]) + }, [onEvent, subscribe, eventTypes]) return { accountAccesses, storageAccesses, isConnected } } diff --git a/frontend/hooks/use-swap-events.ts b/frontend/hooks/use-swap-events.ts index b41245c..5d3852a 100644 --- a/frontend/hooks/use-swap-events.ts +++ b/frontend/hooks/use-swap-events.ts @@ -11,10 +11,11 @@ import { import { AUSD_ADDRESS, WMON_ADDRESS } from '@/constants/transfer-config' import { useEvents } from '@/hooks/use-events' import { parseTopicsString } from '@/lib/abi-decode' -import type { SerializableEventData } from '@/types/events' +import type { EventName, SerializableEventData } from '@/types/events' import type { SwapData } from '@/types/swap' const MAX_SWAPS = 2000 +const SWAP_EVENT_TYPES: readonly EventName[] = ['TxnLog'] /** * Parse raw log data into normalized SwapData based on the provider @@ -351,6 +352,7 @@ export function useSwapEvents() { const { isConnected } = useEvents({ onEvent: handleEvent, + eventTypes: SWAP_EVENT_TYPES, }) const clearSwaps = useCallback(() => { diff --git a/frontend/hooks/use-total-transactions.ts b/frontend/hooks/use-total-transactions.ts index 02b995a..4e6db48 100644 --- a/frontend/hooks/use-total-transactions.ts +++ b/frontend/hooks/use-total-transactions.ts @@ -2,9 +2,10 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { useEvents } from '@/hooks/use-events' -import type { SerializableEventData } from '@/types/events' +import type { EventName, SerializableEventData } from '@/types/events' const PUBLISH_INTERVAL_MS = 1000 +const TXN_HEADER_EVENT_TYPES: readonly EventName[] = ['TxnHeaderStart'] /** * Counts total transactions by observing `TxnHeaderStart`, but only publishes @@ -23,6 +24,7 @@ export function useTotalTransactions(): number { useEvents({ onEvent: handleTxnEvent, + eventTypes: TXN_HEADER_EVENT_TYPES, }) useEffect(() => { diff --git a/frontend/hooks/use-transfer-events.ts b/frontend/hooks/use-transfer-events.ts index 7e219d9..b457811 100644 --- a/frontend/hooks/use-transfer-events.ts +++ b/frontend/hooks/use-transfer-events.ts @@ -9,10 +9,11 @@ import { } from '@/constants/transfer-config' import { useEvents } from '@/hooks/use-events' import { parseTopicsString } from '@/lib/abi-decode' -import type { SerializableEventData } from '@/types/events' +import type { EventName, SerializableEventData } from '@/types/events' import type { TransferData } from '@/types/transfer' const MAX_TRANSFERS = 5000 +const TRANSFER_EVENT_TYPES: readonly EventName[] = ['TxnCallFrame', 'TxnLog'] /** * Parse native transfer event (TxnCallFrame with non-zero value) @@ -128,6 +129,7 @@ export function useTransferEvents() { const { isConnected } = useEvents({ onEvent: handleEvent, + eventTypes: TRANSFER_EVENT_TYPES, }) const clearTransfers = useCallback(() => { diff --git a/frontend/package.json b/frontend/package.json index e514025..5367bdc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,8 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "typecheck": "tsc --noEmit" }, "dependencies": { "@number-flow/react": "0.5.10", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 29937d9..6284395 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -274,56 +274,48 @@ packages: cpu: [arm64] os: [linux] libc: [glibc] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] libc: [glibc] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] libc: [glibc] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] libc: [glibc] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] libc: [glibc] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] libc: [glibc] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] libc: [musl] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] libc: [musl] - libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} @@ -331,7 +323,6 @@ packages: cpu: [arm64] os: [linux] libc: [glibc] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} @@ -339,7 +330,6 @@ packages: cpu: [arm] os: [linux] libc: [glibc] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} @@ -347,7 +337,6 @@ packages: cpu: [ppc64] os: [linux] libc: [glibc] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} @@ -355,7 +344,6 @@ packages: cpu: [riscv64] os: [linux] libc: [glibc] - libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} @@ -363,7 +351,6 @@ packages: cpu: [s390x] os: [linux] libc: [glibc] - libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} @@ -371,7 +358,6 @@ packages: cpu: [x64] os: [linux] libc: [glibc] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} @@ -379,7 +365,6 @@ packages: cpu: [arm64] os: [linux] libc: [musl] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} @@ -387,7 +372,6 @@ packages: cpu: [x64] os: [linux] libc: [musl] - libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -431,8 +415,6 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@next/env@16.2.6': - resolution: {integrity: sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==} '@next/env@16.2.6': resolution: {integrity: sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==} @@ -441,75 +423,27 @@ packages: '@next/swc-darwin-arm64@16.2.6': resolution: {integrity: sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==} - '@next/swc-darwin-arm64@16.2.6': - resolution: {integrity: sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] '@next/swc-darwin-x64@16.2.6': resolution: {integrity: sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==} - '@next/swc-darwin-x64@16.2.6': - resolution: {integrity: sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] '@next/swc-linux-arm64-gnu@16.2.6': resolution: {integrity: sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==} - '@next/swc-linux-arm64-gnu@16.2.6': - resolution: {integrity: sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [glibc] - libc: [glibc] '@next/swc-linux-arm64-musl@16.2.6': resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==} - '@next/swc-linux-arm64-musl@16.2.6': - resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [musl] - libc: [musl] '@next/swc-linux-x64-gnu@16.2.6': resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==} - '@next/swc-linux-x64-gnu@16.2.6': - resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [glibc] - libc: [glibc] '@next/swc-linux-x64-musl@16.2.6': resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==} - '@next/swc-linux-x64-musl@16.2.6': - resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [musl] - libc: [musl] '@next/swc-win32-arm64-msvc@16.2.6': resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==} - '@next/swc-win32-arm64-msvc@16.2.6': - resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] '@next/swc-win32-x64-msvc@16.2.6': resolution: {integrity: sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==} - '@next/swc-win32-x64-msvc@16.2.6': - resolution: {integrity: sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] '@noble/ciphers@1.3.0': resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} @@ -878,7 +812,6 @@ packages: cpu: [arm64] os: [linux] libc: [glibc] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.17': resolution: {integrity: sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==} @@ -886,7 +819,6 @@ packages: cpu: [arm64] os: [linux] libc: [musl] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.17': resolution: {integrity: sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==} @@ -894,7 +826,6 @@ packages: cpu: [x64] os: [linux] libc: [glibc] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.17': resolution: {integrity: sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==} @@ -902,7 +833,6 @@ packages: cpu: [x64] os: [linux] libc: [musl] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.17': resolution: {integrity: sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==} @@ -1084,56 +1014,48 @@ packages: cpu: [arm64] os: [linux] libc: [glibc] - libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] libc: [musl] - libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] libc: [glibc] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] libc: [glibc] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] libc: [musl] - libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] libc: [glibc] - libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] libc: [glibc] - libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] libc: [musl] - libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -1275,10 +1197,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - baseline-browser-mapping@2.10.24: - resolution: {integrity: sha512-I2NkZOOrj2XuguvWCK6OVh9GavsNjZjK908Rq3mIBK25+GD8vPX5w2WdxVqnQ7xx3SrZJiCiZFu+/Oz50oSYSA==} - engines: {node: '>=6.0.0'} - hasBin: true baseline-browser-mapping@2.8.32: resolution: {integrity: sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==} @@ -2024,7 +1942,6 @@ packages: cpu: [arm64] os: [linux] libc: [glibc] - libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} @@ -2032,7 +1949,6 @@ packages: cpu: [arm64] os: [linux] libc: [musl] - libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} @@ -2040,7 +1956,6 @@ packages: cpu: [x64] os: [linux] libc: [glibc] - libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} @@ -2048,7 +1963,6 @@ packages: cpu: [x64] os: [linux] libc: [musl] - libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} @@ -2137,26 +2051,6 @@ packages: next@16.2.6: resolution: {integrity: sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==} - next@16.2.6: - resolution: {integrity: sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==} - engines: {node: '>=20.9.0'} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.51.1 - babel-plugin-react-compiler: '*' - react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 - react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - '@playwright/test': - optional: true - babel-plugin-react-compiler: - optional: true - sass: - optional: true node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} @@ -2946,41 +2840,32 @@ snapshots: optional: true '@next/env@16.2.6': {} - '@next/env@16.2.6': {} '@next/eslint-plugin-next@16.0.6': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@16.2.6': '@next/swc-darwin-arm64@16.2.6': optional: true - '@next/swc-darwin-x64@16.2.6': '@next/swc-darwin-x64@16.2.6': optional: true - '@next/swc-linux-arm64-gnu@16.2.6': '@next/swc-linux-arm64-gnu@16.2.6': optional: true - '@next/swc-linux-arm64-musl@16.2.6': '@next/swc-linux-arm64-musl@16.2.6': optional: true - '@next/swc-linux-x64-gnu@16.2.6': '@next/swc-linux-x64-gnu@16.2.6': optional: true - '@next/swc-linux-x64-musl@16.2.6': '@next/swc-linux-x64-musl@16.2.6': optional: true - '@next/swc-win32-arm64-msvc@16.2.6': '@next/swc-win32-arm64-msvc@16.2.6': optional: true - '@next/swc-win32-x64-msvc@16.2.6': '@next/swc-win32-x64-msvc@16.2.6': optional: true @@ -3661,7 +3546,6 @@ snapshots: baseline-browser-mapping@2.10.24: {} - baseline-browser-mapping@2.10.24: {} baseline-browser-mapping@2.8.32: {} @@ -4613,25 +4497,15 @@ snapshots: next@16.2.6(@babel/core@7.28.5)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: - '@next/env': 16.2.6 '@next/env': 16.2.6 '@swc/helpers': 0.5.15 baseline-browser-mapping: 2.10.24 - baseline-browser-mapping: 2.10.24 caniuse-lite: 1.0.30001759 postcss: 8.4.31 react: 19.2.6 react-dom: 19.2.6(react@19.2.6) styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.6) optionalDependencies: - '@next/swc-darwin-arm64': 16.2.6 - '@next/swc-darwin-x64': 16.2.6 - '@next/swc-linux-arm64-gnu': 16.2.6 - '@next/swc-linux-arm64-musl': 16.2.6 - '@next/swc-linux-x64-gnu': 16.2.6 - '@next/swc-linux-x64-musl': 16.2.6 - '@next/swc-win32-arm64-msvc': 16.2.6 - '@next/swc-win32-x64-msvc': 16.2.6 '@next/swc-darwin-arm64': 16.2.6 '@next/swc-darwin-x64': 16.2.6 '@next/swc-linux-arm64-gnu': 16.2.6 diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml index 56dbf93..b0c9078 100644 --- a/frontend/pnpm-workspace.yaml +++ b/frontend/pnpm-workspace.yaml @@ -1,3 +1,7 @@ +allowBuilds: + sharp: true + unrs-resolver: true + minimumReleaseAge: 10080 minimumReleaseAgeExclude: