Skip to content

Commit c376ae7

Browse files
committed
Serialize sqlite exceptions
1 parent 5a638a0 commit c376ae7

File tree

4 files changed

+139
-13
lines changed

4 files changed

+139
-13
lines changed

sqlite3_web/lib/src/channel.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,10 @@ abstract class ProtocolChannel {
126126
console.error('Original trace: $s'.toJS);
127127

128128
response = ErrorResponse(
129-
message: e.toString(), requestId: message.requestId);
129+
message: e.toString(),
130+
requestId: message.requestId,
131+
serializedException: e,
132+
);
130133
}
131134

132135
_channel.sink.add(response);

sqlite3_web/lib/src/protocol.dart

Lines changed: 83 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import 'dart:async';
21
import 'dart:js_interop';
32
import 'dart:js_interop_unsafe';
43
import 'dart:typed_data';
@@ -64,13 +63,15 @@ class _UniqueFieldNames {
6463
static const onlyOpenVfs = 'o';
6564
static const parameters = 'p';
6665
static const storageMode = 's';
66+
static const serializedExceptionType = 's';
6767
static const sql = 's'; // not used in same message
6868
static const type = 't';
6969
static const wasmUri = 'u';
7070
static const updateTableName = 'u';
7171
static const responseData = 'r';
7272
static const returnRows = 'r';
7373
static const updateRowId = 'r';
74+
static const serializedException = 'r';
7475
static const rows = 'r'; // no clash, used on different message types
7576
static const typeVector = 'v';
7677
}
@@ -162,14 +163,6 @@ sealed class Request extends Message {
162163
object[_UniqueFieldNames.databaseId] = id.toJS;
163164
}
164165
}
165-
166-
Future<Response> tryRespond(FutureOr<Response> Function() function) async {
167-
try {
168-
return await function();
169-
} catch (e) {
170-
return ErrorResponse(message: e.toString(), requestId: requestId);
171-
}
172-
}
173166
}
174167

175168
sealed class Response extends Message {
@@ -767,12 +760,33 @@ final class RowsResponse extends Response {
767760
final class ErrorResponse extends Response {
768761
final String message;
769762

770-
ErrorResponse({required this.message, required super.requestId});
763+
/// We can't send Dart objects over web channels, but we're serializing the
764+
/// most common exception types so that we can reconstruct them on the other
765+
/// end.
766+
final Object? serializedException;
767+
768+
ErrorResponse({
769+
required this.message,
770+
required super.requestId,
771+
this.serializedException,
772+
});
771773

772774
factory ErrorResponse.deserialize(JSObject object) {
775+
Object? serializedException;
776+
if (object.has(_UniqueFieldNames.serializedExceptionType)) {
777+
serializedException = switch (
778+
(object[_UniqueFieldNames.serializedExceptionType] as JSNumber)
779+
.toDartInt) {
780+
_typeSqliteException => deserializeSqliteException(
781+
object[_UniqueFieldNames.serializedException] as JSArray),
782+
_ => null,
783+
};
784+
}
785+
773786
return ErrorResponse(
774787
message: (object[_UniqueFieldNames.errorMessage] as JSString).toDart,
775788
requestId: object.requestId,
789+
serializedException: serializedException,
776790
);
777791
}
778792

@@ -783,12 +797,70 @@ final class ErrorResponse extends Response {
783797
void serialize(JSObject object, List<JSObject> transferred) {
784798
super.serialize(object, transferred);
785799
object[_UniqueFieldNames.errorMessage] = message.toJS;
800+
801+
if (serializedException case final SqliteException e?) {
802+
object[_UniqueFieldNames.serializedExceptionType] =
803+
_typeSqliteException.toJS;
804+
object[_UniqueFieldNames.serializedException] =
805+
serializeSqliteException(e);
806+
}
786807
}
787808

788809
@override
789810
RemoteException interpretAsError() {
790-
return RemoteException(message: message);
811+
return RemoteException(message: message, exception: serializedException);
812+
}
813+
814+
static SqliteException deserializeSqliteException(JSArray data) {
815+
final [
816+
message,
817+
explanation,
818+
extendedResultCode,
819+
operation,
820+
causingStatement,
821+
paramData,
822+
paramTypes,
823+
..._,
824+
] = data.toDart;
825+
826+
String? decodeNullableString(JSAny? jsValue) {
827+
if (jsValue.isDefinedAndNotNull) {
828+
return (jsValue as JSString).toDart;
829+
}
830+
return null;
831+
}
832+
833+
return SqliteException(
834+
(extendedResultCode as JSNumber).toDartInt,
835+
(message as JSString).toDart,
836+
decodeNullableString(explanation),
837+
decodeNullableString(causingStatement),
838+
paramData.isDefinedAndNotNull && paramTypes.isDefinedAndNotNull
839+
? TypeCode.decodeValues(
840+
paramData as JSArray, paramTypes as JSArrayBuffer)
841+
: null,
842+
decodeNullableString(operation),
843+
);
844+
}
845+
846+
static JSArray serializeSqliteException(SqliteException e) {
847+
final params = switch (e.parametersToStatement) {
848+
null => null,
849+
final parameters => TypeCode.encodeValues(parameters),
850+
};
851+
852+
return [
853+
e.message.toJS,
854+
e.explanation?.toJS,
855+
e.extendedResultCode.toJS,
856+
e.operation?.toJS,
857+
e.causingStatement?.toJS,
858+
params?.$1,
859+
params?.$2,
860+
].toJS;
791861
}
862+
863+
static const _typeSqliteException = 0;
792864
}
793865

794866
final class StreamRequest extends Request {

sqlite3_web/lib/src/types.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:typed_data';
2+
import 'package:sqlite3/common.dart';
23

34
/// A [StorageMode], name pair representing an existing database already stored
45
/// by the current browsing context.
@@ -66,8 +67,16 @@ final class RemoteException implements Exception {
6667
/// The [Object.toString] representation of the original exception.
6768
final String message;
6869

70+
/// The exception that happened in the context running the operation.
71+
///
72+
/// Since that context may be a web worker which can't send arbitrary Dart
73+
/// objects to us, only a few common exception types are recognized and
74+
/// serialized.
75+
/// At the moment, this only includes [SqliteException].
76+
final Object? exception;
77+
6978
/// Creates a remote exception from the [message] thrown.
70-
RemoteException({required this.message});
79+
RemoteException({required this.message, this.exception});
7180

7281
@override
7382
String toString() {

sqlite3_web/test/protocol_test.dart

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:js_interop';
66
import 'dart:typed_data';
77

88
import 'package:sqlite3/common.dart';
9+
import 'package:sqlite3_web/sqlite3_web.dart';
910
import 'package:sqlite3_web/src/channel.dart';
1011
import 'package:sqlite3_web/src/protocol.dart';
1112
import 'package:test/test.dart';
@@ -128,6 +129,47 @@ void main() {
128129
[1, Uint8List(10), null, 'string value'],
129130
);
130131
});
132+
133+
test('can serialize SqliteExceptions', () async {
134+
server.handleRequestFunction = expectAsync1((req) {
135+
throw SqliteException(
136+
42,
137+
'test exception',
138+
'explanation',
139+
'causingStatement',
140+
[1, null, 'a'],
141+
'operation',
142+
);
143+
});
144+
145+
await expectLater(
146+
() => client.sendRequest(
147+
RunQuery(
148+
requestId: 0,
149+
databaseId: 0,
150+
sql: 'sql',
151+
parameters: [],
152+
returnRows: true,
153+
),
154+
MessageType.rowsResponse,
155+
),
156+
throwsA(
157+
isA<RemoteException>().having(
158+
(e) => e.exception,
159+
'exception',
160+
isA<SqliteException>()
161+
.having((e) => e.extendedResultCode, 'extendedResultCode', 42)
162+
.having((e) => e.message, 'message', 'test exception')
163+
.having((e) => e.explanation, 'explanation', 'explanation')
164+
.having((e) => e.operation, 'operation', 'operation')
165+
.having((e) => e.causingStatement, 'causingStatement',
166+
'causingStatement')
167+
.having((e) => e.parametersToStatement, 'parametersToStatement',
168+
[1, null, 'a']),
169+
),
170+
),
171+
);
172+
});
131173
}
132174

133175
const isDart2Wasm = bool.fromEnvironment('dart.tool.dart2wasm');

0 commit comments

Comments
 (0)