@powersync/op-sqlite 0.0.0-dev-20241014162857
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/LICENSE +201 -0
- package/README.md +54 -0
- package/android/build.gradle +124 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/java/com/powersync/opsqlite/PowerSyncOpSqliteModule.kt +25 -0
- package/android/src/main/java/com/powersync/opsqlite/PowerSyncOpSqlitePackage.kt +35 -0
- package/android/src/newarch/PowerSyncOpSqliteSpec.kt +7 -0
- package/android/src/oldarch/PowerSyncOpSqliteSpec.kt +11 -0
- package/ios/PowerSyncOpSqlite.h +5 -0
- package/ios/PowerSyncOpSqlite.mm +11 -0
- package/lib/commonjs/NativePowerSyncOpSqlite.js +9 -0
- package/lib/commonjs/NativePowerSyncOpSqlite.js.map +1 -0
- package/lib/commonjs/db/OPSQLiteConnection.js +77 -0
- package/lib/commonjs/db/OPSQLiteConnection.js.map +1 -0
- package/lib/commonjs/db/OPSqliteAdapter.js +214 -0
- package/lib/commonjs/db/OPSqliteAdapter.js.map +1 -0
- package/lib/commonjs/db/OPSqliteDBOpenFactory.js +26 -0
- package/lib/commonjs/db/OPSqliteDBOpenFactory.js.map +1 -0
- package/lib/commonjs/db/SqliteOptions.js +31 -0
- package/lib/commonjs/db/SqliteOptions.js.map +1 -0
- package/lib/commonjs/index.js +35 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/module/NativePowerSyncOpSqlite.js +5 -0
- package/lib/module/NativePowerSyncOpSqlite.js.map +1 -0
- package/lib/module/db/OPSQLiteConnection.js +72 -0
- package/lib/module/db/OPSQLiteConnection.js.map +1 -0
- package/lib/module/db/OPSqliteAdapter.js +209 -0
- package/lib/module/db/OPSqliteAdapter.js.map +1 -0
- package/lib/module/db/OPSqliteDBOpenFactory.js +21 -0
- package/lib/module/db/OPSqliteDBOpenFactory.js.map +1 -0
- package/lib/module/db/SqliteOptions.js +27 -0
- package/lib/module/db/SqliteOptions.js.map +1 -0
- package/lib/module/index.js +19 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/src/NativePowerSyncOpSqlite.d.ts +7 -0
- package/lib/typescript/src/NativePowerSyncOpSqlite.d.ts.map +1 -0
- package/lib/typescript/src/db/OPSQLiteConnection.d.ts +17 -0
- package/lib/typescript/src/db/OPSQLiteConnection.d.ts.map +1 -0
- package/lib/typescript/src/db/OPSqliteAdapter.d.ts +37 -0
- package/lib/typescript/src/db/OPSqliteAdapter.d.ts.map +1 -0
- package/lib/typescript/src/db/OPSqliteDBOpenFactory.d.ts +12 -0
- package/lib/typescript/src/db/OPSqliteDBOpenFactory.d.ts.map +1 -0
- package/lib/typescript/src/db/SqliteOptions.d.ts +39 -0
- package/lib/typescript/src/db/SqliteOptions.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +112 -0
- package/powersync-op-sqlite.podspec +26 -0
- package/src/NativePowerSyncOpSqlite.ts +8 -0
- package/src/db/OPSQLiteConnection.ts +82 -0
- package/src/db/OPSqliteAdapter.ts +259 -0
- package/src/db/OPSqliteDBOpenFactory.ts +25 -0
- package/src/db/SqliteOptions.ts +54 -0
- package/src/index.ts +30 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseObserver,
|
|
3
|
+
DBAdapter,
|
|
4
|
+
DBAdapterListener,
|
|
5
|
+
DBLockOptions,
|
|
6
|
+
QueryResult,
|
|
7
|
+
SQLOpenOptions,
|
|
8
|
+
Transaction
|
|
9
|
+
} from '@powersync/common';
|
|
10
|
+
import { ANDROID_DATABASE_PATH, IOS_LIBRARY_PATH, open, type DB } from '@op-engineering/op-sqlite';
|
|
11
|
+
import Lock from 'async-lock';
|
|
12
|
+
import { OPSQLiteConnection } from './OPSQLiteConnection';
|
|
13
|
+
import { NativeModules, Platform } from 'react-native';
|
|
14
|
+
import { DEFAULT_SQLITE_OPTIONS, SqliteOptions } from './SqliteOptions';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Adapter for React Native Quick SQLite
|
|
18
|
+
*/
|
|
19
|
+
export type OPSQLiteAdapterOptions = {
|
|
20
|
+
name: string;
|
|
21
|
+
dbLocation?: string;
|
|
22
|
+
sqliteOptions?: SqliteOptions;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
enum LockType {
|
|
26
|
+
READ = 'read',
|
|
27
|
+
WRITE = 'write'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const READ_CONNECTIONS = 5;
|
|
31
|
+
|
|
32
|
+
export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implements DBAdapter {
|
|
33
|
+
name: string;
|
|
34
|
+
protected locks: Lock;
|
|
35
|
+
|
|
36
|
+
protected initialized: Promise<void>;
|
|
37
|
+
|
|
38
|
+
protected readConnections: OPSQLiteConnection[] | null;
|
|
39
|
+
|
|
40
|
+
protected writeConnection: OPSQLiteConnection | null;
|
|
41
|
+
|
|
42
|
+
constructor(protected options: OPSQLiteAdapterOptions) {
|
|
43
|
+
super();
|
|
44
|
+
this.name = this.options.name;
|
|
45
|
+
|
|
46
|
+
this.locks = new Lock();
|
|
47
|
+
this.readConnections = null;
|
|
48
|
+
this.writeConnection = null;
|
|
49
|
+
this.initialized = this.init();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
protected async init() {
|
|
53
|
+
const { lockTimeoutMs, journalMode, journalSizeLimit, synchronous } = this.options.sqliteOptions;
|
|
54
|
+
// const { dbFilename, dbLocation } = this.options;
|
|
55
|
+
const dbFilename = this.options.name;
|
|
56
|
+
//This is needed because an undefined dbLocation will cause the open function to fail
|
|
57
|
+
const location = this.getDbLocation(this.options.dbLocation);
|
|
58
|
+
const DB: DB = open({
|
|
59
|
+
name: dbFilename,
|
|
60
|
+
location: location
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const statements: string[] = [
|
|
64
|
+
`PRAGMA busy_timeout = ${lockTimeoutMs}`,
|
|
65
|
+
`PRAGMA journal_mode = ${journalMode}`,
|
|
66
|
+
`PRAGMA journal_size_limit = ${journalSizeLimit}`,
|
|
67
|
+
`PRAGMA synchronous = ${synchronous}`
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
for (const statement of statements) {
|
|
71
|
+
for (let tries = 0; tries < 30; tries++) {
|
|
72
|
+
try {
|
|
73
|
+
await DB.execute(statement);
|
|
74
|
+
break;
|
|
75
|
+
} catch (e: any) {
|
|
76
|
+
if (e instanceof Error && e.message.includes('database is locked') && tries < 29) {
|
|
77
|
+
continue;
|
|
78
|
+
} else {
|
|
79
|
+
throw e;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.loadExtension(DB);
|
|
86
|
+
|
|
87
|
+
await DB.execute('SELECT powersync_init()');
|
|
88
|
+
|
|
89
|
+
this.readConnections = [];
|
|
90
|
+
for (let i = 0; i < READ_CONNECTIONS; i++) {
|
|
91
|
+
// Workaround to create read-only connections
|
|
92
|
+
let dbName = './'.repeat(i + 1) + dbFilename;
|
|
93
|
+
const conn = await this.openConnection(location, dbName);
|
|
94
|
+
await conn.execute('PRAGMA query_only = true');
|
|
95
|
+
this.readConnections.push(conn);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.writeConnection = new OPSQLiteConnection({
|
|
99
|
+
baseDB: DB
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Changes should only occur in the write connection
|
|
103
|
+
this.writeConnection!.registerListener({
|
|
104
|
+
tablesUpdated: (notification) => this.iterateListeners((cb) => cb.tablesUpdated?.(notification))
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
protected async openConnection(dbLocation: string, filenameOverride?: string): Promise<OPSQLiteConnection> {
|
|
109
|
+
const DB: DB = open({
|
|
110
|
+
name: filenameOverride ?? this.options.name,
|
|
111
|
+
location: dbLocation
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
//Load extension for all connections
|
|
115
|
+
this.loadExtension(DB);
|
|
116
|
+
|
|
117
|
+
await DB.execute('SELECT powersync_init()');
|
|
118
|
+
|
|
119
|
+
return new OPSQLiteConnection({
|
|
120
|
+
baseDB: DB
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private getDbLocation(dbLocation?: string): string {
|
|
125
|
+
if (Platform.OS === 'ios') {
|
|
126
|
+
return dbLocation ?? IOS_LIBRARY_PATH;
|
|
127
|
+
} else {
|
|
128
|
+
return dbLocation ?? ANDROID_DATABASE_PATH;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private loadExtension(DB: DB) {
|
|
133
|
+
if (Platform.OS === 'ios') {
|
|
134
|
+
const bundlePath: string = NativeModules.PowerSyncOpSqlite.getBundlePath();
|
|
135
|
+
const libPath = `${bundlePath}/Frameworks/powersync-sqlite-core.framework/powersync-sqlite-core`;
|
|
136
|
+
DB.loadExtension(libPath, 'sqlite3_powersync_init');
|
|
137
|
+
} else {
|
|
138
|
+
DB.loadExtension('libpowersync', 'sqlite3_powersync_init');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
close() {
|
|
143
|
+
this.initialized.then(() => {
|
|
144
|
+
this.writeConnection!.close();
|
|
145
|
+
this.readConnections!.forEach((c) => c.close());
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async readLock<T>(fn: (tx: OPSQLiteConnection) => Promise<T>, options?: DBLockOptions): Promise<T> {
|
|
150
|
+
await this.initialized;
|
|
151
|
+
// TODO: Use async queues to handle multiple read connections
|
|
152
|
+
const sortedConnections = this.readConnections!.map((connection, index) => ({
|
|
153
|
+
lockKey: `${LockType.READ}-${index}`,
|
|
154
|
+
connection
|
|
155
|
+
})).sort((a, b) => {
|
|
156
|
+
const aBusy = this.locks.isBusy(a.lockKey);
|
|
157
|
+
const bBusy = this.locks.isBusy(b.lockKey);
|
|
158
|
+
// Sort by ones which are not busy
|
|
159
|
+
return aBusy > bBusy ? 1 : 0;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return new Promise(async (resolve, reject) => {
|
|
163
|
+
try {
|
|
164
|
+
await this.locks.acquire(
|
|
165
|
+
sortedConnections[0].lockKey,
|
|
166
|
+
async () => {
|
|
167
|
+
resolve(await fn(sortedConnections[0].connection));
|
|
168
|
+
},
|
|
169
|
+
{ timeout: options?.timeoutMs }
|
|
170
|
+
);
|
|
171
|
+
} catch (ex) {
|
|
172
|
+
reject(ex);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async writeLock<T>(fn: (tx: OPSQLiteConnection) => Promise<T>, options?: DBLockOptions): Promise<T> {
|
|
178
|
+
await this.initialized;
|
|
179
|
+
|
|
180
|
+
return new Promise(async (resolve, reject) => {
|
|
181
|
+
try {
|
|
182
|
+
await this.locks.acquire(
|
|
183
|
+
LockType.WRITE,
|
|
184
|
+
async () => {
|
|
185
|
+
resolve(await fn(this.writeConnection!));
|
|
186
|
+
},
|
|
187
|
+
{ timeout: options?.timeoutMs }
|
|
188
|
+
);
|
|
189
|
+
} catch (ex) {
|
|
190
|
+
reject(ex);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
readTransaction<T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions): Promise<T> {
|
|
196
|
+
return this.readLock((ctx) => this.internalTransaction(ctx, fn));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
writeTransaction<T>(fn: (tx: Transaction) => Promise<T>, options?: DBLockOptions): Promise<T> {
|
|
200
|
+
return this.writeLock((ctx) => this.internalTransaction(ctx, fn));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
getAll<T>(sql: string, parameters?: any[]): Promise<T[]> {
|
|
204
|
+
return this.readLock((ctx) => ctx.getAll(sql, parameters));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
getOptional<T>(sql: string, parameters?: any[]): Promise<T | null> {
|
|
208
|
+
return this.readLock((ctx) => ctx.getOptional(sql, parameters));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
get<T>(sql: string, parameters?: any[]): Promise<T> {
|
|
212
|
+
return this.readLock((ctx) => ctx.get(sql, parameters));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
execute(query: string, params?: any[]) {
|
|
216
|
+
return this.writeLock((ctx) => ctx.execute(query, params));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async executeBatch(query: string, params: any[][] = []): Promise<QueryResult> {
|
|
220
|
+
return this.writeLock((ctx) => ctx.executeBatch(query, params));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
protected async internalTransaction<T>(
|
|
224
|
+
connection: OPSQLiteConnection,
|
|
225
|
+
fn: (tx: Transaction) => Promise<T>
|
|
226
|
+
): Promise<T> {
|
|
227
|
+
let finalized = false;
|
|
228
|
+
const commit = async (): Promise<QueryResult> => {
|
|
229
|
+
if (finalized) {
|
|
230
|
+
return { rowsAffected: 0 };
|
|
231
|
+
}
|
|
232
|
+
finalized = true;
|
|
233
|
+
return connection.execute('COMMIT');
|
|
234
|
+
};
|
|
235
|
+
const rollback = async (): Promise<QueryResult> => {
|
|
236
|
+
if (finalized) {
|
|
237
|
+
return { rowsAffected: 0 };
|
|
238
|
+
}
|
|
239
|
+
finalized = true;
|
|
240
|
+
return connection.execute('ROLLBACK');
|
|
241
|
+
};
|
|
242
|
+
try {
|
|
243
|
+
await connection.execute('BEGIN');
|
|
244
|
+
const result = await fn({
|
|
245
|
+
execute: (query, params) => connection.execute(query, params),
|
|
246
|
+
get: (query, params) => connection.get(query, params),
|
|
247
|
+
getAll: (query, params) => connection.getAll(query, params),
|
|
248
|
+
getOptional: (query, params) => connection.getOptional(query, params),
|
|
249
|
+
commit,
|
|
250
|
+
rollback
|
|
251
|
+
});
|
|
252
|
+
await commit();
|
|
253
|
+
return result;
|
|
254
|
+
} catch (ex) {
|
|
255
|
+
await rollback();
|
|
256
|
+
throw ex;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { DBAdapter, SQLOpenFactory, SQLOpenOptions } from '@powersync/common';
|
|
2
|
+
import { OPSQLiteDBAdapter } from './OPSqliteAdapter';
|
|
3
|
+
import { DEFAULT_SQLITE_OPTIONS, SqliteOptions } from './SqliteOptions';
|
|
4
|
+
|
|
5
|
+
export interface OPSQLiteOpenFactoryOptions extends SQLOpenOptions {
|
|
6
|
+
sqliteOptions?: SqliteOptions;
|
|
7
|
+
}
|
|
8
|
+
export class OPSqliteOpenFactory implements SQLOpenFactory {
|
|
9
|
+
private sqliteOptions: Required<SqliteOptions>;
|
|
10
|
+
|
|
11
|
+
constructor(protected options: OPSQLiteOpenFactoryOptions) {
|
|
12
|
+
this.sqliteOptions = {
|
|
13
|
+
...DEFAULT_SQLITE_OPTIONS,
|
|
14
|
+
...this.options.sqliteOptions
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
openDB(): DBAdapter {
|
|
19
|
+
return new OPSQLiteDBAdapter({
|
|
20
|
+
name: this.options.dbFilename,
|
|
21
|
+
dbLocation: this.options.dbLocation,
|
|
22
|
+
sqliteOptions: this.sqliteOptions
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface SqliteOptions {
|
|
2
|
+
/**
|
|
3
|
+
* SQLite journal mode. Defaults to [SqliteJournalMode.wal].
|
|
4
|
+
*/
|
|
5
|
+
journalMode?: SqliteJournalMode;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* SQLite synchronous flag. Defaults to [SqliteSynchronous.normal], which
|
|
9
|
+
* is safe for WAL mode.
|
|
10
|
+
*/
|
|
11
|
+
synchronous?: SqliteSynchronous;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Journal/WAL size limit. Defaults to 6MB.
|
|
15
|
+
* The WAL may grow larger than this limit during writes, but SQLite will
|
|
16
|
+
* attempt to truncate the file afterwards.
|
|
17
|
+
*/
|
|
18
|
+
journalSizeLimit?: number;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Timeout in milliseconds waiting for locks to be released by other connections.
|
|
22
|
+
* Defaults to 30 seconds.
|
|
23
|
+
* Set to null or zero to fail immediately when the database is locked.
|
|
24
|
+
*/
|
|
25
|
+
lockTimeoutMs?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// SQLite journal mode. Set on the primary connection.
|
|
29
|
+
// This library is written with WAL mode in mind - other modes may cause
|
|
30
|
+
// unexpected locking behavior.
|
|
31
|
+
enum SqliteJournalMode {
|
|
32
|
+
// Use a write-ahead log instead of a rollback journal.
|
|
33
|
+
// This provides good performance and concurrency.
|
|
34
|
+
wal = 'WAL',
|
|
35
|
+
delete = 'DELETE',
|
|
36
|
+
truncate = 'TRUNCATE',
|
|
37
|
+
persist = 'PERSIST',
|
|
38
|
+
memory = 'MEMORY',
|
|
39
|
+
off = 'OFF',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// SQLite file commit mode.
|
|
43
|
+
enum SqliteSynchronous {
|
|
44
|
+
normal = 'NORMAL',
|
|
45
|
+
full = 'FULL',
|
|
46
|
+
off = 'OFF',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const DEFAULT_SQLITE_OPTIONS: Required<SqliteOptions> = {
|
|
50
|
+
journalMode: SqliteJournalMode.wal,
|
|
51
|
+
synchronous: SqliteSynchronous.normal,
|
|
52
|
+
journalSizeLimit: 6 * 1024 * 1024,
|
|
53
|
+
lockTimeoutMs: 30000,
|
|
54
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { NativeModules, Platform } from 'react-native';
|
|
2
|
+
|
|
3
|
+
const LINKING_ERROR =
|
|
4
|
+
`The package '@powersync/op-sqlite' doesn't seem to be linked. Make sure: \n\n` +
|
|
5
|
+
Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) +
|
|
6
|
+
'- You rebuilt the app after installing the package\n' +
|
|
7
|
+
'- You are not using Expo Go\n';
|
|
8
|
+
|
|
9
|
+
const isTurboModuleEnabled = global.__turboModuleProxy != null;
|
|
10
|
+
|
|
11
|
+
const PowerSyncOpSqliteModule = isTurboModuleEnabled
|
|
12
|
+
? require('./NativePowerSyncOpSqlite').default
|
|
13
|
+
: NativeModules.PowerSyncOpSqlite;
|
|
14
|
+
|
|
15
|
+
const PowerSyncOpSqlite = PowerSyncOpSqliteModule
|
|
16
|
+
? PowerSyncOpSqliteModule
|
|
17
|
+
: new Proxy(
|
|
18
|
+
{},
|
|
19
|
+
{
|
|
20
|
+
get() {
|
|
21
|
+
throw new Error(LINKING_ERROR);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export function getBundlePath(): string {
|
|
27
|
+
return PowerSyncOpSqlite.getBundlePath();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { OPSqliteOpenFactory, OPSQLiteOpenFactoryOptions } from './db/OPSqliteDBOpenFactory';
|