Skip to content

Commit 9f35b78

Browse files
authored
Merge pull request #226 from powersync-ja/fix-web-locks
Fix web lock issues
2 parents 2140b80 + a1b52be commit 9f35b78

File tree

6 files changed

+43
-12
lines changed

6 files changed

+43
-12
lines changed

.changeset/real-bottles-mate.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/web': patch
3+
---
4+
5+
Fix read statements not using the transaction lock

packages/kysely-driver/tests/setup/db.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,3 @@ export const getPowerSyncDb = () => {
1818

1919
return database;
2020
};
21-
22-
export const getKyselyDb = wrapPowerSyncWithKysely<Database>(getPowerSyncDb());

packages/kysely-driver/tests/sqlite/sqlite-connection.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ describe('PowerSyncConnection', () => {
2121

2222
it('should execute a select query using getAll from the table', async () => {
2323
await powerSyncDb.execute('INSERT INTO users (id, name) VALUES(uuid(), ?)', ['John']);
24-
2524
const getAllSpy = vi.spyOn(powerSyncDb, 'getAll');
2625

2726
const compiledQuery: CompiledQuery = {
@@ -35,7 +34,7 @@ describe('PowerSyncConnection', () => {
3534

3635
expect(rows.length).toEqual(1);
3736
expect(rows[0].name).toEqual('John');
38-
expect(getAllSpy).toHaveBeenCalledTimes(1);
37+
expect(getAllSpy).toHaveBeenCalledWith('SELECT * From users', []);
3938
});
4039

4140
it('should insert to the table', async () => {

packages/web/src/db/adapters/wa-sqlite/WASQLiteDBAdapter.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ export class WASQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
4646
this.dbGetHelpers = null;
4747
this.methods = null;
4848
this.initialized = this.init();
49-
this.dbGetHelpers = this.generateDBHelpers({ execute: this._execute.bind(this) });
49+
this.dbGetHelpers = this.generateDBHelpers({
50+
execute: (query, params) => this.acquireLock(() => this._execute(query, params))
51+
});
5052
}
5153

5254
get name() {

packages/web/src/shared/open-db.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as SQLite from '@journeyapps/wa-sqlite';
22
import '@journeyapps/wa-sqlite';
33
import * as Comlink from 'comlink';
44
import type { DBFunctionsInterface, OnTableChangeCallback, WASQLExecuteResult } from './types';
5+
import { Mutex } from 'async-mutex';
56

67
let nextId = 1;
78

@@ -18,6 +19,7 @@ export async function _openDB(
1819
sqlite3.vfs_register(vfs, true);
1920

2021
const db = await sqlite3.open_v2(dbFileName);
22+
const statementMutex = new Mutex();
2123

2224
/**
2325
* Listeners are exclusive to the DB connection.
@@ -40,10 +42,10 @@ export async function _openDB(
4042

4143
/**
4244
* This requests a lock for executing statements.
43-
* Should only be used interanlly.
45+
* Should only be used internally.
4446
*/
45-
const _acquireExecuteLock = (callback: () => Promise<any>): Promise<any> => {
46-
return navigator.locks.request(`db-execute-${dbFileName}`, callback);
47+
const _acquireExecuteLock = <T>(callback: () => Promise<T>): Promise<T> => {
48+
return statementMutex.runExclusive(callback);
4749
};
4850

4951
/**
@@ -115,7 +117,7 @@ export async function _openDB(
115117
* This executes SQL statements in a batch.
116118
*/
117119
const executeBatch = async (sql: string, bindings?: any[][]): Promise<WASQLExecuteResult> => {
118-
return _acquireExecuteLock(async () => {
120+
return _acquireExecuteLock(async (): Promise<WASQLExecuteResult> => {
119121
let affectedRows = 0;
120122

121123
const str = sqlite3.str_new(db, sql);
@@ -127,7 +129,8 @@ export async function _openDB(
127129
const prepared = await sqlite3.prepare_v2(db, query);
128130
if (prepared === null) {
129131
return {
130-
rowsAffected: 0
132+
rowsAffected: 0,
133+
rows: { _array: [], length: 0 }
131134
};
132135
}
133136
const wrappedBindings = bindings ? bindings : [];
@@ -158,13 +161,15 @@ export async function _openDB(
158161
} catch (err) {
159162
await executeSingleStatement('ROLLBACK');
160163
return {
161-
rowsAffected: 0
164+
rowsAffected: 0,
165+
rows: { _array: [], length: 0 }
162166
};
163167
} finally {
164168
sqlite3.str_finish(str);
165169
}
166170
const result = {
167-
rowsAffected: affectedRows
171+
rowsAffected: affectedRows,
172+
rows: { _array: [], length: 0 }
168173
};
169174

170175
return result;

packages/web/tests/crud.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { AbstractPowerSyncDatabase, Column, ColumnType, CrudEntry, Schema, Table
33
import { PowerSyncDatabase } from '@powersync/web';
44
import { v4 as uuid } from 'uuid';
55
import { generateTestDb } from './utils/testDb';
6+
import pDefer from 'p-defer';
67

78
const testId = '2290de4f-0488-4e50-abed-f8e8eb1d0b42';
89

@@ -289,4 +290,25 @@ describe('CRUD Tests', () => {
289290
await tx2.complete();
290291
expect(await powersync.getNextCrudTransaction()).equals(null);
291292
});
293+
294+
it('Transaction exclusivity', async () => {
295+
const outside = pDefer();
296+
const inTx = pDefer();
297+
298+
const txPromise = powersync.writeTransaction(async (tx) => {
299+
await tx.execute('INSERT INTO assets(id, description) VALUES(?, ?)', [testId, 'test1']);
300+
inTx.resolve();
301+
await outside.promise;
302+
await tx.rollback();
303+
});
304+
305+
await inTx.promise;
306+
307+
const r = powersync.getOptional<any>('SELECT * FROM assets WHERE id = ?', [testId]);
308+
await new Promise((resolve) => setTimeout(resolve, 10));
309+
outside.resolve();
310+
311+
await txPromise;
312+
expect(await r).toEqual(null);
313+
});
292314
});

0 commit comments

Comments
 (0)