04-querying-subscribing.ts raw
1 /**
2 * NDK Query and Subscription Patterns
3 *
4 * Examples from: src/queries/orders.tsx, src/queries/payment.tsx
5 */
6
7 import NDK, { NDKEvent, NDKFilter, NDKSubscription } from '@nostr-dev-kit/ndk'
8
9 // ============================================================
10 // BASIC FETCH (ONE-TIME QUERY)
11 // ============================================================
12
13 const fetchNotes = async (ndk: NDK, authorPubkey: string, limit: number = 50) => {
14 const filter: NDKFilter = {
15 kinds: [1], // Text notes
16 authors: [authorPubkey],
17 limit
18 }
19
20 // Fetch returns a Set
21 const events = await ndk.fetchEvents(filter)
22
23 // Convert to array and sort by timestamp
24 const eventArray = Array.from(events).sort((a, b) =>
25 (b.created_at || 0) - (a.created_at || 0)
26 )
27
28 return eventArray
29 }
30
31 // ============================================================
32 // FETCH WITH MULTIPLE FILTERS
33 // ============================================================
34
35 const fetchProductsByMultipleAuthors = async (
36 ndk: NDK,
37 pubkeys: string[]
38 ) => {
39 const filter: NDKFilter = {
40 kinds: [30402], // Product listings
41 authors: pubkeys,
42 limit: 100
43 }
44
45 const events = await ndk.fetchEvents(filter)
46 return Array.from(events)
47 }
48
49 // ============================================================
50 // FETCH WITH TAG FILTERS
51 // ============================================================
52
53 const fetchOrderEvents = async (ndk: NDK, orderId: string) => {
54 const filter: NDKFilter = {
55 kinds: [16, 17], // Order and payment receipt
56 '#order': [orderId], // Tag filter (note the # prefix)
57 }
58
59 const events = await ndk.fetchEvents(filter)
60 return Array.from(events)
61 }
62
63 // ============================================================
64 // FETCH WITH TIME RANGE
65 // ============================================================
66
67 const fetchRecentEvents = async (
68 ndk: NDK,
69 kind: number,
70 hoursAgo: number = 24
71 ) => {
72 const now = Math.floor(Date.now() / 1000)
73 const since = now - (hoursAgo * 3600)
74
75 const filter: NDKFilter = {
76 kinds: [kind],
77 since,
78 until: now,
79 limit: 100
80 }
81
82 const events = await ndk.fetchEvents(filter)
83 return Array.from(events)
84 }
85
86 // ============================================================
87 // FETCH BY EVENT ID
88 // ============================================================
89
90 const fetchEventById = async (ndk: NDK, eventId: string) => {
91 const filter: NDKFilter = {
92 ids: [eventId]
93 }
94
95 const events = await ndk.fetchEvents(filter)
96
97 if (events.size === 0) {
98 return null
99 }
100
101 return Array.from(events)[0]
102 }
103
104 // ============================================================
105 // BASIC SUBSCRIPTION (REAL-TIME)
106 // ============================================================
107
108 const subscribeToNotes = (
109 ndk: NDK,
110 authorPubkey: string,
111 onEvent: (event: NDKEvent) => void
112 ): NDKSubscription => {
113 const filter: NDKFilter = {
114 kinds: [1],
115 authors: [authorPubkey]
116 }
117
118 const subscription = ndk.subscribe(filter, {
119 closeOnEose: false // Keep open for real-time updates
120 })
121
122 // Event handler
123 subscription.on('event', (event: NDKEvent) => {
124 onEvent(event)
125 })
126
127 // EOSE (End of Stored Events) handler
128 subscription.on('eose', () => {
129 console.log('✅ Received all stored events')
130 })
131
132 return subscription
133 }
134
135 // ============================================================
136 // SUBSCRIPTION WITH CLEANUP
137 // ============================================================
138
139 const createManagedSubscription = (
140 ndk: NDK,
141 filter: NDKFilter,
142 handlers: {
143 onEvent: (event: NDKEvent) => void
144 onEose?: () => void
145 onClose?: () => void
146 }
147 ) => {
148 const subscription = ndk.subscribe(filter, { closeOnEose: false })
149
150 subscription.on('event', handlers.onEvent)
151
152 if (handlers.onEose) {
153 subscription.on('eose', handlers.onEose)
154 }
155
156 if (handlers.onClose) {
157 subscription.on('close', handlers.onClose)
158 }
159
160 // Return cleanup function
161 return () => {
162 subscription.stop()
163 console.log('✅ Subscription stopped')
164 }
165 }
166
167 // ============================================================
168 // MONITORING SPECIFIC EVENT
169 // ============================================================
170
171 const monitorPaymentReceipt = (
172 ndk: NDK,
173 orderId: string,
174 invoiceId: string,
175 onPaymentReceived: (preimage: string) => void
176 ): NDKSubscription => {
177 const sessionStart = Math.floor(Date.now() / 1000)
178
179 const filter: NDKFilter = {
180 kinds: [17], // Payment receipt
181 '#order': [orderId],
182 '#payment-request': [invoiceId],
183 since: sessionStart - 30 // 30 second buffer for clock skew
184 }
185
186 const subscription = ndk.subscribe(filter, { closeOnEose: false })
187
188 subscription.on('event', (event: NDKEvent) => {
189 // Verify event is recent
190 if (event.created_at && event.created_at < sessionStart - 30) {
191 console.log('⏰ Ignoring old receipt')
192 return
193 }
194
195 // Verify it's the correct invoice
196 const paymentRequestTag = event.tags.find(tag => tag[0] === 'payment-request')
197 if (paymentRequestTag?.[1] !== invoiceId) {
198 return
199 }
200
201 // Extract preimage
202 const paymentTag = event.tags.find(tag => tag[0] === 'payment')
203 const preimage = paymentTag?.[3] || 'external-payment'
204
205 console.log('✅ Payment received!')
206 subscription.stop()
207 onPaymentReceived(preimage)
208 })
209
210 return subscription
211 }
212
213 // ============================================================
214 // REACT INTEGRATION PATTERN
215 // ============================================================
216
217 import { useEffect, useState } from 'react'
218
219 function useOrderSubscription(ndk: NDK | null, orderId: string) {
220 const [events, setEvents] = useState<NDKEvent[]>([])
221 const [eosed, setEosed] = useState(false)
222
223 useEffect(() => {
224 if (!ndk || !orderId) return
225
226 const filter: NDKFilter = {
227 kinds: [16, 17],
228 '#order': [orderId]
229 }
230
231 const subscription = ndk.subscribe(filter, { closeOnEose: false })
232
233 subscription.on('event', (event: NDKEvent) => {
234 setEvents(prev => {
235 // Avoid duplicates
236 if (prev.some(e => e.id === event.id)) {
237 return prev
238 }
239 return [...prev, event].sort((a, b) =>
240 (a.created_at || 0) - (b.created_at || 0)
241 )
242 })
243 })
244
245 subscription.on('eose', () => {
246 setEosed(true)
247 })
248
249 // Cleanup on unmount
250 return () => {
251 subscription.stop()
252 }
253 }, [ndk, orderId])
254
255 return { events, eosed }
256 }
257
258 // ============================================================
259 // REACT QUERY INTEGRATION
260 // ============================================================
261
262 import { useQuery, useQueryClient } from '@tanstack/react-query'
263
264 // Query function
265 const fetchProducts = async (ndk: NDK, pubkey: string) => {
266 if (!ndk) throw new Error('NDK not initialized')
267
268 const filter: NDKFilter = {
269 kinds: [30402],
270 authors: [pubkey]
271 }
272
273 const events = await ndk.fetchEvents(filter)
274 return Array.from(events)
275 }
276
277 // Hook with subscription for real-time updates
278 function useProductsWithSubscription(ndk: NDK | null, pubkey: string) {
279 const queryClient = useQueryClient()
280
281 // Initial query
282 const query = useQuery({
283 queryKey: ['products', pubkey],
284 queryFn: () => fetchProducts(ndk!, pubkey),
285 enabled: !!ndk && !!pubkey,
286 staleTime: 30000
287 })
288
289 // Real-time subscription
290 useEffect(() => {
291 if (!ndk || !pubkey) return
292
293 const filter: NDKFilter = {
294 kinds: [30402],
295 authors: [pubkey]
296 }
297
298 const subscription = ndk.subscribe(filter, { closeOnEose: false })
299
300 subscription.on('event', () => {
301 // Invalidate query to trigger refetch
302 queryClient.invalidateQueries({ queryKey: ['products', pubkey] })
303 })
304
305 return () => {
306 subscription.stop()
307 }
308 }, [ndk, pubkey, queryClient])
309
310 return query
311 }
312
313 // ============================================================
314 // ADVANCED: WAITING FOR SPECIFIC EVENT
315 // ============================================================
316
317 const waitForEvent = (
318 ndk: NDK,
319 filter: NDKFilter,
320 condition: (event: NDKEvent) => boolean,
321 timeoutMs: number = 30000
322 ): Promise<NDKEvent | null> => {
323 return new Promise((resolve) => {
324 const subscription = ndk.subscribe(filter, { closeOnEose: false })
325
326 // Timeout
327 const timeout = setTimeout(() => {
328 subscription.stop()
329 resolve(null)
330 }, timeoutMs)
331
332 // Event handler
333 subscription.on('event', (event: NDKEvent) => {
334 if (condition(event)) {
335 clearTimeout(timeout)
336 subscription.stop()
337 resolve(event)
338 }
339 })
340 })
341 }
342
343 // Usage example
344 async function waitForPayment(ndk: NDK, orderId: string, invoiceId: string) {
345 const paymentEvent = await waitForEvent(
346 ndk,
347 {
348 kinds: [17],
349 '#order': [orderId],
350 since: Math.floor(Date.now() / 1000)
351 },
352 (event) => {
353 const tag = event.tags.find(t => t[0] === 'payment-request')
354 return tag?.[1] === invoiceId
355 },
356 60000 // 60 second timeout
357 )
358
359 if (paymentEvent) {
360 console.log('✅ Payment confirmed!')
361 return paymentEvent
362 } else {
363 console.log('⏰ Payment timeout')
364 return null
365 }
366 }
367
368 // ============================================================
369 // USAGE EXAMPLES
370 // ============================================================
371
372 async function queryExample(ndk: NDK) {
373 // Fetch notes
374 const notes = await fetchNotes(ndk, 'pubkey123', 50)
375 console.log(`Found ${notes.length} notes`)
376
377 // Subscribe to new notes
378 const cleanup = subscribeToNotes(ndk, 'pubkey123', (event) => {
379 console.log('New note:', event.content)
380 })
381
382 // Clean up after 60 seconds
383 setTimeout(cleanup, 60000)
384
385 // Monitor payment
386 monitorPaymentReceipt(ndk, 'order-123', 'invoice-456', (preimage) => {
387 console.log('Payment received:', preimage)
388 })
389 }
390
391 export {
392 fetchNotes,
393 fetchProductsByMultipleAuthors,
394 fetchOrderEvents,
395 fetchRecentEvents,
396 fetchEventById,
397 subscribeToNotes,
398 createManagedSubscription,
399 monitorPaymentReceipt,
400 useOrderSubscription,
401 useProductsWithSubscription,
402 waitForEvent
403 }
404
405