@times-components/ts-components 1.172.0 → 1.172.1
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/CHANGELOG.md +11 -0
- package/dist/components/opta/football/opta-match-stats/__tests__/useFetchFeed.test.js +41 -1
- package/dist/components/opta/football/opta-match-stats/useFetchFeed.js +23 -10
- package/package.json +3 -3
- package/rnw.js +1 -1
- package/src/components/opta/football/opta-match-stats/__tests__/useFetchFeed.test.tsx +62 -0
- package/src/components/opta/football/opta-match-stats/useFetchFeed.ts +29 -12
|
@@ -399,4 +399,66 @@ describe('useFetchFeed', () => {
|
|
|
399
399
|
// 1 initial + 1 poll = 2
|
|
400
400
|
expect(global.fetch).toHaveBeenCalledTimes(2);
|
|
401
401
|
});
|
|
402
|
+
|
|
403
|
+
it('stops polling when the only polling subscriber unsubscribes', async () => {
|
|
404
|
+
const mockData = { data: 'test' };
|
|
405
|
+
global.fetch = jest.fn(() => mockJsonResponse(mockData));
|
|
406
|
+
|
|
407
|
+
const url = uniqueUrl('unsub-poll');
|
|
408
|
+
|
|
409
|
+
// Wrapper that lets us toggle the polling subscriber on/off.
|
|
410
|
+
const Wrapper: React.FC<{ showPoller: boolean }> = ({ showPoller }) => (
|
|
411
|
+
<>
|
|
412
|
+
{/* Non-polling subscriber — always mounted */}
|
|
413
|
+
<HookRenderer
|
|
414
|
+
hook={() => useFetchFeed(url)}
|
|
415
|
+
onState={() => {
|
|
416
|
+
/* noop */
|
|
417
|
+
}}
|
|
418
|
+
/>
|
|
419
|
+
{/* Polling subscriber — conditionally mounted */}
|
|
420
|
+
{showPoller && (
|
|
421
|
+
<HookRenderer
|
|
422
|
+
hook={() => useFetchFeed(url, { pollingInterval: 5000 })}
|
|
423
|
+
onState={() => {
|
|
424
|
+
/* noop */
|
|
425
|
+
}}
|
|
426
|
+
/>
|
|
427
|
+
)}
|
|
428
|
+
</>
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
let result: any;
|
|
432
|
+
await act(async () => {
|
|
433
|
+
result = render(<Wrapper showPoller={true} />);
|
|
434
|
+
await Promise.resolve();
|
|
435
|
+
await Promise.resolve();
|
|
436
|
+
await Promise.resolve();
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
440
|
+
|
|
441
|
+
// Confirm polling is active.
|
|
442
|
+
await act(async () => {
|
|
443
|
+
jest.advanceTimersByTime(5000);
|
|
444
|
+
await Promise.resolve();
|
|
445
|
+
await Promise.resolve();
|
|
446
|
+
await Promise.resolve();
|
|
447
|
+
});
|
|
448
|
+
expect(global.fetch).toHaveBeenCalledTimes(2);
|
|
449
|
+
|
|
450
|
+
// Unmount the polling subscriber.
|
|
451
|
+
await act(async () => {
|
|
452
|
+
result.rerender(<Wrapper showPoller={false} />);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// Advance timers — no more fetches should happen.
|
|
456
|
+
await act(async () => {
|
|
457
|
+
jest.advanceTimersByTime(15000);
|
|
458
|
+
await Promise.resolve();
|
|
459
|
+
await Promise.resolve();
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
expect(global.fetch).toHaveBeenCalledTimes(2);
|
|
463
|
+
});
|
|
402
464
|
});
|
|
@@ -35,8 +35,8 @@ type CacheEntry = {
|
|
|
35
35
|
pollTimer: ReturnType<typeof setInterval> | null;
|
|
36
36
|
/** The polling interval (ms) requested by subscribers. Uses the shortest. */
|
|
37
37
|
pollInterval: number | undefined;
|
|
38
|
-
/**
|
|
39
|
-
listeners:
|
|
38
|
+
/** Map of setState dispatchers → polling interval (undefined = no polling). */
|
|
39
|
+
listeners: Map<(s: FetchState<any>) => void, number | undefined>;
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
const cache = new Map<string, CacheEntry>();
|
|
@@ -53,7 +53,7 @@ function getOrCreateEntry(url: string): CacheEntry {
|
|
|
53
53
|
subscribers: 0,
|
|
54
54
|
pollTimer: null,
|
|
55
55
|
pollInterval: undefined,
|
|
56
|
-
listeners: new
|
|
56
|
+
listeners: new Map()
|
|
57
57
|
};
|
|
58
58
|
cache.set(url, entry);
|
|
59
59
|
}
|
|
@@ -67,7 +67,7 @@ function notify(entry: CacheEntry): void {
|
|
|
67
67
|
loading: entry.fetching,
|
|
68
68
|
error: entry.error
|
|
69
69
|
};
|
|
70
|
-
entry.listeners.forEach(fn => fn(snapshot));
|
|
70
|
+
entry.listeners.forEach((_, fn) => fn(snapshot));
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/** Execute a single fetch for `url`, de-duplicating in-flight requests. */
|
|
@@ -184,7 +184,7 @@ export const useFetchFeed = <T>(
|
|
|
184
184
|
// Stable listener wrapper that always calls the latest setState.
|
|
185
185
|
const listener = (s: FetchState<any>) =>
|
|
186
186
|
setStateRef.current(s as FetchState<T>);
|
|
187
|
-
entry.listeners.
|
|
187
|
+
entry.listeners.set(listener, pollingInterval);
|
|
188
188
|
|
|
189
189
|
// If another subscriber already has data, sync immediately.
|
|
190
190
|
if (entry.data !== undefined) {
|
|
@@ -195,14 +195,16 @@ export const useFetchFeed = <T>(
|
|
|
195
195
|
});
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
198
|
+
// Recalculate polling from all listeners.
|
|
199
|
+
{
|
|
200
|
+
const intervals = Array.from(entry.listeners.values()).filter(
|
|
201
|
+
(v): v is number => v != null && v > 0
|
|
202
|
+
);
|
|
203
|
+
const shortest =
|
|
204
|
+
intervals.length > 0 ? Math.min(...intervals) : undefined;
|
|
205
|
+
if (shortest !== entry.pollInterval) {
|
|
204
206
|
stopPolling(entry);
|
|
205
|
-
entry.pollInterval =
|
|
207
|
+
entry.pollInterval = shortest;
|
|
206
208
|
}
|
|
207
209
|
}
|
|
208
210
|
|
|
@@ -217,10 +219,25 @@ export const useFetchFeed = <T>(
|
|
|
217
219
|
if (entry.subscribers <= 0) {
|
|
218
220
|
// Last subscriber unmounted — clean up everything.
|
|
219
221
|
stopPolling(entry);
|
|
222
|
+
entry.pollInterval = undefined;
|
|
220
223
|
if (entry.controller) {
|
|
221
224
|
entry.controller.abort();
|
|
222
225
|
}
|
|
223
226
|
cache.delete(url);
|
|
227
|
+
} else {
|
|
228
|
+
// Recalculate polling from remaining subscribers.
|
|
229
|
+
const remaining = Array.from(entry.listeners.values()).filter(
|
|
230
|
+
(v): v is number => v != null && v > 0
|
|
231
|
+
);
|
|
232
|
+
const shortest =
|
|
233
|
+
remaining.length > 0 ? Math.min(...remaining) : undefined;
|
|
234
|
+
if (shortest !== entry.pollInterval) {
|
|
235
|
+
stopPolling(entry);
|
|
236
|
+
entry.pollInterval = shortest;
|
|
237
|
+
if (shortest) {
|
|
238
|
+
ensurePolling(url);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
224
241
|
}
|
|
225
242
|
};
|
|
226
243
|
},
|