Skip to content

Commit c6e4f34

Browse files
authored
Merge pull request #38 from SiegfriedBz/develop
Merge Develop into Main feat(fe): keep publications list and stats in sync via layout WebSocket and query tuning ## Summary Fixes stale UI on `/publications` when users navigate between submit, list, and detail views, and when another wallet submits or status changes on-chain. Global metric cards now refresh together with the table. ###  Problem - `NewPublicationStatus` subscriptions lived only on the list table, so leaving `/publications` dropped the WebSocket and missed events (e.g. submit redirect, spectator on detail). - The publications list query inherited a 60s global staleTime, so returning to the list could show cached rows/status without refetching. - WebSocket invalidation targeted publications only; `useGlobalStats` (`statsKeys.all`) never refetched, so cards stayed stale while the table updated. - Detail polling was every 5s; with WSS driving status more often, 10s is enough for assigned reviewer events that do not emit NewPublicationStatus. ###  Solution - Add `PublicationsRealtimeProvider` and wrap the publications layout so `useWatchNewPublicationStatusEvent` runs for all /publications/* routes. - Remove the duplicate hook from `publications-table-container``. - Set staleTime: 0 on `usePublications` so the list background-refetches on mount. - On debounced `NewPublicationStatus` logs, invalidateQueries for `publicationsKeys.all` and `statsKeys.all`. - Bump `usePublicationDetail` refetchInterval from 5s to 10s while non-terminal. - Update READMEs accordingly.
2 parents 497db39 + 2d2ea42 commit c6e4f34

8 files changed

Lines changed: 45 additions & 26 deletions

File tree

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
## 🧪 Quick Start
77

8-
1. **Try the Demo:** [🌐 Live Demo](https://bio-verify-ai-dapp.vercel.app/)
8+
1. **Try the Demo:** [🌐 Live Demo](https://bio-verify-agentic-dapp.vercel.app)
99
2. **Get Testnet Sepolia ETH:** [Sepolia Faucet](https://sepolia-faucet.pk910.de/)
1010
3. **Swap for Base Sepolia ETH:** [Superbridge](https://superbridge.app/base-sepolia) (only if you want to use Base)
1111
4. **Connect your wallet** to the DApp (Base Sepolia or Ethereum Sepolia)
@@ -71,7 +71,7 @@ graph TD
7171

7272
### Event-Driven Data Flow
7373

74-
The contract uses a getter-less design: all state mutations emit events. These are projected off-chain into a Postgres read model, which powers all frontend queries. No on-chain reads required. In parallel, the frontend subscribes to `NewPublicationStatus` events via standalone viem WebSocket clients (Alchemy WSS), independent of wallet connection state, debouncing cache invalidations so the publications table stays in sync without polling.
74+
The contract uses a getter-less design: all state mutations emit events. These are projected off-chain into a Postgres read model, which powers all frontend queries. No on-chain reads required. In parallel, the frontend subscribes to `NewPublicationStatus` events via standalone viem WebSocket clients (Alchemy WSS), independent of wallet connection state, debouncing cache invalidations so the publications list, global stats strip, and related TanStack Query caches stay in sync without polling the table.
7575

7676
```mermaid
7777
graph LR
@@ -244,8 +244,8 @@ pnpm lint:format # Biome format
244244

245245
| Network | Contract Address |
246246
|:--------|:-----------------|
247-
| **[Base Sepolia](https://sepolia.basescan.org/address/0xf569d7b5016de6ef0f16fcae82d85d61249df31d)** | `0xf569D7b5016DE6eF0F16FCAe82d85d61249Df31d` |
248-
| **[Ethereum Sepolia](https://sepolia.etherscan.io/address/0xfce6990d551a60f8640a498b6bc34a15822ba3e3)** | `0xfce6990D551a60F8640a498B6bC34A15822BA3e3` |
247+
| **[Base Sepolia](https://sepolia.basescan.org/address/0x76654c2cdadcf869e78928f0785797b6be20f11b)** | `0x76654c2cdadcf869e78928f0785797b6be20f11b` |
248+
| **[Ethereum Sepolia](https://sepolia.etherscan.io/address/0x7d52170db31be4ab3d0166fbba937a031dc6e1ff)** | `0x7d52170db31be4ab3d0166fbba937a031dc6e1ff` |
249249

250250
## Design Decisions & Roadmap
251251

apps/fe/README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ graph TD
6060
Two provider trees serve different route groups:
6161

6262
- **`RootProviders`** (home): ThemeProvider -> CustomWagmiProvider (Reown AppKit + TanStack QueryClientProvider + NuqsAdapter) -> TooltipProvider
63-
- **`SideProvider`** (publications): Same stack plus `AppSideBarProvider` wrapping a collapsible sidebar layout
63+
- **`SideProvider`** (publications): Same stack plus `AppSideBarProvider` wrapping a collapsible sidebar layout, and **`PublicationsRealtimeProvider`** (under `app/(routes)/publications/layout.tsx`) which mounts `useWatchNewPublicationStatusEvent` so Alchemy WSS subscriptions stay active on every `/publications/*` route (list, new submission, detail, assignments)
6464

6565
`CustomWagmiProvider` initializes Reown AppKit with `baseSepolia` and `sepolia` networks, configures Alchemy HTTP transports for wallet-connected operations, and hydrates wallet state from cookies for SSR.
6666

@@ -85,9 +85,9 @@ sequenceDiagram
8585
TQ-->>Client: live data with smart polling
8686
```
8787

88-
**Smart polling**: Query hooks like `usePublicationDetail` use a dynamic `refetchInterval` that polls every 5 seconds while a publication is in a pending state, and automatically stops polling once it reaches a terminal status (`PUBLISHED`, `SLASHED`, or `EARLY_SLASHED`). The `PublicationDetailsProvider` context wraps this pattern, exposing live publication data and a syncing indicator to all child components.
88+
**Smart polling**: Query hooks like `usePublicationDetail` use a dynamic `refetchInterval` that polls every **10 seconds** while a publication is in a pending state, and automatically stops polling once it reaches a terminal status (`PUBLISHED`, `SLASHED`, or `EARLY_SLASHED`). This complements WebSocket-driven status updates: reviewer assignment (`Agent_PickReviewers`) and per-review progress (`Agent_RecordReview`) do not emit `NewPublicationStatus`, so polling still refreshes detail data between status transitions. The `PublicationDetailsProvider` context wraps this pattern, exposing live publication data and a syncing indicator to all child components.
8989

90-
**WebSocket cache invalidation**: The `/publications` table uses `useWatchNewPublicationStatusEvent` to subscribe to `NewPublicationStatus` on-chain events on both chains via standalone viem WebSocket clients (`eth_subscribe` over Alchemy WSS), independent of wagmi's wallet connection state. This means all visitors see real-time updates — even without a connected wallet. When events arrive, a debounced invalidation (3-second delay for CQRS eventual consistency) triggers a TanStack Query refetch, keeping the table in sync without polling or manual refresh.
90+
**WebSocket cache invalidation**: `PublicationsRealtimeProvider` calls `useWatchNewPublicationStatusEvent` to subscribe to `NewPublicationStatus` on both chains via standalone viem WebSocket clients (`eth_subscribe` over Alchemy WSS), independent of wagmi's wallet connection state. When events arrive, a debounced invalidation (3-second delay for CQRS eventual consistency) triggers TanStack Query refetches for **`publicationsKeys.all`** (list + detail queries under that prefix) and **`statsKeys.all`** (global stats strip on `/publications`), so the table and metric cards stay in sync without manual refresh. The publications list query also uses `staleTime: 0` so navigating back to `/publications` always background-refetches.
9191

9292
### CQRS Bridge
9393

@@ -130,9 +130,9 @@ The server action verifies the EIP-712 signature, then resumes the LangGraph rev
130130
| Route | Description |
131131
|-------|-------------|
132132
| `/` | Landing page with protocol mechanism walkthrough and CTAs |
133-
| `/publications` | Server-side paginated and filtered publications table. nuqs syncs filters (chain, status, page) to URL search params; server component passes them to the CQRS query. TanStack Table in manual mode. Real-time updates via WebSocket subscription to `NewPublicationStatus` on-chain events. |
133+
| `/publications` | Server-side paginated and filtered publications table plus global stats cards. nuqs syncs filters (chain, status, page) to URL search params; server component passes them to the CQRS query. TanStack Table in manual mode. Real-time updates: layout-mounted WebSocket on `NewPublicationStatus` invalidates publications + global stats (debounced). |
134134
| `/publications/new` | Submit publication form (react-hook-form + zod, IPFS manifest pinning via Pinata, on-chain `submitPublication`) |
135-
| `/publications/[chainId]/[pubId]` | Publication detail with live smart-polling, verdict timeline, economics sidebar, participants list |
135+
| `/publications/[chainId]/[pubId]` | Publication detail with live smart-polling (10s while non-terminal), verdict timeline, economics sidebar, participants list |
136136
| `/publications/[chainId]/[pubId]/review` | Reviewer form with EIP-712 signing and agent handoff |
137137
| `/publications/assignments` | Reviewer dashboard: stake management, assigned publications table, review status tracking |
138138

@@ -145,8 +145,8 @@ The server action verifies the EIP-712 signature, then resumes the LangGraph rev
145145

146146
## Key Patterns
147147

148-
- **Smart polling** -- `refetchInterval` dynamically stops when a publication reaches a terminal status, eliminating unnecessary network requests
149-
- **WebSocket real-time invalidation** -- `useWatchNewPublicationStatusEvent` subscribes to `NewPublicationStatus` on-chain events via standalone viem WebSocket clients (Alchemy WSS) on both chains, independent of wallet state; rapid-fire events are debounced into a single TanStack Query cache invalidation (3-second delay for CQRS eventual consistency)
148+
- **Smart polling** -- `refetchInterval` (10s while non-terminal) dynamically stops when a publication reaches a terminal status, covering DB fields not tied to `NewPublicationStatus`
149+
- **WebSocket real-time invalidation** -- `useWatchNewPublicationStatusEvent` (via `PublicationsRealtimeProvider` in the publications layout) subscribes to `NewPublicationStatus` on-chain events via standalone viem WebSocket clients (Alchemy WSS) on both chains, independent of wallet state; rapid-fire events are debounced into TanStack Query invalidations for publications and global stats (3-second delay for CQRS eventual consistency)
150150
- **Optimistic updates + delayed invalidation** -- immediate UI feedback on transactions, with a 3-second delayed cache invalidation to account for Alchemy webhook -> CQRS projection latency
151151
- **Server-first data loading** -- RSC fetches from Neon via `@packages/cqrs`, hydrates client hooks via `initialData` for zero-loading-state initial renders
152152
- **`"use server"` CQRS bridge** -- all Drizzle DB queries stay server-only, exposed to client hooks through Next.js Server Actions

apps/fe/_hooks/cqrs/queries/use-publication-detail.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,16 @@ export const usePublicationDetail = (params: Params) => {
1919
initialData,
2020
enabled: !!id,
2121

22-
// Smart Polling: Only poll every 5s if the publication is NOT finalized
22+
// Smart Polling: Only poll every 10s if the publication is NOT finalized
23+
// (WebSocket handles NewPublicationStatus; polling covers Agent_PickReviewers)
2324
refetchInterval: (query) => {
2425
const status = query.state.data?.status
2526
const isFinalized =
2627
status === PublicationStatusSchema.enum.PUBLISHED ||
2728
status === PublicationStatusSchema.enum.SLASHED ||
2829
status === PublicationStatusSchema.enum.EARLY_SLASHED
2930

30-
return isFinalized ? false : 5_000 // Poll every 5s if NOT finalized
31+
return isFinalized ? false : 10_000
3132
},
3233
})
3334

apps/fe/_hooks/cqrs/queries/use-publications.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const usePublications = (props: Props) => {
2323
queryKey: publicationsKeys.byQueryParams(searchQueryParams),
2424
queryFn: () => getPublications(searchQueryParams),
2525
initialData,
26+
staleTime: 0,
2627
})
2728

2829
return { data, isFetching, isError, refetch }

apps/fe/_hooks/websockets/use-watch-new-publication-status-event.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* @title Real-Time Publication Status Watcher
55
* @notice Subscribes to on-chain `NewPublicationStatus` events via WebSocket
66
* (eth_subscribe) on all supported chains, then invalidates the TanStack Query
7-
* publications cache so the UI refreshes automatically.
7+
* publications and stats caches so the list and metric cards refresh together.
88
*
99
* @dev Uses standalone viem WebSocket clients (not wagmi) so that subscriptions
1010
* remain active regardless of the user's wallet connection state. This is
@@ -30,6 +30,7 @@ import { useQueryClient } from "@tanstack/react-query"
3030
import { useCallback, useEffect, useRef } from "react"
3131
import type { Log, WatchContractEventReturnType } from "viem"
3232
import { publicationsKeys } from "../cqrs/query-keys/publications-keys"
33+
import { statsKeys } from "../cqrs/query-keys/stats-keys"
3334

3435
type NewPublicationStatusArgs = {
3536
pubId?: bigint
@@ -77,9 +78,14 @@ export const useWatchNewPublicationStatusEvent = () => {
7778
queryClient.invalidateQueries({
7879
queryKey: publicationsKeys.all,
7980
})
81+
queryClient.invalidateQueries({
82+
queryKey: statsKeys.all,
83+
})
8084

8185
if (process.env.NODE_ENV === "development") {
82-
console.log(`[WS] Cache invalidated for pubIds: ${pubIds.join(", ")}`)
86+
console.log(
87+
`[WS] Cache invalidated (publications + stats) for pubIds: ${pubIds.join(", ")}`,
88+
)
8389
}
8490
}, INVALIDATION_DELAY_MS)
8591
},
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"use client"
2+
3+
import { useWatchNewPublicationStatusEvent } from "@/_hooks/websockets/use-watch-new-publication-status-event"
4+
import type { FC, PropsWithChildren } from "react"
5+
6+
export const PublicationsRealtimeProvider: FC<PropsWithChildren> = ({
7+
children,
8+
}) => {
9+
useWatchNewPublicationStatusEvent()
10+
11+
return <>{children}</>
12+
}

apps/fe/app/(routes)/publications/_components/table/publications-table-container.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"use client"
22

33
import { usePublications } from "@/_hooks/cqrs/queries/use-publications"
4-
import { useWatchNewPublicationStatusEvent } from "@/_hooks/websockets/use-watch-new-publication-status-event"
54
import { FetchError } from "@/app/_components/fetch-error"
65
import type { PublicationsQueryParams } from "@packages/cqrs"
76
import type { PublicationsResponse } from "@packages/schema"
@@ -16,10 +15,7 @@ type Props = {
1615
export const PublicationsTableContainer = (props: Props) => {
1716
const { initialData, searchQueryParams } = props
1817

19-
// Alchemy Websockets (invalidate tanstack query publications keys on NewPublicationStatus on-chain event)
20-
useWatchNewPublicationStatusEvent()
21-
22-
// Fetch publications data
18+
// Fetch publications data (WebSocket invalidation lives in PublicationsRealtimeProvider in layout)
2319
const {
2420
data: publicationsResponse,
2521
isFetching,
Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import { SideProvider } from "@/_context/side-provider"
12
import { headers } from "next/headers"
23
import { Toaster } from "sonner"
3-
import { SideProvider } from "@/_context/side-provider"
4+
import { PublicationsRealtimeProvider } from "./_components/publications-realtime-provider"
45

56
export default async function Layout({
67
children,
@@ -14,11 +15,13 @@ export default async function Layout({
1415

1516
return (
1617
<SideProvider cookies={cookies}>
17-
{breadcrumbs}
18-
<main className="max-w-7xl flex flex-col gap-6 pt-6 pb-2">
19-
{children}
20-
<Toaster />
21-
</main>
18+
<PublicationsRealtimeProvider>
19+
{breadcrumbs}
20+
<main className="max-w-7xl flex flex-col gap-6 pt-6 pb-2">
21+
{children}
22+
<Toaster />
23+
</main>
24+
</PublicationsRealtimeProvider>
2225
</SideProvider>
2326
)
2427
}

0 commit comments

Comments
 (0)