Skip to content

Commit 50ece02

Browse files
committed
Restore real-time updates after Apollo to URQL migration
This commit restores real-time UI updates that were lost during the Apollo Client to URQL migration (b9bc664). The original migration incorrectly assumed URQL would automatically invalidate cache on external events, but URQL requires explicit cache invalidation triggers. Root Cause The Apollo to URQL migration removed three critical cache invalidation mechanisms: 1. 60-second periodic cache reset timer 2. Push notification-triggered cache resets (client.resetStore()) 3. Post-transaction cache resets with 3s + 6s delays All were removed with comments claiming "URQL handles this automatically" - which was false. Solution Implemented context-based cache invalidation system using React Context: Created: - CacheInvalidationContext.tsx - Provides timestamp-based invalidation via invalidate() function Modified: - providers.tsx - Wrapped app with CacheInvalidationProvider - useSwResetMessage.tsx - Calls invalidate() when push notifications arrive - useWriteContractWithNotifications.ts - Calls invalidate() 3s after transaction confirms (allows Subsquid indexing time) - useUserNotifications.tsx, useJobEvents.tsx, useJob.tsx - Added cache invalidation context with cache-and-network request policy Key Benefits: - Push notifications now trigger UI updates without page refresh - Own messages appear 3 seconds after sending (Subsquid indexing delay) - Cleaner architecture using React Context vs manual cache resets - Works across all browsers (Chrome requires notification permissions enabled) Technical Details When invalidate() is called, it updates a timestamp in React Context. URQL queries include this timestamp in their context parameter. When the timestamp changes, URQL treats it as a new query request and refetches with network-only behavior, bypassing the cache.
1 parent ebcfa28 commit 50ece02

File tree

7 files changed

+84
-17
lines changed

7 files changed

+84
-17
lines changed

website/src/app/providers.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { urqlClient } from '@/lib/urql-client';
1212
import { useEffect, useState } from 'react';
1313
import { useMediaDownloadHandler } from '@/hooks/useMediaDownloadHandler';
1414
import { useRegisterWebPushNotifications } from '@/hooks/useRegisterWebPushNotifications';
15+
import { CacheInvalidationProvider } from '@/contexts/CacheInvalidationContext';
1516

1617
declare module 'abitype' {
1718
export interface Register {
@@ -82,14 +83,16 @@ export function Providers({ children }: { children: React.ReactNode }) {
8283
useMediaDownloadHandler();
8384

8485
return (
85-
<UrqlProvider value={urqlClient}>
86-
<WagmiProvider config={config}>
87-
<QueryClientProvider client={queryClient}>
88-
<RainbowKitProvider initialChain={initialChain}>
89-
<Inititalizers>{children}</Inititalizers>
90-
</RainbowKitProvider>
91-
</QueryClientProvider>
92-
</WagmiProvider>
93-
</UrqlProvider>
86+
<CacheInvalidationProvider>
87+
<UrqlProvider value={urqlClient}>
88+
<WagmiProvider config={config}>
89+
<QueryClientProvider client={queryClient}>
90+
<RainbowKitProvider initialChain={initialChain}>
91+
<Inititalizers>{children}</Inititalizers>
92+
</RainbowKitProvider>
93+
</QueryClientProvider>
94+
</WagmiProvider>
95+
</UrqlProvider>
96+
</CacheInvalidationProvider>
9497
);
9598
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
'use client';
2+
3+
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
4+
5+
interface CacheInvalidationContextType {
6+
timestamp: number;
7+
invalidate: () => void;
8+
}
9+
10+
const CacheInvalidationContext = createContext<CacheInvalidationContextType | undefined>(undefined);
11+
12+
export function CacheInvalidationProvider({ children }: { children: ReactNode }) {
13+
const [timestamp, setTimestamp] = useState(Date.now());
14+
15+
const invalidate = useCallback(() => {
16+
setTimestamp(Date.now());
17+
}, []);
18+
19+
return (
20+
<CacheInvalidationContext.Provider value={{ timestamp, invalidate }}>
21+
{children}
22+
</CacheInvalidationContext.Provider>
23+
);
24+
}
25+
26+
export function useCacheInvalidation() {
27+
const context = useContext(CacheInvalidationContext);
28+
if (context === undefined) {
29+
throw new Error('useCacheInvalidation must be used within a CacheInvalidationProvider');
30+
}
31+
return context;
32+
}

website/src/hooks/subsquid/useJob.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,18 @@ import type { Job } from '@effectiveacceleration/contracts';
22
import { useMemo } from 'react';
33
import { useQuery } from 'urql';
44
import { GET_JOB_BY_ID } from './queries';
5+
import { useCacheInvalidation } from '@/contexts/CacheInvalidationContext';
56

67
export default function useJob(id: string) {
8+
const { timestamp } = useCacheInvalidation();
9+
710
const [result] = useQuery({
811
query: GET_JOB_BY_ID,
912
variables: { jobId: id },
13+
requestPolicy: 'cache-and-network',
14+
context: useMemo(() => ({
15+
_invalidationTimestamp: timestamp,
16+
}), [timestamp]),
1017
});
1118

1219
return useMemo(
@@ -15,6 +22,6 @@ export default function useJob(id: string) {
1522
loading: result.fetching,
1623
error: result.error
1724
}),
18-
[id, result]
25+
[result]
1926
);
2027
}

website/src/hooks/subsquid/useJobEvents.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,18 @@ import type { JobEvent } from '@effectiveacceleration/contracts';
22
import { useMemo } from 'react';
33
import { useQuery } from 'urql';
44
import { GET_JOB_EVENTS } from './queries';
5+
import { useCacheInvalidation } from '@/contexts/CacheInvalidationContext';
56

67
export default function useJobEvents(jobId: string) {
8+
const { timestamp } = useCacheInvalidation();
9+
710
const [result] = useQuery({
811
query: GET_JOB_EVENTS,
912
variables: { jobId },
13+
requestPolicy: 'cache-and-network',
14+
context: useMemo(() => ({
15+
_invalidationTimestamp: timestamp,
16+
}), [timestamp]),
1017
});
1118

1219
return useMemo(
@@ -15,6 +22,6 @@ export default function useJobEvents(jobId: string) {
1522
loading: result.fetching,
1623
error: result.error
1724
}),
18-
[jobId, result]
25+
[result]
1926
);
2027
}

website/src/hooks/subsquid/useUserNotifications.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
JobEventType,
99
JobMessageEvent,
1010
} from '@effectiveacceleration/contracts';
11+
import { useCacheInvalidation } from '@/contexts/CacheInvalidationContext';
1112

1213
export interface NotificationWithJob {
1314
id: string;
@@ -33,6 +34,8 @@ export default function useUserNotifications(
3334
limit?: number,
3435
fetchMessageContent: boolean = false // Optional flag to enable message fetching
3536
) {
37+
const { timestamp } = useCacheInvalidation();
38+
3639
const [result] = useQuery({
3740
query: GET_USER_NOTIFICATIONS,
3841
variables: {
@@ -42,6 +45,10 @@ export default function useUserNotifications(
4245
limit: limit ?? 10,
4346
},
4447
pause: !userAddress,
48+
requestPolicy: 'cache-and-network',
49+
context: useMemo(() => ({
50+
_invalidationTimestamp: timestamp,
51+
}), [timestamp]),
4552
});
4653

4754
// Extract unique job IDs from notifications with proper typing
Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
import { useEffect, useState } from 'react';
22
import type { JobEvent } from '@effectiveacceleration/contracts';
33
import { useClient } from 'urql';
4+
import { useCacheInvalidation } from '@/contexts/CacheInvalidationContext';
45

56
type JobEventMessage = Omit<JobEvent, 'data_' | 'details'>;
67

78
// this hook will reset the graphql cache upon a sw notification about a job event or all events if jobId is undefined
89
export const useSwResetMessage = (jobId?: string) => {
910
const urqlClient = useClient();
11+
const { invalidate } = useCacheInvalidation();
1012
const [resets, setResets] = useState(0);
1113

1214
useEffect(() => {
15+
if (!('BroadcastChannel' in window)) {
16+
console.error('BroadcastChannel API not supported in this browser');
17+
return;
18+
}
19+
1320
const channel = new BroadcastChannel('sw-messages');
1421
channel.onmessage = (event: {
1522
data: { body: string; data: JobEventMessage };
@@ -18,16 +25,16 @@ export const useSwResetMessage = (jobId?: string) => {
1825
jobId === undefined ||
1926
String(event.data?.data?.jobId ?? -1n) === jobId
2027
) {
21-
// URQL doesn't need manual cache reset - it handles this automatically
22-
// The cache will be invalidated when new queries are made
28+
// Trigger cache invalidation to force all URQL queries to refetch
29+
invalidate();
2330
setResets((prev) => prev + 1);
2431
}
2532
};
2633

2734
return () => {
2835
channel.close();
2936
};
30-
}, [setResets]);
37+
}, [invalidate, jobId]);
3138

3239
return { resets };
3340
};

website/src/hooks/useWriteContractWithNotifications.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { MARKETPLACE_DATA_V1_ABI } from '@effectiveacceleration/contracts/wagmi/
1919
import { MARKETPLACE_V1_ABI } from '@effectiveacceleration/contracts/wagmi/MarketplaceV1';
2020
import { E_A_C_C_TOKEN_ABI as EACC_TOKEN_ABI } from '@effectiveacceleration/contracts/wagmi/EACCToken';
2121
import { useClient } from 'urql';
22+
import { useCacheInvalidation } from '@/contexts/CacheInvalidationContext';
2223

2324
type ParsedEvent = {
2425
contractName: string;
@@ -92,6 +93,7 @@ type WriteContractConfig = {
9293
export function useWriteContractWithNotifications() {
9394
const config = useConfig();
9495
const urqlClient = useClient();
96+
const { invalidate } = useCacheInvalidation();
9597
const { showError, showSuccess, showLoading, toast } = useToast();
9698
const [simulateError, setSimulateError] = useState<
9799
WriteContractErrorType | undefined
@@ -246,10 +248,12 @@ export function useWriteContractWithNotifications() {
246248
const parsedEvents = parseEvents(receipt);
247249
onSuccessCallbackRef.current?.(receipt, parsedEvents);
248250

249-
// URQL doesn't need manual cache reset - it handles this automatically
250-
// The cache will be invalidated when new queries are made
251+
// Trigger cache invalidation after delay to allow Subsquid to index the data
252+
setTimeout(() => {
253+
invalidate();
254+
}, 3000);
251255
}
252-
}, [isConfirmed, receipt, parseEvents]);
256+
}, [isConfirmed, receipt, parseEvents, invalidate]);
253257

254258
// Handle confirmation and success
255259
useEffect(() => {

0 commit comments

Comments
 (0)