Skip to content

Commit fe98172

Browse files
Improve Tests (#520)
1 parent 76dfb06 commit fe98172

23 files changed

+627
-529
lines changed

.changeset/loud-mangos-kick.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@powersync/common': patch
3+
'@powersync/web': patch
4+
---
5+
6+
Fixed race condition in async closing of databases

package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@
3333
"@actions/core": "^1.10.1",
3434
"@changesets/cli": "2.27.2",
3535
"@pnpm/workspace.find-packages": "^4.0.2",
36-
"@vitest/browser": "^3.0.5",
36+
"@vitest/browser": "^3.0.8",
3737
"husky": "^9.0.11",
3838
"lint-staged": "^15.2.2",
39-
"playwright": "^1.50.1",
39+
"playwright": "^1.51.0",
4040
"prettier": "^3.2.5",
4141
"typescript": "^5.7.2",
42-
"vitest": "^3.0.5"
42+
"vitest": "^3.0.8"
4343
}
4444
}

packages/common/src/client/AbstractPowerSyncDatabase.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,20 @@ import { mutexRunExclusive } from '../utils/mutex.js';
1818
import { throttleTrailing } from '../utils/throttle.js';
1919
import { SQLOpenFactory, SQLOpenOptions, isDBAdapter, isSQLOpenFactory, isSQLOpenOptions } from './SQLOpenFactory.js';
2020
import { PowerSyncBackendConnector } from './connection/PowerSyncBackendConnector.js';
21+
import { runOnSchemaChange } from './runOnSchemaChange.js';
2122
import { BucketStorageAdapter, PSInternalTable } from './sync/bucket/BucketStorageAdapter.js';
2223
import { CrudBatch } from './sync/bucket/CrudBatch.js';
2324
import { CrudEntry, CrudEntryJSON } from './sync/bucket/CrudEntry.js';
2425
import { CrudTransaction } from './sync/bucket/CrudTransaction.js';
2526
import {
2627
DEFAULT_CRUD_UPLOAD_THROTTLE_MS,
27-
type AdditionalConnectionOptions,
28-
type PowerSyncConnectionOptions,
28+
DEFAULT_RETRY_DELAY_MS,
2929
StreamingSyncImplementation,
3030
StreamingSyncImplementationListener,
31-
DEFAULT_RETRY_DELAY_MS,
31+
type AdditionalConnectionOptions,
32+
type PowerSyncConnectionOptions,
3233
type RequiredAdditionalConnectionOptions
3334
} from './sync/stream/AbstractStreamingSyncImplementation.js';
34-
import { runOnSchemaChange } from './runOnSchemaChange.js';
3535

3636
export interface DisconnectAndClearOptions {
3737
/** When set to false, data in local-only tables is preserved. */
@@ -505,13 +505,17 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
505505
async close(options: PowerSyncCloseOptions = DEFAULT_POWERSYNC_CLOSE_OPTIONS) {
506506
await this.waitForReady();
507507

508+
if (this.closed) {
509+
return;
510+
}
511+
508512
const { disconnect } = options;
509513
if (disconnect) {
510514
await this.disconnect();
511515
}
512516

513517
await this.syncStreamImplementation?.dispose();
514-
this.database.close();
518+
await this.database.close();
515519
this.closed = true;
516520
}
517521

packages/common/src/db/DBAdapter.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export interface DBLockOptions {
9393
}
9494

9595
export interface DBAdapter extends BaseObserverInterface<DBAdapterListener>, DBGetUtils {
96-
close: () => void;
96+
close: () => void | Promise<void>;
9797
execute: (query: string, params?: any[]) => Promise<QueryResult>;
9898
executeBatch: (query: string, params?: any[][]) => Promise<QueryResult>;
9999
name: string;

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,9 @@ export class LockedAsyncDatabaseAdapter
147147
* Shared workers might not actually close the connection if other
148148
* tabs are still using it.
149149
*/
150-
close() {
150+
async close() {
151151
this._disposeTableChangeListener?.();
152-
this.baseDB?.close?.();
152+
await this.baseDB?.close?.();
153153
}
154154

155155
async getAll<T>(sql: string, parameters?: any[] | undefined): Promise<T[]> {

packages/web/tests/bucket_storage.test.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {
2+
AbstractPowerSyncDatabase,
23
BucketStorageAdapter,
4+
Checkpoint,
35
OpType,
46
OpTypeEnum,
57
OplogEntry,
@@ -8,10 +10,9 @@ import {
810
SyncDataBatch,
911
SyncDataBucket
1012
} from '@powersync/common';
11-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
12-
import { AbstractPowerSyncDatabase, Checkpoint } from '@powersync/common';
1313
import { PowerSyncDatabase, WASQLitePowerSyncDatabaseOpenFactory } from '@powersync/web';
1414
import { Mutex } from 'async-mutex';
15+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
1516
import { testSchema } from './utils/testDb';
1617

1718
const putAsset1_1 = OplogEntry.fromRow({
@@ -57,7 +58,7 @@ const removeAsset1_5 = OplogEntry.fromRow({
5758
checksum: 5
5859
});
5960

60-
describe('Bucket Storage', () => {
61+
describe('Bucket Storage', { sequential: true }, () => {
6162
let db: AbstractPowerSyncDatabase;
6263
let bucketStorage: BucketStorageAdapter;
6364

packages/web/tests/crud.test.ts

+28-18
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,15 @@
1-
import { AbstractPowerSyncDatabase, Column, ColumnType, CrudEntry, Schema, Table, UpdateType } from '@powersync/common';
2-
import { PowerSyncDatabase } from '@powersync/web';
1+
import { Column, ColumnType, CrudEntry, Schema, Table, UpdateType } from '@powersync/common';
32
import pDefer from 'p-defer';
43
import { v4 as uuid } from 'uuid';
5-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
4+
import { describe, expect, it } from 'vitest';
65
import { generateTestDb } from './utils/testDb';
76

87
const testId = '2290de4f-0488-4e50-abed-f8e8eb1d0b42';
98

10-
describe('CRUD Tests', () => {
11-
let powersync: AbstractPowerSyncDatabase;
12-
13-
beforeEach(async () => {
14-
powersync = generateTestDb();
15-
});
16-
17-
afterEach(async () => {
18-
await powersync.disconnectAndClear();
19-
await powersync.close();
20-
});
21-
9+
describe('CRUD Tests', { sequential: true }, () => {
2210
it('INSERT', async () => {
11+
const powersync = generateTestDb();
12+
2313
expect(await powersync.getAll('SELECT * FROM ps_crud')).empty;
2414

2515
await powersync.execute('INSERT INTO assets(id, description) VALUES(?, ?)', [testId, 'test']);
@@ -37,6 +27,8 @@ describe('CRUD Tests', () => {
3727
});
3828

3929
it('BATCH INSERT', async () => {
30+
const powersync = generateTestDb();
31+
4032
expect(await powersync.getAll('SELECT * FROM ps_crud')).empty;
4133

4234
const query = `INSERT INTO assets(id, description) VALUES(?, ?)`;
@@ -63,6 +55,8 @@ describe('CRUD Tests', () => {
6355
});
6456

6557
it('INSERT OR REPLACE', async () => {
58+
const powersync = generateTestDb();
59+
6660
await powersync.execute('INSERT INTO assets(id, description) VALUES(?, ?)', [testId, 'test']);
6761
await powersync.execute('DELETE FROM ps_crud WHERE 1');
6862

@@ -85,6 +79,8 @@ describe('CRUD Tests', () => {
8579
});
8680

8781
it('UPDATE', async () => {
82+
const powersync = generateTestDb();
83+
8884
await powersync.execute('INSERT INTO assets(id, description, make) VALUES(?, ?, ?)', [testId, 'test', 'test']);
8985
await powersync.execute('DELETE FROM ps_crud WHERE 1');
9086

@@ -105,6 +101,8 @@ describe('CRUD Tests', () => {
105101
});
106102

107103
it('BATCH UPDATE', async () => {
104+
const powersync = generateTestDb();
105+
108106
await powersync.executeBatch('INSERT INTO assets(id, description, make) VALUES(?, ?, ?)', [
109107
[testId, 'test', 'test'],
110108
['mockId', 'test', 'test']
@@ -137,6 +135,8 @@ describe('CRUD Tests', () => {
137135
});
138136

139137
it('DELETE', async () => {
138+
const powersync = generateTestDb();
139+
140140
await powersync.execute('INSERT INTO assets(id, description, make) VALUES(?, ?, ?)', [testId, 'test', 'test']);
141141
await powersync.execute('DELETE FROM ps_crud WHERE 1');
142142

@@ -153,6 +153,8 @@ describe('CRUD Tests', () => {
153153
});
154154

155155
it('UPSERT not supported', async () => {
156+
const powersync = generateTestDb();
157+
156158
// Just shows that we cannot currently do this
157159
await expect(
158160
powersync.execute('INSERT INTO assets(id, description) VALUES(?, ?) ON CONFLICT DO UPDATE SET description = ?', [
@@ -164,9 +166,7 @@ describe('CRUD Tests', () => {
164166
});
165167

166168
it('INSERT-only tables', async () => {
167-
await powersync.disconnectAndClear();
168-
169-
powersync = new PowerSyncDatabase({
169+
const powersync = generateTestDb({
170170
/**
171171
* Deleting the IndexDB seems to freeze the test.
172172
* Use a new DB for each run to keep CRUD counters
@@ -212,6 +212,8 @@ describe('CRUD Tests', () => {
212212
});
213213

214214
it('big numbers - integer', async () => {
215+
const powersync = generateTestDb();
216+
215217
const bigNumber = 1 << 62;
216218
await powersync.execute('INSERT INTO assets(id, quantity) VALUES(?, ?)', [testId, bigNumber]);
217219

@@ -233,6 +235,8 @@ describe('CRUD Tests', () => {
233235
});
234236

235237
it('big numbers - text', async () => {
238+
const powersync = generateTestDb();
239+
236240
const bigNumber = 1 << 62;
237241
await powersync.execute('INSERT INTO assets(id, quantity) VALUES(?, ?)', [testId, `${bigNumber}`]);
238242

@@ -263,6 +267,8 @@ describe('CRUD Tests', () => {
263267
});
264268

265269
it('Transaction grouping', async () => {
270+
const powersync = generateTestDb();
271+
266272
expect(await powersync.getAll('SELECT * FROM ps_crud')).empty;
267273
await powersync.writeTransaction(async (tx) => {
268274
await tx.execute('INSERT INTO assets(id, description) VALUES(?, ?)', [testId, 'test1']);
@@ -292,6 +298,8 @@ describe('CRUD Tests', () => {
292298
});
293299

294300
it('Transaction exclusivity', async () => {
301+
const powersync = generateTestDb();
302+
295303
const outside = pDefer();
296304
const inTx = pDefer();
297305

@@ -313,6 +321,8 @@ describe('CRUD Tests', () => {
313321
});
314322

315323
it('CRUD Batch Limits', async () => {
324+
const powersync = generateTestDb();
325+
316326
const initialBatch = await powersync.getCrudBatch();
317327
expect(initialBatch, 'Initial CRUD batch should be null').null;
318328

packages/web/tests/main.test.ts

+48-38
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,74 @@
1-
import { AbstractPowerSyncDatabase } from '@powersync/common';
21
import { PowerSyncDatabase, WASQLiteOpenFactory, WASQLiteVFS } from '@powersync/web';
32
import { v4 as uuid } from 'uuid';
4-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
3+
import { describe, expect, it } from 'vitest';
54
import { TestDatabase, generateTestDb, testSchema } from './utils/testDb';
65
// TODO import tests from a common package
76

8-
describe('Basic', () => {
9-
let dbWithoutWebWorker: AbstractPowerSyncDatabase;
10-
let dbWithWebWorker: AbstractPowerSyncDatabase;
11-
let dbWithOPFS: AbstractPowerSyncDatabase;
7+
describe(
8+
'Basic - With Web Workers',
9+
{ sequential: true },
10+
describeBasicTests(() => generateTestDb())
11+
);
1212

13-
beforeEach(() => {
14-
dbWithoutWebWorker = generateTestDb({ useWebWorker: false });
15-
dbWithWebWorker = generateTestDb();
16-
dbWithOPFS = new PowerSyncDatabase({
17-
database: new WASQLiteOpenFactory({ dbFilename: 'basic.sqlite', vfs: WASQLiteVFS.OPFSCoopSyncVFS }),
13+
describe(
14+
'Basic - Without Web Workers',
15+
{ sequential: true },
16+
describeBasicTests(() =>
17+
generateTestDb({
18+
database: {
19+
dbFilename: 'basic-no-worker.sqlite'
20+
},
21+
flags: {
22+
useWebWorker: false
23+
},
1824
schema: testSchema
19-
});
20-
});
25+
})
26+
)
27+
);
2128

22-
/**
23-
* Declares a test to be executed with multiple DB connections
24-
*/
25-
const itWithDBs = (name: string, test: (db: AbstractPowerSyncDatabase) => Promise<void>) => {
26-
it(`${name} - with web worker`, () => test(dbWithWebWorker));
27-
it(`${name} - without web worker`, () => test(dbWithoutWebWorker));
28-
it(`${name} - with OPFS`, () => test(dbWithOPFS));
29-
};
29+
describe(
30+
'Basic - With OPFS',
31+
{ sequential: true },
32+
describeBasicTests(() =>
33+
generateTestDb({
34+
database: new WASQLiteOpenFactory({
35+
dbFilename: 'basic-opfs.sqlite',
36+
vfs: WASQLiteVFS.OPFSCoopSyncVFS
37+
}),
38+
schema: testSchema
39+
})
40+
)
41+
);
3042

31-
afterEach(async () => {
32-
await dbWithWebWorker.disconnectAndClear();
33-
await dbWithWebWorker.close();
34-
await dbWithoutWebWorker.disconnectAndClear();
35-
await dbWithoutWebWorker.close();
36-
await dbWithOPFS.disconnectAndClear();
37-
await dbWithOPFS.close();
38-
});
43+
function describeBasicTests(generateDB: () => PowerSyncDatabase) {
44+
return () => {
45+
it('should execute a select query using getAll', async () => {
46+
const db = generateDB();
3947

40-
describe('executeQuery', () => {
41-
itWithDBs('should execute a select query using getAll', async (db) => {
4248
const result = await db.getAll('SELECT * FROM customers');
4349
expect(result.length).toEqual(0);
4450
});
4551

46-
itWithDBs('should allow inserts', async (db) => {
52+
it('should allow inserts', async () => {
53+
const db = generateDB();
54+
4755
const testName = 'Steven';
4856
await db.execute('INSERT INTO customers (id, name) VALUES(?, ?)', [uuid(), testName]);
4957
const result = await db.get<TestDatabase['customers']>('SELECT * FROM customers');
5058

5159
expect(result.name).equals(testName);
5260
});
53-
});
5461

55-
describe('executeBatchQuery', () => {
56-
itWithDBs('should execute a select query using getAll', async (db) => {
62+
it('should execute a select query using getAll', async () => {
63+
const db = generateDB();
64+
5765
const result = await db.getAll('SELECT * FROM customers');
5866
expect(result.length).toEqual(0);
5967
});
6068

61-
itWithDBs('should allow batch inserts', async (db) => {
69+
it('should allow batch inserts', async () => {
70+
const db = generateDB();
71+
6272
const testName = 'Mugi';
6373
await db.executeBatch('INSERT INTO customers (id, name) VALUES(?, ?)', [
6474
[uuid(), testName],
@@ -72,5 +82,5 @@ describe('Basic', () => {
7282
expect(result[1].name).equals('Steven');
7383
expect(result[2].name).equals('Chris');
7484
});
75-
});
76-
});
85+
};
86+
}

0 commit comments

Comments
 (0)