@pyreon/query 0.9.0 → 0.11.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 +164 -2
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +74 -7
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +14 -7
- package/src/index.ts +25 -19
- package/src/query-client.ts +5 -5
- package/src/tests/query.test.tsx +254 -268
- package/src/tests/sse.test.tsx +857 -0
- package/src/tests/subscription.test.tsx +200 -82
- package/src/use-infinite-query.ts +11 -19
- package/src/use-is-fetching.ts +5 -5
- package/src/use-mutation.ts +12 -21
- package/src/use-queries.ts +20 -19
- package/src/use-query-error-reset-boundary.ts +8 -11
- package/src/use-query.ts +10 -17
- package/src/use-sse.ts +266 -0
- package/src/use-subscription.ts +27 -39
- package/src/use-suspense-query.ts +18 -34
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import { signal } from
|
|
2
|
-
import { mount } from
|
|
3
|
-
import { QueryClient } from
|
|
4
|
-
import {
|
|
5
|
-
QueryClientProvider,
|
|
6
|
-
type UseSubscriptionResult,
|
|
7
|
-
useSubscription,
|
|
8
|
-
} from '../index'
|
|
1
|
+
import { signal } from "@pyreon/reactivity"
|
|
2
|
+
import { mount } from "@pyreon/runtime-dom"
|
|
3
|
+
import { QueryClient } from "@tanstack/query-core"
|
|
4
|
+
import { QueryClientProvider, type UseSubscriptionResult, useSubscription } from "../index"
|
|
9
5
|
|
|
10
6
|
// ─── Mock WebSocket ──────────────────────────────────────────────────────────
|
|
11
7
|
|
|
@@ -58,20 +54,20 @@ class MockWebSocketClass {
|
|
|
58
54
|
|
|
59
55
|
_simulateOpen() {
|
|
60
56
|
this.readyState = MockWebSocketClass.OPEN
|
|
61
|
-
this.onopen?.({ type:
|
|
57
|
+
this.onopen?.({ type: "open" })
|
|
62
58
|
}
|
|
63
59
|
|
|
64
60
|
_simulateMessage(data: string) {
|
|
65
|
-
this.onmessage?.({ type:
|
|
61
|
+
this.onmessage?.({ type: "message", data } as unknown as MessageEvent)
|
|
66
62
|
}
|
|
67
63
|
|
|
68
|
-
_simulateClose(code = 1000, reason =
|
|
64
|
+
_simulateClose(code = 1000, reason = "") {
|
|
69
65
|
this.readyState = MockWebSocketClass.CLOSED
|
|
70
|
-
this.onclose?.({ type:
|
|
66
|
+
this.onclose?.({ type: "close", code, reason } as unknown as CloseEvent)
|
|
71
67
|
}
|
|
72
68
|
|
|
73
69
|
_simulateError() {
|
|
74
|
-
this.onerror?.({ type:
|
|
70
|
+
this.onerror?.({ type: "error" })
|
|
75
71
|
}
|
|
76
72
|
}
|
|
77
73
|
|
|
@@ -96,7 +92,7 @@ function makeClient() {
|
|
|
96
92
|
}
|
|
97
93
|
|
|
98
94
|
function withProvider(client: QueryClient, component: () => void): () => void {
|
|
99
|
-
const el = document.createElement(
|
|
95
|
+
const el = document.createElement("div")
|
|
100
96
|
document.body.appendChild(el)
|
|
101
97
|
const unmount = mount(
|
|
102
98
|
<QueryClientProvider client={client}>
|
|
@@ -119,53 +115,53 @@ function lastMockWS(): MockWebSocket {
|
|
|
119
115
|
|
|
120
116
|
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
121
117
|
|
|
122
|
-
describe(
|
|
118
|
+
describe("useSubscription", () => {
|
|
123
119
|
beforeEach(() => {
|
|
124
120
|
mockInstances = []
|
|
125
121
|
})
|
|
126
122
|
|
|
127
|
-
it(
|
|
123
|
+
it("connects to the WebSocket URL", () => {
|
|
128
124
|
const client = makeClient()
|
|
129
125
|
let sub: UseSubscriptionResult | null = null
|
|
130
126
|
|
|
131
127
|
const unmount = withProvider(client, () => {
|
|
132
128
|
sub = useSubscription({
|
|
133
|
-
url:
|
|
129
|
+
url: "wss://example.com/ws",
|
|
134
130
|
onMessage: noop,
|
|
135
131
|
})
|
|
136
132
|
})
|
|
137
133
|
|
|
138
134
|
expect(mockInstances).toHaveLength(1)
|
|
139
|
-
expect(lastMockWS().url).toBe(
|
|
140
|
-
expect(sub!.status()).toBe(
|
|
135
|
+
expect(lastMockWS().url).toBe("wss://example.com/ws")
|
|
136
|
+
expect(sub!.status()).toBe("connecting")
|
|
141
137
|
|
|
142
138
|
unmount()
|
|
143
139
|
})
|
|
144
140
|
|
|
145
|
-
it(
|
|
141
|
+
it("status transitions to connected on open", () => {
|
|
146
142
|
const client = makeClient()
|
|
147
143
|
let sub: UseSubscriptionResult | null = null
|
|
148
144
|
|
|
149
145
|
const unmount = withProvider(client, () => {
|
|
150
146
|
sub = useSubscription({
|
|
151
|
-
url:
|
|
147
|
+
url: "wss://example.com/ws",
|
|
152
148
|
onMessage: noop,
|
|
153
149
|
})
|
|
154
150
|
})
|
|
155
151
|
|
|
156
152
|
lastMockWS()._simulateOpen()
|
|
157
|
-
expect(sub!.status()).toBe(
|
|
153
|
+
expect(sub!.status()).toBe("connected")
|
|
158
154
|
|
|
159
155
|
unmount()
|
|
160
156
|
})
|
|
161
157
|
|
|
162
|
-
it(
|
|
158
|
+
it("calls onMessage with event and queryClient", () => {
|
|
163
159
|
const client = makeClient()
|
|
164
160
|
const messages: string[] = []
|
|
165
161
|
|
|
166
162
|
const unmount = withProvider(client, () => {
|
|
167
163
|
useSubscription({
|
|
168
|
-
url:
|
|
164
|
+
url: "wss://example.com/ws",
|
|
169
165
|
onMessage: (event, qc) => {
|
|
170
166
|
messages.push(event.data as string)
|
|
171
167
|
expect(qc).toBe(client)
|
|
@@ -174,75 +170,75 @@ describe('useSubscription', () => {
|
|
|
174
170
|
})
|
|
175
171
|
|
|
176
172
|
lastMockWS()._simulateOpen()
|
|
177
|
-
lastMockWS()._simulateMessage(
|
|
178
|
-
lastMockWS()._simulateMessage(
|
|
173
|
+
lastMockWS()._simulateMessage("hello")
|
|
174
|
+
lastMockWS()._simulateMessage("world")
|
|
179
175
|
|
|
180
|
-
expect(messages).toEqual([
|
|
176
|
+
expect(messages).toEqual(["hello", "world"])
|
|
181
177
|
unmount()
|
|
182
178
|
})
|
|
183
179
|
|
|
184
|
-
it(
|
|
180
|
+
it("invalidates queries on message", () => {
|
|
185
181
|
const client = makeClient()
|
|
186
|
-
const invalidateSpy = vi.spyOn(client,
|
|
182
|
+
const invalidateSpy = vi.spyOn(client, "invalidateQueries")
|
|
187
183
|
|
|
188
184
|
const unmount = withProvider(client, () => {
|
|
189
185
|
useSubscription({
|
|
190
|
-
url:
|
|
186
|
+
url: "wss://example.com/ws",
|
|
191
187
|
onMessage: (_event, qc) => {
|
|
192
|
-
qc.invalidateQueries({ queryKey: [
|
|
188
|
+
qc.invalidateQueries({ queryKey: ["orders"] })
|
|
193
189
|
},
|
|
194
190
|
})
|
|
195
191
|
})
|
|
196
192
|
|
|
197
193
|
lastMockWS()._simulateOpen()
|
|
198
|
-
lastMockWS()._simulateMessage(
|
|
194
|
+
lastMockWS()._simulateMessage("order-updated")
|
|
199
195
|
|
|
200
|
-
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: [
|
|
196
|
+
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ["orders"] })
|
|
201
197
|
unmount()
|
|
202
198
|
})
|
|
203
199
|
|
|
204
|
-
it(
|
|
200
|
+
it("send() sends data through WebSocket", () => {
|
|
205
201
|
const client = makeClient()
|
|
206
202
|
let sub: UseSubscriptionResult | null = null
|
|
207
203
|
|
|
208
204
|
const unmount = withProvider(client, () => {
|
|
209
205
|
sub = useSubscription({
|
|
210
|
-
url:
|
|
206
|
+
url: "wss://example.com/ws",
|
|
211
207
|
onMessage: noop,
|
|
212
208
|
})
|
|
213
209
|
})
|
|
214
210
|
|
|
215
211
|
lastMockWS()._simulateOpen()
|
|
216
|
-
sub!.send(
|
|
212
|
+
sub!.send("test-message")
|
|
217
213
|
|
|
218
|
-
expect(lastMockWS().send).toHaveBeenCalledWith(
|
|
214
|
+
expect(lastMockWS().send).toHaveBeenCalledWith("test-message")
|
|
219
215
|
unmount()
|
|
220
216
|
})
|
|
221
217
|
|
|
222
|
-
it(
|
|
218
|
+
it("send() is a no-op when not connected", () => {
|
|
223
219
|
const client = makeClient()
|
|
224
220
|
let sub: UseSubscriptionResult | null = null
|
|
225
221
|
|
|
226
222
|
const unmount = withProvider(client, () => {
|
|
227
223
|
sub = useSubscription({
|
|
228
|
-
url:
|
|
224
|
+
url: "wss://example.com/ws",
|
|
229
225
|
onMessage: noop,
|
|
230
226
|
})
|
|
231
227
|
})
|
|
232
228
|
|
|
233
229
|
// Still connecting — send should not throw
|
|
234
|
-
sub!.send(
|
|
230
|
+
sub!.send("ignored")
|
|
235
231
|
expect(lastMockWS().send).not.toHaveBeenCalled()
|
|
236
232
|
unmount()
|
|
237
233
|
})
|
|
238
234
|
|
|
239
|
-
it(
|
|
235
|
+
it("close() disconnects and sets status", () => {
|
|
240
236
|
const client = makeClient()
|
|
241
237
|
let sub: UseSubscriptionResult | null = null
|
|
242
238
|
|
|
243
239
|
const unmount = withProvider(client, () => {
|
|
244
240
|
sub = useSubscription({
|
|
245
|
-
url:
|
|
241
|
+
url: "wss://example.com/ws",
|
|
246
242
|
onMessage: noop,
|
|
247
243
|
})
|
|
248
244
|
})
|
|
@@ -250,18 +246,18 @@ describe('useSubscription', () => {
|
|
|
250
246
|
lastMockWS()._simulateOpen()
|
|
251
247
|
sub!.close()
|
|
252
248
|
|
|
253
|
-
expect(sub!.status()).toBe(
|
|
249
|
+
expect(sub!.status()).toBe("disconnected")
|
|
254
250
|
expect(lastMockWS().close).toHaveBeenCalled()
|
|
255
251
|
unmount()
|
|
256
252
|
})
|
|
257
253
|
|
|
258
|
-
it(
|
|
254
|
+
it("status transitions to disconnected on close", () => {
|
|
259
255
|
const client = makeClient()
|
|
260
256
|
let sub: UseSubscriptionResult | null = null
|
|
261
257
|
|
|
262
258
|
const unmount = withProvider(client, () => {
|
|
263
259
|
sub = useSubscription({
|
|
264
|
-
url:
|
|
260
|
+
url: "wss://example.com/ws",
|
|
265
261
|
onMessage: noop,
|
|
266
262
|
reconnect: false,
|
|
267
263
|
})
|
|
@@ -270,18 +266,18 @@ describe('useSubscription', () => {
|
|
|
270
266
|
lastMockWS()._simulateOpen()
|
|
271
267
|
lastMockWS()._simulateClose()
|
|
272
268
|
|
|
273
|
-
expect(sub!.status()).toBe(
|
|
269
|
+
expect(sub!.status()).toBe("disconnected")
|
|
274
270
|
unmount()
|
|
275
271
|
})
|
|
276
272
|
|
|
277
|
-
it(
|
|
273
|
+
it("status transitions to error on error", () => {
|
|
278
274
|
const client = makeClient()
|
|
279
275
|
let sub: UseSubscriptionResult | null = null
|
|
280
276
|
const errors: Event[] = []
|
|
281
277
|
|
|
282
278
|
const unmount = withProvider(client, () => {
|
|
283
279
|
sub = useSubscription({
|
|
284
|
-
url:
|
|
280
|
+
url: "wss://example.com/ws",
|
|
285
281
|
onMessage: noop,
|
|
286
282
|
reconnect: false,
|
|
287
283
|
onError: (e) => errors.push(e as Event),
|
|
@@ -289,18 +285,18 @@ describe('useSubscription', () => {
|
|
|
289
285
|
})
|
|
290
286
|
|
|
291
287
|
lastMockWS()._simulateError()
|
|
292
|
-
expect(sub!.status()).toBe(
|
|
288
|
+
expect(sub!.status()).toBe("error")
|
|
293
289
|
expect(errors).toHaveLength(1)
|
|
294
290
|
unmount()
|
|
295
291
|
})
|
|
296
292
|
|
|
297
|
-
it(
|
|
293
|
+
it("calls onOpen callback", () => {
|
|
298
294
|
const client = makeClient()
|
|
299
295
|
let opened = false
|
|
300
296
|
|
|
301
297
|
const unmount = withProvider(client, () => {
|
|
302
298
|
useSubscription({
|
|
303
|
-
url:
|
|
299
|
+
url: "wss://example.com/ws",
|
|
304
300
|
onMessage: noop,
|
|
305
301
|
onOpen: () => {
|
|
306
302
|
opened = true
|
|
@@ -313,13 +309,13 @@ describe('useSubscription', () => {
|
|
|
313
309
|
unmount()
|
|
314
310
|
})
|
|
315
311
|
|
|
316
|
-
it(
|
|
312
|
+
it("calls onClose callback", () => {
|
|
317
313
|
const client = makeClient()
|
|
318
314
|
let closed = false
|
|
319
315
|
|
|
320
316
|
const unmount = withProvider(client, () => {
|
|
321
317
|
useSubscription({
|
|
322
|
-
url:
|
|
318
|
+
url: "wss://example.com/ws",
|
|
323
319
|
onMessage: noop,
|
|
324
320
|
reconnect: false,
|
|
325
321
|
onClose: () => {
|
|
@@ -334,12 +330,12 @@ describe('useSubscription', () => {
|
|
|
334
330
|
unmount()
|
|
335
331
|
})
|
|
336
332
|
|
|
337
|
-
it(
|
|
333
|
+
it("auto-reconnects on unexpected close", async () => {
|
|
338
334
|
const client = makeClient()
|
|
339
335
|
|
|
340
336
|
const unmount = withProvider(client, () => {
|
|
341
337
|
useSubscription({
|
|
342
|
-
url:
|
|
338
|
+
url: "wss://example.com/ws",
|
|
343
339
|
onMessage: noop,
|
|
344
340
|
reconnect: true,
|
|
345
341
|
reconnectDelay: 50,
|
|
@@ -357,12 +353,12 @@ describe('useSubscription', () => {
|
|
|
357
353
|
unmount()
|
|
358
354
|
})
|
|
359
355
|
|
|
360
|
-
it(
|
|
356
|
+
it("does not reconnect when reconnect is false", async () => {
|
|
361
357
|
const client = makeClient()
|
|
362
358
|
|
|
363
359
|
const unmount = withProvider(client, () => {
|
|
364
360
|
useSubscription({
|
|
365
|
-
url:
|
|
361
|
+
url: "wss://example.com/ws",
|
|
366
362
|
onMessage: noop,
|
|
367
363
|
reconnect: false,
|
|
368
364
|
})
|
|
@@ -376,13 +372,13 @@ describe('useSubscription', () => {
|
|
|
376
372
|
unmount()
|
|
377
373
|
})
|
|
378
374
|
|
|
379
|
-
it(
|
|
375
|
+
it("does not reconnect after intentional close()", async () => {
|
|
380
376
|
const client = makeClient()
|
|
381
377
|
let sub: UseSubscriptionResult | null = null
|
|
382
378
|
|
|
383
379
|
const unmount = withProvider(client, () => {
|
|
384
380
|
sub = useSubscription({
|
|
385
|
-
url:
|
|
381
|
+
url: "wss://example.com/ws",
|
|
386
382
|
onMessage: noop,
|
|
387
383
|
reconnect: true,
|
|
388
384
|
reconnectDelay: 50,
|
|
@@ -397,12 +393,12 @@ describe('useSubscription', () => {
|
|
|
397
393
|
unmount()
|
|
398
394
|
})
|
|
399
395
|
|
|
400
|
-
it(
|
|
396
|
+
it("respects maxReconnectAttempts", async () => {
|
|
401
397
|
const client = makeClient()
|
|
402
398
|
|
|
403
399
|
const unmount = withProvider(client, () => {
|
|
404
400
|
useSubscription({
|
|
405
|
-
url:
|
|
401
|
+
url: "wss://example.com/ws",
|
|
406
402
|
onMessage: noop,
|
|
407
403
|
reconnect: true,
|
|
408
404
|
reconnectDelay: 10,
|
|
@@ -431,13 +427,13 @@ describe('useSubscription', () => {
|
|
|
431
427
|
unmount()
|
|
432
428
|
})
|
|
433
429
|
|
|
434
|
-
it(
|
|
430
|
+
it("reconnect() resets attempts and reconnects", async () => {
|
|
435
431
|
const client = makeClient()
|
|
436
432
|
let sub: UseSubscriptionResult | null = null
|
|
437
433
|
|
|
438
434
|
const unmount = withProvider(client, () => {
|
|
439
435
|
sub = useSubscription({
|
|
440
|
-
url:
|
|
436
|
+
url: "wss://example.com/ws",
|
|
441
437
|
onMessage: noop,
|
|
442
438
|
reconnect: false,
|
|
443
439
|
})
|
|
@@ -450,35 +446,35 @@ describe('useSubscription', () => {
|
|
|
450
446
|
|
|
451
447
|
sub!.reconnect()
|
|
452
448
|
expect(mockInstances).toHaveLength(2)
|
|
453
|
-
expect(sub!.status()).toBe(
|
|
449
|
+
expect(sub!.status()).toBe("connecting")
|
|
454
450
|
|
|
455
451
|
unmount()
|
|
456
452
|
})
|
|
457
453
|
|
|
458
|
-
it(
|
|
454
|
+
it("enabled: false prevents connection", () => {
|
|
459
455
|
const client = makeClient()
|
|
460
456
|
let sub: UseSubscriptionResult | null = null
|
|
461
457
|
|
|
462
458
|
const unmount = withProvider(client, () => {
|
|
463
459
|
sub = useSubscription({
|
|
464
|
-
url:
|
|
460
|
+
url: "wss://example.com/ws",
|
|
465
461
|
onMessage: noop,
|
|
466
462
|
enabled: false,
|
|
467
463
|
})
|
|
468
464
|
})
|
|
469
465
|
|
|
470
466
|
expect(mockInstances).toHaveLength(0)
|
|
471
|
-
expect(sub!.status()).toBe(
|
|
467
|
+
expect(sub!.status()).toBe("disconnected")
|
|
472
468
|
unmount()
|
|
473
469
|
})
|
|
474
470
|
|
|
475
|
-
it(
|
|
471
|
+
it("reactive enabled signal controls connection", async () => {
|
|
476
472
|
const client = makeClient()
|
|
477
473
|
const enabled = signal(false)
|
|
478
474
|
|
|
479
475
|
const unmount = withProvider(client, () => {
|
|
480
476
|
useSubscription({
|
|
481
|
-
url:
|
|
477
|
+
url: "wss://example.com/ws",
|
|
482
478
|
onMessage: noop,
|
|
483
479
|
enabled: () => enabled(),
|
|
484
480
|
})
|
|
@@ -493,9 +489,9 @@ describe('useSubscription', () => {
|
|
|
493
489
|
unmount()
|
|
494
490
|
})
|
|
495
491
|
|
|
496
|
-
it(
|
|
492
|
+
it("reactive URL reconnects when URL changes", () => {
|
|
497
493
|
const client = makeClient()
|
|
498
|
-
const url = signal(
|
|
494
|
+
const url = signal("wss://example.com/ws1")
|
|
499
495
|
|
|
500
496
|
const unmount = withProvider(client, () => {
|
|
501
497
|
useSubscription({
|
|
@@ -505,37 +501,37 @@ describe('useSubscription', () => {
|
|
|
505
501
|
})
|
|
506
502
|
|
|
507
503
|
expect(mockInstances).toHaveLength(1)
|
|
508
|
-
expect(lastMockWS().url).toBe(
|
|
504
|
+
expect(lastMockWS().url).toBe("wss://example.com/ws1")
|
|
509
505
|
|
|
510
|
-
url.set(
|
|
506
|
+
url.set("wss://example.com/ws2")
|
|
511
507
|
|
|
512
508
|
expect(mockInstances).toHaveLength(2)
|
|
513
|
-
expect(lastMockWS().url).toBe(
|
|
509
|
+
expect(lastMockWS().url).toBe("wss://example.com/ws2")
|
|
514
510
|
|
|
515
511
|
unmount()
|
|
516
512
|
})
|
|
517
513
|
|
|
518
|
-
it(
|
|
514
|
+
it("supports WebSocket protocols", () => {
|
|
519
515
|
const client = makeClient()
|
|
520
516
|
|
|
521
517
|
const unmount = withProvider(client, () => {
|
|
522
518
|
useSubscription({
|
|
523
|
-
url:
|
|
524
|
-
protocols: [
|
|
519
|
+
url: "wss://example.com/ws",
|
|
520
|
+
protocols: ["graphql-ws"],
|
|
525
521
|
onMessage: noop,
|
|
526
522
|
})
|
|
527
523
|
})
|
|
528
524
|
|
|
529
|
-
expect(lastMockWS().protocols).toEqual([
|
|
525
|
+
expect(lastMockWS().protocols).toEqual(["graphql-ws"])
|
|
530
526
|
unmount()
|
|
531
527
|
})
|
|
532
528
|
|
|
533
|
-
it(
|
|
529
|
+
it("cleans up on unmount", () => {
|
|
534
530
|
const client = makeClient()
|
|
535
531
|
|
|
536
532
|
const unmount = withProvider(client, () => {
|
|
537
533
|
useSubscription({
|
|
538
|
-
url:
|
|
534
|
+
url: "wss://example.com/ws",
|
|
539
535
|
onMessage: noop,
|
|
540
536
|
})
|
|
541
537
|
})
|
|
@@ -547,12 +543,134 @@ describe('useSubscription', () => {
|
|
|
547
543
|
expect(ws.close).toHaveBeenCalled()
|
|
548
544
|
})
|
|
549
545
|
|
|
550
|
-
|
|
546
|
+
// ─── Error handling & cleanup ──────────────────────────────────────────────
|
|
547
|
+
|
|
548
|
+
it("reconnects after connection drop (simulated close)", async () => {
|
|
549
|
+
const client = makeClient()
|
|
550
|
+
let sub: UseSubscriptionResult | null = null
|
|
551
|
+
|
|
552
|
+
const unmount = withProvider(client, () => {
|
|
553
|
+
sub = useSubscription({
|
|
554
|
+
url: "wss://example.com/ws",
|
|
555
|
+
onMessage: noop,
|
|
556
|
+
reconnect: true,
|
|
557
|
+
reconnectDelay: 20,
|
|
558
|
+
})
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
// Establish connection then drop it
|
|
562
|
+
lastMockWS()._simulateOpen()
|
|
563
|
+
expect(sub!.status()).toBe("connected")
|
|
564
|
+
|
|
565
|
+
// Simulate unexpected connection drop
|
|
566
|
+
lastMockWS()._simulateClose(1006, "abnormal closure")
|
|
567
|
+
|
|
568
|
+
// Wait for reconnect attempt
|
|
569
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
570
|
+
|
|
571
|
+
expect(mockInstances).toHaveLength(2)
|
|
572
|
+
expect(lastMockWS().url).toBe("wss://example.com/ws")
|
|
573
|
+
expect(sub!.status()).toBe("connecting")
|
|
574
|
+
|
|
575
|
+
unmount()
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
it("message handler that throws does not crash the subscription", () => {
|
|
579
|
+
const client = makeClient()
|
|
580
|
+
let sub: UseSubscriptionResult | null = null
|
|
581
|
+
|
|
582
|
+
const unmount = withProvider(client, () => {
|
|
583
|
+
sub = useSubscription({
|
|
584
|
+
url: "wss://example.com/ws",
|
|
585
|
+
onMessage: () => {
|
|
586
|
+
throw new Error("handler boom")
|
|
587
|
+
},
|
|
588
|
+
})
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
lastMockWS()._simulateOpen()
|
|
592
|
+
|
|
593
|
+
// Should not throw — the error is caught internally
|
|
594
|
+
expect(() => lastMockWS()._simulateMessage("test")).not.toThrow()
|
|
595
|
+
|
|
596
|
+
// Subscription should still be connected
|
|
597
|
+
expect(sub!.status()).toBe("connected")
|
|
598
|
+
|
|
599
|
+
unmount()
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
it("WebSocket is closed on unmount (cleanup)", () => {
|
|
603
|
+
const client = makeClient()
|
|
604
|
+
|
|
605
|
+
const unmount = withProvider(client, () => {
|
|
606
|
+
useSubscription({
|
|
607
|
+
url: "wss://example.com/ws",
|
|
608
|
+
onMessage: noop,
|
|
609
|
+
})
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
lastMockWS()._simulateOpen()
|
|
613
|
+
const ws = lastMockWS()
|
|
614
|
+
|
|
615
|
+
expect(ws.readyState).toBe(MockWebSocketClass.OPEN)
|
|
616
|
+
|
|
617
|
+
unmount()
|
|
618
|
+
|
|
619
|
+
expect(ws.close).toHaveBeenCalled()
|
|
620
|
+
expect(ws.readyState).toBe(MockWebSocketClass.CLOSED)
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
it("no reconnect attempts after unmount", async () => {
|
|
624
|
+
const client = makeClient()
|
|
625
|
+
|
|
626
|
+
const unmount = withProvider(client, () => {
|
|
627
|
+
useSubscription({
|
|
628
|
+
url: "wss://example.com/ws",
|
|
629
|
+
onMessage: noop,
|
|
630
|
+
reconnect: true,
|
|
631
|
+
reconnectDelay: 20,
|
|
632
|
+
})
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
lastMockWS()._simulateOpen()
|
|
636
|
+
unmount()
|
|
637
|
+
|
|
638
|
+
// Wait longer than reconnectDelay — should not create new connections
|
|
639
|
+
await new Promise((r) => setTimeout(r, 80))
|
|
640
|
+
expect(mockInstances).toHaveLength(1)
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
it("send when disconnected does not crash", () => {
|
|
644
|
+
const client = makeClient()
|
|
645
|
+
let sub: UseSubscriptionResult | null = null
|
|
646
|
+
|
|
647
|
+
const unmount = withProvider(client, () => {
|
|
648
|
+
sub = useSubscription({
|
|
649
|
+
url: "wss://example.com/ws",
|
|
650
|
+
onMessage: noop,
|
|
651
|
+
reconnect: false,
|
|
652
|
+
})
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
lastMockWS()._simulateOpen()
|
|
656
|
+
lastMockWS()._simulateClose()
|
|
657
|
+
|
|
658
|
+
expect(sub!.status()).toBe("disconnected")
|
|
659
|
+
|
|
660
|
+
// Should not throw
|
|
661
|
+
expect(() => sub!.send("test")).not.toThrow()
|
|
662
|
+
// The underlying WS send should not have been called (only the one from close)
|
|
663
|
+
expect(lastMockWS().send).not.toHaveBeenCalled()
|
|
664
|
+
|
|
665
|
+
unmount()
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
it("resets reconnect count on successful connection", async () => {
|
|
551
669
|
const client = makeClient()
|
|
552
670
|
|
|
553
671
|
const unmount = withProvider(client, () => {
|
|
554
672
|
useSubscription({
|
|
555
|
-
url:
|
|
673
|
+
url: "wss://example.com/ws",
|
|
556
674
|
onMessage: noop,
|
|
557
675
|
reconnect: true,
|
|
558
676
|
reconnectDelay: 10,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { onUnmount } from
|
|
2
|
-
import type { Signal } from
|
|
3
|
-
import { batch, effect, signal } from
|
|
1
|
+
import { onUnmount } from "@pyreon/core"
|
|
2
|
+
import type { Signal } from "@pyreon/reactivity"
|
|
3
|
+
import { batch, effect, signal } from "@pyreon/reactivity"
|
|
4
4
|
import type {
|
|
5
5
|
DefaultError,
|
|
6
6
|
InfiniteData,
|
|
@@ -8,18 +8,16 @@ import type {
|
|
|
8
8
|
InfiniteQueryObserverResult,
|
|
9
9
|
QueryKey,
|
|
10
10
|
QueryObserverResult,
|
|
11
|
-
} from
|
|
12
|
-
import { InfiniteQueryObserver } from
|
|
13
|
-
import { useQueryClient } from
|
|
11
|
+
} from "@tanstack/query-core"
|
|
12
|
+
import { InfiniteQueryObserver } from "@tanstack/query-core"
|
|
13
|
+
import { useQueryClient } from "./query-client"
|
|
14
14
|
|
|
15
15
|
export interface UseInfiniteQueryResult<TQueryFnData, TError = DefaultError> {
|
|
16
16
|
/** Raw signal — full observer result. */
|
|
17
|
-
result: Signal<
|
|
18
|
-
InfiniteQueryObserverResult<InfiniteData<TQueryFnData>, TError>
|
|
19
|
-
>
|
|
17
|
+
result: Signal<InfiniteQueryObserverResult<InfiniteData<TQueryFnData>, TError>>
|
|
20
18
|
data: Signal<InfiniteData<TQueryFnData> | undefined>
|
|
21
19
|
error: Signal<TError | null>
|
|
22
|
-
status: Signal<
|
|
20
|
+
status: Signal<"pending" | "error" | "success">
|
|
23
21
|
isPending: Signal<boolean>
|
|
24
22
|
isLoading: Signal<boolean>
|
|
25
23
|
isFetching: Signal<boolean>
|
|
@@ -29,15 +27,9 @@ export interface UseInfiniteQueryResult<TQueryFnData, TError = DefaultError> {
|
|
|
29
27
|
isSuccess: Signal<boolean>
|
|
30
28
|
hasNextPage: Signal<boolean>
|
|
31
29
|
hasPreviousPage: Signal<boolean>
|
|
32
|
-
fetchNextPage: () => Promise<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
fetchPreviousPage: () => Promise<
|
|
36
|
-
InfiniteQueryObserverResult<InfiniteData<TQueryFnData>, TError>
|
|
37
|
-
>
|
|
38
|
-
refetch: () => Promise<
|
|
39
|
-
QueryObserverResult<InfiniteData<TQueryFnData>, TError>
|
|
40
|
-
>
|
|
30
|
+
fetchNextPage: () => Promise<InfiniteQueryObserverResult<InfiniteData<TQueryFnData>, TError>>
|
|
31
|
+
fetchPreviousPage: () => Promise<InfiniteQueryObserverResult<InfiniteData<TQueryFnData>, TError>>
|
|
32
|
+
refetch: () => Promise<QueryObserverResult<InfiniteData<TQueryFnData>, TError>>
|
|
41
33
|
}
|
|
42
34
|
|
|
43
35
|
/**
|
package/src/use-is-fetching.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { onUnmount } from
|
|
2
|
-
import type { Signal } from
|
|
3
|
-
import { signal } from
|
|
4
|
-
import type { MutationFilters, QueryFilters } from
|
|
5
|
-
import { useQueryClient } from
|
|
1
|
+
import { onUnmount } from "@pyreon/core"
|
|
2
|
+
import type { Signal } from "@pyreon/reactivity"
|
|
3
|
+
import { signal } from "@pyreon/reactivity"
|
|
4
|
+
import type { MutationFilters, QueryFilters } from "@tanstack/query-core"
|
|
5
|
+
import { useQueryClient } from "./query-client"
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Returns a signal that tracks how many queries are currently in-flight.
|