@powersync/react-native 0.0.0-dev-20260525085311 → 0.0.0-dev-20260630144038
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/README.md +72 -6
- package/android/build.gradle +90 -0
- package/android/gradle.properties +4 -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/PowerSyncOpSqlitePackage.kt +46 -0
- package/ios/PowerSyncOpSqlite.h +5 -0
- package/ios/PowerSyncOpSqlite.mm +6 -0
- package/lib/db/PowerSyncDatabase.d.ts +9 -10
- package/lib/db/PowerSyncDatabase.js +33 -25
- package/lib/db/PowerSyncDatabase.js.map +1 -1
- package/lib/db/adapters/op-sqlite/OPSQLiteConnection.d.ts +25 -0
- package/lib/db/adapters/op-sqlite/OPSQLiteConnection.js +63 -0
- package/lib/db/adapters/op-sqlite/OPSQLiteConnection.js.map +1 -0
- package/lib/db/adapters/op-sqlite/OPSqliteAdapter.d.ts +32 -0
- package/lib/db/adapters/op-sqlite/OPSqliteAdapter.js +203 -0
- package/lib/db/adapters/op-sqlite/OPSqliteAdapter.js.map +1 -0
- package/lib/db/adapters/op-sqlite/OPSqliteDBOpenFactory.d.ts +11 -0
- package/lib/db/adapters/op-sqlite/OPSqliteDBOpenFactory.js +21 -0
- package/lib/db/adapters/op-sqlite/OPSqliteDBOpenFactory.js.map +1 -0
- package/lib/db/adapters/op-sqlite/SqliteOptions.d.ts +68 -0
- package/lib/db/adapters/op-sqlite/SqliteOptions.js +37 -0
- package/lib/db/adapters/op-sqlite/SqliteOptions.js.map +1 -0
- package/lib/index.d.ts +1 -3
- package/lib/index.js +0 -3
- package/lib/index.js.map +1 -1
- package/lib/sync/bucket/ReactNativeBucketStorageAdapter.d.ts +1 -1
- package/lib/sync/bucket/ReactNativeBucketStorageAdapter.js +1 -1
- package/lib/sync/bucket/ReactNativeBucketStorageAdapter.js.map +1 -1
- package/lib/sync/stream/ReactNativeRemote.d.ts +11 -11
- package/lib/sync/stream/ReactNativeRemote.js +42 -66
- package/lib/sync/stream/ReactNativeRemote.js.map +1 -1
- package/lib/sync/stream/ReactNativeStreamingSyncImplementation.d.ts +2 -1
- package/lib/sync/stream/ReactNativeStreamingSyncImplementation.js +2 -1
- package/lib/sync/stream/ReactNativeStreamingSyncImplementation.js.map +1 -1
- package/lib/sync/stream/fetch.d.ts +13 -0
- package/lib/sync/stream/fetch.js +31 -0
- package/lib/sync/stream/fetch.js.map +1 -0
- package/package.json +20 -33
- package/powersync-react-native.podspec +32 -0
- package/src/db/PowerSyncDatabase.ts +58 -31
- package/src/db/adapters/op-sqlite/OPSQLiteConnection.ts +95 -0
- package/src/db/adapters/op-sqlite/OPSqliteAdapter.ts +245 -0
- package/src/db/adapters/op-sqlite/OPSqliteDBOpenFactory.ts +25 -0
- package/src/db/adapters/op-sqlite/SqliteOptions.ts +93 -0
- package/src/index.ts +1 -3
- package/src/sync/bucket/ReactNativeBucketStorageAdapter.ts +1 -1
- package/src/sync/stream/ReactNativeRemote.ts +49 -86
- package/src/sync/stream/ReactNativeStreamingSyncImplementation.ts +4 -4
- package/src/sync/stream/fetch.ts +45 -0
- package/dist/index.js +0 -8741
- package/dist/index.js.map +0 -1
- package/lib/db/adapters/react-native-quick-sqlite/RNQSDBAdapter.d.ts +0 -55
- package/lib/db/adapters/react-native-quick-sqlite/RNQSDBAdapter.js +0 -66
- package/lib/db/adapters/react-native-quick-sqlite/RNQSDBAdapter.js.map +0 -1
- package/lib/db/adapters/react-native-quick-sqlite/RNQSDBOpenFactory.d.ts +0 -19
- package/lib/db/adapters/react-native-quick-sqlite/RNQSDBOpenFactory.js +0 -34
- package/lib/db/adapters/react-native-quick-sqlite/RNQSDBOpenFactory.js.map +0 -1
- package/lib/db/adapters/react-native-quick-sqlite/ReactNativeQuickSQLiteOpenFactory.d.ts +0 -9
- package/lib/db/adapters/react-native-quick-sqlite/ReactNativeQuickSQLiteOpenFactory.js +0 -45
- package/lib/db/adapters/react-native-quick-sqlite/ReactNativeQuickSQLiteOpenFactory.js.map +0 -1
- package/src/db/adapters/react-native-quick-sqlite/RNQSDBAdapter.ts +0 -85
- package/src/db/adapters/react-native-quick-sqlite/RNQSDBOpenFactory.ts +0 -44
- package/src/db/adapters/react-native-quick-sqlite/ReactNativeQuickSQLiteOpenFactory.ts +0 -43
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { LogLevels } from '@powersync/common';
|
|
2
|
+
export function defaultFetchImplementation(logger) {
|
|
3
|
+
return (resolvedDefault ??= resolveDefaultFetchImplementation(logger));
|
|
4
|
+
}
|
|
5
|
+
let resolvedDefault;
|
|
6
|
+
function resolveDefaultFetchImplementation(logger) {
|
|
7
|
+
try {
|
|
8
|
+
const { fetch } = require('expo/fetch');
|
|
9
|
+
return {
|
|
10
|
+
supportsStreams: true,
|
|
11
|
+
run({ resource, request }) {
|
|
12
|
+
return fetch(resource, request);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
catch (expoNotFound) {
|
|
17
|
+
logger.log({
|
|
18
|
+
level: LogLevels.debug,
|
|
19
|
+
message: 'Could not resolve expo/fetch, HTTP streams are unavailable.',
|
|
20
|
+
error: expoNotFound
|
|
21
|
+
});
|
|
22
|
+
// Fetch polyfill built in to React Native. This one doesn't support streaming responses.
|
|
23
|
+
return {
|
|
24
|
+
supportsStreams: false,
|
|
25
|
+
run({ resource, request }) {
|
|
26
|
+
return fetch(resource, request);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../../src/sync/stream/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAmB,MAAM,mBAAmB,CAAC;AAc/D,MAAM,UAAU,0BAA0B,CAAC,MAAuB;IAChE,OAAO,CAAC,eAAe,KAAK,iCAAiC,CAAC,MAAM,CAAC,CAAC,CAAC;AACzE,CAAC;AAED,IAAI,eAAyD,CAAC;AAE9D,SAAS,iCAAiC,CAAC,MAAuB;IAChE,IAAI,CAAC;QACH,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;QACxC,OAAO;YACL,eAAe,EAAE,IAAI;YACrB,GAAG,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE;gBACvB,OAAO,KAAK,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YAClC,CAAC;SACF,CAAC;IACJ,CAAC;IAAC,OAAO,YAAY,EAAE,CAAC;QACtB,MAAM,CAAC,GAAG,CAAC;YACT,KAAK,EAAE,SAAS,CAAC,KAAK;YACtB,OAAO,EAAE,6DAA6D;YACtE,KAAK,EAAE,YAAY;SACpB,CAAC,CAAC;QAEH,yFAAyF;QACzF,OAAO;YACL,eAAe,EAAE,KAAK;YACtB,GAAG,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE;gBACvB,OAAO,KAAK,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YAClC,CAAC;SACF,CAAC;IACJ,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@powersync/react-native",
|
|
3
|
-
"version": "0.0.0-dev-
|
|
3
|
+
"version": "0.0.0-dev-20260630144038",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"registry": "https://registry.npmjs.org/",
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
8
8
|
"description": "PowerSync React Native SDK",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./lib/index.js",
|
|
11
|
+
"module": "./lib/index.js",
|
|
11
12
|
"types": "./lib/index.d.ts",
|
|
12
13
|
"files": [
|
|
13
14
|
"lib",
|
|
14
|
-
"
|
|
15
|
-
"
|
|
15
|
+
"src",
|
|
16
|
+
"android",
|
|
17
|
+
"ios",
|
|
18
|
+
"*.podspec"
|
|
16
19
|
],
|
|
17
20
|
"repository": {
|
|
18
21
|
"type": "git",
|
|
@@ -25,37 +28,22 @@
|
|
|
25
28
|
},
|
|
26
29
|
"homepage": "https://docs.powersync.com/",
|
|
27
30
|
"peerDependencies": {
|
|
28
|
-
"@
|
|
29
|
-
"@powersync/common": "
|
|
31
|
+
"@op-engineering/op-sqlite": "^17.1.0",
|
|
32
|
+
"@powersync/common": "0.0.0-dev-20260630144038",
|
|
30
33
|
"react": "*",
|
|
31
34
|
"react-native": "*"
|
|
32
35
|
},
|
|
33
|
-
"peerDependenciesMeta": {
|
|
34
|
-
"@journeyapps/react-native-quick-sqlite": {
|
|
35
|
-
"optional": true
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
36
|
"dependencies": {
|
|
39
|
-
"@powersync/
|
|
40
|
-
"@powersync/
|
|
37
|
+
"@powersync/common": "0.0.0-dev-20260630144038",
|
|
38
|
+
"@powersync/react": "0.0.0-dev-20260630144038",
|
|
39
|
+
"@powersync/shared-internals": "1.0.0"
|
|
41
40
|
},
|
|
42
41
|
"devDependencies": {
|
|
43
|
-
"@
|
|
44
|
-
"
|
|
45
|
-
"@rollup/plugin-alias": "^5.1.0",
|
|
46
|
-
"@rollup/plugin-commonjs": "^29.0.0",
|
|
47
|
-
"@rollup/plugin-inject": "^5.0.5",
|
|
48
|
-
"@rollup/plugin-json": "^6.1.0",
|
|
49
|
-
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
50
|
-
"@rollup/plugin-replace": "^5.0.7",
|
|
51
|
-
"@rollup/plugin-terser": "^0.4.4",
|
|
42
|
+
"@op-engineering/op-sqlite": "^17.1.0",
|
|
43
|
+
"expo": "^56.0.0",
|
|
52
44
|
"@types/react": "^19.1.1",
|
|
53
45
|
"react": "^19.2.0",
|
|
54
|
-
"react-native": "0.
|
|
55
|
-
"react-native-fetch-api": "^3.0.0",
|
|
56
|
-
"rollup": "^4.52.5",
|
|
57
|
-
"text-encoding": "^0.7.0",
|
|
58
|
-
"web-streams-polyfill": "3.2.1"
|
|
46
|
+
"react-native": "^0.85.0"
|
|
59
47
|
},
|
|
60
48
|
"keywords": [
|
|
61
49
|
"data sync",
|
|
@@ -65,11 +53,10 @@
|
|
|
65
53
|
"live data"
|
|
66
54
|
],
|
|
67
55
|
"scripts": {
|
|
68
|
-
"build": "tsc -b
|
|
69
|
-
"build:prod": "tsc -b
|
|
70
|
-
"clean": "rm -rf lib
|
|
56
|
+
"build": "tsc -b",
|
|
57
|
+
"build:prod": "tsc -b",
|
|
58
|
+
"clean": "rm -rf lib tsconfig.tsbuildinfo",
|
|
71
59
|
"test": "vitest --config vitest.config.ts",
|
|
72
|
-
"watch": "tsc -b -w"
|
|
73
|
-
"test:exports": "attw --pack ."
|
|
60
|
+
"watch": "tsc -b -w"
|
|
74
61
|
}
|
|
75
62
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "powersync-react-native"
|
|
7
|
+
# Our development versions are not recognized by Cocoapods
|
|
8
|
+
version = package['version']
|
|
9
|
+
if version.include?('-dev')
|
|
10
|
+
s.version = '0.0.0'
|
|
11
|
+
else
|
|
12
|
+
s.version = version
|
|
13
|
+
end
|
|
14
|
+
s.summary = package["description"]
|
|
15
|
+
s.homepage = package["homepage"]
|
|
16
|
+
s.license = package["license"]
|
|
17
|
+
s.authors = package["author"]
|
|
18
|
+
|
|
19
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
20
|
+
s.source = { :git => "https://github.com/powersync-ja/powersync-js.git", :tag => "#{s.version}" }
|
|
21
|
+
|
|
22
|
+
s.source_files = "ios/**/*.{h,m,mm,cpp}"
|
|
23
|
+
|
|
24
|
+
s.dependency "React-callinvoker"
|
|
25
|
+
s.dependency "React"
|
|
26
|
+
s.dependency "powersync-sqlite-core", "~> 0.4.12"
|
|
27
|
+
if defined?(install_modules_dependencies())
|
|
28
|
+
install_modules_dependencies(s)
|
|
29
|
+
else
|
|
30
|
+
s.dependency "React-Core"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -1,41 +1,45 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
BasePowerSyncDatabaseOptions,
|
|
3
|
+
CommonPowerSyncDatabase,
|
|
4
|
+
DatabaseSource,
|
|
5
5
|
DBAdapter,
|
|
6
6
|
PowerSyncBackendConnector,
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
PowerSyncDatabaseConstructor,
|
|
8
|
+
SyncStreamConnectionMethod
|
|
9
9
|
} from '@powersync/common';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
BasePowerSyncDatabase,
|
|
12
|
+
AbstractStreamingSyncImplementation,
|
|
13
|
+
BucketStorageAdapter,
|
|
14
|
+
CreateSyncImplementationOptions,
|
|
15
|
+
openDatabase
|
|
16
|
+
} from '@powersync/shared-internals';
|
|
17
|
+
import { ReactNativeRemote, ReactNativeRemoteOptions } from '../sync/stream/ReactNativeRemote';
|
|
11
18
|
import { ReactNativeStreamingSyncImplementation } from '../sync/stream/ReactNativeStreamingSyncImplementation';
|
|
12
19
|
import { ReactNativeBucketStorageAdapter } from './../sync/bucket/ReactNativeBucketStorageAdapter';
|
|
13
|
-
import {
|
|
20
|
+
import { OPSqliteOpenFactory, OPSQLiteOpenFactoryOptions } from './adapters/op-sqlite/OPSqliteDBOpenFactory';
|
|
21
|
+
import { defaultFetchImplementation } from '../sync/stream/fetch';
|
|
22
|
+
|
|
23
|
+
export type ReactNativeDatabaseOptions = BasePowerSyncDatabaseOptions &
|
|
24
|
+
DatabaseSource<OPSQLiteOpenFactoryOptions> &
|
|
25
|
+
ReactNativeSpecificOptions;
|
|
26
|
+
|
|
27
|
+
export interface ReactNativeSpecificOptions {
|
|
28
|
+
remote?: ReactNativeRemoteOptions;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class ReactNativePowerSyncDatabase extends BasePowerSyncDatabase<ReactNativeDatabaseOptions> {
|
|
32
|
+
constructor(options: ReactNativeDatabaseOptions) {
|
|
33
|
+
super(options);
|
|
34
|
+
}
|
|
14
35
|
|
|
15
|
-
/**
|
|
16
|
-
* A PowerSync database which provides SQLite functionality
|
|
17
|
-
* which is automatically synced.
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* ```typescript
|
|
21
|
-
* export const db = new PowerSyncDatabase({
|
|
22
|
-
* schema: AppSchema,
|
|
23
|
-
* database: {
|
|
24
|
-
* dbFilename: 'example.db'
|
|
25
|
-
* }
|
|
26
|
-
* });
|
|
27
|
-
* ```
|
|
28
|
-
*/
|
|
29
|
-
export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
|
|
30
36
|
async _initialize(): Promise<void> {}
|
|
31
37
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const defaultFactory = new ReactNativeQuickSqliteOpenFactory(options.database);
|
|
38
|
-
return defaultFactory.openDB();
|
|
38
|
+
protected override openDBAdapter(): DBAdapter {
|
|
39
|
+
return openDatabase(this.options, (database) => {
|
|
40
|
+
const defaultFactory = new OPSqliteOpenFactory(database);
|
|
41
|
+
return defaultFactory.openDB();
|
|
42
|
+
});
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
protected generateBucketStorageAdapter(): BucketStorageAdapter {
|
|
@@ -44,9 +48,9 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
|
|
|
44
48
|
|
|
45
49
|
protected generateSyncStreamImplementation(
|
|
46
50
|
connector: PowerSyncBackendConnector,
|
|
47
|
-
options:
|
|
51
|
+
options: CreateSyncImplementationOptions
|
|
48
52
|
): AbstractStreamingSyncImplementation {
|
|
49
|
-
const remote = new ReactNativeRemote(connector, this.logger);
|
|
53
|
+
const remote = new ReactNativeRemote(connector, this.logger, this.options.remote);
|
|
50
54
|
|
|
51
55
|
return new ReactNativeStreamingSyncImplementation({
|
|
52
56
|
...options,
|
|
@@ -60,4 +64,27 @@ export class PowerSyncDatabase extends AbstractPowerSyncDatabase {
|
|
|
60
64
|
logger: this.logger
|
|
61
65
|
});
|
|
62
66
|
}
|
|
67
|
+
|
|
68
|
+
protected get defaultConnectionMethod(): SyncStreamConnectionMethod {
|
|
69
|
+
const fetch = this.options.remote?.fetchImplementation ?? defaultFetchImplementation(this.logger);
|
|
70
|
+
return fetch.supportsStreams ? SyncStreamConnectionMethod.HTTP : SyncStreamConnectionMethod.WEB_SOCKET;
|
|
71
|
+
}
|
|
63
72
|
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A PowerSync database which provides SQLite functionality
|
|
76
|
+
* which is automatically synced.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* export const db = new PowerSyncDatabase({
|
|
81
|
+
* schema: AppSchema,
|
|
82
|
+
* database: {
|
|
83
|
+
* dbFilename: 'example.db'
|
|
84
|
+
* }
|
|
85
|
+
* });
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export const PowerSyncDatabase: PowerSyncDatabaseConstructor<ReactNativeDatabaseOptions> = ReactNativePowerSyncDatabase;
|
|
89
|
+
|
|
90
|
+
export interface PowerSyncDatabase extends CommonPowerSyncDatabase {}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { DB, SQLBatchTuple, UpdateHookOperation } from '@op-engineering/op-sqlite';
|
|
2
|
+
import {
|
|
3
|
+
BaseObserver,
|
|
4
|
+
BatchedUpdateNotification,
|
|
5
|
+
LockContext,
|
|
6
|
+
QueryResult,
|
|
7
|
+
queryResultFromMapped,
|
|
8
|
+
queryResultWithoutRows,
|
|
9
|
+
RawQueryResult,
|
|
10
|
+
SqliteValue
|
|
11
|
+
} from '@powersync/common';
|
|
12
|
+
|
|
13
|
+
export type OPSQLiteConnectionOptions = {
|
|
14
|
+
baseDB: DB;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type OPSQLiteUpdateNotification = {
|
|
18
|
+
table: string;
|
|
19
|
+
operation: UpdateHookOperation;
|
|
20
|
+
row?: any;
|
|
21
|
+
rowId: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export class OPSQLiteConnection extends LockContext {
|
|
25
|
+
protected DB: DB;
|
|
26
|
+
private updateBuffer: Set<string>;
|
|
27
|
+
readonly tableUpdateDispatcher = new BaseObserver();
|
|
28
|
+
|
|
29
|
+
constructor(protected options: OPSQLiteConnectionOptions) {
|
|
30
|
+
super();
|
|
31
|
+
this.DB = options.baseDB;
|
|
32
|
+
this.updateBuffer = new Set();
|
|
33
|
+
|
|
34
|
+
this.DB.rollbackHook(() => {
|
|
35
|
+
this.updateBuffer = new Set();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
this.DB.updateHook((update) => {
|
|
39
|
+
this.addTableUpdate(update);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
addTableUpdate(update: OPSQLiteUpdateNotification) {
|
|
44
|
+
this.updateBuffer.add(update.table);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
flushUpdates() {
|
|
48
|
+
if (!this.updateBuffer.size) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const batchedUpdate: BatchedUpdateNotification = {
|
|
53
|
+
tables: Array.from(this.updateBuffer)
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
this.updateBuffer = new Set();
|
|
57
|
+
this.tableUpdateDispatcher.iterateListeners((l) => l.tablesUpdated?.(batchedUpdate));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
close() {
|
|
61
|
+
return this.DB.close();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async execute<T>(query: string, params?: any[]): Promise<QueryResult<T>> {
|
|
65
|
+
const res = await this.DB.execute(query, params);
|
|
66
|
+
return queryResultFromMapped(res, res.rows as T[]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async executeRaw(query: string, params?: any[]): Promise<RawQueryResult> {
|
|
70
|
+
const { insertId, rowsAffected, columnNames, rawRows } = await this.DB.executeRaw(query, params);
|
|
71
|
+
return {
|
|
72
|
+
insertId: insertId,
|
|
73
|
+
rowsAffected: rowsAffected,
|
|
74
|
+
columnNames,
|
|
75
|
+
rawRows: (rawRows ?? []) as SqliteValue[][]
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// NOTE: Do not override executeBatch here. OP-sqlite starts a transaction in executeBatch, so we can't use it if
|
|
80
|
+
// we're already in a transaction. To be safe, we only call this method from the OPSqliteAdapter class overriding
|
|
81
|
+
// executeBatch when called on the adapter directly (not within readLock / writeLock).
|
|
82
|
+
async executeNativeBatch(query: string, params: any[][] = []): Promise<QueryResult<never>> {
|
|
83
|
+
const tuple: SQLBatchTuple[] = [[query, params[0]]];
|
|
84
|
+
params.slice(1).forEach((p) => tuple.push([query, p]));
|
|
85
|
+
|
|
86
|
+
const result = await this.DB.executeBatch(tuple);
|
|
87
|
+
return queryResultWithoutRows({
|
|
88
|
+
rowsAffected: result.rowsAffected ?? 0
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async refreshSchema() {
|
|
93
|
+
await this.get("PRAGMA table_info('sqlite_master')");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { NativeModules } from 'react-native';
|
|
2
|
+
import { getDylibPath, open, type DB } from '@op-engineering/op-sqlite';
|
|
3
|
+
import { DBAdapter, DBLockOptions, QueryResult } from '@powersync/common';
|
|
4
|
+
import { timeoutSignal, Semaphore } from '@powersync/shared-internals';
|
|
5
|
+
import { Platform } from 'react-native';
|
|
6
|
+
import { OPSQLiteConnection } from './OPSQLiteConnection';
|
|
7
|
+
import { SqliteOptions } from './SqliteOptions';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Adapter for React Native Quick SQLite
|
|
11
|
+
*/
|
|
12
|
+
export type OPSQLiteAdapterOptions = {
|
|
13
|
+
name: string;
|
|
14
|
+
dbLocation?: string;
|
|
15
|
+
sqliteOptions?: SqliteOptions;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const READ_CONNECTIONS = 5;
|
|
19
|
+
|
|
20
|
+
export class OPSQLiteDBAdapter extends DBAdapter {
|
|
21
|
+
name: string;
|
|
22
|
+
|
|
23
|
+
protected initialized: Promise<void>;
|
|
24
|
+
|
|
25
|
+
protected readConnections: Semaphore<OPSQLiteConnection> | null;
|
|
26
|
+
protected writeConnection: Semaphore<OPSQLiteConnection> | null;
|
|
27
|
+
|
|
28
|
+
private abortController: AbortController;
|
|
29
|
+
|
|
30
|
+
constructor(protected options: OPSQLiteAdapterOptions) {
|
|
31
|
+
super();
|
|
32
|
+
this.name = this.options.name;
|
|
33
|
+
|
|
34
|
+
this.readConnections = null;
|
|
35
|
+
this.writeConnection = null;
|
|
36
|
+
this.abortController = new AbortController();
|
|
37
|
+
this.initialized = this.init();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
protected async init() {
|
|
41
|
+
const { lockTimeoutMs, journalMode, journalSizeLimit, synchronous, cacheSizeKb, temporaryStorage } =
|
|
42
|
+
this.options.sqliteOptions!;
|
|
43
|
+
const dbFilename = this.options.name;
|
|
44
|
+
|
|
45
|
+
const underlyingWriteConnection = await this.openConnection(false, dbFilename);
|
|
46
|
+
|
|
47
|
+
const baseStatements = [
|
|
48
|
+
`PRAGMA busy_timeout = ${lockTimeoutMs}`,
|
|
49
|
+
`PRAGMA cache_size = -${cacheSizeKb}`,
|
|
50
|
+
`PRAGMA temp_store = ${temporaryStorage}`
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const writeConnectionStatements = [
|
|
54
|
+
...baseStatements,
|
|
55
|
+
`PRAGMA journal_mode = ${journalMode}`,
|
|
56
|
+
`PRAGMA journal_size_limit = ${journalSizeLimit}`,
|
|
57
|
+
`PRAGMA synchronous = ${synchronous}`
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
for (const statement of writeConnectionStatements) {
|
|
61
|
+
for (let tries = 0; tries < 30; tries++) {
|
|
62
|
+
try {
|
|
63
|
+
await underlyingWriteConnection.execute(statement);
|
|
64
|
+
break;
|
|
65
|
+
} catch (e: any) {
|
|
66
|
+
if (e instanceof Error && e.message.includes('database is locked') && tries < 29) {
|
|
67
|
+
continue;
|
|
68
|
+
} else {
|
|
69
|
+
throw e;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Changes should only occur in the write connection
|
|
76
|
+
underlyingWriteConnection.tableUpdateDispatcher.registerListener({
|
|
77
|
+
tablesUpdated: (notification) => this.iterateListeners((cb) => cb.tablesUpdated?.(notification))
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const underlyingReadConnections = [];
|
|
81
|
+
for (let i = 0; i < READ_CONNECTIONS; i++) {
|
|
82
|
+
const conn = await this.openConnection(true, dbFilename);
|
|
83
|
+
for (let statement of baseStatements) {
|
|
84
|
+
await conn.execute(statement);
|
|
85
|
+
}
|
|
86
|
+
underlyingReadConnections.push(conn);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.writeConnection = new Semaphore([underlyingWriteConnection]);
|
|
90
|
+
this.readConnections = new Semaphore(underlyingReadConnections);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
protected async openConnection(readOnly: boolean, filenameOverride?: string): Promise<OPSQLiteConnection> {
|
|
94
|
+
const dbFilename = filenameOverride ?? this.options.name;
|
|
95
|
+
const DB: DB = this.openDatabase(dbFilename, readOnly, this.options.sqliteOptions?.encryptionKey ?? undefined);
|
|
96
|
+
|
|
97
|
+
//Load extensions for all connections
|
|
98
|
+
this.loadAdditionalExtensions(DB);
|
|
99
|
+
this.loadPowerSyncExtension(DB);
|
|
100
|
+
|
|
101
|
+
await DB.execute('SELECT powersync_init()');
|
|
102
|
+
|
|
103
|
+
return new OPSQLiteConnection({
|
|
104
|
+
baseDB: DB
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private openDatabase(dbFilename: string, readOnly: boolean, encryptionKey?: string): DB {
|
|
109
|
+
const openOptions: Parameters<typeof open>[0] = {
|
|
110
|
+
name: dbFilename,
|
|
111
|
+
readOnly
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
if (this.options.dbLocation) {
|
|
115
|
+
openOptions.location = this.options.dbLocation;
|
|
116
|
+
} else if ('NativePowerSyncHelper' in NativeModules) {
|
|
117
|
+
// In older versions of the PowerSync React Native SDK, we used React Native Quick SQLite instead of OP-SQLite.
|
|
118
|
+
// On Android, RQNS uses context.getFilesDir() instead of context.getDatabasePath() (which OP-SQLite uses as a
|
|
119
|
+
// default). So, to ensure that databases opened with RNQS continue to work with OP-SQLite, we have a native
|
|
120
|
+
// helper method that checks whether the database exists in the old path and would apply that as an explicit
|
|
121
|
+
// location in that case.
|
|
122
|
+
const helper: NativePowerSyncHelper = NativeModules.NativePowerSyncHelper;
|
|
123
|
+
const defaultLocation = helper.resolveDefaultDatabaseLocation(dbFilename);
|
|
124
|
+
if (defaultLocation) {
|
|
125
|
+
openOptions.location = defaultLocation;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// If the encryption key is undefined/null when using SQLCipher it will cause the open function to fail
|
|
130
|
+
if (encryptionKey) {
|
|
131
|
+
openOptions.encryptionKey = encryptionKey;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return open(openOptions);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private loadAdditionalExtensions(DB: DB) {
|
|
138
|
+
if (this.options.sqliteOptions?.extensions && this.options.sqliteOptions.extensions.length > 0) {
|
|
139
|
+
for (const extension of this.options.sqliteOptions.extensions) {
|
|
140
|
+
DB.loadExtension(extension.path, extension.entryPoint);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private async loadPowerSyncExtension(DB: DB) {
|
|
146
|
+
if (Platform.OS === 'ios') {
|
|
147
|
+
const libPath = getDylibPath('co.powersync.sqlitecore', 'powersync-sqlite-core');
|
|
148
|
+
DB.loadExtension(libPath, 'sqlite3_powersync_init');
|
|
149
|
+
} else {
|
|
150
|
+
DB.loadExtension('libpowersync', 'sqlite3_powersync_init');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async close() {
|
|
155
|
+
await this.initialized;
|
|
156
|
+
// Abort any pending operations
|
|
157
|
+
this.abortController.abort();
|
|
158
|
+
|
|
159
|
+
const { item: writeConnection, release: returnWrite } = await this.writeConnection!.requestOne();
|
|
160
|
+
const { items: readers, release: returnReaders } = await this.readConnections!.requestAll();
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
writeConnection.close();
|
|
164
|
+
readers.forEach((c) => c.close());
|
|
165
|
+
} finally {
|
|
166
|
+
returnWrite();
|
|
167
|
+
returnReaders();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private generateNestedAbortSignal(options?: DBLockOptions) {
|
|
172
|
+
const outerSignal = this.abortController.signal;
|
|
173
|
+
let signal: AbortSignal;
|
|
174
|
+
let cleanUpInnerSignal: (() => void) | undefined;
|
|
175
|
+
|
|
176
|
+
if (options?.timeoutMs && !outerSignal.aborted) {
|
|
177
|
+
// This is essentially an AbortSignal.any() polyfill.
|
|
178
|
+
const innerController = new AbortController();
|
|
179
|
+
cleanUpInnerSignal = () => {
|
|
180
|
+
innerController.abort();
|
|
181
|
+
outerSignal.removeEventListener('abort', cleanUpInnerSignal!);
|
|
182
|
+
timeout.removeEventListener('abort', cleanUpInnerSignal!);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
outerSignal.addEventListener('abort', cleanUpInnerSignal);
|
|
186
|
+
const timeout = timeoutSignal(options.timeoutMs);
|
|
187
|
+
timeout.addEventListener('abort', cleanUpInnerSignal);
|
|
188
|
+
|
|
189
|
+
signal = innerController.signal;
|
|
190
|
+
} else {
|
|
191
|
+
signal = outerSignal;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { signal, cleanUpInnerSignal };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async readLock<T>(fn: (tx: OPSQLiteConnection) => Promise<T>, options?: DBLockOptions): Promise<T> {
|
|
198
|
+
await this.initialized;
|
|
199
|
+
|
|
200
|
+
const { signal, cleanUpInnerSignal } = this.generateNestedAbortSignal(options);
|
|
201
|
+
const { item, release } = await this.readConnections!.requestOne(signal);
|
|
202
|
+
try {
|
|
203
|
+
return await fn(item);
|
|
204
|
+
} finally {
|
|
205
|
+
release();
|
|
206
|
+
cleanUpInnerSignal?.();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async writeLock<T>(fn: (tx: OPSQLiteConnection) => Promise<T>, options?: DBLockOptions): Promise<T> {
|
|
211
|
+
await this.initialized;
|
|
212
|
+
|
|
213
|
+
const { signal, cleanUpInnerSignal } = this.generateNestedAbortSignal(options);
|
|
214
|
+
const { item, release } = await this.writeConnection!.requestOne(signal);
|
|
215
|
+
try {
|
|
216
|
+
return await fn(item).finally(() => item.flushUpdates());
|
|
217
|
+
} finally {
|
|
218
|
+
release();
|
|
219
|
+
cleanUpInnerSignal?.();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async refreshSchema(): Promise<void> {
|
|
224
|
+
await this.initialized;
|
|
225
|
+
await this.writeLock((l) => l.refreshSchema());
|
|
226
|
+
const { items, release } = await this.readConnections!.requestAll();
|
|
227
|
+
try {
|
|
228
|
+
for (let readConnection of items) {
|
|
229
|
+
await readConnection.refreshSchema();
|
|
230
|
+
}
|
|
231
|
+
} finally {
|
|
232
|
+
release();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
executeBatch(query: string, params?: any[][]): Promise<QueryResult<never>> {
|
|
237
|
+
// We need to override this because we don't support executeBatch in connection contexts / transactions, only when
|
|
238
|
+
// called directly on the adapter.
|
|
239
|
+
return this.writeLock((conn) => conn.executeNativeBatch(query, params));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
interface NativePowerSyncHelper {
|
|
244
|
+
resolveDefaultDatabaseLocation(dbName: string): string | null;
|
|
245
|
+
}
|
|
@@ -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
|
+
}
|