@capgo/capacitor-fast-sql 8.2.17 → 8.3.0

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 (34) hide show
  1. package/Package.swift +2 -1
  2. package/README.md +27 -18
  3. package/android/build.gradle +4 -0
  4. package/android/consumer-rules.pro +14 -0
  5. package/android/src/main/java/app/capgo/capacitor/fastsql/BinaryFrame.java +152 -0
  6. package/android/src/main/java/app/capgo/capacitor/fastsql/CapgoCapacitorFastSqlPlugin.java +5 -1
  7. package/android/src/main/java/app/capgo/capacitor/fastsql/DatabaseConnection.java +2 -0
  8. package/android/src/main/java/app/capgo/capacitor/fastsql/EncryptedSQLDatabase.java +13 -2
  9. package/android/src/main/java/app/capgo/capacitor/fastsql/SQLDatabase.java +13 -2
  10. package/android/src/main/java/app/capgo/capacitor/fastsql/SQLHTTPServer.java +114 -57
  11. package/dist/esm/binary-frame.d.ts +6 -0
  12. package/dist/esm/binary-frame.d.ts.map +1 -0
  13. package/dist/esm/binary-frame.js +100 -0
  14. package/dist/esm/definitions.d.ts +28 -0
  15. package/dist/esm/definitions.d.ts.map +1 -1
  16. package/dist/esm/fast-sql.d.ts.map +1 -1
  17. package/dist/esm/fast-sql.js +13 -3
  18. package/dist/esm/native-sql-connection.d.ts +18 -7
  19. package/dist/esm/native-sql-connection.d.ts.map +1 -1
  20. package/dist/esm/native-sql-connection.js +145 -59
  21. package/dist/esm/web.d.ts +2 -0
  22. package/dist/esm/web.d.ts.map +1 -1
  23. package/dist/esm/web.js +24 -2
  24. package/dist/plugin.cjs.js +282 -64
  25. package/dist/plugin.cjs.js.map +1 -1
  26. package/dist/plugin.js +282 -64
  27. package/dist/plugin.js.map +1 -1
  28. package/ios/Sources/CapgoCapacitorFastSqlPlugin/BinaryFrame.swift +95 -0
  29. package/ios/Sources/CapgoCapacitorFastSqlPlugin/CapgoCapacitorFastSqlPlugin.swift +7 -3
  30. package/ios/Sources/CapgoCapacitorFastSqlPlugin/SQLDatabase.swift +20 -3
  31. package/ios/Sources/CapgoCapacitorFastSqlPlugin/SQLHTTPServer.swift +59 -76
  32. package/ios/Tests/CapgoCapacitorFastSqlPluginTests/BinaryFrameTests.swift +65 -0
  33. package/ios/Tests/CapgoCapacitorFastSqlPluginTests/Fixtures/binary-v1.bin +0 -0
  34. package/package.json +5 -2
package/dist/plugin.js CHANGED
@@ -19,6 +19,107 @@ var capacitorCapgoCapacitorFastSql = (function (exports, core) {
19
19
  web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.CapgoCapacitorFastSqlWeb()),
20
20
  });
21
21
 
22
+ /** Version 1 frame: FSQ1, uint32-LE metadata length, uint32-LE attachment length. */
23
+ const BINARY_CONTENT_TYPE = 'application/vnd.capgo.fast-sql.v1+binary';
24
+ const MAX_FRAME_BYTES = 64 * 1024 * 1024;
25
+ const MAX_METADATA_BYTES = 1024 * 1024;
26
+ const MAX_ATTACHMENTS = 4096;
27
+ const HEADER_BYTES = 12;
28
+ const magic = [70, 83, 81, 49];
29
+ function encodeBinaryFrame(value) {
30
+ const attachments = [];
31
+ let size = 0;
32
+ let nodes = 0;
33
+ const encode = (item, depth) => {
34
+ if (++nodes > 100000 || depth > 32)
35
+ throw new Error('Binary metadata exceeds nesting or node limits');
36
+ if (typeof item === 'number' && !Number.isFinite(item))
37
+ throw new Error('Non-finite binary protocol number');
38
+ if (item instanceof Uint8Array) {
39
+ if (attachments.length >= MAX_ATTACHMENTS || size + item.byteLength > MAX_FRAME_BYTES - HEADER_BYTES)
40
+ throw new Error('Binary attachments exceed protocol limits');
41
+ const reference = { $blob: [size, item.byteLength] };
42
+ attachments.push(item);
43
+ size += item.byteLength;
44
+ return reference;
45
+ }
46
+ if (Array.isArray(item))
47
+ return item.map((value) => encode(value, depth + 1));
48
+ if (item && typeof item === 'object') {
49
+ if (Object.getPrototypeOf(item) !== Object.prototype && Object.getPrototypeOf(item) !== null)
50
+ throw new Error('Unsupported binary metadata object');
51
+ return Object.fromEntries(Object.entries(item).map(([key, value]) => [key, encode(value, depth + 1)]));
52
+ }
53
+ return item;
54
+ };
55
+ const json = JSON.stringify(encode(value, 0));
56
+ if (json === undefined)
57
+ throw new Error('Missing binary metadata');
58
+ const metadata = new TextEncoder().encode(json);
59
+ const length = HEADER_BYTES + metadata.byteLength + size;
60
+ if (metadata.byteLength > MAX_METADATA_BYTES || length > MAX_FRAME_BYTES)
61
+ throw new Error('Binary frame exceeds protocol limits');
62
+ const frame = new Uint8Array(length);
63
+ frame.set(magic);
64
+ const view = new DataView(frame.buffer);
65
+ view.setUint32(4, metadata.byteLength, true);
66
+ view.setUint32(8, size, true);
67
+ frame.set(metadata, HEADER_BYTES);
68
+ let offset = HEADER_BYTES + metadata.byteLength;
69
+ for (const attachment of attachments) {
70
+ frame.set(attachment, offset);
71
+ offset += attachment.byteLength;
72
+ }
73
+ return frame;
74
+ }
75
+ function decodeBinaryFrame(frame) {
76
+ if (frame.length < HEADER_BYTES ||
77
+ frame.length > MAX_FRAME_BYTES ||
78
+ magic.some((byte, index) => frame[index] !== byte)) {
79
+ throw new Error('Invalid binary frame version or length');
80
+ }
81
+ const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
82
+ const metadataLength = view.getUint32(4, true);
83
+ const payloadLength = view.getUint32(8, true);
84
+ const payloadOffset = HEADER_BYTES + metadataLength;
85
+ if (metadataLength === 0 || metadataLength > MAX_METADATA_BYTES || payloadOffset + payloadLength !== frame.length) {
86
+ throw new Error('Invalid binary frame lengths');
87
+ }
88
+ const metadata = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(frame.subarray(HEADER_BYTES, payloadOffset)));
89
+ let nodes = 0;
90
+ let blobs = 0;
91
+ let referencedBytes = 0;
92
+ const decode = (value, depth) => {
93
+ if (++nodes > 100000 || depth > 32)
94
+ throw new Error('Binary metadata exceeds nesting or node limits');
95
+ if (Array.isArray(value))
96
+ return value.map((item) => decode(item, depth + 1));
97
+ if (value === null || typeof value !== 'object')
98
+ return value;
99
+ if ('$blob' in value && Object.keys(value).length === 1) {
100
+ const reference = value.$blob;
101
+ if (!Array.isArray(reference) || reference.length !== 2)
102
+ throw new Error('Invalid binary attachment reference');
103
+ const [offset, length] = reference;
104
+ if (typeof offset !== 'number' ||
105
+ typeof length !== 'number' ||
106
+ !Number.isSafeInteger(offset) ||
107
+ !Number.isSafeInteger(length) ||
108
+ offset < 0 ||
109
+ length < 0 ||
110
+ offset + length > payloadLength) {
111
+ throw new Error('Binary attachment is outside its frame');
112
+ }
113
+ referencedBytes += length;
114
+ if (++blobs > MAX_ATTACHMENTS || referencedBytes > MAX_FRAME_BYTES)
115
+ throw new Error('Decoded attachments exceed protocol limits');
116
+ return frame.slice(payloadOffset + offset, payloadOffset + offset + length);
117
+ }
118
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, decode(item, depth + 1)]));
119
+ };
120
+ return decode(metadata, 0);
121
+ }
122
+
22
123
  function assertSupportedIsolation(level) {
23
124
  if (level !== undefined && level !== exports.IsolationLevel.Serializable) {
24
125
  throw new Error('Only SERIALIZABLE transaction isolation is supported');
@@ -32,8 +133,20 @@ var capacitorCapgoCapacitorFastSql = (function (exports, core) {
32
133
  * Inspired by capacitor-blob-writer's approach to avoid serialization overhead.
33
134
  */
34
135
  class NativeSQLConnection {
35
- constructor(database, port, token) {
136
+ /** Cumulative data-request payload bytes and JS codec time; excludes HTTP headers/compression. */
137
+ getTransportMetrics() {
138
+ return Object.assign({}, this.metrics);
139
+ }
140
+ constructor(database, port, token, transport = 'http-json') {
141
+ this.transport = transport;
36
142
  this.inTransaction = false;
143
+ this.metrics = {
144
+ dataRequests: 0,
145
+ requestBytes: 0,
146
+ responseBytes: 0,
147
+ encodeMs: 0,
148
+ decodeMs: 0,
149
+ };
37
150
  this.database = database;
38
151
  this.port = port;
39
152
  this.token = token;
@@ -58,24 +171,7 @@ var capacitorCapgoCapacitorFastSql = (function (exports, core) {
58
171
  * @returns Query results
59
172
  */
60
173
  async execute(statement, params) {
61
- const response = await this.browserFetch(`${this.baseUrl}/execute`, {
62
- method: 'POST',
63
- headers: {
64
- 'Content-Type': 'application/json',
65
- Authorization: `Bearer ${this.token}`,
66
- 'X-Database': this.database,
67
- },
68
- body: JSON.stringify({
69
- statement,
70
- params: params ? this.serializeParams(params) : [],
71
- }),
72
- });
73
- if (!response.ok) {
74
- const error = await response.text();
75
- throw new Error(`SQL execution failed: ${error}`);
76
- }
77
- const result = await response.json();
78
- return this.deserializeResult(result);
174
+ return this.parseResult(await this.request('/execute', { statement, params: params !== null && params !== void 0 ? params : [] }));
79
175
  }
80
176
  /**
81
177
  * Execute multiple SQL statements in a batch for better performance
@@ -84,26 +180,138 @@ var capacitorCapgoCapacitorFastSql = (function (exports, core) {
84
180
  * @returns Array of results for each operation
85
181
  */
86
182
  async executeBatch(operations) {
87
- const response = await this.browserFetch(`${this.baseUrl}/batch`, {
88
- method: 'POST',
89
- headers: {
90
- 'Content-Type': 'application/json',
91
- Authorization: `Bearer ${this.token}`,
92
- 'X-Database': this.database,
93
- },
94
- body: JSON.stringify({
95
- operations: operations.map((op) => ({
96
- statement: op.statement,
97
- params: op.params ? this.serializeParams(op.params) : [],
98
- })),
99
- }),
183
+ const result = await this.request('/batch', {
184
+ operations: operations.map((operation) => { var _a; return ({ statement: operation.statement, params: (_a = operation.params) !== null && _a !== void 0 ? _a : [] }); }),
100
185
  });
101
- if (!response.ok) {
102
- const error = await response.text();
103
- throw new Error(`SQL batch execution failed: ${error}`);
186
+ if (!Array.isArray(result))
187
+ throw new Error('Invalid batch response');
188
+ return result.map((item) => this.parseResult(item));
189
+ }
190
+ /** Authenticated transport round trip without SQLite, for diagnostic benchmarks. */
191
+ async echo(value) {
192
+ const result = this.parseResult(await this.request('/echo', { statement: '', params: [value] }));
193
+ if (result.rows.length !== 1 || !('value' in result.rows[0]))
194
+ throw new Error('Invalid echo response');
195
+ return result.rows[0].value;
196
+ }
197
+ async request(path, payload) {
198
+ var _a;
199
+ const binary = this.transport === 'http-binary';
200
+ const start = performance.now();
201
+ const body = binary
202
+ ? encodeBinaryFrame(payload)
203
+ : new TextEncoder().encode(JSON.stringify('params' in payload
204
+ ? Object.assign(Object.assign({}, payload), { params: this.serializeParams(payload.params) }) : {
205
+ operations: payload.operations.map((operation) => (Object.assign(Object.assign({}, operation), { params: this.serializeParams(operation.params) }))),
206
+ }));
207
+ if (body.byteLength > MAX_FRAME_BYTES)
208
+ throw new Error('Request exceeds 64 MiB');
209
+ this.metrics.encodeMs += performance.now() - start;
210
+ this.metrics.dataRequests++;
211
+ this.metrics.requestBytes += body.byteLength;
212
+ const controller = new AbortController();
213
+ const timeout = setTimeout(() => controller.abort(), 600000);
214
+ try {
215
+ const response = await this.browserFetch(`${this.baseUrl}${path}`, {
216
+ method: 'POST',
217
+ headers: {
218
+ 'Content-Type': binary ? BINARY_CONTENT_TYPE : 'application/json',
219
+ Authorization: `Bearer ${this.token}`,
220
+ 'X-Database': this.database,
221
+ },
222
+ body: body.buffer,
223
+ signal: controller.signal,
224
+ });
225
+ if (!response.ok) {
226
+ const errorBody = await this.readResponse(response);
227
+ throw new Error(`SQL request failed (${response.status}): ${new TextDecoder().decode(errorBody).slice(0, 4096)}`);
228
+ }
229
+ if (binary && ((_a = response.headers.get('content-type')) === null || _a === void 0 ? void 0 : _a.split(';')[0].trim()) !== BINARY_CONTENT_TYPE)
230
+ throw new Error('Server did not return binary HTTP v1');
231
+ const bytes = await this.readResponse(response);
232
+ this.metrics.responseBytes += bytes.byteLength;
233
+ const decodeStart = performance.now();
234
+ const result = binary ? decodeBinaryFrame(bytes) : JSON.parse(new TextDecoder().decode(bytes));
235
+ this.metrics.decodeMs += performance.now() - decodeStart;
236
+ return result;
237
+ }
238
+ finally {
239
+ clearTimeout(timeout);
104
240
  }
105
- const results = await response.json();
106
- return results.map((r) => this.deserializeResult(r));
241
+ }
242
+ async readResponse(response) {
243
+ var _a;
244
+ const declared = response.headers.get('content-length');
245
+ if (declared !== null && Number(declared) > MAX_FRAME_BYTES) {
246
+ await ((_a = response.body) === null || _a === void 0 ? void 0 : _a.cancel());
247
+ throw new Error('Response exceeds 64 MiB');
248
+ }
249
+ if (!response.body)
250
+ return new Uint8Array();
251
+ const reader = response.body.getReader();
252
+ const chunks = [];
253
+ let length = 0;
254
+ try {
255
+ while (true) {
256
+ const chunk = await reader.read();
257
+ if (chunk.done)
258
+ break;
259
+ length += chunk.value.byteLength;
260
+ if (length > MAX_FRAME_BYTES) {
261
+ await reader.cancel();
262
+ throw new Error('Response exceeds 64 MiB');
263
+ }
264
+ chunks.push(chunk.value);
265
+ }
266
+ }
267
+ finally {
268
+ reader.releaseLock();
269
+ }
270
+ const result = new Uint8Array(length);
271
+ let offset = 0;
272
+ for (const chunk of chunks) {
273
+ result.set(chunk, offset);
274
+ offset += chunk.byteLength;
275
+ }
276
+ return result;
277
+ }
278
+ parseResult(result) {
279
+ const start = performance.now();
280
+ if (!result ||
281
+ typeof result !== 'object' ||
282
+ !('rows' in result) ||
283
+ !Array.isArray(result.rows) ||
284
+ !('rowsAffected' in result) ||
285
+ typeof result.rowsAffected !== 'number')
286
+ throw new Error('Invalid SQL response');
287
+ const rows = result.rows.map((row) => {
288
+ if (!row || typeof row !== 'object' || Array.isArray(row))
289
+ throw new Error('Invalid SQL row');
290
+ const parsed = {};
291
+ for (const [key, value] of Object.entries(row)) {
292
+ let scalar;
293
+ if (value === null ||
294
+ typeof value === 'string' ||
295
+ typeof value === 'boolean' ||
296
+ value instanceof Uint8Array ||
297
+ (typeof value === 'number' && Number.isFinite(value)))
298
+ scalar = value;
299
+ else if (value &&
300
+ typeof value === 'object' &&
301
+ '_type' in value &&
302
+ value._type === 'binary' &&
303
+ '_data' in value &&
304
+ typeof value._data === 'string')
305
+ scalar = this.base64ToUint8Array(value._data);
306
+ else
307
+ throw new Error('Invalid SQL value');
308
+ Object.defineProperty(parsed, key, { value: scalar, enumerable: true, writable: true, configurable: true });
309
+ }
310
+ return parsed;
311
+ });
312
+ const insertId = 'insertId' in result && typeof result.insertId === 'number' ? result.insertId : undefined;
313
+ this.metrics.decodeMs += performance.now() - start;
314
+ return { rows, rowsAffected: result.rowsAffected, insertId };
107
315
  }
108
316
  /**
109
317
  * Begin a transaction
@@ -230,28 +438,6 @@ var capacitorCapgoCapacitorFastSql = (function (exports, core) {
230
438
  return param;
231
439
  });
232
440
  }
233
- /**
234
- * Deserialize result from server
235
- * Base64-encoded binary data is converted back to Uint8Array
236
- */
237
- deserializeResult(result) {
238
- return {
239
- rows: result.rows.map((row) => {
240
- const deserializedRow = {};
241
- for (const [key, value] of Object.entries(row)) {
242
- if (value && typeof value === 'object' && value._type === 'binary') {
243
- deserializedRow[key] = this.base64ToUint8Array(value._data);
244
- }
245
- else {
246
- deserializedRow[key] = value;
247
- }
248
- }
249
- return deserializedRow;
250
- }),
251
- rowsAffected: result.rowsAffected || 0,
252
- insertId: result.insertId,
253
- };
254
- }
255
441
  /**
256
442
  * Convert Uint8Array to base64 string
257
443
  */
@@ -442,20 +628,30 @@ var capacitorCapgoCapacitorFastSql = (function (exports, core) {
442
628
  }
443
629
  }
444
630
  static optionsMatch(left, right) {
445
- return (Boolean(left.encrypted) === Boolean(right.encrypted) &&
631
+ var _a, _b;
632
+ return (((_a = left.transport) !== null && _a !== void 0 ? _a : 'http-json') === ((_b = right.transport) !== null && _b !== void 0 ? _b : 'http-json') &&
633
+ Boolean(left.encrypted) === Boolean(right.encrypted) &&
446
634
  left.encryptionKey === right.encryptionKey &&
447
635
  Boolean(left.readOnly) === Boolean(right.readOnly) &&
448
636
  Boolean(left.walMode) === Boolean(right.walMode) &&
449
637
  Boolean(left.performancePresets) === Boolean(right.performancePresets));
450
638
  }
451
639
  static async open(options) {
640
+ const platform = core.Capacitor.getPlatform();
641
+ if (options.transport === 'http-binary' && platform !== 'ios' && platform !== 'android') {
642
+ throw new Error('Binary HTTP transport requires iOS or Android');
643
+ }
644
+ if (options.transport === 'http-binary') {
645
+ const capabilities = await CapgoCapacitorFastSql.getPluginVersion();
646
+ if (capabilities.binaryProtocolVersion !== 1)
647
+ throw new Error('Native plugin does not support binary HTTP v1');
648
+ }
452
649
  // Connect via native plugin
453
650
  const info = await CapgoCapacitorFastSql.connect(options);
454
651
  // Create connection instance appropriate for the current platform
455
652
  let connection;
456
- const platform = core.Capacitor.getPlatform();
457
653
  if (platform === 'android' || platform === 'ios') {
458
- connection = new NativeSQLConnection(info.database, info.port, info.token);
654
+ connection = new NativeSQLConnection(info.database, info.port, info.token, options.transport);
459
655
  }
460
656
  else {
461
657
  connection = new WebSQLConnection(info.database, CapgoCapacitorFastSql);
@@ -16936,7 +17132,13 @@ var capacitorCapgoCapacitorFastSql = (function (exports, core) {
16936
17132
  * Must be called before connect() when overriding the default worker.
16937
17133
  */
16938
17134
  async configureWeb(config) {
16939
- this.webConfig = config;
17135
+ if (this.promiserPromise || this.databases.size > 0) {
17136
+ throw new Error('Configure web storage before opening the first database');
17137
+ }
17138
+ if (config.requirePersistence && config.useOpfs === false) {
17139
+ throw new Error('requirePersistence requires OPFS');
17140
+ }
17141
+ this.webConfig = Object.assign({}, config);
16940
17142
  this.promiserPromise = null;
16941
17143
  }
16942
17144
  getPromiser() {
@@ -16953,6 +17155,8 @@ var capacitorCapgoCapacitorFastSql = (function (exports, core) {
16953
17155
  return `/capgo-fast-sql/${database}.sqlite3`;
16954
17156
  }
16955
17157
  async connect(options) {
17158
+ if (options.transport === 'http-binary')
17159
+ throw new Error('Binary HTTP transport requires iOS or Android');
16956
17160
  if (options.readOnly) {
16957
17161
  throw new Error('Read-only opens are not supported by the SQLite Wasm Worker transport');
16958
17162
  }
@@ -16962,6 +17166,7 @@ var capacitorCapgoCapacitorFastSql = (function (exports, core) {
16962
17166
  port: existing.port,
16963
17167
  token: existing.token,
16964
17168
  database: options.database,
17169
+ persistent: existing.persistent,
16965
17170
  };
16966
17171
  }
16967
17172
  const path = this.dbPath(options.database);
@@ -16975,6 +17180,8 @@ var capacitorCapgoCapacitorFastSql = (function (exports, core) {
16975
17180
  });
16976
17181
  }
16977
17182
  catch (opfsError) {
17183
+ if (this.webConfig.requirePersistence)
17184
+ throw opfsError;
16978
17185
  console.warn('[CapgoCapacitorFastSql] OPFS unavailable; web database will not persist across reloads. ' +
16979
17186
  'Serve with Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp.', opfsError);
16980
17187
  openResult = await promiser('open', { filename: path });
@@ -16988,7 +17195,16 @@ var capacitorCapgoCapacitorFastSql = (function (exports, core) {
16988
17195
  if (!dbId) {
16989
17196
  throw new Error('Failed to open SQLite Wasm database: missing dbId');
16990
17197
  }
16991
- await this.applyPerformanceConfig(promiser, dbId, options);
17198
+ try {
17199
+ if (this.webConfig.requirePersistence && !persistent) {
17200
+ throw new Error('SQLite Worker did not confirm persistent storage');
17201
+ }
17202
+ await this.applyPerformanceConfig(promiser, dbId, options);
17203
+ }
17204
+ catch (error) {
17205
+ await promiser({ type: 'close', dbId, args: {} });
17206
+ throw error;
17207
+ }
16992
17208
  const token = this.generateToken();
16993
17209
  const port = this.nextPort++;
16994
17210
  this.databases.set(options.database, { dbId, token, port, persistent });
@@ -16996,6 +17212,7 @@ var capacitorCapgoCapacitorFastSql = (function (exports, core) {
16996
17212
  port,
16997
17213
  token,
16998
17214
  database: options.database,
17215
+ persistent,
16999
17216
  };
17000
17217
  }
17001
17218
  async disconnect(options) {
@@ -17016,6 +17233,7 @@ var capacitorCapgoCapacitorFastSql = (function (exports, core) {
17016
17233
  return {
17017
17234
  port: dbInfo.port,
17018
17235
  token: dbInfo.token,
17236
+ persistent: dbInfo.persistent,
17019
17237
  };
17020
17238
  }
17021
17239
  async execute(options) {