1- import { BaseObserver , DBAdapter , DBAdapterListener , DBLockOptions , QueryResult , Transaction } from '@powersync/common' ;
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' ;
211import Lock from 'async-lock' ;
312import { OPSQLiteConnection } from './OPSQLiteConnection' ;
13+ import { NativeModules , Platform } from 'react-native' ;
14+ import { DEFAULT_SQLITE_OPTIONS , SqliteOptions } from './SqliteOptions' ;
415
516/**
617 * Adapter for React Native Quick SQLite
718 */
819export type OPSQLiteAdapterOptions = {
9- writeConnection : OPSQLiteConnection ;
10- readConnections : OPSQLiteConnection [ ] ;
1120 name : string ;
21+ dbLocation ?: string ;
22+ sqliteOptions ?: SqliteOptions ;
1223} ;
1324
1425enum LockType {
1526 READ = 'read' ,
1627 WRITE = 'write'
1728}
1829
30+ const READ_CONNECTIONS = 5 ;
31+
1932export class OPSQLiteDBAdapter extends BaseObserver < DBAdapterListener > implements DBAdapter {
2033 name : string ;
2134 protected locks : Lock ;
35+
36+ protected initialized : Promise < void > ;
37+
38+ protected readConnections : OPSQLiteConnection [ ] | null ;
39+
40+ protected writeConnection : OPSQLiteConnection | null ;
41+
2242 constructor ( protected options : OPSQLiteAdapterOptions ) {
2343 super ( ) ;
2444 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 ) {
76+ //TODO better error handling for SQLITE_BUSY(5)
77+ console . error ( 'Error executing pragma statement' , statement , e ) ;
78+ // if (e.errorCode === 5 && tries < 29) {
79+ // continue;
80+ // } else {
81+ // throw e;
82+ // }
83+ }
84+ }
85+ }
86+
87+ this . loadExtension ( DB ) ;
88+
89+ await DB . execute ( 'SELECT powersync_init()' ) ;
90+
91+ this . readConnections = [ ] ;
92+ for ( let i = 0 ; i < READ_CONNECTIONS ; i ++ ) {
93+ // Workaround to create read-only connections
94+ let dbName = './' . repeat ( i + 1 ) + dbFilename ;
95+ const conn = await this . openConnection ( location , dbName ) ;
96+ await conn . execute ( 'PRAGMA query_only = true' ) ;
97+ this . readConnections . push ( conn ) ;
98+ }
99+
100+ this . writeConnection = new OPSQLiteConnection ( {
101+ baseDB : DB
102+ } ) ;
103+
25104 // Changes should only occur in the write connection
26- options . writeConnection . registerListener ( {
105+ this . writeConnection ! . registerListener ( {
27106 tablesUpdated : ( notification ) => this . iterateListeners ( ( cb ) => cb . tablesUpdated ?.( notification ) )
28107 } ) ;
29- this . locks = new Lock ( ) ;
108+ }
109+
110+ protected async openConnection ( dbLocation : string , filenameOverride ?: string ) : Promise < OPSQLiteConnection > {
111+ const DB : DB = open ( {
112+ name : filenameOverride ?? this . options . name ,
113+ location : dbLocation
114+ } ) ;
115+
116+ //Load extension for all connections
117+ this . loadExtension ( DB ) ;
118+
119+ await DB . execute ( 'SELECT powersync_init()' ) ;
120+
121+ return new OPSQLiteConnection ( {
122+ baseDB : DB
123+ } ) ;
124+ }
125+
126+ private getDbLocation ( dbLocation ?: string ) : string {
127+ if ( Platform . OS === 'ios' ) {
128+ return dbLocation ?? IOS_LIBRARY_PATH ;
129+ } else {
130+ return dbLocation ?? ANDROID_DATABASE_PATH ;
131+ }
132+ }
133+
134+ private loadExtension ( DB : DB ) {
135+ if ( Platform . OS === 'ios' ) {
136+ const bundlePath : string = NativeModules . PowerSyncOpSqlite . getBundlePath ( ) ;
137+ const libPath = `${ bundlePath } /Frameworks/powersync-sqlite-core.framework/powersync-sqlite-core` ;
138+ DB . loadExtension ( libPath , 'sqlite3_powersync_init' ) ;
139+ } else {
140+ DB . loadExtension ( 'libpowersync' , 'sqlite3_powersync_init' ) ;
141+ }
30142 }
31143
32144 close ( ) {
33- this . options . writeConnection . close ( ) ;
34- this . options . readConnections . forEach ( ( c ) => c . close ( ) ) ;
145+ this . initialized . then ( ( ) => {
146+ this . writeConnection ! . close ( ) ;
147+ this . readConnections ! . forEach ( ( c ) => c . close ( ) ) ;
148+ } ) ;
35149 }
36150
37151 async readLock < T > ( fn : ( tx : OPSQLiteConnection ) => Promise < T > , options ?: DBLockOptions ) : Promise < T > {
38- // TODO better
39- const sortedConnections = this . options . readConnections
40- . map ( ( connection , index ) => ( {
41- lockKey : `${ LockType . READ } -${ index } ` ,
42- connection
43- } ) )
44- . sort ( ( a , b ) => {
45- const aBusy = this . locks . isBusy ( a . lockKey ) ;
46- const bBusy = this . locks . isBusy ( b . lockKey ) ;
47- // Sort by ones which are not busy
48- return aBusy > bBusy ? 1 : 0 ;
49- } ) ;
152+ await this . initialized ;
153+ // TODO: Use async queues to handle multiple read connections
154+ const sortedConnections = this . readConnections ! . map ( ( connection , index ) => ( {
155+ lockKey : `${ LockType . READ } -${ index } ` ,
156+ connection
157+ } ) ) . sort ( ( a , b ) => {
158+ const aBusy = this . locks . isBusy ( a . lockKey ) ;
159+ const bBusy = this . locks . isBusy ( b . lockKey ) ;
160+ // Sort by ones which are not busy
161+ return aBusy > bBusy ? 1 : 0 ;
162+ } ) ;
50163
51164 return new Promise ( async ( resolve , reject ) => {
52165 try {
@@ -63,13 +176,15 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
63176 } ) ;
64177 }
65178
66- writeLock < T > ( fn : ( tx : OPSQLiteConnection ) => Promise < T > , options ?: DBLockOptions ) : Promise < T > {
179+ async writeLock < T > ( fn : ( tx : OPSQLiteConnection ) => Promise < T > , options ?: DBLockOptions ) : Promise < T > {
180+ await this . initialized ;
181+
67182 return new Promise ( async ( resolve , reject ) => {
68183 try {
69184 await this . locks . acquire (
70185 LockType . WRITE ,
71186 async ( ) => {
72- resolve ( await fn ( this . options . writeConnection ) ) ;
187+ resolve ( await fn ( this . writeConnection ! ) ) ;
73188 } ,
74189 { timeout : options ?. timeoutMs }
75190 ) ;
0 commit comments