@powersync/capacitor 0.5.1 → 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.
- package/PowersyncCapacitor.podspec +1 -1
- package/README.md +3 -1
- package/android/build.gradle +1 -1
- package/dist/esm/PowerSyncDatabase.d.ts +8 -1
- package/dist/esm/PowerSyncDatabase.js +20 -2
- package/dist/esm/PowerSyncDatabase.js.map +1 -1
- package/dist/esm/adapter/CapacitorSQLiteAdapter.d.ts +3 -4
- package/dist/esm/adapter/CapacitorSQLiteAdapter.js +73 -28
- package/dist/esm/adapter/CapacitorSQLiteAdapter.js.map +1 -1
- package/dist/esm/sync/CapacitorRemote.d.ts +4 -0
- package/dist/esm/sync/CapacitorRemote.js +23 -0
- package/dist/esm/sync/CapacitorRemote.js.map +1 -0
- package/dist/esm/sync/CapacitorSyncImplementation.js +3 -8
- package/dist/esm/sync/CapacitorSyncImplementation.js.map +1 -1
- package/dist/plugin.cjs +118 -38
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.d.cts +11 -4
- package/dist/plugin.js +120 -39
- package/dist/plugin.js.map +1 -1
- package/package.json +2 -5
- package/src/PowerSyncDatabase.ts +30 -3
- package/src/adapter/CapacitorSQLiteAdapter.ts +96 -53
- package/src/sync/CapacitorRemote.ts +23 -0
- package/src/sync/CapacitorSyncImplementation.ts +3 -7
|
@@ -10,11 +10,10 @@ import {
|
|
|
10
10
|
DBAdapterListener,
|
|
11
11
|
DBLockOptions,
|
|
12
12
|
LockContext,
|
|
13
|
-
|
|
13
|
+
Mutex,
|
|
14
14
|
QueryResult,
|
|
15
|
-
|
|
15
|
+
timeoutSignal
|
|
16
16
|
} from '@powersync/web';
|
|
17
|
-
import { Mutex } from 'async-mutex';
|
|
18
17
|
import { PowerSyncCore } from '../plugin/PowerSyncCore.js';
|
|
19
18
|
import { messageForErrorCode } from '../plugin/PowerSyncPlugin.js';
|
|
20
19
|
import { CapacitorSQLiteOpenFactoryOptions, DEFAULT_SQLITE_OPTIONS } from './CapacitorSQLiteOpenFactory.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
|
|
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 (
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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,
|
|
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:
|
|
258
|
+
values: mapSQLiteParameterValues({
|
|
259
|
+
platform,
|
|
260
|
+
values: param
|
|
261
|
+
})
|
|
211
262
|
}))
|
|
212
263
|
);
|
|
213
264
|
|
|
@@ -228,39 +279,31 @@ class CapacitorConnectionPool extends BaseObserver<DBAdapterListener> implements
|
|
|
228
279
|
}
|
|
229
280
|
|
|
230
281
|
readLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
|
|
231
|
-
return
|
|
232
|
-
this.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
return await fn(this.generateLockContext(this.readConnection));
|
|
236
|
-
},
|
|
237
|
-
options
|
|
238
|
-
);
|
|
282
|
+
return this.readMutex.runExclusive(async () => {
|
|
283
|
+
await this.initializedPromise;
|
|
284
|
+
return fn(this.generateLockContext(this.readConnection));
|
|
285
|
+
}, timeoutSignal(options?.timeoutMs));
|
|
239
286
|
}
|
|
240
287
|
|
|
241
288
|
writeLock<T>(fn: (tx: LockContext) => Promise<T>, options?: DBLockOptions): Promise<T> {
|
|
242
|
-
return
|
|
243
|
-
this.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
return result;
|
|
261
|
-
},
|
|
262
|
-
options
|
|
263
|
-
);
|
|
289
|
+
return this.writeMutex.runExclusive(async () => {
|
|
290
|
+
await this.initializedPromise;
|
|
291
|
+
const result = await fn(this.generateLockContext(this.writeConnection));
|
|
292
|
+
|
|
293
|
+
// Fetch table updates
|
|
294
|
+
const updates = await this.writeConnection.query("SELECT powersync_update_hooks('get') AS table_name");
|
|
295
|
+
const jsonUpdates = updates.values?.[0];
|
|
296
|
+
if (!jsonUpdates || !jsonUpdates.table_name) {
|
|
297
|
+
throw new Error('Could not fetch table updates');
|
|
298
|
+
}
|
|
299
|
+
const notification: BatchedUpdateNotification = {
|
|
300
|
+
rawUpdates: [],
|
|
301
|
+
tables: JSON.parse(jsonUpdates.table_name),
|
|
302
|
+
groupedUpdates: {}
|
|
303
|
+
};
|
|
304
|
+
this.iterateListeners((l) => l.tablesUpdated?.(notification));
|
|
305
|
+
return result;
|
|
306
|
+
}, timeoutSignal(options?.timeoutMs));
|
|
264
307
|
}
|
|
265
308
|
|
|
266
309
|
refreshSchema(): Promise<void> {
|
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { AbstractStreamingSyncImplementation, LockOptions, LockType,
|
|
2
|
-
import { Mutex } from 'async-mutex';
|
|
1
|
+
import { AbstractStreamingSyncImplementation, LockOptions, LockType, Mutex } from '@powersync/web';
|
|
3
2
|
|
|
4
3
|
type MutexMap = {
|
|
5
4
|
/**
|
|
@@ -56,11 +55,8 @@ export class CapacitorStreamingSyncImplementation extends AbstractStreamingSyncI
|
|
|
56
55
|
mutexRecord.tracking.add(this.instanceId);
|
|
57
56
|
const mutex = mutexRecord.locks[lockOptions.type];
|
|
58
57
|
|
|
59
|
-
return
|
|
60
|
-
if (lockOptions.signal?.aborted) {
|
|
61
|
-
throw new Error('Aborted');
|
|
62
|
-
}
|
|
58
|
+
return mutex.runExclusive(async () => {
|
|
63
59
|
return await lockOptions.callback();
|
|
64
|
-
});
|
|
60
|
+
}, lockOptions.signal);
|
|
65
61
|
}
|
|
66
62
|
}
|