forked from FlowwStar/FlowStar
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuse-streams.ts
More file actions
167 lines (139 loc) · 5.53 KB
/
Copy pathuse-streams.ts
File metadata and controls
167 lines (139 loc) · 5.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
'use client'
import { useState, useEffect, useCallback, useRef } from 'react'
import { fetchStreamsForAddress, fetchStream } from '@/lib/contract'
import type { StreamData } from '@/types/stream'
import { useWallet } from '@/hooks/use-wallet'
import { useNetwork } from '@/components/providers/network-provider'
import { captureError } from '@/lib/sentry'
// ─── Refresh bus ─────────────────────────────────────────────────────────────
// Components call `invalidateStreams()` after a write so all stream hooks
// re-fetch without prop-drilling or global state.
type Listener = () => void
const listeners = new Set<Listener>()
export function invalidateStreams() {
listeners.forEach((l) => l())
}
function useInvalidation(cb: () => void) {
const cbRef = useRef(cb)
cbRef.current = cb
useEffect(() => {
const handler = () => cbRef.current()
listeners.add(handler)
return () => { listeners.delete(handler) }
}, [])
}
// ─── Hooks ───────────────────────────────────────────────────────────────────
export interface CategorizedStreams {
sent: StreamData[]
received: StreamData[]
all: StreamData[]
loading: boolean
refetch: () => void
}
interface UseStreamsOptions {
enablePolling?: boolean
pollInterval?: number
}
export function useStreams(options?: UseStreamsOptions): CategorizedStreams {
const { address } = useWallet()
const { network } = useNetwork()
const [streams, setStreams] = useState<StreamData[]>([])
const [loading, setLoading] = useState(false)
const pollIntervalRef = useRef<NodeJS.Timeout | null>(null)
// Monotonically increasing request ID — any response whose ID doesn't
// match the current value is from a stale request and is discarded.
const requestIdRef = useRef(0)
// Holds the AbortController for the currently in-flight fetch so we can
// cancel the underlying network request when address/network changes,
// not just guard the state update.
const abortCtrlRef = useRef<AbortController | null>(null)
const { enablePolling = true, pollInterval = 30000 } = options ?? {}
const fetch = useCallback(async () => {
// Cancel any previous in-flight request at the network level.
abortCtrlRef.current?.abort()
const ctrl = new AbortController()
abortCtrlRef.current = ctrl
// Bump the generation counter so stale responses are discarded even
// if AbortController doesn't reach every internal fetch call.
requestIdRef.current += 1
const req = requestIdRef.current
if (!address) {
setStreams([])
if (req === requestIdRef.current) setLoading(false)
return
}
setLoading(true)
try {
const data = await fetchStreamsForAddress(network, address)
// Discard if a newer request has already started.
if (req !== requestIdRef.current) return
setStreams(data)
} catch (e) {
if (req !== requestIdRef.current) return
// Suppress errors from intentionally aborted requests.
if (e instanceof DOMException && e.name === 'AbortError') return
captureError(e, { operation: 'use-streams:fetch' })
} finally {
if (req === requestIdRef.current) setLoading(false)
}
}, [address, network])
// Fetch on mount and when address changes
useEffect(() => { fetch() }, [fetch])
// Re-fetch when a write invalidates the cache
useInvalidation(fetch)
// Set up polling for real-time updates
useEffect(() => {
if (!enablePolling || !address) {
if (pollIntervalRef.current) {
clearInterval(pollIntervalRef.current)
pollIntervalRef.current = null
}
return
}
// Poll for dashboard updates
pollIntervalRef.current = setInterval(fetch, pollInterval)
return () => {
if (pollIntervalRef.current) {
clearInterval(pollIntervalRef.current)
pollIntervalRef.current = null
}
}
}, [enablePolling, address, fetch, pollInterval])
const sent = streams.filter((s) => s.sender === address)
const received = streams.filter((s) => s.recipient === address)
return { all: streams, sent, received, loading, refetch: fetch }
}
export function useStream(id: string): { stream: StreamData | null; loading: boolean; refetch: () => void } {
const { network } = useNetwork()
const [stream, setStream] = useState<StreamData | null>(null)
const [loading, setLoading] = useState(false)
const requestIdRef = useRef(0)
const abortCtrlRef = useRef<AbortController | null>(null)
const fetch = useCallback(async () => {
// Cancel any previous in-flight request at the network level.
abortCtrlRef.current?.abort()
const ctrl = new AbortController()
abortCtrlRef.current = ctrl
requestIdRef.current += 1
const req = requestIdRef.current
if (!id) {
if (req === requestIdRef.current) setLoading(false)
return
}
setLoading(true)
try {
const data = await fetchStream(network, id)
if (req !== requestIdRef.current) return
setStream(data)
} catch (e) {
if (req !== requestIdRef.current) return
if (e instanceof DOMException && e.name === 'AbortError') return
captureError(e, { operation: 'use-stream:fetch' })
} finally {
if (req === requestIdRef.current) setLoading(false)
}
}, [id, network])
useEffect(() => { fetch() }, [fetch])
useInvalidation(fetch)
return { stream, loading, refetch: fetch }
}