Skip to content

Commit 5e7850b

Browse files
Add more test checks. Use update promise to populate isFetching
1 parent bf80bbf commit 5e7850b

File tree

4 files changed

+57
-10
lines changed

4 files changed

+57
-10
lines changed

packages/common/src/client/watched/processors/AbstractQueryProcessor.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export abstract class AbstractQueryProcessor<
8181

8282
protected async updateSettingsInternal(settings: Settings, signal: AbortSignal) {
8383
// This may have been aborted while awaiting or if multiple calls to `updateSettings` were made
84-
if (signal.aborted) {
84+
if (this._closed || signal.aborted) {
8585
return;
8686
}
8787

@@ -124,6 +124,10 @@ export abstract class AbstractQueryProcessor<
124124
protected abstract linkQuery(options: LinkQueryOptions<Data>): Promise<void>;
125125

126126
protected async updateState(update: Partial<MutableWatchedQueryState<Data>>) {
127+
if (this._closed) {
128+
return;
129+
}
130+
127131
if (typeof update.error !== 'undefined') {
128132
await this.iterateAsyncListenersWithError(async (l) => l.onError?.(update.error!));
129133
// An error always stops for the current fetching state
@@ -173,11 +177,11 @@ export abstract class AbstractQueryProcessor<
173177
}
174178

175179
async close() {
180+
this._closed = true;
176181
await this.initialized;
177182
this.abortController.abort();
178183
this.disposeListeners?.();
179184
this.disposeListeners = null;
180-
this._closed = true;
181185
this.iterateListeners((l) => l.closed?.());
182186
this.listeners.clear();
183187
}

packages/react/src/hooks/watched/useWatchedQuery.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const useWatchedQuery = <RowType = unknown>(
3535
}
3636

3737
const [watchedQuery, setWatchedQuery] = React.useState(createWatchedQuery);
38+
const updatingPromise = React.useRef<Promise<void> | null>();
3839

3940
React.useEffect(() => {
4041
watchedQuery?.close();
@@ -57,18 +58,29 @@ export const useWatchedQuery = <RowType = unknown>(
5758
* We only need to override the isFetching status on the initial render where the query changed
5859
* (if we report the fetching status). The fetching status will be updated internally in the `updateSettings`
5960
* method eventually.
60-
* Only overriding when the `queryChanged` value is true also prevents races if the query changes
61-
* rapidly.
6261
*/
6362
if (queryChanged) {
64-
watchedQuery?.updateSettings({
63+
// Keep track of this pending operation
64+
let pendingUpdate = watchedQuery?.updateSettings({
6565
query,
6666
throttleMs: hookOptions.throttleMs,
6767
reportFetching: hookOptions.reportFetching
6868
});
69+
70+
// Keep track of the latest pending update operation
71+
updatingPromise.current = pendingUpdate;
72+
73+
// Clear the latest pending update operation when the latest
74+
// operation has completed.
75+
pendingUpdate.then(() => {
76+
// Only clear if this iteration was the latest iteration
77+
if (pendingUpdate == updatingPromise.current) {
78+
updatingPromise.current = null;
79+
}
80+
});
6981
}
7082

71-
const shouldReportCurrentlyFetching = (hookOptions.reportFetching ?? true) && queryChanged;
83+
const shouldReportCurrentlyFetching = (hookOptions.reportFetching ?? true) && !!updatingPromise.current;
7284

7385
const result = useNullableWatchedQuerySubscription(watchedQuery);
7486
return {

packages/react/tests/useQuery.test.tsx

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,10 +255,12 @@ describe('useQuery', () => {
255255
})
256256
);
257257

258-
// We should only receive the first list due to the WHERE clause
258+
// Wait for the first result to be emitted
259259
await vi.waitFor(
260260
() => {
261261
expect(result.current.data[0]?.name).toEqual('first');
262+
expect(result.current.isFetching).false;
263+
expect(result.current.isLoading).false;
262264
},
263265
{ timeout: 500, interval: 100 }
264266
);
@@ -268,6 +270,7 @@ describe('useQuery', () => {
268270
expect(
269271
hookEvents.find((event) => event.parameters[0] == '' && event.hookResults.isLoading == false)
270272
).toBeUndefined();
273+
// We should have an event where this was both loading and fetching
271274
expect(
272275
hookEvents.find(
273276
(event) =>
@@ -276,6 +279,15 @@ describe('useQuery', () => {
276279
event.hookResults.isFetching == true
277280
)
278281
).toBeDefined();
282+
// We should not have any event where isLoading or isFetching is false without data being presented
283+
expect(
284+
hookEvents.find(
285+
(event) =>
286+
event.parameters[0] == 'first' &&
287+
(event.hookResults.isLoading || event.hookResults.isFetching) == false &&
288+
event.hookResults.data[0]?.name != 'first'
289+
)
290+
).toBeUndefined();
279291

280292
// Verify that the fetching status was correlated to the parameters
281293
const firstResultEvent = hookEvents.find((event) => event.hookResults.data.length == 1);
@@ -287,14 +299,13 @@ describe('useQuery', () => {
287299
queryObserver.iterateListeners((l) =>
288300
l.queryUpdated?.({
289301
sql: 'select this is a broken query',
290-
params: ['first']
302+
params: ['error']
291303
})
292304
);
293305

294306
// wait for the error to have been found
295307
await vi.waitFor(
296308
() => {
297-
console.log(result.current);
298309
expect(result.current.error).not.equal(null);
299310
expect(result.current.isFetching).false;
300311
},
@@ -305,6 +316,15 @@ describe('useQuery', () => {
305316
expect(
306317
hookEvents.find((event) => event.hookResults.error != null && event.hookResults.isFetching == true)
307318
).toBeUndefined();
319+
// there should not be any results where the fetching status is false, but the error is not presented
320+
expect(
321+
hookEvents.find(
322+
(event) =>
323+
event.parameters[0] == 'error' &&
324+
event.hookResults.error == null &&
325+
(event.hookResults.isFetching || event.hookResults.isLoading) == false
326+
)
327+
).toBeUndefined();
308328

309329
queryObserver.iterateListeners((l) =>
310330
l.queryUpdated?.({
@@ -318,6 +338,8 @@ describe('useQuery', () => {
318338
() => {
319339
expect(result.current.data[0]?.name).toEqual('second');
320340
expect(result.current.error).null;
341+
expect(result.current.isFetching).false;
342+
expect(result.current.isLoading).false;
321343
},
322344
{ timeout: 500, interval: 100 }
323345
);
@@ -326,6 +348,15 @@ describe('useQuery', () => {
326348
expect(secondFetchingEvent).toBeDefined();
327349
// We should immediately report that we are fetching once we detect new params
328350
expect(secondFetchingEvent?.hookResults.isFetching).true;
351+
// We should never have reported that fetching was false before the results were present
352+
expect(
353+
hookEvents.find(
354+
(event) =>
355+
event.parameters[0] == 'second' &&
356+
event.hookResults.data[0]?.name != 'second' &&
357+
(event.hookResults.isFetching == false || event.hookResults.isLoading == false)
358+
)
359+
);
329360
});
330361

331362
it('should execute compatible queries', async () => {

packages/react/vitest.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const config: UserConfigExport = {
3232
*/
3333
isolate: true,
3434
provider: 'playwright',
35-
headless: true,
35+
headless: false,
3636
instances: [
3737
{
3838
browser: 'chromium'

0 commit comments

Comments
 (0)