You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Merge pull request #37 from SiegfriedBz/feature/improve-publications-realtime-wss-stats
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.
3.**Swap for Base Sepolia ETH:**[Superbridge](https://superbridge.app/base-sepolia) (only if you want to use Base)
11
11
4.**Connect your wallet** to the DApp (Base Sepolia or Ethereum Sepolia)
@@ -71,7 +71,7 @@ graph TD
71
71
72
72
### Event-Driven Data Flow
73
73
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.
-**`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)
64
64
65
65
`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.
66
66
@@ -85,9 +85,9 @@ sequenceDiagram
85
85
TQ-->>Client: live data with smart polling
86
86
```
87
87
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.
89
89
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` onboth 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.
91
91
92
92
### CQRS Bridge
93
93
@@ -130,9 +130,9 @@ The server action verifies the EIP-712 signature, then resumes the LangGraph rev
130
130
| Route | Description |
131
131
|-------|-------------|
132
132
|`/`| 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). |
134
134
|`/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 |
136
136
|`/publications/[chainId]/[pubId]/review`| Reviewer form with EIP-712 signing and agent handoff |
@@ -145,8 +145,8 @@ The server action verifies the EIP-712 signature, then resumes the LangGraph rev
145
145
146
146
## Key Patterns
147
147
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)
150
150
-**Optimistic updates + delayed invalidation** -- immediate UI feedback on transactions, with a 3-second delayed cache invalidation to account for Alchemy webhook -> CQRS projection latency
151
151
-**Server-first data loading** -- RSC fetches from Neon via `@packages/cqrs`, hydrates client hooks via `initialData` for zero-loading-state initial renders
152
152
-**`"use server"` CQRS bridge** -- all Drizzle DB queries stay server-only, exposed to client hooks through Next.js Server Actions
0 commit comments