@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
@@ -1,3 +1,4 @@
1
+ import { BINARY_CONTENT_TYPE, decodeBinaryFrame, encodeBinaryFrame, MAX_FRAME_BYTES } from './binary-frame';
1
2
  import { IsolationLevel } from './definitions';
2
3
  import { assertSupportedIsolation } from './transaction-isolation';
3
4
  /**
@@ -7,8 +8,20 @@ import { assertSupportedIsolation } from './transaction-isolation';
7
8
  * Inspired by capacitor-blob-writer's approach to avoid serialization overhead.
8
9
  */
9
10
  export class NativeSQLConnection {
10
- constructor(database, port, token) {
11
+ /** Cumulative data-request payload bytes and JS codec time; excludes HTTP headers/compression. */
12
+ getTransportMetrics() {
13
+ return Object.assign({}, this.metrics);
14
+ }
15
+ constructor(database, port, token, transport = 'http-json') {
16
+ this.transport = transport;
11
17
  this.inTransaction = false;
18
+ this.metrics = {
19
+ dataRequests: 0,
20
+ requestBytes: 0,
21
+ responseBytes: 0,
22
+ encodeMs: 0,
23
+ decodeMs: 0,
24
+ };
12
25
  this.database = database;
13
26
  this.port = port;
14
27
  this.token = token;
@@ -33,24 +46,7 @@ export class NativeSQLConnection {
33
46
  * @returns Query results
34
47
  */
35
48
  async execute(statement, params) {
36
- const response = await this.browserFetch(`${this.baseUrl}/execute`, {
37
- method: 'POST',
38
- headers: {
39
- 'Content-Type': 'application/json',
40
- Authorization: `Bearer ${this.token}`,
41
- 'X-Database': this.database,
42
- },
43
- body: JSON.stringify({
44
- statement,
45
- params: params ? this.serializeParams(params) : [],
46
- }),
47
- });
48
- if (!response.ok) {
49
- const error = await response.text();
50
- throw new Error(`SQL execution failed: ${error}`);
51
- }
52
- const result = await response.json();
53
- return this.deserializeResult(result);
49
+ return this.parseResult(await this.request('/execute', { statement, params: params !== null && params !== void 0 ? params : [] }));
54
50
  }
55
51
  /**
56
52
  * Execute multiple SQL statements in a batch for better performance
@@ -59,26 +55,138 @@ export class NativeSQLConnection {
59
55
  * @returns Array of results for each operation
60
56
  */
61
57
  async executeBatch(operations) {
62
- const response = await this.browserFetch(`${this.baseUrl}/batch`, {
63
- method: 'POST',
64
- headers: {
65
- 'Content-Type': 'application/json',
66
- Authorization: `Bearer ${this.token}`,
67
- 'X-Database': this.database,
68
- },
69
- body: JSON.stringify({
70
- operations: operations.map((op) => ({
71
- statement: op.statement,
72
- params: op.params ? this.serializeParams(op.params) : [],
73
- })),
74
- }),
58
+ const result = await this.request('/batch', {
59
+ operations: operations.map((operation) => { var _a; return ({ statement: operation.statement, params: (_a = operation.params) !== null && _a !== void 0 ? _a : [] }); }),
75
60
  });
76
- if (!response.ok) {
77
- const error = await response.text();
78
- throw new Error(`SQL batch execution failed: ${error}`);
61
+ if (!Array.isArray(result))
62
+ throw new Error('Invalid batch response');
63
+ return result.map((item) => this.parseResult(item));
64
+ }
65
+ /** Authenticated transport round trip without SQLite, for diagnostic benchmarks. */
66
+ async echo(value) {
67
+ const result = this.parseResult(await this.request('/echo', { statement: '', params: [value] }));
68
+ if (result.rows.length !== 1 || !('value' in result.rows[0]))
69
+ throw new Error('Invalid echo response');
70
+ return result.rows[0].value;
71
+ }
72
+ async request(path, payload) {
73
+ var _a;
74
+ const binary = this.transport === 'http-binary';
75
+ const start = performance.now();
76
+ const body = binary
77
+ ? encodeBinaryFrame(payload)
78
+ : new TextEncoder().encode(JSON.stringify('params' in payload
79
+ ? Object.assign(Object.assign({}, payload), { params: this.serializeParams(payload.params) }) : {
80
+ operations: payload.operations.map((operation) => (Object.assign(Object.assign({}, operation), { params: this.serializeParams(operation.params) }))),
81
+ }));
82
+ if (body.byteLength > MAX_FRAME_BYTES)
83
+ throw new Error('Request exceeds 64 MiB');
84
+ this.metrics.encodeMs += performance.now() - start;
85
+ this.metrics.dataRequests++;
86
+ this.metrics.requestBytes += body.byteLength;
87
+ const controller = new AbortController();
88
+ const timeout = setTimeout(() => controller.abort(), 600000);
89
+ try {
90
+ const response = await this.browserFetch(`${this.baseUrl}${path}`, {
91
+ method: 'POST',
92
+ headers: {
93
+ 'Content-Type': binary ? BINARY_CONTENT_TYPE : 'application/json',
94
+ Authorization: `Bearer ${this.token}`,
95
+ 'X-Database': this.database,
96
+ },
97
+ body: body.buffer,
98
+ signal: controller.signal,
99
+ });
100
+ if (!response.ok) {
101
+ const errorBody = await this.readResponse(response);
102
+ throw new Error(`SQL request failed (${response.status}): ${new TextDecoder().decode(errorBody).slice(0, 4096)}`);
103
+ }
104
+ if (binary && ((_a = response.headers.get('content-type')) === null || _a === void 0 ? void 0 : _a.split(';')[0].trim()) !== BINARY_CONTENT_TYPE)
105
+ throw new Error('Server did not return binary HTTP v1');
106
+ const bytes = await this.readResponse(response);
107
+ this.metrics.responseBytes += bytes.byteLength;
108
+ const decodeStart = performance.now();
109
+ const result = binary ? decodeBinaryFrame(bytes) : JSON.parse(new TextDecoder().decode(bytes));
110
+ this.metrics.decodeMs += performance.now() - decodeStart;
111
+ return result;
112
+ }
113
+ finally {
114
+ clearTimeout(timeout);
115
+ }
116
+ }
117
+ async readResponse(response) {
118
+ var _a;
119
+ const declared = response.headers.get('content-length');
120
+ if (declared !== null && Number(declared) > MAX_FRAME_BYTES) {
121
+ await ((_a = response.body) === null || _a === void 0 ? void 0 : _a.cancel());
122
+ throw new Error('Response exceeds 64 MiB');
123
+ }
124
+ if (!response.body)
125
+ return new Uint8Array();
126
+ const reader = response.body.getReader();
127
+ const chunks = [];
128
+ let length = 0;
129
+ try {
130
+ while (true) {
131
+ const chunk = await reader.read();
132
+ if (chunk.done)
133
+ break;
134
+ length += chunk.value.byteLength;
135
+ if (length > MAX_FRAME_BYTES) {
136
+ await reader.cancel();
137
+ throw new Error('Response exceeds 64 MiB');
138
+ }
139
+ chunks.push(chunk.value);
140
+ }
141
+ }
142
+ finally {
143
+ reader.releaseLock();
79
144
  }
80
- const results = await response.json();
81
- return results.map((r) => this.deserializeResult(r));
145
+ const result = new Uint8Array(length);
146
+ let offset = 0;
147
+ for (const chunk of chunks) {
148
+ result.set(chunk, offset);
149
+ offset += chunk.byteLength;
150
+ }
151
+ return result;
152
+ }
153
+ parseResult(result) {
154
+ const start = performance.now();
155
+ if (!result ||
156
+ typeof result !== 'object' ||
157
+ !('rows' in result) ||
158
+ !Array.isArray(result.rows) ||
159
+ !('rowsAffected' in result) ||
160
+ typeof result.rowsAffected !== 'number')
161
+ throw new Error('Invalid SQL response');
162
+ const rows = result.rows.map((row) => {
163
+ if (!row || typeof row !== 'object' || Array.isArray(row))
164
+ throw new Error('Invalid SQL row');
165
+ const parsed = {};
166
+ for (const [key, value] of Object.entries(row)) {
167
+ let scalar;
168
+ if (value === null ||
169
+ typeof value === 'string' ||
170
+ typeof value === 'boolean' ||
171
+ value instanceof Uint8Array ||
172
+ (typeof value === 'number' && Number.isFinite(value)))
173
+ scalar = value;
174
+ else if (value &&
175
+ typeof value === 'object' &&
176
+ '_type' in value &&
177
+ value._type === 'binary' &&
178
+ '_data' in value &&
179
+ typeof value._data === 'string')
180
+ scalar = this.base64ToUint8Array(value._data);
181
+ else
182
+ throw new Error('Invalid SQL value');
183
+ Object.defineProperty(parsed, key, { value: scalar, enumerable: true, writable: true, configurable: true });
184
+ }
185
+ return parsed;
186
+ });
187
+ const insertId = 'insertId' in result && typeof result.insertId === 'number' ? result.insertId : undefined;
188
+ this.metrics.decodeMs += performance.now() - start;
189
+ return { rows, rowsAffected: result.rowsAffected, insertId };
82
190
  }
83
191
  /**
84
192
  * Begin a transaction
@@ -205,28 +313,6 @@ export class NativeSQLConnection {
205
313
  return param;
206
314
  });
207
315
  }
208
- /**
209
- * Deserialize result from server
210
- * Base64-encoded binary data is converted back to Uint8Array
211
- */
212
- deserializeResult(result) {
213
- return {
214
- rows: result.rows.map((row) => {
215
- const deserializedRow = {};
216
- for (const [key, value] of Object.entries(row)) {
217
- if (value && typeof value === 'object' && value._type === 'binary') {
218
- deserializedRow[key] = this.base64ToUint8Array(value._data);
219
- }
220
- else {
221
- deserializedRow[key] = value;
222
- }
223
- }
224
- return deserializedRow;
225
- }),
226
- rowsAffected: result.rowsAffected || 0,
227
- insertId: result.insertId,
228
- };
229
- }
230
316
  /**
231
317
  * Convert Uint8Array to base64 string
232
318
  */
package/dist/esm/web.d.ts CHANGED
@@ -23,6 +23,7 @@ export declare class CapgoCapacitorFastSqlWeb extends WebPlugin implements Capgo
23
23
  port: number;
24
24
  token: string;
25
25
  database: string;
26
+ persistent: boolean;
26
27
  }>;
27
28
  disconnect(options: {
28
29
  database: string;
@@ -32,6 +33,7 @@ export declare class CapgoCapacitorFastSqlWeb extends WebPlugin implements Capgo
32
33
  }): Promise<{
33
34
  port: number;
34
35
  token: string;
36
+ persistent: boolean;
35
37
  }>;
36
38
  execute(options: {
37
39
  database: string;
@@ -1 +1 @@
1
- {"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAI5C,OAAO,KAAK,EACV,2BAA2B,EAC3B,oBAAoB,EACpB,SAAS,EACT,QAAQ,EACR,cAAc,EACd,SAAS,EACV,MAAM,eAAe,CAAC;AAevB;;;;;;GAMG;AACH,qBAAa,wBAAyB,SAAQ,SAAU,YAAW,2BAA2B;IAC5F,OAAO,CAAC,SAAS,CAAkC;IACnD,OAAO,CAAC,eAAe,CAAyC;IAChE,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,SAAS,CAAiB;IAElC;;;OAGG;IACG,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAKpD,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,MAAM;IAOR,OAAO,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAsDlG,UAAU,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAYxD,aAAa,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAYtF,OAAO,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,SAAS,CAAC;IA+ClG,gBAAgB,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,cAAc,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ/F,iBAAiB,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAO/D,mBAAmB,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;YAOzD,sBAAsB;IAkBpC,OAAO,CAAC,aAAa;IAMf,gBAAgB,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAGvD"}
1
+ {"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAI5C,OAAO,KAAK,EACV,2BAA2B,EAC3B,oBAAoB,EACpB,SAAS,EACT,QAAQ,EACR,cAAc,EACd,SAAS,EACV,MAAM,eAAe,CAAC;AAevB;;;;;;GAMG;AACH,qBAAa,wBAAyB,SAAQ,SAAU,YAAW,2BAA2B;IAC5F,OAAO,CAAC,SAAS,CAAkC;IACnD,OAAO,CAAC,eAAe,CAAyC;IAChE,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,SAAS,CAAiB;IAElC;;;OAGG;IACG,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAWpD,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,MAAM;IAOR,OAAO,CACX,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,OAAO,CAAA;KAAE,CAAC;IAkE5E,UAAU,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAYxD,aAAa,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,OAAO,CAAA;KAAE,CAAC;IAa3G,OAAO,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,SAAS,CAAC;IA+ClG,gBAAgB,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,cAAc,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ/F,iBAAiB,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAO/D,mBAAmB,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;YAOzD,sBAAsB;IAkBpC,OAAO,CAAC,aAAa;IAMf,gBAAgB,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAGvD"}
package/dist/esm/web.js CHANGED
@@ -21,7 +21,13 @@ export class CapgoCapacitorFastSqlWeb extends WebPlugin {
21
21
  * Must be called before connect() when overriding the default worker.
22
22
  */
23
23
  async configureWeb(config) {
24
- this.webConfig = config;
24
+ if (this.promiserPromise || this.databases.size > 0) {
25
+ throw new Error('Configure web storage before opening the first database');
26
+ }
27
+ if (config.requirePersistence && config.useOpfs === false) {
28
+ throw new Error('requirePersistence requires OPFS');
29
+ }
30
+ this.webConfig = Object.assign({}, config);
25
31
  this.promiserPromise = null;
26
32
  }
27
33
  getPromiser() {
@@ -38,6 +44,8 @@ export class CapgoCapacitorFastSqlWeb extends WebPlugin {
38
44
  return `/capgo-fast-sql/${database}.sqlite3`;
39
45
  }
40
46
  async connect(options) {
47
+ if (options.transport === 'http-binary')
48
+ throw new Error('Binary HTTP transport requires iOS or Android');
41
49
  if (options.readOnly) {
42
50
  throw new Error('Read-only opens are not supported by the SQLite Wasm Worker transport');
43
51
  }
@@ -47,6 +55,7 @@ export class CapgoCapacitorFastSqlWeb extends WebPlugin {
47
55
  port: existing.port,
48
56
  token: existing.token,
49
57
  database: options.database,
58
+ persistent: existing.persistent,
50
59
  };
51
60
  }
52
61
  const path = this.dbPath(options.database);
@@ -60,6 +69,8 @@ export class CapgoCapacitorFastSqlWeb extends WebPlugin {
60
69
  });
61
70
  }
62
71
  catch (opfsError) {
72
+ if (this.webConfig.requirePersistence)
73
+ throw opfsError;
63
74
  console.warn('[CapgoCapacitorFastSql] OPFS unavailable; web database will not persist across reloads. ' +
64
75
  'Serve with Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp.', opfsError);
65
76
  openResult = await promiser('open', { filename: path });
@@ -73,7 +84,16 @@ export class CapgoCapacitorFastSqlWeb extends WebPlugin {
73
84
  if (!dbId) {
74
85
  throw new Error('Failed to open SQLite Wasm database: missing dbId');
75
86
  }
76
- await this.applyPerformanceConfig(promiser, dbId, options);
87
+ try {
88
+ if (this.webConfig.requirePersistence && !persistent) {
89
+ throw new Error('SQLite Worker did not confirm persistent storage');
90
+ }
91
+ await this.applyPerformanceConfig(promiser, dbId, options);
92
+ }
93
+ catch (error) {
94
+ await promiser({ type: 'close', dbId, args: {} });
95
+ throw error;
96
+ }
77
97
  const token = this.generateToken();
78
98
  const port = this.nextPort++;
79
99
  this.databases.set(options.database, { dbId, token, port, persistent });
@@ -81,6 +101,7 @@ export class CapgoCapacitorFastSqlWeb extends WebPlugin {
81
101
  port,
82
102
  token,
83
103
  database: options.database,
104
+ persistent,
84
105
  };
85
106
  }
86
107
  async disconnect(options) {
@@ -101,6 +122,7 @@ export class CapgoCapacitorFastSqlWeb extends WebPlugin {
101
122
  return {
102
123
  port: dbInfo.port,
103
124
  token: dbInfo.token,
125
+ persistent: dbInfo.persistent,
104
126
  };
105
127
  }
106
128
  async execute(options) {