Skip to content

Commit 0b128d2

Browse files
committed
Web: Encode values with type for dart2wasm
1 parent bd03b58 commit 0b128d2

File tree

4 files changed

+321
-8
lines changed

4 files changed

+321
-8
lines changed

sqlite3_web/dart_test.yaml

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
platforms: [vm, chrome, firefox]
2+
compilers: [dart2wasm, dart2js]
3+
4+
override_platforms:
5+
firefox:
6+
settings:
7+
arguments: "-headless"

sqlite3_web/lib/src/protocol.dart

+144-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import 'dart:async';
22
import 'dart:js_interop';
33
import 'dart:js_interop_unsafe';
4+
import 'dart:typed_data';
45

56
import 'package:sqlite3/common.dart';
67
import 'package:sqlite3/wasm.dart' as wasm_vfs;
8+
// ignore: implementation_imports
9+
import 'package:sqlite3/src/wasm/js_interop/core.dart';
710
import 'package:web/web.dart';
811

912
import 'types.dart';
@@ -69,6 +72,7 @@ class _UniqueFieldNames {
6972
static const returnRows = 'r';
7073
static const updateRowId = 'r';
7174
static const rows = 'r'; // no clash, used on different message types
75+
static const typeVector = 'v';
7276
}
7377

7478
sealed class Message {
@@ -450,15 +454,25 @@ final class RunQuery extends Request {
450454
});
451455

452456
factory RunQuery.deserialize(JSObject object) {
457+
final rawParameters =
458+
(object[_UniqueFieldNames.parameters] as JSArray).toDart;
459+
final typeVector = switch (object[_UniqueFieldNames.typeVector]) {
460+
final types? => (types as JSArrayBuffer).toDart.asUint8List(),
461+
null => null,
462+
};
463+
464+
final parameters = List<Object?>.filled(rawParameters.length, null);
465+
for (var i = 0; i < parameters.length; i++) {
466+
final typeCode =
467+
typeVector != null ? TypeCode.of(typeVector[i]) : TypeCode.unknown;
468+
parameters[i] = typeCode.decodeColumn(rawParameters[i]);
469+
}
470+
453471
return RunQuery(
454472
requestId: object.requestId,
455473
databaseId: object.databaseId,
456474
sql: (object[_UniqueFieldNames.sql] as JSString).toDart,
457-
parameters: [
458-
for (final raw
459-
in (object[_UniqueFieldNames.parameters] as JSArray).toDart)
460-
raw.dartify()
461-
],
475+
parameters: parameters,
462476
returnRows: (object[_UniqueFieldNames.returnRows] as JSBoolean).toDart,
463477
);
464478
}
@@ -470,9 +484,25 @@ final class RunQuery extends Request {
470484
void serialize(JSObject object, List<JSObject> transferred) {
471485
super.serialize(object, transferred);
472486
object[_UniqueFieldNames.sql] = sql.toJS;
473-
object[_UniqueFieldNames.parameters] =
474-
<JSAny?>[for (final parameter in parameters) parameter.jsify()].toJS;
475487
object[_UniqueFieldNames.returnRows] = returnRows.toJS;
488+
489+
if (parameters.isNotEmpty) {
490+
final jsParams = <JSAny?>[];
491+
final typeCodes = Uint8List(parameters.length);
492+
for (var i = 0; i < parameters.length; i++) {
493+
final (code, jsParam) = TypeCode.encodeValue(parameters[i]);
494+
typeCodes[i] = code.index;
495+
jsParams.add(jsParam);
496+
}
497+
498+
final jsTypes = typeCodes.buffer.toJS;
499+
transferred.add(jsTypes);
500+
501+
object[_UniqueFieldNames.parameters] = jsParams.toJS;
502+
object[_UniqueFieldNames.typeVector] = jsTypes;
503+
} else {
504+
object[_UniqueFieldNames.parameters] = JSArray();
505+
}
476506
}
477507
}
478508

@@ -557,6 +587,81 @@ final class EndpointResponse extends Response {
557587
}
558588
}
559589

590+
enum TypeCode {
591+
unknown,
592+
integer,
593+
bigInt,
594+
float,
595+
text,
596+
blob,
597+
$null,
598+
boolean;
599+
600+
static TypeCode of(int i) {
601+
return i >= TypeCode.values.length ? TypeCode.unknown : TypeCode.values[i];
602+
}
603+
604+
Object? decodeColumn(JSAny? column) {
605+
const hasNativeInts = !identical(0, 0.0);
606+
607+
return switch (this) {
608+
TypeCode.unknown => column.dartify(),
609+
TypeCode.integer => (column as JSNumber).toDartInt,
610+
TypeCode.bigInt => hasNativeInts
611+
? (column as JsBigInt).asDartInt
612+
: (column as JsBigInt).asDartBigInt,
613+
TypeCode.float => (column as JSNumber).toDartDouble,
614+
TypeCode.text => (column as JSString).toDart,
615+
TypeCode.blob => (column as JSUint8Array).toDart,
616+
TypeCode.boolean => (column as JSBoolean).toDart,
617+
TypeCode.$null => null,
618+
};
619+
}
620+
621+
static (TypeCode, JSAny?) encodeValue(Object? dart) {
622+
// In previous clients/workers, values were encoded with dartify() and
623+
// jsify() only. For backwards-compatibility, this value must be compatible
624+
// with dartify() used on the other end.
625+
// An exception are BigInts, which have not been sent correctly before this
626+
// encoder.
627+
// The reasons for adopting a custom format are: Being able to properly
628+
// serialize BigInts, possible dartify/jsify incompatibilities between
629+
// dart2js and dart2wasm and most importantly, being able to keep 1 and 1.0
630+
// apart in dart2wasm when the worker is compiled with dart2js.
631+
final JSAny? value;
632+
final TypeCode code;
633+
634+
switch (dart) {
635+
case null:
636+
value = null;
637+
code = TypeCode.$null;
638+
case final int integer:
639+
value = integer.toJS;
640+
code = TypeCode.integer;
641+
case final BigInt bi:
642+
value = JsBigInt.fromBigInt(bi);
643+
code = TypeCode.bigInt;
644+
case final double d:
645+
value = d.toJS;
646+
code = TypeCode.float;
647+
case final String s:
648+
value = s.toJS;
649+
code = TypeCode.text;
650+
case final Uint8List blob:
651+
value = blob.toJS;
652+
code = TypeCode.blob;
653+
case final bool boolean:
654+
value = boolean.toJS;
655+
code = TypeCode.boolean;
656+
case final other:
657+
value = other.jsify();
658+
code = TypeCode.unknown;
659+
}
660+
661+
return (code, value);
662+
}
663+
}
664+
560665
final class RowsResponse extends Response {
561666
final ResultSet resultSet;
562667

@@ -576,12 +681,20 @@ final class RowsResponse extends Response {
576681
]
577682
: null;
578683

684+
final typeVector = switch (object[_UniqueFieldNames.typeVector]) {
685+
final types? => (types as JSArrayBuffer).toDart.asUint8List(),
686+
null => null,
687+
};
579688
final rows = <List<Object?>>[];
689+
var i = 0;
580690
for (final row in (object[_UniqueFieldNames.rows] as JSArray).toDart) {
581691
final dartRow = <Object?>[];
582692

583693
for (final column in (row as JSArray).toDart) {
584-
dartRow.add(column.dartify());
694+
final typeCode =
695+
typeVector != null ? TypeCode.of(typeVector[i]) : TypeCode.unknown;
696+
dartRow.add(typeCode.decodeColumn(column));
697+
i++;
585698
}
586699

587700
rows.add(dartRow);
@@ -599,6 +712,29 @@ final class RowsResponse extends Response {
599712
@override
600713
void serialize(JSObject object, List<JSObject> transferred) {
601714
super.serialize(object, transferred);
715+
final jsRows = <JSArray>[];
716+
final columns = resultSet.columnNames.length;
717+
final typeVector = Uint8List(resultSet.length * columns);
718+
719+
for (var i = 0; i < resultSet.length; i++) {
720+
final row = resultSet.rows[i];
721+
assert(row.length == columns);
722+
final jsRow = List<JSAny?>.filled(row.length, null);
723+
724+
for (var j = 0; j < columns; j++) {
725+
final (code, value) = TypeCode.encodeValue(row[j]);
726+
727+
jsRow[j] = value;
728+
typeVector[i * columns + j] = code.index;
729+
}
730+
731+
jsRows.add(jsRow.toJS);
732+
}
733+
734+
final jsTypes = typeVector.buffer.toJS;
735+
object[_UniqueFieldNames.typeVector] = jsTypes;
736+
transferred.add(jsTypes);
737+
602738
object[_UniqueFieldNames.rows] = <JSArray>[
603739
for (final row in resultSet.rows)
604740
<JSAny?>[

sqlite3_web/test/integration_test.dart

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
@TestOn('vm')
2+
library;
3+
14
import 'dart:async';
25
import 'dart:io';
36

sqlite3_web/test/protocol_test.dart

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
@TestOn('browser')
2+
library;
3+
4+
import 'dart:async';
5+
import 'dart:js_interop';
6+
import 'dart:typed_data';
7+
8+
import 'package:sqlite3/common.dart';
9+
import 'package:sqlite3_web/src/channel.dart';
10+
import 'package:sqlite3_web/src/protocol.dart';
11+
import 'package:test/test.dart';
12+
13+
void main() {
14+
late TestServer server;
15+
late TestClient client;
16+
17+
setUp(() async {
18+
final (endpoint, channel) = await createChannel();
19+
20+
server = TestServer(channel);
21+
client = TestClient(endpoint.connect());
22+
});
23+
24+
tearDown(() async {
25+
await server.close();
26+
await client.close();
27+
});
28+
29+
group('TypeCode', () {
30+
test('is compatible with dartify()', () {
31+
for (final value in [
32+
1,
33+
3.4,
34+
true,
35+
null,
36+
{'custom': 'object'},
37+
'string',
38+
Uint8List(10),
39+
]) {
40+
final (_, jsified) = TypeCode.encodeValue(value);
41+
expect(jsified.dartify(), value);
42+
}
43+
});
44+
});
45+
46+
test('serializes types in request', () async {
47+
server.handleRequestFunction = expectAsync1((request) async {
48+
final run = request as RunQuery;
49+
expect(run.sql, 'sql');
50+
expect(run.parameters, [
51+
1,
52+
1.0,
53+
true,
54+
false,
55+
'a string',
56+
isA<Uint8List>().having((e) => e.length, 'length', 10),
57+
isDart2Wasm ? 100 : BigInt.from(100),
58+
null,
59+
{'custom': 'object'},
60+
]);
61+
if (isDart2Wasm) {
62+
// Make sure we don't loose type information in the js conversion across
63+
// the message ports.
64+
expect(run.parameters[0].runtimeType, int);
65+
expect(run.parameters[1].runtimeType, double);
66+
}
67+
68+
return SimpleSuccessResponse(
69+
requestId: request.requestId,
70+
response: null,
71+
);
72+
});
73+
74+
await client.sendRequest(
75+
RunQuery(
76+
requestId: 0,
77+
databaseId: 0,
78+
sql: 'sql',
79+
parameters: [
80+
1,
81+
1.0,
82+
true,
83+
false,
84+
'a string',
85+
Uint8List(10),
86+
BigInt.from(100),
87+
null,
88+
{'custom': 'object'},
89+
],
90+
returnRows: true,
91+
),
92+
MessageType.simpleSuccessResponse,
93+
);
94+
});
95+
96+
test('serializes types in response', () async {
97+
server.handleRequestFunction = expectAsync1((request) async {
98+
return RowsResponse(
99+
requestId: request.requestId,
100+
resultSet: ResultSet(
101+
['a'],
102+
null,
103+
[
104+
[1],
105+
[Uint8List(10)],
106+
[null],
107+
['string value'],
108+
],
109+
),
110+
);
111+
});
112+
113+
final response = await client.sendRequest(
114+
RunQuery(
115+
requestId: 0,
116+
databaseId: 0,
117+
sql: 'sql',
118+
parameters: [],
119+
returnRows: true,
120+
),
121+
MessageType.rowsResponse,
122+
);
123+
final resultSet = response.resultSet;
124+
125+
expect(resultSet.length, 4);
126+
expect(
127+
resultSet.map((e) => e['a']),
128+
[1, Uint8List(10), null, 'string value'],
129+
);
130+
});
131+
}
132+
133+
const isDart2Wasm = bool.fromEnvironment('dart.tool.dart2wasm');
134+
135+
final class TestServer extends ProtocolChannel {
136+
final StreamController<Notification> _notifications = StreamController();
137+
Future<Response> Function(Request request) handleRequestFunction =
138+
(req) async {
139+
throw 'unsupported';
140+
};
141+
142+
TestServer(super.channel);
143+
144+
Stream<Notification> get notification => _notifications.stream;
145+
146+
@override
147+
void handleNotification(Notification notification) {
148+
_notifications.add(notification);
149+
}
150+
151+
@override
152+
Future<Response> handleRequest(Request request) {
153+
return handleRequestFunction(request);
154+
}
155+
}
156+
157+
final class TestClient extends ProtocolChannel {
158+
TestClient(super.channel);
159+
160+
@override
161+
void handleNotification(Notification notification) {}
162+
163+
@override
164+
Future<Response> handleRequest(Request request) {
165+
throw UnimplementedError();
166+
}
167+
}

0 commit comments

Comments
 (0)