@pyreon/query 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +270 -139
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +278 -147
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index2.d.ts +162 -104
- package/lib/types/index2.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +42 -44
- package/src/query-client.ts +3 -4
- package/src/tests/{query.test.ts → query.test.tsx} +254 -353
- package/src/tests/subscription.test.tsx +581 -0
- package/src/use-infinite-query.ts +2 -2
- package/src/use-is-fetching.ts +1 -1
- package/src/use-mutation.ts +2 -2
- package/src/use-queries.ts +2 -2
- package/src/use-query-error-reset-boundary.ts +3 -4
- package/src/use-query.ts +2 -2
- package/src/use-subscription.ts +226 -0
- package/src/use-suspense-query.ts +3 -3
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
import { signal } from '@pyreon/reactivity'
|
|
2
|
+
import { mount } from '@pyreon/runtime-dom'
|
|
3
|
+
import { QueryClient } from '@tanstack/query-core'
|
|
4
|
+
import {
|
|
5
|
+
QueryClientProvider,
|
|
6
|
+
type UseSubscriptionResult,
|
|
7
|
+
useSubscription,
|
|
8
|
+
} from '../index'
|
|
9
|
+
|
|
10
|
+
// ─── Mock WebSocket ──────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
type MockWSListener = ((event: unknown) => void) | null
|
|
13
|
+
|
|
14
|
+
interface MockWebSocket {
|
|
15
|
+
url: string
|
|
16
|
+
protocols?: string | string[]
|
|
17
|
+
readyState: number
|
|
18
|
+
onopen: MockWSListener
|
|
19
|
+
onmessage: MockWSListener
|
|
20
|
+
onclose: MockWSListener
|
|
21
|
+
onerror: MockWSListener
|
|
22
|
+
send: ReturnType<typeof vi.fn>
|
|
23
|
+
close: ReturnType<typeof vi.fn>
|
|
24
|
+
// Test helpers
|
|
25
|
+
_simulateOpen: () => void
|
|
26
|
+
_simulateMessage: (data: string) => void
|
|
27
|
+
_simulateClose: (code?: number, reason?: string) => void
|
|
28
|
+
_simulateError: () => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let mockInstances: MockWebSocket[] = []
|
|
32
|
+
|
|
33
|
+
class MockWebSocketClass {
|
|
34
|
+
static CONNECTING = 0
|
|
35
|
+
static OPEN = 1
|
|
36
|
+
static CLOSING = 2
|
|
37
|
+
static CLOSED = 3
|
|
38
|
+
|
|
39
|
+
url: string
|
|
40
|
+
protocols?: string | string[]
|
|
41
|
+
readyState = 0
|
|
42
|
+
onopen: MockWSListener = null
|
|
43
|
+
onmessage: MockWSListener = null
|
|
44
|
+
onclose: MockWSListener = null
|
|
45
|
+
onerror: MockWSListener = null
|
|
46
|
+
|
|
47
|
+
send = vi.fn()
|
|
48
|
+
close = vi.fn(() => {
|
|
49
|
+
this.readyState = MockWebSocketClass.CLOSED
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
constructor(url: string, protocols?: string | string[]) {
|
|
53
|
+
this.url = url
|
|
54
|
+
this.protocols = protocols
|
|
55
|
+
this.readyState = MockWebSocketClass.CONNECTING
|
|
56
|
+
mockInstances.push(this as unknown as MockWebSocket)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_simulateOpen() {
|
|
60
|
+
this.readyState = MockWebSocketClass.OPEN
|
|
61
|
+
this.onopen?.({ type: 'open' })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_simulateMessage(data: string) {
|
|
65
|
+
this.onmessage?.({ type: 'message', data } as unknown as MessageEvent)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_simulateClose(code = 1000, reason = '') {
|
|
69
|
+
this.readyState = MockWebSocketClass.CLOSED
|
|
70
|
+
this.onclose?.({ type: 'close', code, reason } as unknown as CloseEvent)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_simulateError() {
|
|
74
|
+
this.onerror?.({ type: 'error' })
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Install mock
|
|
79
|
+
const OriginalWebSocket = globalThis.WebSocket
|
|
80
|
+
beforeAll(() => {
|
|
81
|
+
;(globalThis as any).WebSocket = MockWebSocketClass
|
|
82
|
+
})
|
|
83
|
+
afterAll(() => {
|
|
84
|
+
globalThis.WebSocket = OriginalWebSocket
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// biome-ignore lint/suspicious/noEmptyBlockStatements: intentional no-op for tests where onMessage isn't the focus
|
|
88
|
+
const noop = () => {}
|
|
89
|
+
|
|
90
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
function makeClient() {
|
|
93
|
+
return new QueryClient({
|
|
94
|
+
defaultOptions: { queries: { retry: false, gcTime: Infinity } },
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function withProvider(client: QueryClient, component: () => void): () => void {
|
|
99
|
+
const el = document.createElement('div')
|
|
100
|
+
document.body.appendChild(el)
|
|
101
|
+
const unmount = mount(
|
|
102
|
+
<QueryClientProvider client={client}>
|
|
103
|
+
{() => {
|
|
104
|
+
component()
|
|
105
|
+
return null
|
|
106
|
+
}}
|
|
107
|
+
</QueryClientProvider>,
|
|
108
|
+
el,
|
|
109
|
+
)
|
|
110
|
+
return () => {
|
|
111
|
+
unmount()
|
|
112
|
+
el.remove()
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function lastMockWS(): MockWebSocket {
|
|
117
|
+
return mockInstances[mockInstances.length - 1]!
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
describe('useSubscription', () => {
|
|
123
|
+
beforeEach(() => {
|
|
124
|
+
mockInstances = []
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('connects to the WebSocket URL', () => {
|
|
128
|
+
const client = makeClient()
|
|
129
|
+
let sub: UseSubscriptionResult | null = null
|
|
130
|
+
|
|
131
|
+
const unmount = withProvider(client, () => {
|
|
132
|
+
sub = useSubscription({
|
|
133
|
+
url: 'wss://example.com/ws',
|
|
134
|
+
onMessage: noop,
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
expect(mockInstances).toHaveLength(1)
|
|
139
|
+
expect(lastMockWS().url).toBe('wss://example.com/ws')
|
|
140
|
+
expect(sub!.status()).toBe('connecting')
|
|
141
|
+
|
|
142
|
+
unmount()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('status transitions to connected on open', () => {
|
|
146
|
+
const client = makeClient()
|
|
147
|
+
let sub: UseSubscriptionResult | null = null
|
|
148
|
+
|
|
149
|
+
const unmount = withProvider(client, () => {
|
|
150
|
+
sub = useSubscription({
|
|
151
|
+
url: 'wss://example.com/ws',
|
|
152
|
+
onMessage: noop,
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
lastMockWS()._simulateOpen()
|
|
157
|
+
expect(sub!.status()).toBe('connected')
|
|
158
|
+
|
|
159
|
+
unmount()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('calls onMessage with event and queryClient', () => {
|
|
163
|
+
const client = makeClient()
|
|
164
|
+
const messages: string[] = []
|
|
165
|
+
|
|
166
|
+
const unmount = withProvider(client, () => {
|
|
167
|
+
useSubscription({
|
|
168
|
+
url: 'wss://example.com/ws',
|
|
169
|
+
onMessage: (event, qc) => {
|
|
170
|
+
messages.push(event.data as string)
|
|
171
|
+
expect(qc).toBe(client)
|
|
172
|
+
},
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
lastMockWS()._simulateOpen()
|
|
177
|
+
lastMockWS()._simulateMessage('hello')
|
|
178
|
+
lastMockWS()._simulateMessage('world')
|
|
179
|
+
|
|
180
|
+
expect(messages).toEqual(['hello', 'world'])
|
|
181
|
+
unmount()
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('invalidates queries on message', () => {
|
|
185
|
+
const client = makeClient()
|
|
186
|
+
const invalidateSpy = vi.spyOn(client, 'invalidateQueries')
|
|
187
|
+
|
|
188
|
+
const unmount = withProvider(client, () => {
|
|
189
|
+
useSubscription({
|
|
190
|
+
url: 'wss://example.com/ws',
|
|
191
|
+
onMessage: (_event, qc) => {
|
|
192
|
+
qc.invalidateQueries({ queryKey: ['orders'] })
|
|
193
|
+
},
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
lastMockWS()._simulateOpen()
|
|
198
|
+
lastMockWS()._simulateMessage('order-updated')
|
|
199
|
+
|
|
200
|
+
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['orders'] })
|
|
201
|
+
unmount()
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('send() sends data through WebSocket', () => {
|
|
205
|
+
const client = makeClient()
|
|
206
|
+
let sub: UseSubscriptionResult | null = null
|
|
207
|
+
|
|
208
|
+
const unmount = withProvider(client, () => {
|
|
209
|
+
sub = useSubscription({
|
|
210
|
+
url: 'wss://example.com/ws',
|
|
211
|
+
onMessage: noop,
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
lastMockWS()._simulateOpen()
|
|
216
|
+
sub!.send('test-message')
|
|
217
|
+
|
|
218
|
+
expect(lastMockWS().send).toHaveBeenCalledWith('test-message')
|
|
219
|
+
unmount()
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('send() is a no-op when not connected', () => {
|
|
223
|
+
const client = makeClient()
|
|
224
|
+
let sub: UseSubscriptionResult | null = null
|
|
225
|
+
|
|
226
|
+
const unmount = withProvider(client, () => {
|
|
227
|
+
sub = useSubscription({
|
|
228
|
+
url: 'wss://example.com/ws',
|
|
229
|
+
onMessage: noop,
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
// Still connecting — send should not throw
|
|
234
|
+
sub!.send('ignored')
|
|
235
|
+
expect(lastMockWS().send).not.toHaveBeenCalled()
|
|
236
|
+
unmount()
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('close() disconnects and sets status', () => {
|
|
240
|
+
const client = makeClient()
|
|
241
|
+
let sub: UseSubscriptionResult | null = null
|
|
242
|
+
|
|
243
|
+
const unmount = withProvider(client, () => {
|
|
244
|
+
sub = useSubscription({
|
|
245
|
+
url: 'wss://example.com/ws',
|
|
246
|
+
onMessage: noop,
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
lastMockWS()._simulateOpen()
|
|
251
|
+
sub!.close()
|
|
252
|
+
|
|
253
|
+
expect(sub!.status()).toBe('disconnected')
|
|
254
|
+
expect(lastMockWS().close).toHaveBeenCalled()
|
|
255
|
+
unmount()
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('status transitions to disconnected on close', () => {
|
|
259
|
+
const client = makeClient()
|
|
260
|
+
let sub: UseSubscriptionResult | null = null
|
|
261
|
+
|
|
262
|
+
const unmount = withProvider(client, () => {
|
|
263
|
+
sub = useSubscription({
|
|
264
|
+
url: 'wss://example.com/ws',
|
|
265
|
+
onMessage: noop,
|
|
266
|
+
reconnect: false,
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
lastMockWS()._simulateOpen()
|
|
271
|
+
lastMockWS()._simulateClose()
|
|
272
|
+
|
|
273
|
+
expect(sub!.status()).toBe('disconnected')
|
|
274
|
+
unmount()
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('status transitions to error on error', () => {
|
|
278
|
+
const client = makeClient()
|
|
279
|
+
let sub: UseSubscriptionResult | null = null
|
|
280
|
+
const errors: Event[] = []
|
|
281
|
+
|
|
282
|
+
const unmount = withProvider(client, () => {
|
|
283
|
+
sub = useSubscription({
|
|
284
|
+
url: 'wss://example.com/ws',
|
|
285
|
+
onMessage: noop,
|
|
286
|
+
reconnect: false,
|
|
287
|
+
onError: (e) => errors.push(e as Event),
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
lastMockWS()._simulateError()
|
|
292
|
+
expect(sub!.status()).toBe('error')
|
|
293
|
+
expect(errors).toHaveLength(1)
|
|
294
|
+
unmount()
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('calls onOpen callback', () => {
|
|
298
|
+
const client = makeClient()
|
|
299
|
+
let opened = false
|
|
300
|
+
|
|
301
|
+
const unmount = withProvider(client, () => {
|
|
302
|
+
useSubscription({
|
|
303
|
+
url: 'wss://example.com/ws',
|
|
304
|
+
onMessage: noop,
|
|
305
|
+
onOpen: () => {
|
|
306
|
+
opened = true
|
|
307
|
+
},
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
lastMockWS()._simulateOpen()
|
|
312
|
+
expect(opened).toBe(true)
|
|
313
|
+
unmount()
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('calls onClose callback', () => {
|
|
317
|
+
const client = makeClient()
|
|
318
|
+
let closed = false
|
|
319
|
+
|
|
320
|
+
const unmount = withProvider(client, () => {
|
|
321
|
+
useSubscription({
|
|
322
|
+
url: 'wss://example.com/ws',
|
|
323
|
+
onMessage: noop,
|
|
324
|
+
reconnect: false,
|
|
325
|
+
onClose: () => {
|
|
326
|
+
closed = true
|
|
327
|
+
},
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
lastMockWS()._simulateOpen()
|
|
332
|
+
lastMockWS()._simulateClose()
|
|
333
|
+
expect(closed).toBe(true)
|
|
334
|
+
unmount()
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
it('auto-reconnects on unexpected close', async () => {
|
|
338
|
+
const client = makeClient()
|
|
339
|
+
|
|
340
|
+
const unmount = withProvider(client, () => {
|
|
341
|
+
useSubscription({
|
|
342
|
+
url: 'wss://example.com/ws',
|
|
343
|
+
onMessage: noop,
|
|
344
|
+
reconnect: true,
|
|
345
|
+
reconnectDelay: 50,
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
expect(mockInstances).toHaveLength(1)
|
|
350
|
+
lastMockWS()._simulateOpen()
|
|
351
|
+
lastMockWS()._simulateClose()
|
|
352
|
+
|
|
353
|
+
// Wait for reconnect
|
|
354
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
355
|
+
|
|
356
|
+
expect(mockInstances).toHaveLength(2)
|
|
357
|
+
unmount()
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
it('does not reconnect when reconnect is false', async () => {
|
|
361
|
+
const client = makeClient()
|
|
362
|
+
|
|
363
|
+
const unmount = withProvider(client, () => {
|
|
364
|
+
useSubscription({
|
|
365
|
+
url: 'wss://example.com/ws',
|
|
366
|
+
onMessage: noop,
|
|
367
|
+
reconnect: false,
|
|
368
|
+
})
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
lastMockWS()._simulateOpen()
|
|
372
|
+
lastMockWS()._simulateClose()
|
|
373
|
+
|
|
374
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
375
|
+
expect(mockInstances).toHaveLength(1)
|
|
376
|
+
unmount()
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it('does not reconnect after intentional close()', async () => {
|
|
380
|
+
const client = makeClient()
|
|
381
|
+
let sub: UseSubscriptionResult | null = null
|
|
382
|
+
|
|
383
|
+
const unmount = withProvider(client, () => {
|
|
384
|
+
sub = useSubscription({
|
|
385
|
+
url: 'wss://example.com/ws',
|
|
386
|
+
onMessage: noop,
|
|
387
|
+
reconnect: true,
|
|
388
|
+
reconnectDelay: 50,
|
|
389
|
+
})
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
lastMockWS()._simulateOpen()
|
|
393
|
+
sub!.close()
|
|
394
|
+
|
|
395
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
396
|
+
expect(mockInstances).toHaveLength(1)
|
|
397
|
+
unmount()
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
it('respects maxReconnectAttempts', async () => {
|
|
401
|
+
const client = makeClient()
|
|
402
|
+
|
|
403
|
+
const unmount = withProvider(client, () => {
|
|
404
|
+
useSubscription({
|
|
405
|
+
url: 'wss://example.com/ws',
|
|
406
|
+
onMessage: noop,
|
|
407
|
+
reconnect: true,
|
|
408
|
+
reconnectDelay: 10,
|
|
409
|
+
maxReconnectAttempts: 2,
|
|
410
|
+
})
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
// First connection
|
|
414
|
+
lastMockWS()._simulateOpen()
|
|
415
|
+
lastMockWS()._simulateClose()
|
|
416
|
+
|
|
417
|
+
// Reconnect 1
|
|
418
|
+
await new Promise((r) => setTimeout(r, 30))
|
|
419
|
+
expect(mockInstances).toHaveLength(2)
|
|
420
|
+
lastMockWS()._simulateClose()
|
|
421
|
+
|
|
422
|
+
// Reconnect 2
|
|
423
|
+
await new Promise((r) => setTimeout(r, 40))
|
|
424
|
+
expect(mockInstances).toHaveLength(3)
|
|
425
|
+
lastMockWS()._simulateClose()
|
|
426
|
+
|
|
427
|
+
// Should not reconnect again (max 2 attempts reached)
|
|
428
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
429
|
+
expect(mockInstances).toHaveLength(3)
|
|
430
|
+
|
|
431
|
+
unmount()
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
it('reconnect() resets attempts and reconnects', async () => {
|
|
435
|
+
const client = makeClient()
|
|
436
|
+
let sub: UseSubscriptionResult | null = null
|
|
437
|
+
|
|
438
|
+
const unmount = withProvider(client, () => {
|
|
439
|
+
sub = useSubscription({
|
|
440
|
+
url: 'wss://example.com/ws',
|
|
441
|
+
onMessage: noop,
|
|
442
|
+
reconnect: false,
|
|
443
|
+
})
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
lastMockWS()._simulateOpen()
|
|
447
|
+
lastMockWS()._simulateClose()
|
|
448
|
+
|
|
449
|
+
expect(mockInstances).toHaveLength(1)
|
|
450
|
+
|
|
451
|
+
sub!.reconnect()
|
|
452
|
+
expect(mockInstances).toHaveLength(2)
|
|
453
|
+
expect(sub!.status()).toBe('connecting')
|
|
454
|
+
|
|
455
|
+
unmount()
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
it('enabled: false prevents connection', () => {
|
|
459
|
+
const client = makeClient()
|
|
460
|
+
let sub: UseSubscriptionResult | null = null
|
|
461
|
+
|
|
462
|
+
const unmount = withProvider(client, () => {
|
|
463
|
+
sub = useSubscription({
|
|
464
|
+
url: 'wss://example.com/ws',
|
|
465
|
+
onMessage: noop,
|
|
466
|
+
enabled: false,
|
|
467
|
+
})
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
expect(mockInstances).toHaveLength(0)
|
|
471
|
+
expect(sub!.status()).toBe('disconnected')
|
|
472
|
+
unmount()
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
it('reactive enabled signal controls connection', async () => {
|
|
476
|
+
const client = makeClient()
|
|
477
|
+
const enabled = signal(false)
|
|
478
|
+
|
|
479
|
+
const unmount = withProvider(client, () => {
|
|
480
|
+
useSubscription({
|
|
481
|
+
url: 'wss://example.com/ws',
|
|
482
|
+
onMessage: noop,
|
|
483
|
+
enabled: () => enabled(),
|
|
484
|
+
})
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
expect(mockInstances).toHaveLength(0)
|
|
488
|
+
|
|
489
|
+
enabled.set(true)
|
|
490
|
+
|
|
491
|
+
// Effect runs synchronously in Pyreon
|
|
492
|
+
expect(mockInstances).toHaveLength(1)
|
|
493
|
+
unmount()
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
it('reactive URL reconnects when URL changes', () => {
|
|
497
|
+
const client = makeClient()
|
|
498
|
+
const url = signal('wss://example.com/ws1')
|
|
499
|
+
|
|
500
|
+
const unmount = withProvider(client, () => {
|
|
501
|
+
useSubscription({
|
|
502
|
+
url: () => url(),
|
|
503
|
+
onMessage: noop,
|
|
504
|
+
})
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
expect(mockInstances).toHaveLength(1)
|
|
508
|
+
expect(lastMockWS().url).toBe('wss://example.com/ws1')
|
|
509
|
+
|
|
510
|
+
url.set('wss://example.com/ws2')
|
|
511
|
+
|
|
512
|
+
expect(mockInstances).toHaveLength(2)
|
|
513
|
+
expect(lastMockWS().url).toBe('wss://example.com/ws2')
|
|
514
|
+
|
|
515
|
+
unmount()
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
it('supports WebSocket protocols', () => {
|
|
519
|
+
const client = makeClient()
|
|
520
|
+
|
|
521
|
+
const unmount = withProvider(client, () => {
|
|
522
|
+
useSubscription({
|
|
523
|
+
url: 'wss://example.com/ws',
|
|
524
|
+
protocols: ['graphql-ws'],
|
|
525
|
+
onMessage: noop,
|
|
526
|
+
})
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
expect(lastMockWS().protocols).toEqual(['graphql-ws'])
|
|
530
|
+
unmount()
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
it('cleans up on unmount', () => {
|
|
534
|
+
const client = makeClient()
|
|
535
|
+
|
|
536
|
+
const unmount = withProvider(client, () => {
|
|
537
|
+
useSubscription({
|
|
538
|
+
url: 'wss://example.com/ws',
|
|
539
|
+
onMessage: noop,
|
|
540
|
+
})
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
lastMockWS()._simulateOpen()
|
|
544
|
+
const ws = lastMockWS()
|
|
545
|
+
|
|
546
|
+
unmount()
|
|
547
|
+
expect(ws.close).toHaveBeenCalled()
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
it('resets reconnect count on successful connection', async () => {
|
|
551
|
+
const client = makeClient()
|
|
552
|
+
|
|
553
|
+
const unmount = withProvider(client, () => {
|
|
554
|
+
useSubscription({
|
|
555
|
+
url: 'wss://example.com/ws',
|
|
556
|
+
onMessage: noop,
|
|
557
|
+
reconnect: true,
|
|
558
|
+
reconnectDelay: 10,
|
|
559
|
+
maxReconnectAttempts: 2,
|
|
560
|
+
})
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
// First connection then close
|
|
564
|
+
lastMockWS()._simulateOpen()
|
|
565
|
+
lastMockWS()._simulateClose()
|
|
566
|
+
|
|
567
|
+
// Reconnect 1
|
|
568
|
+
await new Promise((r) => setTimeout(r, 30))
|
|
569
|
+
expect(mockInstances).toHaveLength(2)
|
|
570
|
+
|
|
571
|
+
// This reconnect succeeds — should reset the counter
|
|
572
|
+
lastMockWS()._simulateOpen()
|
|
573
|
+
lastMockWS()._simulateClose()
|
|
574
|
+
|
|
575
|
+
// Should be able to reconnect again (counter was reset)
|
|
576
|
+
await new Promise((r) => setTimeout(r, 30))
|
|
577
|
+
expect(mockInstances).toHaveLength(3)
|
|
578
|
+
|
|
579
|
+
unmount()
|
|
580
|
+
})
|
|
581
|
+
})
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { onUnmount } from '@pyreon/core'
|
|
2
|
-
import { signal, effect, batch } from '@pyreon/reactivity'
|
|
3
2
|
import type { Signal } from '@pyreon/reactivity'
|
|
4
|
-
import {
|
|
3
|
+
import { batch, effect, signal } from '@pyreon/reactivity'
|
|
5
4
|
import type {
|
|
6
5
|
DefaultError,
|
|
7
6
|
InfiniteData,
|
|
@@ -10,6 +9,7 @@ import type {
|
|
|
10
9
|
QueryKey,
|
|
11
10
|
QueryObserverResult,
|
|
12
11
|
} from '@tanstack/query-core'
|
|
12
|
+
import { InfiniteQueryObserver } from '@tanstack/query-core'
|
|
13
13
|
import { useQueryClient } from './query-client'
|
|
14
14
|
|
|
15
15
|
export interface UseInfiniteQueryResult<TQueryFnData, TError = DefaultError> {
|
package/src/use-is-fetching.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { onUnmount } from '@pyreon/core'
|
|
2
|
-
import { signal } from '@pyreon/reactivity'
|
|
3
2
|
import type { Signal } from '@pyreon/reactivity'
|
|
3
|
+
import { signal } from '@pyreon/reactivity'
|
|
4
4
|
import type { MutationFilters, QueryFilters } from '@tanstack/query-core'
|
|
5
5
|
import { useQueryClient } from './query-client'
|
|
6
6
|
|
package/src/use-mutation.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { onUnmount } from '@pyreon/core'
|
|
2
|
-
import { signal, batch } from '@pyreon/reactivity'
|
|
3
2
|
import type { Signal } from '@pyreon/reactivity'
|
|
4
|
-
import {
|
|
3
|
+
import { batch, signal } from '@pyreon/reactivity'
|
|
5
4
|
import type {
|
|
6
5
|
DefaultError,
|
|
7
6
|
MutateFunction,
|
|
8
7
|
MutationObserverOptions,
|
|
9
8
|
MutationObserverResult,
|
|
10
9
|
} from '@tanstack/query-core'
|
|
10
|
+
import { MutationObserver } from '@tanstack/query-core'
|
|
11
11
|
import { useQueryClient } from './query-client'
|
|
12
12
|
|
|
13
13
|
export interface UseMutationResult<
|
package/src/use-queries.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { onUnmount } from '@pyreon/core'
|
|
2
|
-
import { signal, effect } from '@pyreon/reactivity'
|
|
3
2
|
import type { Signal } from '@pyreon/reactivity'
|
|
4
|
-
import {
|
|
3
|
+
import { effect, signal } from '@pyreon/reactivity'
|
|
5
4
|
import type {
|
|
6
5
|
DefaultError,
|
|
7
6
|
QueryKey,
|
|
8
7
|
QueryObserverOptions,
|
|
9
8
|
QueryObserverResult,
|
|
10
9
|
} from '@tanstack/query-core'
|
|
10
|
+
import { QueriesObserver } from '@tanstack/query-core'
|
|
11
11
|
import { useQueryClient } from './query-client'
|
|
12
12
|
|
|
13
13
|
export type UseQueriesOptions<TQueryKey extends QueryKey = QueryKey> =
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
+
import type { Props, VNode, VNodeChild } from '@pyreon/core'
|
|
1
2
|
import {
|
|
2
3
|
createContext,
|
|
3
|
-
pushContext,
|
|
4
|
-
popContext,
|
|
5
4
|
onUnmount,
|
|
5
|
+
popContext,
|
|
6
|
+
pushContext,
|
|
6
7
|
useContext,
|
|
7
8
|
} from '@pyreon/core'
|
|
8
|
-
import type { VNodeChild, VNode } from '@pyreon/core'
|
|
9
|
-
import type { Props } from '@pyreon/core'
|
|
10
9
|
import { useQueryClient } from './query-client'
|
|
11
10
|
|
|
12
11
|
// ─── Context ────────────────────────────────────────────────────────────────
|
package/src/use-query.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { onUnmount } from '@pyreon/core'
|
|
2
|
-
import { signal, effect, batch } from '@pyreon/reactivity'
|
|
3
2
|
import type { Signal } from '@pyreon/reactivity'
|
|
4
|
-
import {
|
|
3
|
+
import { batch, effect, signal } from '@pyreon/reactivity'
|
|
5
4
|
import type {
|
|
6
5
|
DefaultError,
|
|
7
6
|
QueryKey,
|
|
8
7
|
QueryObserverOptions,
|
|
9
8
|
QueryObserverResult,
|
|
10
9
|
} from '@tanstack/query-core'
|
|
10
|
+
import { QueryObserver } from '@tanstack/query-core'
|
|
11
11
|
import { useQueryClient } from './query-client'
|
|
12
12
|
|
|
13
13
|
export interface UseQueryResult<TData, TError = DefaultError> {
|