@powersync/capacitor 0.5.2 → 0.5.3

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.
@@ -1,16 +1,19 @@
1
1
  import { Capacitor } from '@capacitor/core';
2
2
  import {
3
3
  DBAdapter,
4
+ DEFAULT_STREAM_CONNECTION_OPTIONS,
4
5
  MEMORY_TRIGGER_CLAIM_MANAGER,
5
6
  PowerSyncBackendConnector,
7
+ PowerSyncConnectionOptions,
6
8
  RequiredAdditionalConnectionOptions,
7
9
  StreamingSyncImplementation,
10
+ SyncStreamConnectionMethod,
8
11
  TriggerManagerConfig,
9
12
  PowerSyncDatabase as WebPowerSyncDatabase,
10
- WebPowerSyncDatabaseOptionsWithSettings,
11
- WebRemote
13
+ WebPowerSyncDatabaseOptionsWithSettings
12
14
  } from '@powersync/web';
13
15
  import { CapacitorSQLiteAdapter } from './adapter/CapacitorSQLiteAdapter.js';
16
+ import { CapacitorRemote } from './sync/CapacitorRemote.js';
14
17
  import { CapacitorStreamingSyncImplementation } from './sync/CapacitorSyncImplementation.js';
15
18
 
16
19
  /**
@@ -22,6 +25,29 @@ import { CapacitorStreamingSyncImplementation } from './sync/CapacitorSyncImplem
22
25
  * @alpha
23
26
  */
24
27
  export class PowerSyncDatabase extends WebPowerSyncDatabase {
28
+ /**
29
+ * Connects to stream of events from the PowerSync instance.
30
+ * {@link PowerSyncConnectionOptions#connectionMethod} defaults to WebSocket connection on Web platforms
31
+ * or HTTP connections if using {@link CapacitorSQLiteAdapter} - this is due to poor performance with
32
+ * the Capacitor Community SQLite library and binary payloads.
33
+ */
34
+ connect(connector: PowerSyncBackendConnector, options?: PowerSyncConnectionOptions): Promise<void> {
35
+ const isUsingCapacitorDriver = this.database instanceof CapacitorSQLiteAdapter;
36
+ const defaultConnectionMethod = isUsingCapacitorDriver
37
+ ? SyncStreamConnectionMethod.HTTP
38
+ : DEFAULT_STREAM_CONNECTION_OPTIONS.connectionMethod;
39
+ if (options?.connectionMethod == SyncStreamConnectionMethod.WEB_SOCKET && isUsingCapacitorDriver) {
40
+ this.logger.warn(
41
+ `Connecting via 'SyncStreamConnectionMethod.WEB_SOCKET' when using the 'CapacitorSQLiteAdapter' will result in poor sync performance. Use 'SyncStreamConnectionMethod.HTTP' (the default for native) instead.`
42
+ );
43
+ }
44
+
45
+ return super.connect(connector, {
46
+ ...(options ?? {}),
47
+ connectionMethod: options?.connectionMethod ?? defaultConnectionMethod
48
+ });
49
+ }
50
+
25
51
  protected get isNativeCapacitorPlatform(): boolean {
26
52
  const platform = Capacitor.getPlatform();
27
53
  return platform == 'ios' || platform == 'android';
@@ -80,7 +106,8 @@ export class PowerSyncDatabase extends WebPowerSyncDatabase {
80
106
  if (this.options.flags?.enableMultiTabs) {
81
107
  this.logger.warn(`enableMultiTabs is not supported on Capacitor mobile platforms. Ignoring the flag.`);
82
108
  }
83
- const remote = new WebRemote(connector, this.logger);
109
+
110
+ const remote = new CapacitorRemote(connector, this.logger);
84
111
 
85
112
  return new CapacitorStreamingSyncImplementation({
86
113
  ...(this.options as {}),
@@ -12,8 +12,7 @@ import {
12
12
  LockContext,
13
13
  Mutex,
14
14
  QueryResult,
15
- timeoutSignal,
16
- Transaction
15
+ timeoutSignal
17
16
  } from '@powersync/web';
18
17
  import { PowerSyncCore } from '../plugin/PowerSyncCore.js';
19
18
  import { messageForErrorCode } from '../plugin/PowerSyncPlugin.js';
@@ -33,6 +32,46 @@ async function monitorQuery(sql: string, executor: () => Promise<QueryResult>):
33
32
  }
34
33
  }
35
34
 
35
+ /**
36
+ * Maps SQLite query parameter values to Capacitor Community supported formats.
37
+ * This handles binary payloads for both iOS and Android.
38
+ */
39
+ function mapSQLiteParameterValues({ platform, values }: { platform: string; values: any[] }) {
40
+ return values.map((value) => {
41
+ if (value instanceof Uint8Array) {
42
+ switch (platform) {
43
+ case 'ios': {
44
+ /**
45
+ * The Buffer polyfill, used in @powersync/common, is a Uint8Array subclass which defines additional fields like
46
+ * `_isBuffer` and `parent` on its `prototype`. The additional fields are serialized when passed through the native bridge.
47
+ * The Capacitor Community SQLite library expects a dictionary of indexes to numerical bytes.
48
+ * The additional fields (which are not an index to numerical byte mapping) cause the parsing logic in the SQLite library to throw an error:
49
+ * "Error in reading buffer".
50
+ *
51
+ * Re-wrapping the same backing buffer as a plain Uint8Array removes the Buffer subclass wrapper
52
+ * while keeping the same underlying bytes. This creates a new view, not a byte copy, so the
53
+ * overhead should be minimal.
54
+ */
55
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
56
+ }
57
+ case 'android': {
58
+ /**
59
+ * Android expects an object of the form:
60
+ * { type: 'Buffer', data: [...]}
61
+ */
62
+ return {
63
+ type: 'Buffer',
64
+ data: Array.from(value)
65
+ };
66
+ }
67
+ }
68
+ }
69
+
70
+ // return value as-is
71
+ return value;
72
+ });
73
+ }
74
+
36
75
  class CapacitorConnectionPool extends BaseObserver<DBAdapterListener> implements ConnectionPool {
37
76
  protected _writeConnection: SQLiteDBConnection | null;
38
77
  protected _readConnection: SQLiteDBConnection | null;
@@ -119,7 +158,11 @@ class CapacitorConnectionPool extends BaseObserver<DBAdapterListener> implements
119
158
 
120
159
  protected generateLockContext(db: SQLiteDBConnection): LockContext {
121
160
  const _query = async (query: string, params: any[] = []) => {
122
- const result = await db.query(query, params);
161
+ const mappedParams = mapSQLiteParameterValues({
162
+ platform: Capacitor.getPlatform(),
163
+ values: params
164
+ });
165
+ const result = await db.query(query, mappedParams);
123
166
  const arrayResult = result.values ?? [];
124
167
  return {
125
168
  rowsAffected: 0,
@@ -134,31 +177,35 @@ class CapacitorConnectionPool extends BaseObserver<DBAdapterListener> implements
134
177
  const _execute = async (query: string, params: any[] = []): Promise<QueryResult> => {
135
178
  const platform = Capacitor.getPlatform();
136
179
 
137
- if (db.getConnectionReadOnly()) {
180
+ if (
181
+ db.getConnectionReadOnly() ||
182
+ // Android: use query for SELECT and executeSet for mutations
183
+ // We cannot use `run` here for both cases.
184
+ (platform == 'android' && query.toLowerCase().trim().startsWith('select'))
185
+ ) {
138
186
  return _query(query, params);
139
187
  }
140
188
 
189
+ const mappedParams = mapSQLiteParameterValues({
190
+ platform,
191
+ values: params
192
+ });
193
+
141
194
  if (platform == 'android') {
142
- // Android: use query for SELECT and executeSet for mutations
143
- // We cannot use `run` here for both cases.
144
- if (query.toLowerCase().trim().startsWith('select')) {
145
- return _query(query, params);
146
- } else {
147
- const result = await db.executeSet([{ statement: query, values: params }], false);
148
- return {
149
- insertId: result.changes?.lastId,
150
- rowsAffected: result.changes?.changes ?? 0,
151
- rows: {
152
- _array: [],
153
- length: 0,
154
- item: () => null
155
- }
156
- };
157
- }
195
+ const result = await db.executeSet([{ statement: query, values: mappedParams }], false);
196
+ return {
197
+ insertId: result.changes?.lastId,
198
+ rowsAffected: result.changes?.changes ?? 0,
199
+ rows: {
200
+ _array: [],
201
+ length: 0,
202
+ item: () => null
203
+ }
204
+ };
158
205
  }
159
206
 
160
207
  // iOS (and other platforms): use run("all")
161
- const result = await db.run(query, params, false, 'all');
208
+ const result = await db.run(query, mappedParams, false, 'all');
162
209
  const resultSet = result.changes?.values ?? [];
163
210
  return {
164
211
  insertId: result.changes?.lastId,
@@ -204,10 +251,14 @@ class CapacitorConnectionPool extends BaseObserver<DBAdapterListener> implements
204
251
  };
205
252
 
206
253
  const executeBatch = async (query: string, params: any[][] = []): Promise<QueryResult> => {
254
+ const platform = Capacitor.getPlatform();
207
255
  let result = await db.executeSet(
208
256
  params.map((param) => ({
209
257
  statement: query,
210
- values: param
258
+ values: mapSQLiteParameterValues({
259
+ platform,
260
+ values: param
261
+ })
211
262
  }))
212
263
  );
213
264
 
@@ -0,0 +1,23 @@
1
+ import { WebRemote } from '@powersync/web';
2
+
3
+ export class CapacitorRemote extends WebRemote {
4
+ protected get supportsStreamingBinaryResponses(): boolean {
5
+ /**
6
+ * We'd like to avoid passing Binary buffers to SQLite when using
7
+ * iOS and Android for now. This is due to inefficient binary processing.
8
+ * Syncing using Buffers and Capacitor Community SQLite has been observed to be notably
9
+ * slower than the NDJSON option.
10
+ * Capacitor Community SQLite serializes Buffer objects, which causes slowdown
11
+ * ios: https://github.com/capacitor-community/sqlite/blob/f507a1e779688ea72b9d7e8744c647f7b688c568/ios/Plugin/CapacitorSQLite.swift#L888-L912
12
+ * android: https://github.com/capacitor-community/sqlite/blob/master/android/src/main/java/com/getcapacitor/community/database/sqlite/SQLite/UtilsSQLite.java#L141-L147
13
+ * As a rough guideline, the time to locally sync 10_000 small records was observed as:
14
+ * iOS:
15
+ * - NDJSON: 449ms
16
+ * - Binary: 68_982ms
17
+ * Android:
18
+ * - NDJSON: 452ms
19
+ * - Binary: 1_847ms
20
+ */
21
+ return false;
22
+ }
23
+ }