@capgo/capacitor-fast-sql 8.2.18 → 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.
- package/Package.swift +2 -1
- package/README.md +22 -14
- package/android/build.gradle +3 -0
- package/android/src/main/java/app/capgo/capacitor/fastsql/BinaryFrame.java +152 -0
- package/android/src/main/java/app/capgo/capacitor/fastsql/CapgoCapacitorFastSqlPlugin.java +5 -1
- package/android/src/main/java/app/capgo/capacitor/fastsql/DatabaseConnection.java +2 -0
- package/android/src/main/java/app/capgo/capacitor/fastsql/EncryptedSQLDatabase.java +13 -2
- package/android/src/main/java/app/capgo/capacitor/fastsql/SQLDatabase.java +13 -2
- package/android/src/main/java/app/capgo/capacitor/fastsql/SQLHTTPServer.java +114 -57
- package/dist/esm/binary-frame.d.ts +6 -0
- package/dist/esm/binary-frame.d.ts.map +1 -0
- package/dist/esm/binary-frame.js +100 -0
- package/dist/esm/definitions.d.ts +17 -0
- package/dist/esm/definitions.d.ts.map +1 -1
- package/dist/esm/fast-sql.d.ts.map +1 -1
- package/dist/esm/fast-sql.js +13 -3
- package/dist/esm/native-sql-connection.d.ts +18 -7
- package/dist/esm/native-sql-connection.d.ts.map +1 -1
- package/dist/esm/native-sql-connection.js +145 -59
- package/dist/esm/web.d.ts.map +1 -1
- package/dist/esm/web.js +2 -0
- package/dist/plugin.cjs.js +260 -62
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +260 -62
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapgoCapacitorFastSqlPlugin/BinaryFrame.swift +95 -0
- package/ios/Sources/CapgoCapacitorFastSqlPlugin/CapgoCapacitorFastSqlPlugin.swift +7 -3
- package/ios/Sources/CapgoCapacitorFastSqlPlugin/SQLDatabase.swift +20 -3
- package/ios/Sources/CapgoCapacitorFastSqlPlugin/SQLHTTPServer.swift +59 -76
- package/ios/Tests/CapgoCapacitorFastSqlPluginTests/BinaryFrameTests.swift +65 -0
- package/ios/Tests/CapgoCapacitorFastSqlPluginTests/Fixtures/binary-v1.bin +0 -0
- package/package.json +3 -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
|
-
|
|
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
|
-
|
|
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
|
|
63
|
-
|
|
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 (!
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
81
|
-
|
|
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.map
CHANGED
|
@@ -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;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;
|
|
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
|
@@ -44,6 +44,8 @@ export class CapgoCapacitorFastSqlWeb extends WebPlugin {
|
|
|
44
44
|
return `/capgo-fast-sql/${database}.sqlite3`;
|
|
45
45
|
}
|
|
46
46
|
async connect(options) {
|
|
47
|
+
if (options.transport === 'http-binary')
|
|
48
|
+
throw new Error('Binary HTTP transport requires iOS or Android');
|
|
47
49
|
if (options.readOnly) {
|
|
48
50
|
throw new Error('Read-only opens are not supported by the SQLite Wasm Worker transport');
|
|
49
51
|
}
|
package/dist/plugin.cjs.js
CHANGED
|
@@ -20,6 +20,107 @@ const CapgoCapacitorFastSql = core.registerPlugin('CapgoCapacitorFastSql', {
|
|
|
20
20
|
web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.CapgoCapacitorFastSqlWeb()),
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
+
/** Version 1 frame: FSQ1, uint32-LE metadata length, uint32-LE attachment length. */
|
|
24
|
+
const BINARY_CONTENT_TYPE = 'application/vnd.capgo.fast-sql.v1+binary';
|
|
25
|
+
const MAX_FRAME_BYTES = 64 * 1024 * 1024;
|
|
26
|
+
const MAX_METADATA_BYTES = 1024 * 1024;
|
|
27
|
+
const MAX_ATTACHMENTS = 4096;
|
|
28
|
+
const HEADER_BYTES = 12;
|
|
29
|
+
const magic = [70, 83, 81, 49];
|
|
30
|
+
function encodeBinaryFrame(value) {
|
|
31
|
+
const attachments = [];
|
|
32
|
+
let size = 0;
|
|
33
|
+
let nodes = 0;
|
|
34
|
+
const encode = (item, depth) => {
|
|
35
|
+
if (++nodes > 100000 || depth > 32)
|
|
36
|
+
throw new Error('Binary metadata exceeds nesting or node limits');
|
|
37
|
+
if (typeof item === 'number' && !Number.isFinite(item))
|
|
38
|
+
throw new Error('Non-finite binary protocol number');
|
|
39
|
+
if (item instanceof Uint8Array) {
|
|
40
|
+
if (attachments.length >= MAX_ATTACHMENTS || size + item.byteLength > MAX_FRAME_BYTES - HEADER_BYTES)
|
|
41
|
+
throw new Error('Binary attachments exceed protocol limits');
|
|
42
|
+
const reference = { $blob: [size, item.byteLength] };
|
|
43
|
+
attachments.push(item);
|
|
44
|
+
size += item.byteLength;
|
|
45
|
+
return reference;
|
|
46
|
+
}
|
|
47
|
+
if (Array.isArray(item))
|
|
48
|
+
return item.map((value) => encode(value, depth + 1));
|
|
49
|
+
if (item && typeof item === 'object') {
|
|
50
|
+
if (Object.getPrototypeOf(item) !== Object.prototype && Object.getPrototypeOf(item) !== null)
|
|
51
|
+
throw new Error('Unsupported binary metadata object');
|
|
52
|
+
return Object.fromEntries(Object.entries(item).map(([key, value]) => [key, encode(value, depth + 1)]));
|
|
53
|
+
}
|
|
54
|
+
return item;
|
|
55
|
+
};
|
|
56
|
+
const json = JSON.stringify(encode(value, 0));
|
|
57
|
+
if (json === undefined)
|
|
58
|
+
throw new Error('Missing binary metadata');
|
|
59
|
+
const metadata = new TextEncoder().encode(json);
|
|
60
|
+
const length = HEADER_BYTES + metadata.byteLength + size;
|
|
61
|
+
if (metadata.byteLength > MAX_METADATA_BYTES || length > MAX_FRAME_BYTES)
|
|
62
|
+
throw new Error('Binary frame exceeds protocol limits');
|
|
63
|
+
const frame = new Uint8Array(length);
|
|
64
|
+
frame.set(magic);
|
|
65
|
+
const view = new DataView(frame.buffer);
|
|
66
|
+
view.setUint32(4, metadata.byteLength, true);
|
|
67
|
+
view.setUint32(8, size, true);
|
|
68
|
+
frame.set(metadata, HEADER_BYTES);
|
|
69
|
+
let offset = HEADER_BYTES + metadata.byteLength;
|
|
70
|
+
for (const attachment of attachments) {
|
|
71
|
+
frame.set(attachment, offset);
|
|
72
|
+
offset += attachment.byteLength;
|
|
73
|
+
}
|
|
74
|
+
return frame;
|
|
75
|
+
}
|
|
76
|
+
function decodeBinaryFrame(frame) {
|
|
77
|
+
if (frame.length < HEADER_BYTES ||
|
|
78
|
+
frame.length > MAX_FRAME_BYTES ||
|
|
79
|
+
magic.some((byte, index) => frame[index] !== byte)) {
|
|
80
|
+
throw new Error('Invalid binary frame version or length');
|
|
81
|
+
}
|
|
82
|
+
const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
|
|
83
|
+
const metadataLength = view.getUint32(4, true);
|
|
84
|
+
const payloadLength = view.getUint32(8, true);
|
|
85
|
+
const payloadOffset = HEADER_BYTES + metadataLength;
|
|
86
|
+
if (metadataLength === 0 || metadataLength > MAX_METADATA_BYTES || payloadOffset + payloadLength !== frame.length) {
|
|
87
|
+
throw new Error('Invalid binary frame lengths');
|
|
88
|
+
}
|
|
89
|
+
const metadata = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(frame.subarray(HEADER_BYTES, payloadOffset)));
|
|
90
|
+
let nodes = 0;
|
|
91
|
+
let blobs = 0;
|
|
92
|
+
let referencedBytes = 0;
|
|
93
|
+
const decode = (value, depth) => {
|
|
94
|
+
if (++nodes > 100000 || depth > 32)
|
|
95
|
+
throw new Error('Binary metadata exceeds nesting or node limits');
|
|
96
|
+
if (Array.isArray(value))
|
|
97
|
+
return value.map((item) => decode(item, depth + 1));
|
|
98
|
+
if (value === null || typeof value !== 'object')
|
|
99
|
+
return value;
|
|
100
|
+
if ('$blob' in value && Object.keys(value).length === 1) {
|
|
101
|
+
const reference = value.$blob;
|
|
102
|
+
if (!Array.isArray(reference) || reference.length !== 2)
|
|
103
|
+
throw new Error('Invalid binary attachment reference');
|
|
104
|
+
const [offset, length] = reference;
|
|
105
|
+
if (typeof offset !== 'number' ||
|
|
106
|
+
typeof length !== 'number' ||
|
|
107
|
+
!Number.isSafeInteger(offset) ||
|
|
108
|
+
!Number.isSafeInteger(length) ||
|
|
109
|
+
offset < 0 ||
|
|
110
|
+
length < 0 ||
|
|
111
|
+
offset + length > payloadLength) {
|
|
112
|
+
throw new Error('Binary attachment is outside its frame');
|
|
113
|
+
}
|
|
114
|
+
referencedBytes += length;
|
|
115
|
+
if (++blobs > MAX_ATTACHMENTS || referencedBytes > MAX_FRAME_BYTES)
|
|
116
|
+
throw new Error('Decoded attachments exceed protocol limits');
|
|
117
|
+
return frame.slice(payloadOffset + offset, payloadOffset + offset + length);
|
|
118
|
+
}
|
|
119
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, decode(item, depth + 1)]));
|
|
120
|
+
};
|
|
121
|
+
return decode(metadata, 0);
|
|
122
|
+
}
|
|
123
|
+
|
|
23
124
|
function assertSupportedIsolation(level) {
|
|
24
125
|
if (level !== undefined && level !== exports.IsolationLevel.Serializable) {
|
|
25
126
|
throw new Error('Only SERIALIZABLE transaction isolation is supported');
|
|
@@ -33,8 +134,20 @@ function assertSupportedIsolation(level) {
|
|
|
33
134
|
* Inspired by capacitor-blob-writer's approach to avoid serialization overhead.
|
|
34
135
|
*/
|
|
35
136
|
class NativeSQLConnection {
|
|
36
|
-
|
|
137
|
+
/** Cumulative data-request payload bytes and JS codec time; excludes HTTP headers/compression. */
|
|
138
|
+
getTransportMetrics() {
|
|
139
|
+
return Object.assign({}, this.metrics);
|
|
140
|
+
}
|
|
141
|
+
constructor(database, port, token, transport = 'http-json') {
|
|
142
|
+
this.transport = transport;
|
|
37
143
|
this.inTransaction = false;
|
|
144
|
+
this.metrics = {
|
|
145
|
+
dataRequests: 0,
|
|
146
|
+
requestBytes: 0,
|
|
147
|
+
responseBytes: 0,
|
|
148
|
+
encodeMs: 0,
|
|
149
|
+
decodeMs: 0,
|
|
150
|
+
};
|
|
38
151
|
this.database = database;
|
|
39
152
|
this.port = port;
|
|
40
153
|
this.token = token;
|
|
@@ -59,24 +172,7 @@ class NativeSQLConnection {
|
|
|
59
172
|
* @returns Query results
|
|
60
173
|
*/
|
|
61
174
|
async execute(statement, params) {
|
|
62
|
-
|
|
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
|
-
statement,
|
|
71
|
-
params: params ? this.serializeParams(params) : [],
|
|
72
|
-
}),
|
|
73
|
-
});
|
|
74
|
-
if (!response.ok) {
|
|
75
|
-
const error = await response.text();
|
|
76
|
-
throw new Error(`SQL execution failed: ${error}`);
|
|
77
|
-
}
|
|
78
|
-
const result = await response.json();
|
|
79
|
-
return this.deserializeResult(result);
|
|
175
|
+
return this.parseResult(await this.request('/execute', { statement, params: params !== null && params !== void 0 ? params : [] }));
|
|
80
176
|
}
|
|
81
177
|
/**
|
|
82
178
|
* Execute multiple SQL statements in a batch for better performance
|
|
@@ -85,26 +181,138 @@ class NativeSQLConnection {
|
|
|
85
181
|
* @returns Array of results for each operation
|
|
86
182
|
*/
|
|
87
183
|
async executeBatch(operations) {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
headers: {
|
|
91
|
-
'Content-Type': 'application/json',
|
|
92
|
-
Authorization: `Bearer ${this.token}`,
|
|
93
|
-
'X-Database': this.database,
|
|
94
|
-
},
|
|
95
|
-
body: JSON.stringify({
|
|
96
|
-
operations: operations.map((op) => ({
|
|
97
|
-
statement: op.statement,
|
|
98
|
-
params: op.params ? this.serializeParams(op.params) : [],
|
|
99
|
-
})),
|
|
100
|
-
}),
|
|
184
|
+
const result = await this.request('/batch', {
|
|
185
|
+
operations: operations.map((operation) => { var _a; return ({ statement: operation.statement, params: (_a = operation.params) !== null && _a !== void 0 ? _a : [] }); }),
|
|
101
186
|
});
|
|
102
|
-
if (!
|
|
103
|
-
|
|
104
|
-
|
|
187
|
+
if (!Array.isArray(result))
|
|
188
|
+
throw new Error('Invalid batch response');
|
|
189
|
+
return result.map((item) => this.parseResult(item));
|
|
190
|
+
}
|
|
191
|
+
/** Authenticated transport round trip without SQLite, for diagnostic benchmarks. */
|
|
192
|
+
async echo(value) {
|
|
193
|
+
const result = this.parseResult(await this.request('/echo', { statement: '', params: [value] }));
|
|
194
|
+
if (result.rows.length !== 1 || !('value' in result.rows[0]))
|
|
195
|
+
throw new Error('Invalid echo response');
|
|
196
|
+
return result.rows[0].value;
|
|
197
|
+
}
|
|
198
|
+
async request(path, payload) {
|
|
199
|
+
var _a;
|
|
200
|
+
const binary = this.transport === 'http-binary';
|
|
201
|
+
const start = performance.now();
|
|
202
|
+
const body = binary
|
|
203
|
+
? encodeBinaryFrame(payload)
|
|
204
|
+
: new TextEncoder().encode(JSON.stringify('params' in payload
|
|
205
|
+
? Object.assign(Object.assign({}, payload), { params: this.serializeParams(payload.params) }) : {
|
|
206
|
+
operations: payload.operations.map((operation) => (Object.assign(Object.assign({}, operation), { params: this.serializeParams(operation.params) }))),
|
|
207
|
+
}));
|
|
208
|
+
if (body.byteLength > MAX_FRAME_BYTES)
|
|
209
|
+
throw new Error('Request exceeds 64 MiB');
|
|
210
|
+
this.metrics.encodeMs += performance.now() - start;
|
|
211
|
+
this.metrics.dataRequests++;
|
|
212
|
+
this.metrics.requestBytes += body.byteLength;
|
|
213
|
+
const controller = new AbortController();
|
|
214
|
+
const timeout = setTimeout(() => controller.abort(), 600000);
|
|
215
|
+
try {
|
|
216
|
+
const response = await this.browserFetch(`${this.baseUrl}${path}`, {
|
|
217
|
+
method: 'POST',
|
|
218
|
+
headers: {
|
|
219
|
+
'Content-Type': binary ? BINARY_CONTENT_TYPE : 'application/json',
|
|
220
|
+
Authorization: `Bearer ${this.token}`,
|
|
221
|
+
'X-Database': this.database,
|
|
222
|
+
},
|
|
223
|
+
body: body.buffer,
|
|
224
|
+
signal: controller.signal,
|
|
225
|
+
});
|
|
226
|
+
if (!response.ok) {
|
|
227
|
+
const errorBody = await this.readResponse(response);
|
|
228
|
+
throw new Error(`SQL request failed (${response.status}): ${new TextDecoder().decode(errorBody).slice(0, 4096)}`);
|
|
229
|
+
}
|
|
230
|
+
if (binary && ((_a = response.headers.get('content-type')) === null || _a === void 0 ? void 0 : _a.split(';')[0].trim()) !== BINARY_CONTENT_TYPE)
|
|
231
|
+
throw new Error('Server did not return binary HTTP v1');
|
|
232
|
+
const bytes = await this.readResponse(response);
|
|
233
|
+
this.metrics.responseBytes += bytes.byteLength;
|
|
234
|
+
const decodeStart = performance.now();
|
|
235
|
+
const result = binary ? decodeBinaryFrame(bytes) : JSON.parse(new TextDecoder().decode(bytes));
|
|
236
|
+
this.metrics.decodeMs += performance.now() - decodeStart;
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
clearTimeout(timeout);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async readResponse(response) {
|
|
244
|
+
var _a;
|
|
245
|
+
const declared = response.headers.get('content-length');
|
|
246
|
+
if (declared !== null && Number(declared) > MAX_FRAME_BYTES) {
|
|
247
|
+
await ((_a = response.body) === null || _a === void 0 ? void 0 : _a.cancel());
|
|
248
|
+
throw new Error('Response exceeds 64 MiB');
|
|
249
|
+
}
|
|
250
|
+
if (!response.body)
|
|
251
|
+
return new Uint8Array();
|
|
252
|
+
const reader = response.body.getReader();
|
|
253
|
+
const chunks = [];
|
|
254
|
+
let length = 0;
|
|
255
|
+
try {
|
|
256
|
+
while (true) {
|
|
257
|
+
const chunk = await reader.read();
|
|
258
|
+
if (chunk.done)
|
|
259
|
+
break;
|
|
260
|
+
length += chunk.value.byteLength;
|
|
261
|
+
if (length > MAX_FRAME_BYTES) {
|
|
262
|
+
await reader.cancel();
|
|
263
|
+
throw new Error('Response exceeds 64 MiB');
|
|
264
|
+
}
|
|
265
|
+
chunks.push(chunk.value);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
reader.releaseLock();
|
|
105
270
|
}
|
|
106
|
-
const
|
|
107
|
-
|
|
271
|
+
const result = new Uint8Array(length);
|
|
272
|
+
let offset = 0;
|
|
273
|
+
for (const chunk of chunks) {
|
|
274
|
+
result.set(chunk, offset);
|
|
275
|
+
offset += chunk.byteLength;
|
|
276
|
+
}
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
parseResult(result) {
|
|
280
|
+
const start = performance.now();
|
|
281
|
+
if (!result ||
|
|
282
|
+
typeof result !== 'object' ||
|
|
283
|
+
!('rows' in result) ||
|
|
284
|
+
!Array.isArray(result.rows) ||
|
|
285
|
+
!('rowsAffected' in result) ||
|
|
286
|
+
typeof result.rowsAffected !== 'number')
|
|
287
|
+
throw new Error('Invalid SQL response');
|
|
288
|
+
const rows = result.rows.map((row) => {
|
|
289
|
+
if (!row || typeof row !== 'object' || Array.isArray(row))
|
|
290
|
+
throw new Error('Invalid SQL row');
|
|
291
|
+
const parsed = {};
|
|
292
|
+
for (const [key, value] of Object.entries(row)) {
|
|
293
|
+
let scalar;
|
|
294
|
+
if (value === null ||
|
|
295
|
+
typeof value === 'string' ||
|
|
296
|
+
typeof value === 'boolean' ||
|
|
297
|
+
value instanceof Uint8Array ||
|
|
298
|
+
(typeof value === 'number' && Number.isFinite(value)))
|
|
299
|
+
scalar = value;
|
|
300
|
+
else if (value &&
|
|
301
|
+
typeof value === 'object' &&
|
|
302
|
+
'_type' in value &&
|
|
303
|
+
value._type === 'binary' &&
|
|
304
|
+
'_data' in value &&
|
|
305
|
+
typeof value._data === 'string')
|
|
306
|
+
scalar = this.base64ToUint8Array(value._data);
|
|
307
|
+
else
|
|
308
|
+
throw new Error('Invalid SQL value');
|
|
309
|
+
Object.defineProperty(parsed, key, { value: scalar, enumerable: true, writable: true, configurable: true });
|
|
310
|
+
}
|
|
311
|
+
return parsed;
|
|
312
|
+
});
|
|
313
|
+
const insertId = 'insertId' in result && typeof result.insertId === 'number' ? result.insertId : undefined;
|
|
314
|
+
this.metrics.decodeMs += performance.now() - start;
|
|
315
|
+
return { rows, rowsAffected: result.rowsAffected, insertId };
|
|
108
316
|
}
|
|
109
317
|
/**
|
|
110
318
|
* Begin a transaction
|
|
@@ -231,28 +439,6 @@ class NativeSQLConnection {
|
|
|
231
439
|
return param;
|
|
232
440
|
});
|
|
233
441
|
}
|
|
234
|
-
/**
|
|
235
|
-
* Deserialize result from server
|
|
236
|
-
* Base64-encoded binary data is converted back to Uint8Array
|
|
237
|
-
*/
|
|
238
|
-
deserializeResult(result) {
|
|
239
|
-
return {
|
|
240
|
-
rows: result.rows.map((row) => {
|
|
241
|
-
const deserializedRow = {};
|
|
242
|
-
for (const [key, value] of Object.entries(row)) {
|
|
243
|
-
if (value && typeof value === 'object' && value._type === 'binary') {
|
|
244
|
-
deserializedRow[key] = this.base64ToUint8Array(value._data);
|
|
245
|
-
}
|
|
246
|
-
else {
|
|
247
|
-
deserializedRow[key] = value;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return deserializedRow;
|
|
251
|
-
}),
|
|
252
|
-
rowsAffected: result.rowsAffected || 0,
|
|
253
|
-
insertId: result.insertId,
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
442
|
/**
|
|
257
443
|
* Convert Uint8Array to base64 string
|
|
258
444
|
*/
|
|
@@ -443,20 +629,30 @@ class FastSQL {
|
|
|
443
629
|
}
|
|
444
630
|
}
|
|
445
631
|
static optionsMatch(left, right) {
|
|
446
|
-
|
|
632
|
+
var _a, _b;
|
|
633
|
+
return (((_a = left.transport) !== null && _a !== void 0 ? _a : 'http-json') === ((_b = right.transport) !== null && _b !== void 0 ? _b : 'http-json') &&
|
|
634
|
+
Boolean(left.encrypted) === Boolean(right.encrypted) &&
|
|
447
635
|
left.encryptionKey === right.encryptionKey &&
|
|
448
636
|
Boolean(left.readOnly) === Boolean(right.readOnly) &&
|
|
449
637
|
Boolean(left.walMode) === Boolean(right.walMode) &&
|
|
450
638
|
Boolean(left.performancePresets) === Boolean(right.performancePresets));
|
|
451
639
|
}
|
|
452
640
|
static async open(options) {
|
|
641
|
+
const platform = core.Capacitor.getPlatform();
|
|
642
|
+
if (options.transport === 'http-binary' && platform !== 'ios' && platform !== 'android') {
|
|
643
|
+
throw new Error('Binary HTTP transport requires iOS or Android');
|
|
644
|
+
}
|
|
645
|
+
if (options.transport === 'http-binary') {
|
|
646
|
+
const capabilities = await CapgoCapacitorFastSql.getPluginVersion();
|
|
647
|
+
if (capabilities.binaryProtocolVersion !== 1)
|
|
648
|
+
throw new Error('Native plugin does not support binary HTTP v1');
|
|
649
|
+
}
|
|
453
650
|
// Connect via native plugin
|
|
454
651
|
const info = await CapgoCapacitorFastSql.connect(options);
|
|
455
652
|
// Create connection instance appropriate for the current platform
|
|
456
653
|
let connection;
|
|
457
|
-
const platform = core.Capacitor.getPlatform();
|
|
458
654
|
if (platform === 'android' || platform === 'ios') {
|
|
459
|
-
connection = new NativeSQLConnection(info.database, info.port, info.token);
|
|
655
|
+
connection = new NativeSQLConnection(info.database, info.port, info.token, options.transport);
|
|
460
656
|
}
|
|
461
657
|
else {
|
|
462
658
|
connection = new WebSQLConnection(info.database, CapgoCapacitorFastSql);
|
|
@@ -1166,6 +1362,8 @@ class CapgoCapacitorFastSqlWeb extends core.WebPlugin {
|
|
|
1166
1362
|
return `/capgo-fast-sql/${database}.sqlite3`;
|
|
1167
1363
|
}
|
|
1168
1364
|
async connect(options) {
|
|
1365
|
+
if (options.transport === 'http-binary')
|
|
1366
|
+
throw new Error('Binary HTTP transport requires iOS or Android');
|
|
1169
1367
|
if (options.readOnly) {
|
|
1170
1368
|
throw new Error('Read-only opens are not supported by the SQLite Wasm Worker transport');
|
|
1171
1369
|
}
|