@milaboratories/uikit 2.5.7 → 2.6.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/.turbo/turbo-build.log +27 -26
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +6 -0
- package/dist/__tests__/compositions/usePollingQuery.spec.d.ts +1 -0
- package/dist/components/PlAccordion/ExpandTransition.vue.js +27 -0
- package/dist/components/PlAccordion/ExpandTransition.vue.js.map +1 -0
- package/dist/composition/usePollingQuery.d.ts +121 -0
- package/dist/composition/usePollingQuery.js +137 -0
- package/dist/composition/usePollingQuery.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +83 -81
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/compositions/usePollingQuery.spec.ts +1218 -0
- package/src/composition/usePollingQuery.ts +415 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,1218 @@
|
|
|
1
|
+
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { effectScope, ref } from 'vue';
|
|
3
|
+
import { usePollingQuery } from '../../composition/usePollingQuery';
|
|
4
|
+
|
|
5
|
+
type ControlledCall<Args, Result> = {
|
|
6
|
+
args: Args;
|
|
7
|
+
signal: AbortSignal;
|
|
8
|
+
pause: () => void;
|
|
9
|
+
startTime: number;
|
|
10
|
+
resolve: (value: Result) => void;
|
|
11
|
+
reject: (error?: unknown) => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function createDeferred<T>() {
|
|
15
|
+
let resolve!: (value: T) => void;
|
|
16
|
+
let reject!: (reason?: unknown) => void;
|
|
17
|
+
const promise = new Promise<T>((res, rej) => {
|
|
18
|
+
resolve = res;
|
|
19
|
+
reject = rej;
|
|
20
|
+
});
|
|
21
|
+
return { promise, resolve, reject };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createControlledQuery<Args, Result>() {
|
|
25
|
+
const calls: ControlledCall<Args, Result>[] = [];
|
|
26
|
+
const fn = vi.fn(
|
|
27
|
+
(callArgs: Args, options: { signal: AbortSignal; pause: () => void }) => {
|
|
28
|
+
const deferred = createDeferred<Result>();
|
|
29
|
+
calls.push({
|
|
30
|
+
args: callArgs,
|
|
31
|
+
signal: options.signal,
|
|
32
|
+
pause: options.pause,
|
|
33
|
+
startTime: Date.now(),
|
|
34
|
+
resolve: deferred.resolve,
|
|
35
|
+
reject: deferred.reject,
|
|
36
|
+
});
|
|
37
|
+
return deferred.promise;
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
return { fn, calls };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const flushMicrotasks = async () => {
|
|
44
|
+
await Promise.resolve();
|
|
45
|
+
await Promise.resolve();
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const advanceTime = async (ms: number) => {
|
|
49
|
+
await vi.advanceTimersByTimeAsync(ms);
|
|
50
|
+
await flushMicrotasks();
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function runInScope<T>(factory: () => T) {
|
|
54
|
+
const scope = effectScope();
|
|
55
|
+
const result = scope.run(factory);
|
|
56
|
+
if (!result) {
|
|
57
|
+
scope.stop();
|
|
58
|
+
throw new Error('Failed to initialise polling scope');
|
|
59
|
+
}
|
|
60
|
+
return { scope, result };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('usePollingQuery', () => {
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
vi.useFakeTimers();
|
|
66
|
+
vi.setSystemTime(0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
vi.runAllTimers();
|
|
71
|
+
vi.useRealTimers();
|
|
72
|
+
vi.restoreAllMocks();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('runs immediately when triggerOnResume is true and respects timing constraints', async () => {
|
|
76
|
+
const args = ref({ id: 'alpha' });
|
|
77
|
+
const { fn, calls } = createControlledQuery<typeof args.value, string>();
|
|
78
|
+
|
|
79
|
+
const { scope, result } = runInScope(() =>
|
|
80
|
+
usePollingQuery(args, fn, {
|
|
81
|
+
minInterval: 1000,
|
|
82
|
+
minDelay: 200,
|
|
83
|
+
autoStart: true,
|
|
84
|
+
triggerOnResume: true,
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
const { data, lastError, isActive } = result;
|
|
88
|
+
|
|
89
|
+
expect(data.value).toEqual({ status: 'idle' });
|
|
90
|
+
expect(isActive.value).toBe(true);
|
|
91
|
+
|
|
92
|
+
await advanceTime(0);
|
|
93
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
94
|
+
expect(calls[0].startTime).toBe(0);
|
|
95
|
+
|
|
96
|
+
await advanceTime(400);
|
|
97
|
+
calls[0].resolve('first');
|
|
98
|
+
await flushMicrotasks();
|
|
99
|
+
|
|
100
|
+
expect(data.value.status).toBe('synced');
|
|
101
|
+
if (data.value.status === 'synced') {
|
|
102
|
+
expect(data.value.value).toBe('first');
|
|
103
|
+
}
|
|
104
|
+
expect(lastError.value).toBeNull();
|
|
105
|
+
|
|
106
|
+
await advanceTime(599);
|
|
107
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
108
|
+
|
|
109
|
+
await advanceTime(1);
|
|
110
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
111
|
+
expect(calls[1].startTime).toBe(1000);
|
|
112
|
+
expect(isActive.value).toBe(true);
|
|
113
|
+
|
|
114
|
+
scope.stop();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('cycles according to minInterval when callback resolves immediately', async () => {
|
|
118
|
+
const args = ref({ id: 0 });
|
|
119
|
+
const startTimes: number[] = [];
|
|
120
|
+
const fn = vi.fn(async () => {
|
|
121
|
+
startTimes.push(Date.now());
|
|
122
|
+
return 'value';
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const minInterval = ref(300);
|
|
126
|
+
|
|
127
|
+
const { scope } = runInScope(() =>
|
|
128
|
+
usePollingQuery(args, fn, {
|
|
129
|
+
minInterval,
|
|
130
|
+
autoStart: true,
|
|
131
|
+
triggerOnResume: true,
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
await advanceTime(0);
|
|
136
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
137
|
+
expect(startTimes[0]).toBe(0);
|
|
138
|
+
|
|
139
|
+
await advanceTime(300);
|
|
140
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
141
|
+
expect(startTimes[1]).toBe(300);
|
|
142
|
+
|
|
143
|
+
await advanceTime(300);
|
|
144
|
+
expect(fn).toHaveBeenCalledTimes(3);
|
|
145
|
+
expect(startTimes[2]).toBe(600);
|
|
146
|
+
|
|
147
|
+
scope.stop();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('marks status stale on argument change and resyncs after success', async () => {
|
|
151
|
+
const args = ref({ id: 'initial' });
|
|
152
|
+
const { fn, calls } = createControlledQuery<typeof args.value, string>();
|
|
153
|
+
|
|
154
|
+
const { scope, result } = runInScope(() =>
|
|
155
|
+
usePollingQuery(args, fn, {
|
|
156
|
+
minInterval: 100,
|
|
157
|
+
triggerOnResume: true,
|
|
158
|
+
autoStart: true,
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
const { data } = result;
|
|
162
|
+
|
|
163
|
+
await advanceTime(0);
|
|
164
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
165
|
+
|
|
166
|
+
calls[0].resolve('first');
|
|
167
|
+
await flushMicrotasks();
|
|
168
|
+
expect(data.value).toEqual({ status: 'synced', value: 'first' });
|
|
169
|
+
|
|
170
|
+
args.value = { id: 'next' };
|
|
171
|
+
expect(data.value).toEqual({ status: 'stale', value: 'first' });
|
|
172
|
+
|
|
173
|
+
await advanceTime(100);
|
|
174
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
175
|
+
expect(calls[1].args).toEqual({ id: 'next' });
|
|
176
|
+
|
|
177
|
+
calls[1].resolve('second');
|
|
178
|
+
await flushMicrotasks();
|
|
179
|
+
expect(data.value).toEqual({ status: 'synced', value: 'second' });
|
|
180
|
+
|
|
181
|
+
scope.stop();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('remains idle on argument changes before first successful poll', async () => {
|
|
185
|
+
const args = ref({ id: 'initial' });
|
|
186
|
+
const { fn, calls } = createControlledQuery<typeof args.value, string>();
|
|
187
|
+
|
|
188
|
+
const { scope, result } = runInScope(() =>
|
|
189
|
+
usePollingQuery(args, fn, {
|
|
190
|
+
minInterval: 100,
|
|
191
|
+
triggerOnResume: true,
|
|
192
|
+
autoStart: true,
|
|
193
|
+
}),
|
|
194
|
+
);
|
|
195
|
+
const { data } = result;
|
|
196
|
+
|
|
197
|
+
await advanceTime(0);
|
|
198
|
+
expect(data.value).toEqual({ status: 'idle' });
|
|
199
|
+
|
|
200
|
+
args.value = { id: 'next' };
|
|
201
|
+
expect(data.value).toEqual({ status: 'idle' });
|
|
202
|
+
|
|
203
|
+
scope.stop();
|
|
204
|
+
expect(calls[0]?.signal.aborted).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('remains idle until resumed when created inactive', async () => {
|
|
208
|
+
const args = ref({ id: 'alpha' });
|
|
209
|
+
const fn = vi.fn(async () => 'first');
|
|
210
|
+
|
|
211
|
+
const { scope, result } = runInScope(() =>
|
|
212
|
+
usePollingQuery(args, fn, {
|
|
213
|
+
minInterval: 250,
|
|
214
|
+
autoStart: false,
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
const { resume, data } = result;
|
|
218
|
+
|
|
219
|
+
expect(fn).not.toHaveBeenCalled();
|
|
220
|
+
expect(data.value.status).toBe('idle');
|
|
221
|
+
|
|
222
|
+
await advanceTime(1000);
|
|
223
|
+
expect(fn).not.toHaveBeenCalled();
|
|
224
|
+
|
|
225
|
+
resume();
|
|
226
|
+
await advanceTime(249);
|
|
227
|
+
expect(fn).not.toHaveBeenCalled();
|
|
228
|
+
|
|
229
|
+
await advanceTime(1);
|
|
230
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
231
|
+
|
|
232
|
+
scope.stop();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('debounces argument changes and aborts in-flight requests', async () => {
|
|
236
|
+
const args = ref({ q: 'initial' });
|
|
237
|
+
const { fn, calls } = createControlledQuery<typeof args.value, string>();
|
|
238
|
+
|
|
239
|
+
const { scope } = runInScope(() =>
|
|
240
|
+
usePollingQuery(args, fn, {
|
|
241
|
+
minInterval: 100,
|
|
242
|
+
autoStart: true,
|
|
243
|
+
triggerOnResume: true,
|
|
244
|
+
debounce: 200,
|
|
245
|
+
}),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
await advanceTime(0);
|
|
249
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
250
|
+
|
|
251
|
+
args.value = { q: 'first' };
|
|
252
|
+
expect(calls[0].signal.aborted).toBe(true);
|
|
253
|
+
args.value = { q: 'second' };
|
|
254
|
+
expect(calls[0].signal.aborted).toBe(true);
|
|
255
|
+
|
|
256
|
+
calls[0].reject(new Error('aborted'));
|
|
257
|
+
await flushMicrotasks();
|
|
258
|
+
await advanceTime(0);
|
|
259
|
+
|
|
260
|
+
await advanceTime(199);
|
|
261
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
262
|
+
|
|
263
|
+
await advanceTime(201);
|
|
264
|
+
await advanceTime(0);
|
|
265
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
266
|
+
expect(calls[1].args).toEqual({ q: 'second' });
|
|
267
|
+
|
|
268
|
+
scope.stop();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('restarts debounce window on rapid argument updates', async () => {
|
|
272
|
+
const args = ref({ value: 1 });
|
|
273
|
+
const { fn, calls } = createControlledQuery<typeof args.value, string>();
|
|
274
|
+
|
|
275
|
+
const { scope } = runInScope(() =>
|
|
276
|
+
usePollingQuery(args, fn, {
|
|
277
|
+
minInterval: 100,
|
|
278
|
+
autoStart: true,
|
|
279
|
+
triggerOnResume: true,
|
|
280
|
+
debounce: 150,
|
|
281
|
+
}),
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
await advanceTime(0);
|
|
285
|
+
expect(calls).toHaveLength(1);
|
|
286
|
+
|
|
287
|
+
args.value = { value: 2 };
|
|
288
|
+
expect(calls[0].signal.aborted).toBe(true);
|
|
289
|
+
calls[0].reject(new Error('aborted'));
|
|
290
|
+
await flushMicrotasks();
|
|
291
|
+
await advanceTime(0);
|
|
292
|
+
|
|
293
|
+
await advanceTime(100);
|
|
294
|
+
args.value = { value: 3 };
|
|
295
|
+
|
|
296
|
+
await advanceTime(100);
|
|
297
|
+
args.value = { value: 4 };
|
|
298
|
+
|
|
299
|
+
await advanceTime(149);
|
|
300
|
+
expect(calls).toHaveLength(1);
|
|
301
|
+
|
|
302
|
+
await advanceTime(1);
|
|
303
|
+
await advanceTime(200);
|
|
304
|
+
await advanceTime(0);
|
|
305
|
+
expect(calls).toHaveLength(2);
|
|
306
|
+
expect(calls[1].args).toEqual({ value: 4 });
|
|
307
|
+
expect(calls[1].startTime).toBeGreaterThanOrEqual(350);
|
|
308
|
+
|
|
309
|
+
scope.stop();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('pauses polling and aborts controllers', async () => {
|
|
313
|
+
const args = ref({ id: 1 });
|
|
314
|
+
const { fn, calls } = createControlledQuery<typeof args.value, number>();
|
|
315
|
+
|
|
316
|
+
const { scope, result } = runInScope(() =>
|
|
317
|
+
usePollingQuery(args, fn, {
|
|
318
|
+
minInterval: 100,
|
|
319
|
+
autoStart: true,
|
|
320
|
+
triggerOnResume: true,
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
323
|
+
const { pause, resume, isActive } = result;
|
|
324
|
+
|
|
325
|
+
await advanceTime(0);
|
|
326
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
327
|
+
expect(isActive.value).toBe(true);
|
|
328
|
+
|
|
329
|
+
pause();
|
|
330
|
+
expect(isActive.value).toBe(false);
|
|
331
|
+
expect(calls[0].signal.aborted).toBe(true);
|
|
332
|
+
|
|
333
|
+
calls[0].reject(new Error('aborted'));
|
|
334
|
+
await flushMicrotasks();
|
|
335
|
+
|
|
336
|
+
await advanceTime(500);
|
|
337
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
338
|
+
|
|
339
|
+
resume();
|
|
340
|
+
await advanceTime(0);
|
|
341
|
+
expect(isActive.value).toBe(true);
|
|
342
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
343
|
+
|
|
344
|
+
scope.stop();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('ignores argument changes while paused until resumed', async () => {
|
|
348
|
+
const args = ref({ id: 1 });
|
|
349
|
+
const { fn, calls } = createControlledQuery<typeof args.value, string>();
|
|
350
|
+
|
|
351
|
+
const { scope, result } = runInScope(() =>
|
|
352
|
+
usePollingQuery(args, fn, {
|
|
353
|
+
minInterval: 100,
|
|
354
|
+
autoStart: true,
|
|
355
|
+
triggerOnResume: true,
|
|
356
|
+
}),
|
|
357
|
+
);
|
|
358
|
+
const { pause, resume } = result;
|
|
359
|
+
|
|
360
|
+
await advanceTime(0);
|
|
361
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
362
|
+
|
|
363
|
+
pause();
|
|
364
|
+
args.value = { id: 2 };
|
|
365
|
+
await advanceTime(500);
|
|
366
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
367
|
+
|
|
368
|
+
calls[0].reject(new Error('aborted'));
|
|
369
|
+
await flushMicrotasks();
|
|
370
|
+
|
|
371
|
+
resume();
|
|
372
|
+
await advanceTime(0);
|
|
373
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
374
|
+
expect(calls[1].args).toEqual({ id: 2 });
|
|
375
|
+
|
|
376
|
+
scope.stop();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('auto pauses on error when pauseOnError is enabled', async () => {
|
|
380
|
+
const args = ref('value');
|
|
381
|
+
const { fn, calls } = createControlledQuery<string, string>();
|
|
382
|
+
|
|
383
|
+
const { scope, result } = runInScope(() =>
|
|
384
|
+
usePollingQuery(args, fn, {
|
|
385
|
+
minInterval: 100,
|
|
386
|
+
autoStart: true,
|
|
387
|
+
triggerOnResume: true,
|
|
388
|
+
pauseOnError: true,
|
|
389
|
+
}),
|
|
390
|
+
);
|
|
391
|
+
const { lastError, isActive } = result;
|
|
392
|
+
|
|
393
|
+
await advanceTime(0);
|
|
394
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
395
|
+
|
|
396
|
+
calls[0].reject(new Error('failure'));
|
|
397
|
+
await flushMicrotasks();
|
|
398
|
+
|
|
399
|
+
expect(lastError.value?.message).toBe('failure');
|
|
400
|
+
expect(isActive.value).toBe(false);
|
|
401
|
+
|
|
402
|
+
await advanceTime(500);
|
|
403
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
404
|
+
|
|
405
|
+
scope.stop();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('continues polling after errors when pauseOnError is disabled', async () => {
|
|
409
|
+
const args = ref('value');
|
|
410
|
+
let attempt = 0;
|
|
411
|
+
const fn = vi.fn(async () => {
|
|
412
|
+
attempt += 1;
|
|
413
|
+
if (attempt === 1) throw new Error('boom');
|
|
414
|
+
return 'ok';
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const { scope, result } = runInScope(() =>
|
|
418
|
+
usePollingQuery(args, fn, {
|
|
419
|
+
minInterval: 100,
|
|
420
|
+
autoStart: true,
|
|
421
|
+
triggerOnResume: true,
|
|
422
|
+
}),
|
|
423
|
+
);
|
|
424
|
+
const { lastError, data } = result;
|
|
425
|
+
|
|
426
|
+
await advanceTime(0);
|
|
427
|
+
expect(lastError.value).toBeInstanceOf(Error);
|
|
428
|
+
expect(data.value.status).toBe('idle');
|
|
429
|
+
|
|
430
|
+
await advanceTime(100);
|
|
431
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
432
|
+
expect(lastError.value).toBeNull();
|
|
433
|
+
if (data.value.status === 'synced') {
|
|
434
|
+
expect(data.value.value).toBe('ok');
|
|
435
|
+
} else {
|
|
436
|
+
throw new Error('Expected polling data to be synced after successful retry');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
scope.stop();
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('clears lastError on resume', async () => {
|
|
443
|
+
const args = ref('value');
|
|
444
|
+
const { fn, calls } = createControlledQuery<string, string>();
|
|
445
|
+
|
|
446
|
+
const { scope, result } = runInScope(() =>
|
|
447
|
+
usePollingQuery(args, fn, {
|
|
448
|
+
minInterval: 100,
|
|
449
|
+
autoStart: true,
|
|
450
|
+
triggerOnResume: true,
|
|
451
|
+
pauseOnError: true,
|
|
452
|
+
}),
|
|
453
|
+
);
|
|
454
|
+
const { lastError, resume } = result;
|
|
455
|
+
|
|
456
|
+
await advanceTime(0);
|
|
457
|
+
calls[0].reject(new Error('failure'));
|
|
458
|
+
await flushMicrotasks();
|
|
459
|
+
expect(lastError.value).toBeInstanceOf(Error);
|
|
460
|
+
|
|
461
|
+
resume();
|
|
462
|
+
expect(lastError.value).toBeNull();
|
|
463
|
+
|
|
464
|
+
scope.stop();
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('converts thrown primitives into Error instances', async () => {
|
|
468
|
+
const args = ref('value');
|
|
469
|
+
const fn = vi.fn(async () => {
|
|
470
|
+
throw 'failure';
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
const { scope, result } = runInScope(() =>
|
|
474
|
+
usePollingQuery(args, fn, {
|
|
475
|
+
minInterval: 100,
|
|
476
|
+
autoStart: true,
|
|
477
|
+
triggerOnResume: true,
|
|
478
|
+
}),
|
|
479
|
+
);
|
|
480
|
+
const { lastError } = result;
|
|
481
|
+
|
|
482
|
+
await advanceTime(0);
|
|
483
|
+
expect(lastError.value).toBeInstanceOf(Error);
|
|
484
|
+
expect(lastError.value?.message).toBe('failure');
|
|
485
|
+
|
|
486
|
+
scope.stop();
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('ignores outdated results thanks to version tracking', async () => {
|
|
490
|
+
const args = ref({ id: 'a' });
|
|
491
|
+
const { fn, calls } = createControlledQuery<typeof args.value, string>();
|
|
492
|
+
|
|
493
|
+
const { scope, result } = runInScope(() =>
|
|
494
|
+
usePollingQuery(args, fn, {
|
|
495
|
+
minInterval: 10,
|
|
496
|
+
autoStart: true,
|
|
497
|
+
triggerOnResume: true,
|
|
498
|
+
maxInFlightRequests: 2,
|
|
499
|
+
}),
|
|
500
|
+
);
|
|
501
|
+
const { data } = result;
|
|
502
|
+
|
|
503
|
+
await advanceTime(0);
|
|
504
|
+
args.value = { id: 'b' };
|
|
505
|
+
await advanceTime(10);
|
|
506
|
+
|
|
507
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
508
|
+
|
|
509
|
+
calls[1].resolve('latest');
|
|
510
|
+
await flushMicrotasks();
|
|
511
|
+
expect(data.value).toEqual({ status: 'synced', value: 'latest' });
|
|
512
|
+
|
|
513
|
+
calls[0].resolve('stale');
|
|
514
|
+
await flushMicrotasks();
|
|
515
|
+
expect(data.value).toEqual({ status: 'synced', value: 'latest' });
|
|
516
|
+
|
|
517
|
+
scope.stop();
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('allows overlapping requests up to the configured limit', async () => {
|
|
521
|
+
const args = ref({ id: 'a' });
|
|
522
|
+
const { fn, calls } = createControlledQuery<typeof args.value, string>();
|
|
523
|
+
|
|
524
|
+
const { scope } = runInScope(() =>
|
|
525
|
+
usePollingQuery(args, fn, {
|
|
526
|
+
minInterval: 20,
|
|
527
|
+
autoStart: true,
|
|
528
|
+
triggerOnResume: true,
|
|
529
|
+
maxInFlightRequests: 2,
|
|
530
|
+
}),
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
await advanceTime(0);
|
|
534
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
535
|
+
|
|
536
|
+
args.value = { id: 'b' };
|
|
537
|
+
await advanceTime(20);
|
|
538
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
539
|
+
|
|
540
|
+
args.value = { id: 'c' };
|
|
541
|
+
await advanceTime(20);
|
|
542
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
543
|
+
|
|
544
|
+
calls[0].resolve('first');
|
|
545
|
+
await flushMicrotasks();
|
|
546
|
+
await advanceTime(0);
|
|
547
|
+
expect(fn).toHaveBeenCalledTimes(3);
|
|
548
|
+
expect(calls[2].args).toEqual({ id: 'c' });
|
|
549
|
+
|
|
550
|
+
scope.stop();
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it('exposes reactive inFlightCount', async () => {
|
|
554
|
+
const args = ref({ id: 'seed' });
|
|
555
|
+
const { fn, calls } = createControlledQuery<typeof args.value, string>();
|
|
556
|
+
|
|
557
|
+
const { scope, result } = runInScope(() =>
|
|
558
|
+
usePollingQuery(args, fn, {
|
|
559
|
+
minInterval: 20,
|
|
560
|
+
autoStart: true,
|
|
561
|
+
triggerOnResume: true,
|
|
562
|
+
maxInFlightRequests: 2,
|
|
563
|
+
}),
|
|
564
|
+
);
|
|
565
|
+
const { inFlightCount } = result;
|
|
566
|
+
|
|
567
|
+
expect(inFlightCount.value).toBe(0);
|
|
568
|
+
|
|
569
|
+
await advanceTime(0);
|
|
570
|
+
expect(inFlightCount.value).toBe(1);
|
|
571
|
+
|
|
572
|
+
args.value = { id: 'next' };
|
|
573
|
+
await advanceTime(20);
|
|
574
|
+
expect(inFlightCount.value).toBe(2);
|
|
575
|
+
|
|
576
|
+
calls[0].resolve('first');
|
|
577
|
+
await flushMicrotasks();
|
|
578
|
+
expect(inFlightCount.value).toBe(1);
|
|
579
|
+
|
|
580
|
+
calls[1].resolve('second');
|
|
581
|
+
await flushMicrotasks();
|
|
582
|
+
expect(inFlightCount.value).toBe(0);
|
|
583
|
+
|
|
584
|
+
scope.stop();
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('keeps inFlightCount > 0 for aborted but unresolved calls', async () => {
|
|
588
|
+
const args = ref({ id: 1 });
|
|
589
|
+
const { fn, calls } = createControlledQuery<typeof args.value, string>();
|
|
590
|
+
|
|
591
|
+
const { scope, result } = runInScope(() =>
|
|
592
|
+
usePollingQuery(args, fn, {
|
|
593
|
+
minInterval: 100,
|
|
594
|
+
autoStart: true,
|
|
595
|
+
triggerOnResume: true,
|
|
596
|
+
}),
|
|
597
|
+
);
|
|
598
|
+
const { pause, inFlightCount } = result;
|
|
599
|
+
|
|
600
|
+
await advanceTime(0);
|
|
601
|
+
expect(inFlightCount.value).toBe(1);
|
|
602
|
+
|
|
603
|
+
pause();
|
|
604
|
+
expect(calls[0].signal.aborted).toBe(true);
|
|
605
|
+
expect(inFlightCount.value).toBe(1);
|
|
606
|
+
|
|
607
|
+
calls[0].reject(new Error('aborted'));
|
|
608
|
+
await flushMicrotasks();
|
|
609
|
+
expect(inFlightCount.value).toBe(0);
|
|
610
|
+
|
|
611
|
+
scope.stop();
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it('respects maxInFlightRequests when limit reached', async () => {
|
|
615
|
+
const args = ref({ id: 'a' });
|
|
616
|
+
const { fn, calls } = createControlledQuery<typeof args.value, string>();
|
|
617
|
+
|
|
618
|
+
const { scope } = runInScope(() =>
|
|
619
|
+
usePollingQuery(args, fn, {
|
|
620
|
+
minInterval: 50,
|
|
621
|
+
autoStart: true,
|
|
622
|
+
triggerOnResume: true,
|
|
623
|
+
maxInFlightRequests: 1,
|
|
624
|
+
}),
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
await advanceTime(0);
|
|
628
|
+
await advanceTime(0);
|
|
629
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
630
|
+
|
|
631
|
+
args.value = { id: 'b' };
|
|
632
|
+
await advanceTime(50);
|
|
633
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
634
|
+
|
|
635
|
+
calls[0].resolve('ignored');
|
|
636
|
+
await flushMicrotasks();
|
|
637
|
+
|
|
638
|
+
await advanceTime(0);
|
|
639
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
640
|
+
expect(calls[1].args).toEqual({ id: 'b' });
|
|
641
|
+
|
|
642
|
+
scope.stop();
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it('allows callback to pause polling via provided helper', async () => {
|
|
646
|
+
const args = ref(1);
|
|
647
|
+
const fn = vi.fn(async (_: number, { pause }: { pause: () => void }) => {
|
|
648
|
+
pause();
|
|
649
|
+
return 42;
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
const { scope, result } = runInScope(() =>
|
|
653
|
+
usePollingQuery(args, fn, {
|
|
654
|
+
minInterval: 100,
|
|
655
|
+
autoStart: true,
|
|
656
|
+
triggerOnResume: true,
|
|
657
|
+
}),
|
|
658
|
+
);
|
|
659
|
+
const { isActive } = result;
|
|
660
|
+
|
|
661
|
+
await advanceTime(0);
|
|
662
|
+
|
|
663
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
664
|
+
expect(isActive.value).toBe(false);
|
|
665
|
+
|
|
666
|
+
await advanceTime(500);
|
|
667
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
668
|
+
|
|
669
|
+
scope.stop();
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it('resumes after pause when triggerOnResume is disabled', async () => {
|
|
673
|
+
const args = ref(1);
|
|
674
|
+
const fn = vi.fn(async () => 'value');
|
|
675
|
+
|
|
676
|
+
const { scope, result } = runInScope(() =>
|
|
677
|
+
usePollingQuery(args, fn, {
|
|
678
|
+
minInterval: 200,
|
|
679
|
+
autoStart: false,
|
|
680
|
+
triggerOnResume: false,
|
|
681
|
+
}),
|
|
682
|
+
);
|
|
683
|
+
const { resume } = result;
|
|
684
|
+
|
|
685
|
+
resume();
|
|
686
|
+
await advanceTime(199);
|
|
687
|
+
expect(fn).not.toHaveBeenCalled();
|
|
688
|
+
|
|
689
|
+
await advanceTime(1);
|
|
690
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
691
|
+
|
|
692
|
+
scope.stop();
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it('does not start when minInterval is non-positive', async () => {
|
|
696
|
+
const args = ref({ id: 'nope' });
|
|
697
|
+
const fn = vi.fn(async () => 'value');
|
|
698
|
+
|
|
699
|
+
const minInterval = ref(0);
|
|
700
|
+
|
|
701
|
+
const { scope, result } = runInScope(() =>
|
|
702
|
+
usePollingQuery(args, fn, {
|
|
703
|
+
minInterval,
|
|
704
|
+
autoStart: true,
|
|
705
|
+
triggerOnResume: true,
|
|
706
|
+
}),
|
|
707
|
+
);
|
|
708
|
+
const { isActive, resume } = result;
|
|
709
|
+
|
|
710
|
+
expect(isActive.value).toBe(false);
|
|
711
|
+
await advanceTime(0);
|
|
712
|
+
expect(fn).not.toHaveBeenCalled();
|
|
713
|
+
|
|
714
|
+
resume();
|
|
715
|
+
await advanceTime(0);
|
|
716
|
+
expect(fn).not.toHaveBeenCalled();
|
|
717
|
+
|
|
718
|
+
scope.stop();
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it('enforces minDelay when callback duration exceeds minInterval', async () => {
|
|
722
|
+
const args = ref({ id: 'delayed' });
|
|
723
|
+
const { fn, calls } = createControlledQuery<typeof args.value, string>();
|
|
724
|
+
|
|
725
|
+
const minDelay = ref(150);
|
|
726
|
+
|
|
727
|
+
const { scope } = runInScope(() =>
|
|
728
|
+
usePollingQuery(args, fn, {
|
|
729
|
+
minInterval: 200,
|
|
730
|
+
minDelay,
|
|
731
|
+
autoStart: true,
|
|
732
|
+
triggerOnResume: true,
|
|
733
|
+
}),
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
await advanceTime(0);
|
|
737
|
+
const firstStart = calls[0].startTime;
|
|
738
|
+
await advanceTime(350);
|
|
739
|
+
calls[0].resolve('done');
|
|
740
|
+
await flushMicrotasks();
|
|
741
|
+
|
|
742
|
+
await advanceTime(149);
|
|
743
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
744
|
+
|
|
745
|
+
await advanceTime(1);
|
|
746
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
747
|
+
expect(calls[1].startTime - firstStart).toBeGreaterThanOrEqual(500);
|
|
748
|
+
|
|
749
|
+
scope.stop();
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
it('aborts active requests when scope is disposed', async () => {
|
|
753
|
+
const args = ref({ id: 'cleanup' });
|
|
754
|
+
const { fn, calls } = createControlledQuery<typeof args.value, string>();
|
|
755
|
+
|
|
756
|
+
const { scope } = runInScope(() =>
|
|
757
|
+
usePollingQuery(args, fn, {
|
|
758
|
+
minInterval: 100,
|
|
759
|
+
autoStart: true,
|
|
760
|
+
triggerOnResume: true,
|
|
761
|
+
}),
|
|
762
|
+
);
|
|
763
|
+
|
|
764
|
+
await advanceTime(0);
|
|
765
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
766
|
+
const controller = calls[0].signal;
|
|
767
|
+
expect(controller.aborted).toBe(false);
|
|
768
|
+
|
|
769
|
+
scope.stop();
|
|
770
|
+
expect(controller.aborted).toBe(true);
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
// EDGE CASE TESTS - These should fail with current implementation
|
|
774
|
+
|
|
775
|
+
it('handles concurrent pause() calls from within callback', async () => {
|
|
776
|
+
const args = ref({ id: 'concurrent' });
|
|
777
|
+
let pauseFn: (() => void) | undefined;
|
|
778
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
779
|
+
const fn = vi.fn(async (_: any, { pause }: { pause: () => void }) => {
|
|
780
|
+
pauseFn = pause;
|
|
781
|
+
// Simulate async operation where pause might be called multiple times
|
|
782
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
783
|
+
pause(); // First pause
|
|
784
|
+
pause(); // Second pause - should be idempotent
|
|
785
|
+
return 'result';
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
const { scope, result } = runInScope(() =>
|
|
789
|
+
usePollingQuery(args, fn, {
|
|
790
|
+
minInterval: 100,
|
|
791
|
+
autoStart: true,
|
|
792
|
+
triggerOnResume: true,
|
|
793
|
+
}),
|
|
794
|
+
);
|
|
795
|
+
const { isActive, pause: externalPause } = result;
|
|
796
|
+
|
|
797
|
+
await advanceTime(0);
|
|
798
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
799
|
+
expect(isActive.value).toBe(true);
|
|
800
|
+
|
|
801
|
+
// Call pause from outside while callback is executing
|
|
802
|
+
externalPause();
|
|
803
|
+
expect(isActive.value).toBe(false);
|
|
804
|
+
|
|
805
|
+
// The callback's pause should not reactivate or cause issues
|
|
806
|
+
pauseFn?.();
|
|
807
|
+
|
|
808
|
+
await advanceTime(100);
|
|
809
|
+
expect(isActive.value).toBe(false);
|
|
810
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
811
|
+
|
|
812
|
+
scope.stop();
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
it('eventually processes latest args after debounce pause/resume', async () => {
|
|
816
|
+
const args = ref({ value: 1 });
|
|
817
|
+
const { fn, calls } = createControlledQuery<typeof args.value, string>();
|
|
818
|
+
|
|
819
|
+
const { scope, result } = runInScope(() =>
|
|
820
|
+
usePollingQuery(args, fn, {
|
|
821
|
+
minInterval: 100,
|
|
822
|
+
autoStart: true,
|
|
823
|
+
triggerOnResume: true,
|
|
824
|
+
debounce: 200,
|
|
825
|
+
}),
|
|
826
|
+
);
|
|
827
|
+
const { pause, resume, data } = result;
|
|
828
|
+
|
|
829
|
+
await advanceTime(0);
|
|
830
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
831
|
+
calls[0].resolve('initial');
|
|
832
|
+
await flushMicrotasks();
|
|
833
|
+
expect(data.value).toEqual({ status: 'synced', value: 'initial' });
|
|
834
|
+
|
|
835
|
+
// Change args to start debounce
|
|
836
|
+
args.value = { value: 2 };
|
|
837
|
+
expect(data.value).toEqual({ status: 'stale', value: 'initial' });
|
|
838
|
+
|
|
839
|
+
// Pause during debounce
|
|
840
|
+
await advanceTime(100);
|
|
841
|
+
pause();
|
|
842
|
+
|
|
843
|
+
// Change args while paused
|
|
844
|
+
args.value = { value: 3 };
|
|
845
|
+
|
|
846
|
+
// Resume - first poll may use the pre-pause snapshot, but subsequent polls must converge
|
|
847
|
+
resume();
|
|
848
|
+
await advanceTime(0);
|
|
849
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
850
|
+
expect(calls[1].args).toEqual({ value: 2 });
|
|
851
|
+
|
|
852
|
+
calls[1].resolve('second');
|
|
853
|
+
await flushMicrotasks();
|
|
854
|
+
|
|
855
|
+
await advanceTime(100);
|
|
856
|
+
expect(fn).toHaveBeenCalledTimes(3);
|
|
857
|
+
expect(calls[2].args).toEqual({ value: 3 });
|
|
858
|
+
|
|
859
|
+
calls[2].resolve('latest');
|
|
860
|
+
await flushMicrotasks();
|
|
861
|
+
expect(data.value).toEqual({ status: 'synced', value: 'latest' });
|
|
862
|
+
|
|
863
|
+
scope.stop();
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
it('correctly tracks versions when error occurs during argument change', async () => {
|
|
867
|
+
const args = ref({ id: 'a' });
|
|
868
|
+
const { fn, calls } = createControlledQuery<typeof args.value, string>();
|
|
869
|
+
|
|
870
|
+
const { scope, result } = runInScope(() =>
|
|
871
|
+
usePollingQuery(args, fn, {
|
|
872
|
+
minInterval: 50,
|
|
873
|
+
autoStart: true,
|
|
874
|
+
triggerOnResume: true,
|
|
875
|
+
maxInFlightRequests: 2,
|
|
876
|
+
}),
|
|
877
|
+
);
|
|
878
|
+
const { data, lastError } = result;
|
|
879
|
+
|
|
880
|
+
await advanceTime(0);
|
|
881
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
882
|
+
|
|
883
|
+
// Change args while first request is in flight
|
|
884
|
+
args.value = { id: 'b' };
|
|
885
|
+
await advanceTime(50);
|
|
886
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
887
|
+
|
|
888
|
+
// First request fails after args changed
|
|
889
|
+
calls[0].reject(new Error('error-a'));
|
|
890
|
+
await flushMicrotasks();
|
|
891
|
+
|
|
892
|
+
// Error from old version should not set lastError
|
|
893
|
+
expect(lastError.value).toBeNull();
|
|
894
|
+
expect(data.value).toEqual({ status: 'idle' });
|
|
895
|
+
|
|
896
|
+
// Second request succeeds
|
|
897
|
+
calls[1].resolve('result-b');
|
|
898
|
+
await flushMicrotasks();
|
|
899
|
+
expect(data.value).toEqual({ status: 'synced', value: 'result-b' });
|
|
900
|
+
expect(lastError.value).toBeNull();
|
|
901
|
+
|
|
902
|
+
scope.stop();
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
it('handles rapid minDelay changes during execution', async () => {
|
|
906
|
+
const args = ref({ id: 'test' });
|
|
907
|
+
const { fn, calls } = createControlledQuery<typeof args.value, string>();
|
|
908
|
+
const minDelay = ref<number | undefined>(100);
|
|
909
|
+
|
|
910
|
+
const { scope } = runInScope(() =>
|
|
911
|
+
usePollingQuery(args, fn, {
|
|
912
|
+
minInterval: 50,
|
|
913
|
+
minDelay,
|
|
914
|
+
autoStart: true,
|
|
915
|
+
triggerOnResume: true,
|
|
916
|
+
}),
|
|
917
|
+
);
|
|
918
|
+
|
|
919
|
+
await advanceTime(0);
|
|
920
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
921
|
+
|
|
922
|
+
// Start time of first request
|
|
923
|
+
const firstStart = calls[0].startTime;
|
|
924
|
+
|
|
925
|
+
// Change minDelay to undefined while request is in flight
|
|
926
|
+
minDelay.value = undefined;
|
|
927
|
+
|
|
928
|
+
// Complete first request after 30ms
|
|
929
|
+
await advanceTime(30);
|
|
930
|
+
calls[0].resolve('first');
|
|
931
|
+
await flushMicrotasks();
|
|
932
|
+
|
|
933
|
+
// With minDelay now undefined, next poll should start at minInterval (50ms from start)
|
|
934
|
+
await advanceTime(19);
|
|
935
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
936
|
+
|
|
937
|
+
await advanceTime(1);
|
|
938
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
939
|
+
expect(calls[1].startTime).toBe(firstStart + 50);
|
|
940
|
+
|
|
941
|
+
scope.stop();
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
it('prevents duplicate scheduling when args change completes a request', async () => {
|
|
945
|
+
const args = ref({ id: 'a' });
|
|
946
|
+
const { fn, calls } = createControlledQuery<typeof args.value, string>();
|
|
947
|
+
|
|
948
|
+
const { scope } = runInScope(() =>
|
|
949
|
+
usePollingQuery(args, fn, {
|
|
950
|
+
minInterval: 100,
|
|
951
|
+
autoStart: true,
|
|
952
|
+
triggerOnResume: true,
|
|
953
|
+
}),
|
|
954
|
+
);
|
|
955
|
+
|
|
956
|
+
await advanceTime(0);
|
|
957
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
958
|
+
|
|
959
|
+
// Change args while request is in flight
|
|
960
|
+
args.value = { id: 'b' };
|
|
961
|
+
|
|
962
|
+
// This aborts the first request and schedules a new one
|
|
963
|
+
// But when the first request completes (even if aborted), it should not schedule again
|
|
964
|
+
|
|
965
|
+
calls[0].reject(new Error('aborted'));
|
|
966
|
+
await flushMicrotasks();
|
|
967
|
+
|
|
968
|
+
// Should schedule only once for the new args
|
|
969
|
+
await advanceTime(100);
|
|
970
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
971
|
+
expect(calls[1].args).toEqual({ id: 'b' });
|
|
972
|
+
|
|
973
|
+
// Complete second request and wait for next poll
|
|
974
|
+
calls[1].resolve('result-b');
|
|
975
|
+
await flushMicrotasks();
|
|
976
|
+
|
|
977
|
+
await advanceTime(100);
|
|
978
|
+
// Should have exactly one more call, not duplicated
|
|
979
|
+
expect(fn).toHaveBeenCalledTimes(3);
|
|
980
|
+
expect(calls[2].args).toEqual({ id: 'b' });
|
|
981
|
+
|
|
982
|
+
scope.stop();
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
it('cleans up waiters array on dispose to prevent memory leak', async () => {
|
|
986
|
+
const args = ref({ id: 'test' });
|
|
987
|
+
const { fn, calls } = createControlledQuery<typeof args.value, string>();
|
|
988
|
+
|
|
989
|
+
const { scope, result } = runInScope(() =>
|
|
990
|
+
usePollingQuery(args, fn, {
|
|
991
|
+
minInterval: 50,
|
|
992
|
+
autoStart: true,
|
|
993
|
+
triggerOnResume: true,
|
|
994
|
+
maxInFlightRequests: 1,
|
|
995
|
+
}),
|
|
996
|
+
);
|
|
997
|
+
const { inFlightCount } = result;
|
|
998
|
+
|
|
999
|
+
// Start first request
|
|
1000
|
+
await advanceTime(0);
|
|
1001
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
1002
|
+
expect(inFlightCount.value).toBe(1);
|
|
1003
|
+
|
|
1004
|
+
// Try to change args multiple times while first request is in flight
|
|
1005
|
+
// This should queue up waiters
|
|
1006
|
+
args.value = { id: 'b' };
|
|
1007
|
+
await advanceTime(50);
|
|
1008
|
+
args.value = { id: 'c' };
|
|
1009
|
+
await advanceTime(50);
|
|
1010
|
+
args.value = { id: 'd' };
|
|
1011
|
+
|
|
1012
|
+
// Dispose scope while waiters are queued
|
|
1013
|
+
scope.stop();
|
|
1014
|
+
|
|
1015
|
+
// Complete the in-flight request after dispose
|
|
1016
|
+
calls[0].resolve('result');
|
|
1017
|
+
await flushMicrotasks();
|
|
1018
|
+
|
|
1019
|
+
// Should not throw or cause issues
|
|
1020
|
+
expect(inFlightCount.value).toBe(0);
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
it('handles argument changes after pause but before resumeimmediately after arguments provided', async () => {
|
|
1024
|
+
const args = ref({ value: 1 });
|
|
1025
|
+
const { fn, calls } = createControlledQuery<typeof args.value, string>();
|
|
1026
|
+
|
|
1027
|
+
const { scope, result } = runInScope(() =>
|
|
1028
|
+
usePollingQuery(args, fn, {
|
|
1029
|
+
minInterval: 100,
|
|
1030
|
+
autoStart: false,
|
|
1031
|
+
triggerOnResume: false,
|
|
1032
|
+
}),
|
|
1033
|
+
);
|
|
1034
|
+
const { resume, data, isActive } = result;
|
|
1035
|
+
|
|
1036
|
+
// Provide args but don't resume yet
|
|
1037
|
+
expect(data.value).toEqual({ status: 'idle' });
|
|
1038
|
+
expect(isActive.value).toBe(false);
|
|
1039
|
+
|
|
1040
|
+
// Change args before first resume
|
|
1041
|
+
args.value = { value: 2 };
|
|
1042
|
+
args.value = { value: 3 };
|
|
1043
|
+
|
|
1044
|
+
// Now resume - should use latest args
|
|
1045
|
+
resume();
|
|
1046
|
+
expect(isActive.value).toBe(true);
|
|
1047
|
+
|
|
1048
|
+
await advanceTime(100);
|
|
1049
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
1050
|
+
expect(calls[0].args).toEqual({ value: 3 });
|
|
1051
|
+
|
|
1052
|
+
scope.stop();
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
it('correctly handles error from outdated version when maxInFlightRequests > 1', async () => {
|
|
1056
|
+
const args = ref({ id: 'a' });
|
|
1057
|
+
const { fn, calls } = createControlledQuery<typeof args.value, string>();
|
|
1058
|
+
|
|
1059
|
+
const { scope, result } = runInScope(() =>
|
|
1060
|
+
usePollingQuery(args, fn, {
|
|
1061
|
+
minInterval: 50,
|
|
1062
|
+
autoStart: true,
|
|
1063
|
+
triggerOnResume: true,
|
|
1064
|
+
maxInFlightRequests: 3,
|
|
1065
|
+
}),
|
|
1066
|
+
);
|
|
1067
|
+
const { data: _data, lastError } = result;
|
|
1068
|
+
|
|
1069
|
+
await advanceTime(0);
|
|
1070
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
1071
|
+
|
|
1072
|
+
// Change args - this aborts the in-flight request and schedules a new poll
|
|
1073
|
+
args.value = { id: 'b' };
|
|
1074
|
+
await advanceTime(50);
|
|
1075
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
1076
|
+
|
|
1077
|
+
// Change args again to ensure multiple versions overlap
|
|
1078
|
+
args.value = { id: 'c' };
|
|
1079
|
+
await advanceTime(50);
|
|
1080
|
+
expect(fn).toHaveBeenCalledTimes(3);
|
|
1081
|
+
|
|
1082
|
+
// First request (outdated) fails
|
|
1083
|
+
calls[0].reject(new Error('error-old-1'));
|
|
1084
|
+
await flushMicrotasks();
|
|
1085
|
+
|
|
1086
|
+
// Should not set lastError from old version
|
|
1087
|
+
expect(lastError.value).toBeNull();
|
|
1088
|
+
|
|
1089
|
+
// Second request (also outdated) fails
|
|
1090
|
+
calls[1].reject(new Error('error-old-2'));
|
|
1091
|
+
await flushMicrotasks();
|
|
1092
|
+
|
|
1093
|
+
// Still should not set lastError
|
|
1094
|
+
expect(lastError.value).toBeNull();
|
|
1095
|
+
|
|
1096
|
+
// Current request fails
|
|
1097
|
+
calls[2].reject(new Error('error-current'));
|
|
1098
|
+
await flushMicrotasks();
|
|
1099
|
+
|
|
1100
|
+
// Now lastError should be set
|
|
1101
|
+
expect(lastError.value?.message).toBe('error-current');
|
|
1102
|
+
|
|
1103
|
+
scope.stop();
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
it('respects minInterval when triggerOnResume is false and resuming after pause', async () => {
|
|
1107
|
+
const args = ref({ id: 'test' });
|
|
1108
|
+
const { fn, calls } = createControlledQuery<typeof args.value, string>();
|
|
1109
|
+
|
|
1110
|
+
const { scope, result } = runInScope(() =>
|
|
1111
|
+
usePollingQuery(args, fn, {
|
|
1112
|
+
minInterval: 300,
|
|
1113
|
+
autoStart: true,
|
|
1114
|
+
triggerOnResume: false, // Important: false here
|
|
1115
|
+
}),
|
|
1116
|
+
);
|
|
1117
|
+
const { pause, resume } = result;
|
|
1118
|
+
|
|
1119
|
+
// Start with autoStart, should wait for minInterval
|
|
1120
|
+
await advanceTime(299);
|
|
1121
|
+
expect(fn).not.toHaveBeenCalled();
|
|
1122
|
+
await advanceTime(1);
|
|
1123
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
1124
|
+
|
|
1125
|
+
// Pause immediately
|
|
1126
|
+
pause();
|
|
1127
|
+
calls[0].reject(new Error('aborted'));
|
|
1128
|
+
await flushMicrotasks();
|
|
1129
|
+
|
|
1130
|
+
// Resume - should wait for minInterval again, not trigger immediately
|
|
1131
|
+
resume();
|
|
1132
|
+
await advanceTime(299);
|
|
1133
|
+
expect(fn).toHaveBeenCalledTimes(1); // Still just 1 call
|
|
1134
|
+
|
|
1135
|
+
await advanceTime(1);
|
|
1136
|
+
expect(fn).toHaveBeenCalledTimes(2); // Now 2 calls
|
|
1137
|
+
|
|
1138
|
+
scope.stop();
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
it('handles simultaneous pause from callback and external pause', async () => {
|
|
1142
|
+
const args = ref({ id: 'test' });
|
|
1143
|
+
let callbackPause: (() => void) | undefined;
|
|
1144
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1145
|
+
const fn = vi.fn(async (_: any, { pause }: { pause: () => void }) => {
|
|
1146
|
+
callbackPause = pause;
|
|
1147
|
+
// Wait for external code to run
|
|
1148
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1149
|
+
return 'result';
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
const { scope, result } = runInScope(() =>
|
|
1153
|
+
usePollingQuery(args, fn, {
|
|
1154
|
+
minInterval: 100,
|
|
1155
|
+
autoStart: true,
|
|
1156
|
+
triggerOnResume: true,
|
|
1157
|
+
}),
|
|
1158
|
+
);
|
|
1159
|
+
const { pause: externalPause, resume, isActive } = result;
|
|
1160
|
+
|
|
1161
|
+
await advanceTime(0);
|
|
1162
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
1163
|
+
expect(isActive.value).toBe(true);
|
|
1164
|
+
|
|
1165
|
+
// Both pause at nearly the same time
|
|
1166
|
+
await advanceTime(5);
|
|
1167
|
+
callbackPause?.(); // Pause from callback
|
|
1168
|
+
externalPause(); // Pause from external
|
|
1169
|
+
|
|
1170
|
+
expect(isActive.value).toBe(false);
|
|
1171
|
+
|
|
1172
|
+
await advanceTime(100);
|
|
1173
|
+
expect(fn).toHaveBeenCalledTimes(1); // No additional calls
|
|
1174
|
+
|
|
1175
|
+
// Resume should work normally
|
|
1176
|
+
resume();
|
|
1177
|
+
await advanceTime(0);
|
|
1178
|
+
expect(isActive.value).toBe(true);
|
|
1179
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
1180
|
+
|
|
1181
|
+
scope.stop();
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
it('handles reactive minInterval becoming positive after being zero', async () => {
|
|
1185
|
+
const args = ref({ id: 'test' });
|
|
1186
|
+
const fn = vi.fn(async () => 'result');
|
|
1187
|
+
const minInterval = ref(0);
|
|
1188
|
+
|
|
1189
|
+
const { scope, result } = runInScope(() =>
|
|
1190
|
+
usePollingQuery(args, fn, {
|
|
1191
|
+
minInterval,
|
|
1192
|
+
autoStart: true,
|
|
1193
|
+
triggerOnResume: true,
|
|
1194
|
+
}),
|
|
1195
|
+
);
|
|
1196
|
+
const { isActive, resume } = result;
|
|
1197
|
+
|
|
1198
|
+
// Should not start because minInterval is 0
|
|
1199
|
+
expect(isActive.value).toBe(false);
|
|
1200
|
+
await advanceTime(100);
|
|
1201
|
+
expect(fn).not.toHaveBeenCalled();
|
|
1202
|
+
|
|
1203
|
+
// Change minInterval to positive value
|
|
1204
|
+
minInterval.value = 100;
|
|
1205
|
+
|
|
1206
|
+
// Resume should now work
|
|
1207
|
+
resume();
|
|
1208
|
+
expect(isActive.value).toBe(true);
|
|
1209
|
+
await advanceTime(0);
|
|
1210
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
1211
|
+
|
|
1212
|
+
// Should continue polling
|
|
1213
|
+
await advanceTime(100);
|
|
1214
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
1215
|
+
|
|
1216
|
+
scope.stop();
|
|
1217
|
+
});
|
|
1218
|
+
});
|