Skip to content

Commit c930e0e

Browse files
fix unit tests. Improve closing behaviour.
1 parent 6036652 commit c930e0e

File tree

10 files changed

+70
-46
lines changed

10 files changed

+70
-46
lines changed

.changeset/little-bananas-fetch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/common': minor
3+
---
4+
5+
Added additional listeners for `closing` and `closed` events in `AbstractPowerSyncDatabase`.

.changeset/stale-dots-jog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/web': minor
3+
---
4+
5+
Improved query behaviour when client is closed. Pending requests will be aborted, future requests will be rejected with an Error. Fixed read and write lock requests not respecting timeout parameter.

packages/common/src/client/AbstractPowerSyncDatabase.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ export interface WatchOnChangeHandler {
125125
export interface PowerSyncDBListener extends StreamingSyncImplementationListener {
126126
initialized: () => void;
127127
schemaChanged: (schema: Schema) => void;
128-
closing: () => void;
128+
closing: () => Promise<void> | void;
129+
closed: () => Promise<void> | void;
129130
}
130131

131132
export interface PowerSyncCloseOptions {
@@ -532,7 +533,7 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
532533
return;
533534
}
534535

535-
this.iterateListeners((cb) => cb.closing?.());
536+
await this.iterateAsyncListeners(async (cb) => cb.closing?.());
536537

537538
const { disconnect } = options;
538539
if (disconnect) {
@@ -542,6 +543,7 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
542543
await this.syncStreamImplementation?.dispose();
543544
await this.database.close();
544545
this.closed = true;
546+
await this.iterateAsyncListeners(async (cb) => cb.closed?.());
545547
}
546548

547549
/**

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@ export abstract class AbstractQueryProcessor<Data = unknown[]>
119119
const { db } = this.options;
120120

121121
const disposeCloseListener = db.registerListener({
122-
closed: async () => {
123-
this.close();
122+
closing: async () => {
123+
await this.close();
124124
}
125125
});
126126

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

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,37 +12,6 @@ export interface OnChangeQueryProcessorOptions<Data> extends AbstractQueryProces
1212
comparator?: WatchedQueryComparator<Data>;
1313
}
1414

15-
/**
16-
* @internal
17-
*/
18-
export class ArrayComparator<Element> implements WatchedQueryComparator<Element[]> {
19-
constructor(protected compareBy: (element: Element) => string) {}
20-
21-
checkEquality(current: Element[], previous: Element[]) {
22-
if (current.length == 0 && previous.length == 0) {
23-
return true;
24-
}
25-
26-
if (current.length !== previous.length) {
27-
return false;
28-
}
29-
30-
const { compareBy } = this;
31-
32-
// At this point the lengths are equal
33-
for (let i = 0; i < current.length; i++) {
34-
const currentItem = compareBy(current[i]);
35-
const previousItem = compareBy(previous[i]);
36-
37-
if (currentItem !== previousItem) {
38-
return false;
39-
}
40-
}
41-
42-
return true;
43-
}
44-
}
45-
4615
/**
4716
* Uses the PowerSync onChange event to trigger watched queries.
4817
* Results are emitted on every change of the relevant tables.
@@ -71,9 +40,12 @@ export class OnChangeQueryProcessor<Data> extends AbstractQueryProcessor<Data> {
7140
db.onChangeWithCallback(
7241
{
7342
onChange: async () => {
43+
if (this.closed) {
44+
return;
45+
}
7446
// This fires for each change of the relevant tables
7547
try {
76-
if (this.reportFetching) {
48+
if (this.reportFetching && !this.state.isFetching) {
7749
await this.updateState({ isFetching: true });
7850
}
7951

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: false,
35+
headless: true,
3636
instances: [
3737
{
3838
browser: 'chromium'

packages/vue/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: false,
35+
headless: true,
3636
instances: [
3737
{
3838
browser: 'chromium'

packages/web/src/db/adapters/LockedAsyncDatabaseAdapter.ts

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,18 @@ export class LockedAsyncDatabaseAdapter
4747
private _db: AsyncDatabaseConnection | null = null;
4848
protected _disposeTableChangeListener: (() => void) | null = null;
4949
private _config: ResolvedWebSQLOpenOptions | null = null;
50+
protected pendingAbortControllers: Set<AbortController>;
51+
52+
closing: boolean;
53+
closed: boolean;
5054

5155
constructor(protected options: LockedAsyncDatabaseAdapterOptions) {
5256
super();
5357
this._dbIdentifier = options.name;
5458
this.logger = options.logger ?? createLogger(`LockedAsyncDatabaseAdapter - ${this._dbIdentifier}`);
59+
this.pendingAbortControllers = new Set<AbortController>();
60+
this.closed = false;
61+
this.closing = false;
5562
// Set the name if provided. We can query for the name if not available yet
5663
this.debugMode = options.debugMode ?? false;
5764
if (this.debugMode) {
@@ -154,8 +161,11 @@ export class LockedAsyncDatabaseAdapter
154161
* tabs are still using it.
155162
*/
156163
async close() {
164+
this.closing = true;
157165
this._disposeTableChangeListener?.();
166+
this.pendingAbortControllers.forEach((controller) => controller.abort('Closed'));
158167
await this.baseDB?.close?.();
168+
this.closed = true;
159169
}
160170

161171
async getAll<T>(sql: string, parameters?: any[] | undefined): Promise<T[]> {
@@ -175,20 +185,49 @@ export class LockedAsyncDatabaseAdapter
175185

176186
async readLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions | undefined): Promise<T> {
177187
await this.waitForInitialized();
178-
return this.acquireLock(async () =>
179-
fn(this.generateDBHelpers({ execute: this._execute, executeRaw: this._executeRaw }))
188+
return this.acquireLock(
189+
async () => fn(this.generateDBHelpers({ execute: this._execute, executeRaw: this._executeRaw })),
190+
{
191+
timeoutMs: options?.timeoutMs
192+
}
180193
);
181194
}
182195

183196
async writeLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions | undefined): Promise<T> {
184197
await this.waitForInitialized();
185-
return this.acquireLock(async () =>
186-
fn(this.generateDBHelpers({ execute: this._execute, executeRaw: this._executeRaw }))
198+
return this.acquireLock(
199+
async () => fn(this.generateDBHelpers({ execute: this._execute, executeRaw: this._executeRaw })),
200+
{
201+
timeoutMs: options?.timeoutMs
202+
}
187203
);
188204
}
189205

190-
protected acquireLock(callback: () => Promise<any>): Promise<any> {
191-
return getNavigatorLocks().request(`db-lock-${this._dbIdentifier}`, callback);
206+
protected async acquireLock(callback: () => Promise<any>, options?: { timeoutMs?: number }): Promise<any> {
207+
await this.waitForInitialized();
208+
209+
if (this.closing) {
210+
throw new Error(`Cannot acquire lock, the database is closing`);
211+
}
212+
213+
const abortController = new AbortController();
214+
this.pendingAbortControllers.add(abortController);
215+
const { timeoutMs } = options ?? {};
216+
217+
const timoutId = timeoutMs
218+
? setTimeout(() => {
219+
abortController.abort(`Timeout after ${timeoutMs}ms`);
220+
this.pendingAbortControllers.delete(abortController);
221+
}, timeoutMs)
222+
: null;
223+
224+
return getNavigatorLocks().request(`db-lock-${this._dbIdentifier}`, { signal: abortController.signal }, () => {
225+
this.pendingAbortControllers.delete(abortController);
226+
if (timoutId) {
227+
clearTimeout(timoutId);
228+
}
229+
return callback();
230+
});
192231
}
193232

194233
async readTransaction<T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions | undefined): Promise<T> {
@@ -286,6 +325,7 @@ export class LockedAsyncDatabaseAdapter
286325
*/
287326
private _execute = async (sql: string, bindings?: any[]): Promise<QueryResult> => {
288327
await this.waitForInitialized();
328+
289329
const result = await this.baseDB.execute(sql, bindings);
290330
return {
291331
...result,

packages/web/tests/watch.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ describe('Watch Tests', { sequential: true }, () => {
346346
});
347347
});
348348

349-
let state = await getNextState();
349+
let state = watch.state;
350350
expect(state.isFetching).true;
351351
expect(state.isLoading).true;
352352

packages/web/vitest.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const config: UserConfigExport = {
5050
*/
5151
isolate: true,
5252
provider: 'playwright',
53-
headless: false,
53+
headless: true,
5454
instances: [
5555
{
5656
browser: 'chromium'

0 commit comments

Comments
 (0)