@powersync/op-sqlite 0.0.0-dev-20260311081226 → 0.0.0-dev-20260414110516
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/db/OPSQLiteConnection.js +4 -17
- package/lib/commonjs/db/OPSQLiteConnection.js.map +1 -1
- package/lib/commonjs/db/OPSqliteAdapter.js +89 -137
- package/lib/commonjs/db/OPSqliteAdapter.js.map +1 -1
- package/lib/module/db/OPSQLiteConnection.js +5 -18
- package/lib/module/db/OPSQLiteConnection.js.map +1 -1
- package/lib/module/db/OPSqliteAdapter.js +90 -138
- package/lib/module/db/OPSqliteAdapter.js.map +1 -1
- package/lib/typescript/commonjs/src/db/OPSQLiteConnection.d.ts +11 -3
- package/lib/typescript/commonjs/src/db/OPSQLiteConnection.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/db/OPSqliteAdapter.d.ts +18 -13
- package/lib/typescript/commonjs/src/db/OPSqliteAdapter.d.ts.map +1 -1
- package/lib/typescript/module/src/db/OPSQLiteConnection.d.ts +11 -3
- package/lib/typescript/module/src/db/OPSQLiteConnection.d.ts.map +1 -1
- package/lib/typescript/module/src/db/OPSqliteAdapter.d.ts +18 -13
- package/lib/typescript/module/src/db/OPSqliteAdapter.d.ts.map +1 -1
- package/package.json +3 -4
- package/src/db/OPSQLiteConnection.ts +7 -20
- package/src/db/OPSqliteAdapter.ts +75 -150
|
@@ -3,8 +3,11 @@ import {
|
|
|
3
3
|
BaseObserver,
|
|
4
4
|
BatchedUpdateNotification,
|
|
5
5
|
DBAdapterListener,
|
|
6
|
+
DBGetUtilsDefaultMixin,
|
|
7
|
+
LockContext,
|
|
6
8
|
QueryResult,
|
|
7
9
|
RowUpdateType,
|
|
10
|
+
SqlExecutor,
|
|
8
11
|
UpdateNotification
|
|
9
12
|
} from '@powersync/common';
|
|
10
13
|
|
|
@@ -19,7 +22,7 @@ export type OPSQLiteUpdateNotification = {
|
|
|
19
22
|
rowId: number;
|
|
20
23
|
};
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
class OPSQLiteExecutor extends BaseObserver<DBAdapterListener> implements Omit<SqlExecutor, 'executeBatch'> {
|
|
23
26
|
protected DB: DB;
|
|
24
27
|
private updateBuffer: UpdateNotification[];
|
|
25
28
|
|
|
@@ -101,7 +104,7 @@ export class OPSQLiteConnection extends BaseObserver<DBAdapterListener> {
|
|
|
101
104
|
return await this.DB.executeRaw(query, params);
|
|
102
105
|
}
|
|
103
106
|
|
|
104
|
-
async
|
|
107
|
+
async executeNativeBatch(query: string, params: any[][] = []): Promise<QueryResult> {
|
|
105
108
|
const tuple: SQLBatchTuple[] = [[query, params[0]]];
|
|
106
109
|
params.slice(1).forEach((p) => tuple.push([query, p]));
|
|
107
110
|
|
|
@@ -110,25 +113,9 @@ export class OPSQLiteConnection extends BaseObserver<DBAdapterListener> {
|
|
|
110
113
|
rowsAffected: result.rowsAffected ?? 0
|
|
111
114
|
};
|
|
112
115
|
}
|
|
116
|
+
}
|
|
113
117
|
|
|
114
|
-
|
|
115
|
-
const result = await this.DB.execute(sql, parameters);
|
|
116
|
-
return (result.rows ?? []) as T[];
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async getOptional<T>(sql: string, parameters?: any[]): Promise<T | null> {
|
|
120
|
-
const result = await this.DB.execute(sql, parameters);
|
|
121
|
-
return (result.rows?.[0] as T) ?? null;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async get<T>(sql: string, parameters?: any[]): Promise<T> {
|
|
125
|
-
const result = await this.getOptional(sql, parameters);
|
|
126
|
-
if (!result) {
|
|
127
|
-
throw new Error('Result set is empty');
|
|
128
|
-
}
|
|
129
|
-
return result as T;
|
|
130
|
-
}
|
|
131
|
-
|
|
118
|
+
export class OPSQLiteConnection extends DBGetUtilsDefaultMixin(OPSQLiteExecutor) implements LockContext {
|
|
132
119
|
async refreshSchema() {
|
|
133
120
|
await this.get("PRAGMA table_info('sqlite_master')");
|
|
134
121
|
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { getDylibPath, open, type DB } from '@op-engineering/op-sqlite';
|
|
2
2
|
import {
|
|
3
3
|
BaseObserver,
|
|
4
|
+
ConnectionPool,
|
|
4
5
|
DBAdapter,
|
|
6
|
+
DBAdapterDefaultMixin,
|
|
5
7
|
DBAdapterListener,
|
|
6
8
|
DBLockOptions,
|
|
7
9
|
QueryResult,
|
|
8
10
|
Transaction,
|
|
9
|
-
|
|
11
|
+
timeoutSignal,
|
|
12
|
+
Semaphore
|
|
10
13
|
} from '@powersync/common';
|
|
11
|
-
import { Mutex } from 'async-mutex';
|
|
12
14
|
import { Platform } from 'react-native';
|
|
13
15
|
import { OPSQLiteConnection } from './OPSQLiteConnection';
|
|
14
16
|
import { SqliteOptions } from './SqliteOptions';
|
|
@@ -24,24 +26,20 @@ export type OPSQLiteAdapterOptions = {
|
|
|
24
26
|
|
|
25
27
|
const READ_CONNECTIONS = 5;
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
class OPSQLiteConnectionPool extends BaseObserver<DBAdapterListener> implements ConnectionPool {
|
|
28
30
|
name: string;
|
|
29
|
-
protected writeMutex: Mutex;
|
|
30
31
|
|
|
31
32
|
protected initialized: Promise<void>;
|
|
32
33
|
|
|
33
|
-
protected readConnections:
|
|
34
|
+
protected readConnections: Semaphore<OPSQLiteConnection> | null;
|
|
35
|
+
protected writeConnection: Semaphore<OPSQLiteConnection> | null;
|
|
34
36
|
|
|
35
|
-
protected writeConnection: OPSQLiteConnection | null;
|
|
36
|
-
|
|
37
|
-
private readQueue: Array<() => void> = [];
|
|
38
37
|
private abortController: AbortController;
|
|
39
38
|
|
|
40
39
|
constructor(protected options: OPSQLiteAdapterOptions) {
|
|
41
40
|
super();
|
|
42
41
|
this.name = this.options.name;
|
|
43
42
|
|
|
44
|
-
this.writeMutex = new Mutex();
|
|
45
43
|
this.readConnections = null;
|
|
46
44
|
this.writeConnection = null;
|
|
47
45
|
this.abortController = new AbortController();
|
|
@@ -53,7 +51,7 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
|
|
|
53
51
|
this.options.sqliteOptions!;
|
|
54
52
|
const dbFilename = this.options.name;
|
|
55
53
|
|
|
56
|
-
|
|
54
|
+
const underlyingWriteConnection = await this.openConnection(dbFilename);
|
|
57
55
|
|
|
58
56
|
const baseStatements = [
|
|
59
57
|
`PRAGMA busy_timeout = ${lockTimeoutMs}`,
|
|
@@ -73,7 +71,7 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
|
|
|
73
71
|
for (const statement of writeConnectionStatements) {
|
|
74
72
|
for (let tries = 0; tries < 30; tries++) {
|
|
75
73
|
try {
|
|
76
|
-
await
|
|
74
|
+
await underlyingWriteConnection.execute(statement);
|
|
77
75
|
break;
|
|
78
76
|
} catch (e: any) {
|
|
79
77
|
if (e instanceof Error && e.message.includes('database is locked') && tries < 29) {
|
|
@@ -86,18 +84,21 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
|
|
|
86
84
|
}
|
|
87
85
|
|
|
88
86
|
// Changes should only occur in the write connection
|
|
89
|
-
|
|
87
|
+
underlyingWriteConnection.registerListener({
|
|
90
88
|
tablesUpdated: (notification) => this.iterateListeners((cb) => cb.tablesUpdated?.(notification))
|
|
91
89
|
});
|
|
92
90
|
|
|
93
|
-
|
|
91
|
+
const underlyingReadConnections = [];
|
|
94
92
|
for (let i = 0; i < READ_CONNECTIONS; i++) {
|
|
95
93
|
const conn = await this.openConnection(dbFilename);
|
|
96
94
|
for (let statement of readConnectionStatements) {
|
|
97
95
|
await conn.execute(statement);
|
|
98
96
|
}
|
|
99
|
-
|
|
97
|
+
underlyingReadConnections.push(conn);
|
|
100
98
|
}
|
|
99
|
+
|
|
100
|
+
this.writeConnection = new Semaphore([underlyingWriteConnection]);
|
|
101
|
+
this.readConnections = new Semaphore(underlyingReadConnections);
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
protected async openConnection(filenameOverride?: string): Promise<OPSQLiteConnection> {
|
|
@@ -153,165 +154,89 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
|
|
|
153
154
|
await this.initialized;
|
|
154
155
|
// Abort any pending operations
|
|
155
156
|
this.abortController.abort();
|
|
156
|
-
this.readQueue = [];
|
|
157
|
-
|
|
158
|
-
this.writeConnection!.close();
|
|
159
|
-
this.readConnections!.forEach((c) => c.connection.close());
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async readLock<T>(fn: (tx: OPSQLiteConnection) => Promise<T>, options?: DBLockOptions): Promise<T> {
|
|
163
|
-
await this.initialized;
|
|
164
|
-
return new Promise(async (resolve, reject) => {
|
|
165
|
-
const execute = async () => {
|
|
166
|
-
// Find an available connection that is not busy
|
|
167
|
-
const availableConnection = this.readConnections!.find((conn) => !conn.busy);
|
|
168
|
-
|
|
169
|
-
// If we have an available connection, use it
|
|
170
|
-
if (availableConnection) {
|
|
171
|
-
availableConnection.busy = true;
|
|
172
|
-
try {
|
|
173
|
-
resolve(await fn(availableConnection.connection));
|
|
174
|
-
} catch (error) {
|
|
175
|
-
reject(error);
|
|
176
|
-
} finally {
|
|
177
|
-
availableConnection.busy = false;
|
|
178
|
-
// After query execution, process any queued tasks
|
|
179
|
-
this.processQueue();
|
|
180
|
-
}
|
|
181
|
-
} else {
|
|
182
|
-
// If no available connections, add to the queue
|
|
183
|
-
this.readQueue.push(execute);
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
157
|
|
|
187
|
-
|
|
188
|
-
});
|
|
189
|
-
}
|
|
158
|
+
const { item: writeConnection, release: returnWrite } = await this.writeConnection!.requestOne();
|
|
159
|
+
const { items: readers, release: returnReaders } = await this.readConnections!.requestAll();
|
|
190
160
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
161
|
+
try {
|
|
162
|
+
writeConnection.close();
|
|
163
|
+
readers.forEach((c) => c.close());
|
|
164
|
+
} finally {
|
|
165
|
+
returnWrite();
|
|
166
|
+
returnReaders();
|
|
197
167
|
}
|
|
198
168
|
}
|
|
199
169
|
|
|
200
|
-
|
|
201
|
-
|
|
170
|
+
private generateNestedAbortSignal(options?: DBLockOptions) {
|
|
171
|
+
const outerSignal = this.abortController.signal;
|
|
172
|
+
let signal: AbortSignal;
|
|
173
|
+
let cleanUpInnerSignal: (() => void) | undefined;
|
|
202
174
|
|
|
203
|
-
|
|
204
|
-
//
|
|
205
|
-
const
|
|
206
|
-
|
|
175
|
+
if (options?.timeoutMs && !outerSignal.aborted) {
|
|
176
|
+
// This is essentially an AbortSignal.any() polyfill.
|
|
177
|
+
const innerController = new AbortController();
|
|
178
|
+
cleanUpInnerSignal = () => {
|
|
179
|
+
innerController.abort();
|
|
180
|
+
outerSignal.removeEventListener('abort', cleanUpInnerSignal!);
|
|
181
|
+
timeout.removeEventListener('abort', cleanUpInnerSignal!);
|
|
207
182
|
};
|
|
208
|
-
this.abortController.signal.addEventListener('abort', abortListener);
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
await mutexRunExclusive(
|
|
212
|
-
this.writeMutex,
|
|
213
|
-
async () => {
|
|
214
|
-
// Check if operation was aborted before executing
|
|
215
|
-
if (this.abortController.signal.aborted) {
|
|
216
|
-
reject(new Error('Database connection was closed'));
|
|
217
|
-
}
|
|
218
|
-
resolve(await fn(this.writeConnection!));
|
|
219
|
-
},
|
|
220
|
-
options
|
|
221
|
-
);
|
|
222
|
-
// flush updates once a write lock has been released
|
|
223
|
-
this.writeConnection!.flushUpdates();
|
|
224
|
-
} catch (ex) {
|
|
225
|
-
reject(ex);
|
|
226
|
-
} finally {
|
|
227
|
-
this.abortController.signal.removeEventListener('abort', abortListener);
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
readTransaction<T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions): Promise<T> {
|
|
233
|
-
return this.readLock((ctx) => this.internalTransaction(ctx, fn));
|
|
234
|
-
}
|
|
235
183
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
getAll<T>(sql: string, parameters?: any[]): Promise<T[]> {
|
|
241
|
-
return this.readLock((ctx) => ctx.getAll(sql, parameters));
|
|
242
|
-
}
|
|
184
|
+
outerSignal.addEventListener('abort', cleanUpInnerSignal);
|
|
185
|
+
const timeout = timeoutSignal(options.timeoutMs);
|
|
186
|
+
timeout.addEventListener('abort', cleanUpInnerSignal);
|
|
243
187
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
188
|
+
signal = innerController.signal;
|
|
189
|
+
} else {
|
|
190
|
+
signal = outerSignal;
|
|
191
|
+
}
|
|
247
192
|
|
|
248
|
-
|
|
249
|
-
return this.readLock((ctx) => ctx.get(sql, parameters));
|
|
193
|
+
return { signal, cleanUpInnerSignal };
|
|
250
194
|
}
|
|
251
195
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
196
|
+
async readLock<T>(fn: (tx: OPSQLiteConnection) => Promise<T>, options?: DBLockOptions): Promise<T> {
|
|
197
|
+
await this.initialized;
|
|
255
198
|
|
|
256
|
-
|
|
257
|
-
|
|
199
|
+
const { signal, cleanUpInnerSignal } = this.generateNestedAbortSignal(options);
|
|
200
|
+
const { item, release } = await this.readConnections!.requestOne(signal);
|
|
201
|
+
try {
|
|
202
|
+
return await fn(item);
|
|
203
|
+
} finally {
|
|
204
|
+
release();
|
|
205
|
+
cleanUpInnerSignal?.();
|
|
206
|
+
}
|
|
258
207
|
}
|
|
259
208
|
|
|
260
|
-
async
|
|
261
|
-
|
|
262
|
-
}
|
|
209
|
+
async writeLock<T>(fn: (tx: OPSQLiteConnection) => Promise<T>, options?: DBLockOptions): Promise<T> {
|
|
210
|
+
await this.initialized;
|
|
263
211
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
fn: (tx: Transaction) => Promise<T>
|
|
267
|
-
): Promise<T> {
|
|
268
|
-
let finalized = false;
|
|
269
|
-
const commit = async (): Promise<QueryResult> => {
|
|
270
|
-
if (finalized) {
|
|
271
|
-
return { rowsAffected: 0 };
|
|
272
|
-
}
|
|
273
|
-
finalized = true;
|
|
274
|
-
return connection.execute('COMMIT');
|
|
275
|
-
};
|
|
276
|
-
const rollback = async (): Promise<QueryResult> => {
|
|
277
|
-
if (finalized) {
|
|
278
|
-
return { rowsAffected: 0 };
|
|
279
|
-
}
|
|
280
|
-
finalized = true;
|
|
281
|
-
return connection.execute('ROLLBACK');
|
|
282
|
-
};
|
|
212
|
+
const { signal, cleanUpInnerSignal } = this.generateNestedAbortSignal(options);
|
|
213
|
+
const { item, release } = await this.writeConnection!.requestOne(signal);
|
|
283
214
|
try {
|
|
284
|
-
await
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
get: (query, params) => connection.get(query, params),
|
|
289
|
-
getAll: (query, params) => connection.getAll(query, params),
|
|
290
|
-
getOptional: (query, params) => connection.getOptional(query, params),
|
|
291
|
-
commit,
|
|
292
|
-
rollback
|
|
293
|
-
});
|
|
294
|
-
await commit();
|
|
295
|
-
return result;
|
|
296
|
-
} catch (ex) {
|
|
297
|
-
try {
|
|
298
|
-
await rollback();
|
|
299
|
-
} catch (ex2) {
|
|
300
|
-
// In rare cases, a rollback may fail.
|
|
301
|
-
// Safe to ignore.
|
|
302
|
-
}
|
|
303
|
-
throw ex;
|
|
215
|
+
return await fn(item).finally(() => item.flushUpdates());
|
|
216
|
+
} finally {
|
|
217
|
+
release();
|
|
218
|
+
cleanUpInnerSignal?.();
|
|
304
219
|
}
|
|
305
220
|
}
|
|
306
221
|
|
|
307
222
|
async refreshSchema(): Promise<void> {
|
|
308
223
|
await this.initialized;
|
|
309
|
-
await this.
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
for (let readConnection of
|
|
313
|
-
await readConnection.
|
|
224
|
+
await this.writeLock((l) => l.refreshSchema());
|
|
225
|
+
const { items, release } = await this.readConnections!.requestAll();
|
|
226
|
+
try {
|
|
227
|
+
for (let readConnection of items) {
|
|
228
|
+
await readConnection.refreshSchema();
|
|
314
229
|
}
|
|
230
|
+
} finally {
|
|
231
|
+
release();
|
|
315
232
|
}
|
|
316
233
|
}
|
|
317
234
|
}
|
|
235
|
+
|
|
236
|
+
export class OPSQLiteDBAdapter extends DBAdapterDefaultMixin(OPSQLiteConnectionPool) implements DBAdapter {
|
|
237
|
+
async executeBatch(query: string, params: any[][] = []): Promise<QueryResult> {
|
|
238
|
+
return await this.writeLock(async (tx) => {
|
|
239
|
+
return await (tx as OPSQLiteConnection).executeNativeBatch(query, params);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|