@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.
Files changed (64) hide show
  1. package/README.md +72 -6
  2. package/android/build.gradle +90 -0
  3. package/android/gradle.properties +4 -0
  4. package/android/src/main/AndroidManifest.xml +3 -0
  5. package/android/src/main/AndroidManifestNew.xml +2 -0
  6. package/android/src/main/java/com/powersync/opsqlite/PowerSyncOpSqlitePackage.kt +46 -0
  7. package/ios/PowerSyncOpSqlite.h +5 -0
  8. package/ios/PowerSyncOpSqlite.mm +6 -0
  9. package/lib/db/PowerSyncDatabase.d.ts +9 -10
  10. package/lib/db/PowerSyncDatabase.js +33 -25
  11. package/lib/db/PowerSyncDatabase.js.map +1 -1
  12. package/lib/db/adapters/op-sqlite/OPSQLiteConnection.d.ts +25 -0
  13. package/lib/db/adapters/op-sqlite/OPSQLiteConnection.js +63 -0
  14. package/lib/db/adapters/op-sqlite/OPSQLiteConnection.js.map +1 -0
  15. package/lib/db/adapters/op-sqlite/OPSqliteAdapter.d.ts +32 -0
  16. package/lib/db/adapters/op-sqlite/OPSqliteAdapter.js +203 -0
  17. package/lib/db/adapters/op-sqlite/OPSqliteAdapter.js.map +1 -0
  18. package/lib/db/adapters/op-sqlite/OPSqliteDBOpenFactory.d.ts +11 -0
  19. package/lib/db/adapters/op-sqlite/OPSqliteDBOpenFactory.js +21 -0
  20. package/lib/db/adapters/op-sqlite/OPSqliteDBOpenFactory.js.map +1 -0
  21. package/lib/db/adapters/op-sqlite/SqliteOptions.d.ts +68 -0
  22. package/lib/db/adapters/op-sqlite/SqliteOptions.js +37 -0
  23. package/lib/db/adapters/op-sqlite/SqliteOptions.js.map +1 -0
  24. package/lib/index.d.ts +1 -3
  25. package/lib/index.js +0 -3
  26. package/lib/index.js.map +1 -1
  27. package/lib/sync/bucket/ReactNativeBucketStorageAdapter.d.ts +1 -1
  28. package/lib/sync/bucket/ReactNativeBucketStorageAdapter.js +1 -1
  29. package/lib/sync/bucket/ReactNativeBucketStorageAdapter.js.map +1 -1
  30. package/lib/sync/stream/ReactNativeRemote.d.ts +11 -11
  31. package/lib/sync/stream/ReactNativeRemote.js +42 -66
  32. package/lib/sync/stream/ReactNativeRemote.js.map +1 -1
  33. package/lib/sync/stream/ReactNativeStreamingSyncImplementation.d.ts +2 -1
  34. package/lib/sync/stream/ReactNativeStreamingSyncImplementation.js +2 -1
  35. package/lib/sync/stream/ReactNativeStreamingSyncImplementation.js.map +1 -1
  36. package/lib/sync/stream/fetch.d.ts +13 -0
  37. package/lib/sync/stream/fetch.js +31 -0
  38. package/lib/sync/stream/fetch.js.map +1 -0
  39. package/package.json +20 -33
  40. package/powersync-react-native.podspec +32 -0
  41. package/src/db/PowerSyncDatabase.ts +58 -31
  42. package/src/db/adapters/op-sqlite/OPSQLiteConnection.ts +95 -0
  43. package/src/db/adapters/op-sqlite/OPSqliteAdapter.ts +245 -0
  44. package/src/db/adapters/op-sqlite/OPSqliteDBOpenFactory.ts +25 -0
  45. package/src/db/adapters/op-sqlite/SqliteOptions.ts +93 -0
  46. package/src/index.ts +1 -3
  47. package/src/sync/bucket/ReactNativeBucketStorageAdapter.ts +1 -1
  48. package/src/sync/stream/ReactNativeRemote.ts +49 -86
  49. package/src/sync/stream/ReactNativeStreamingSyncImplementation.ts +4 -4
  50. package/src/sync/stream/fetch.ts +45 -0
  51. package/dist/index.js +0 -8741
  52. package/dist/index.js.map +0 -1
  53. package/lib/db/adapters/react-native-quick-sqlite/RNQSDBAdapter.d.ts +0 -55
  54. package/lib/db/adapters/react-native-quick-sqlite/RNQSDBAdapter.js +0 -66
  55. package/lib/db/adapters/react-native-quick-sqlite/RNQSDBAdapter.js.map +0 -1
  56. package/lib/db/adapters/react-native-quick-sqlite/RNQSDBOpenFactory.d.ts +0 -19
  57. package/lib/db/adapters/react-native-quick-sqlite/RNQSDBOpenFactory.js +0 -34
  58. package/lib/db/adapters/react-native-quick-sqlite/RNQSDBOpenFactory.js.map +0 -1
  59. package/lib/db/adapters/react-native-quick-sqlite/ReactNativeQuickSQLiteOpenFactory.d.ts +0 -9
  60. package/lib/db/adapters/react-native-quick-sqlite/ReactNativeQuickSQLiteOpenFactory.js +0 -45
  61. package/lib/db/adapters/react-native-quick-sqlite/ReactNativeQuickSQLiteOpenFactory.js.map +0 -1
  62. package/src/db/adapters/react-native-quick-sqlite/RNQSDBAdapter.ts +0 -85
  63. package/src/db/adapters/react-native-quick-sqlite/RNQSDBOpenFactory.ts +0 -44
  64. 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-20260525085311",
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
- "main": "./dist/index.js",
10
- "module": "./dist/index.js",
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
- "dist",
15
- "src"
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
- "@journeyapps/react-native-quick-sqlite": "^2.5.2",
29
- "@powersync/common": "^1.53.1",
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/react": "1.10.0",
40
- "@powersync/common": "1.53.1"
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
- "@craftzdog/react-native-buffer": "^6.0.5",
44
- "@journeyapps/react-native-quick-sqlite": "^2.5.2",
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.83.1",
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 && rollup -c rollup.config.mjs",
69
- "build:prod": "tsc -b && rollup -c rollup.config.mjs",
70
- "clean": "rm -rf lib dist tsconfig.tsbuildinfo",
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
- AbstractPowerSyncDatabase,
3
- AbstractStreamingSyncImplementation,
4
- BucketStorageAdapter,
2
+ BasePowerSyncDatabaseOptions,
3
+ CommonPowerSyncDatabase,
4
+ DatabaseSource,
5
5
  DBAdapter,
6
6
  PowerSyncBackendConnector,
7
- PowerSyncDatabaseOptionsWithSettings,
8
- type RequiredAdditionalConnectionOptions
7
+ PowerSyncDatabaseConstructor,
8
+ SyncStreamConnectionMethod
9
9
  } from '@powersync/common';
10
- import { ReactNativeRemote } from '../sync/stream/ReactNativeRemote';
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 { ReactNativeQuickSqliteOpenFactory } from './adapters/react-native-quick-sqlite/ReactNativeQuickSQLiteOpenFactory';
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
- * Opens a DBAdapter using React Native Quick SQLite as the
34
- * default SQLite open factory.
35
- */
36
- protected openDBAdapter(options: PowerSyncDatabaseOptionsWithSettings): DBAdapter {
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: RequiredAdditionalConnectionOptions
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
+ }