Skip to content

Commit 0c5f167

Browse files
fix potential race conditions
1 parent ba72a58 commit 0c5f167

File tree

3 files changed

+60
-4
lines changed

3 files changed

+60
-4
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/common': patch
3+
---
4+
5+
Fixed potential race conditions in WatchedQueries when updateSettings is called frequently.

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,21 +83,31 @@ export abstract class AbstractQueryProcessor<
8383
* Updates the underlying query.
8484
*/
8585
async updateSettings(settings: Settings) {
86+
// Abort any previous requests
8687
this.abortController.abort();
88+
89+
this.options.watchOptions = settings;
90+
// Keep track of this controller's abort status
91+
const abortController = new AbortController();
92+
// Allow this to be aborted externally
93+
this.abortController = abortController;
94+
8795
await this.initialized;
8896

97+
// This may have been aborted while awaiting or if multiple calls to `updateSettings` were made
98+
if (abortController.signal.aborted) {
99+
return;
100+
}
101+
89102
if (!this.state.isFetching && this.reportFetching) {
90103
await this.updateState({
91104
isFetching: true
92105
});
93106
}
94107

95-
this.options.watchOptions = settings;
96-
97-
this.abortController = new AbortController();
98108
await this.runWithReporting(() =>
99109
this.linkQuery({
100-
abortSignal: this.abortController.signal,
110+
abortSignal: abortController.signal,
101111
settings
102112
})
103113
);

packages/web/tests/watch.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,47 @@ describe('Watch Tests', { sequential: true }, () => {
733733
expect(watch.state.data[0].make).equals('nottest');
734734
});
735735

736+
it('should allow updating queries', async () => {
737+
const watch = powersync
738+
.query<{ make: string }>({
739+
sql: 'SELECT ? as result',
740+
parameters: [0]
741+
})
742+
.watch({
743+
reportFetching: false
744+
});
745+
746+
let states: WatchedQueryState<any>[] = [];
747+
748+
// Keep track of all the states which have been updated
749+
const dispose = watch.registerListener({
750+
onStateChange: (state) => {
751+
states.push(state);
752+
}
753+
});
754+
755+
// Spam the updateSettings
756+
let updatePromises = Array.from({ length: 100 }).map(async (_, index) =>
757+
watch.updateSettings({
758+
query: new GetAllQuery({
759+
sql: 'SELECT ? as result',
760+
parameters: [index + 1]
761+
})
762+
})
763+
);
764+
765+
await Promise.all(updatePromises);
766+
767+
await vi.waitFor(
768+
() => {
769+
expect(states[states.length - 1].isFetching).false;
770+
expect(states[states.length - 1].data[0].result).eq(100);
771+
},
772+
{ timeout: 3000 }
773+
);
774+
dispose();
775+
});
776+
736777
it('should report differential query results', async () => {
737778
const watch = powersync
738779
.query({

0 commit comments

Comments
 (0)