Skip to content

Commit

Permalink
Web: Encode values with type for dart2wasm
Browse files Browse the repository at this point in the history
  • Loading branch information
simolus3 committed Jan 24, 2025
1 parent bd03b58 commit 0b128d2
Show file tree
Hide file tree
Showing 4 changed files with 321 additions and 8 deletions.
7 changes: 7 additions & 0 deletions sqlite3_web/dart_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
platforms: [vm, chrome, firefox]
compilers: [dart2wasm, dart2js]

override_platforms:
firefox:
settings:
arguments: "-headless"
152 changes: 144 additions & 8 deletions sqlite3_web/lib/src/protocol.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import 'dart:async';
import 'dart:js_interop';
import 'dart:js_interop_unsafe';
import 'dart:typed_data';

import 'package:sqlite3/common.dart';
import 'package:sqlite3/wasm.dart' as wasm_vfs;
// ignore: implementation_imports
import 'package:sqlite3/src/wasm/js_interop/core.dart';
import 'package:web/web.dart';

import 'types.dart';
Expand Down Expand Up @@ -69,6 +72,7 @@ class _UniqueFieldNames {
static const returnRows = 'r';
static const updateRowId = 'r';
static const rows = 'r'; // no clash, used on different message types
static const typeVector = 'v';
}

sealed class Message {
Expand Down Expand Up @@ -450,15 +454,25 @@ final class RunQuery extends Request {
});

factory RunQuery.deserialize(JSObject object) {
final rawParameters =
(object[_UniqueFieldNames.parameters] as JSArray).toDart;
final typeVector = switch (object[_UniqueFieldNames.typeVector]) {
final types? => (types as JSArrayBuffer).toDart.asUint8List(),
null => null,
};

final parameters = List<Object?>.filled(rawParameters.length, null);
for (var i = 0; i < parameters.length; i++) {
final typeCode =
typeVector != null ? TypeCode.of(typeVector[i]) : TypeCode.unknown;
parameters[i] = typeCode.decodeColumn(rawParameters[i]);
}

return RunQuery(
requestId: object.requestId,
databaseId: object.databaseId,
sql: (object[_UniqueFieldNames.sql] as JSString).toDart,
parameters: [
for (final raw
in (object[_UniqueFieldNames.parameters] as JSArray).toDart)
raw.dartify()
],
parameters: parameters,
returnRows: (object[_UniqueFieldNames.returnRows] as JSBoolean).toDart,
);
}
Expand All @@ -470,9 +484,25 @@ final class RunQuery extends Request {
void serialize(JSObject object, List<JSObject> transferred) {
super.serialize(object, transferred);
object[_UniqueFieldNames.sql] = sql.toJS;
object[_UniqueFieldNames.parameters] =
<JSAny?>[for (final parameter in parameters) parameter.jsify()].toJS;
object[_UniqueFieldNames.returnRows] = returnRows.toJS;

if (parameters.isNotEmpty) {
final jsParams = <JSAny?>[];
final typeCodes = Uint8List(parameters.length);
for (var i = 0; i < parameters.length; i++) {
final (code, jsParam) = TypeCode.encodeValue(parameters[i]);
typeCodes[i] = code.index;
jsParams.add(jsParam);
}

final jsTypes = typeCodes.buffer.toJS;
transferred.add(jsTypes);

object[_UniqueFieldNames.parameters] = jsParams.toJS;
object[_UniqueFieldNames.typeVector] = jsTypes;
} else {
object[_UniqueFieldNames.parameters] = JSArray();
}
}
}

Expand Down Expand Up @@ -557,6 +587,81 @@ final class EndpointResponse extends Response {
}
}

enum TypeCode {
unknown,
integer,
bigInt,
float,
text,
blob,
$null,
boolean;

static TypeCode of(int i) {
return i >= TypeCode.values.length ? TypeCode.unknown : TypeCode.values[i];
}

Object? decodeColumn(JSAny? column) {
const hasNativeInts = !identical(0, 0.0);

return switch (this) {
TypeCode.unknown => column.dartify(),
TypeCode.integer => (column as JSNumber).toDartInt,
TypeCode.bigInt => hasNativeInts
? (column as JsBigInt).asDartInt
: (column as JsBigInt).asDartBigInt,
TypeCode.float => (column as JSNumber).toDartDouble,
TypeCode.text => (column as JSString).toDart,
TypeCode.blob => (column as JSUint8Array).toDart,
TypeCode.boolean => (column as JSBoolean).toDart,
TypeCode.$null => null,
};
}

static (TypeCode, JSAny?) encodeValue(Object? dart) {
// In previous clients/workers, values were encoded with dartify() and
// jsify() only. For backwards-compatibility, this value must be compatible
// with dartify() used on the other end.
// An exception are BigInts, which have not been sent correctly before this
// encoder.
// The reasons for adopting a custom format are: Being able to properly
// serialize BigInts, possible dartify/jsify incompatibilities between
// dart2js and dart2wasm and most importantly, being able to keep 1 and 1.0
// apart in dart2wasm when the worker is compiled with dart2js.
final JSAny? value;
final TypeCode code;

switch (dart) {
case null:
value = null;
code = TypeCode.$null;
case final int integer:
value = integer.toJS;
code = TypeCode.integer;
case final BigInt bi:
value = JsBigInt.fromBigInt(bi);
code = TypeCode.bigInt;
case final double d:
value = d.toJS;
code = TypeCode.float;
case final String s:
value = s.toJS;
code = TypeCode.text;
case final Uint8List blob:
value = blob.toJS;
code = TypeCode.blob;
case final bool boolean:
value = boolean.toJS;
code = TypeCode.boolean;
case final other:
value = other.jsify();
code = TypeCode.unknown;
}

return (code, value);
}
}

final class RowsResponse extends Response {
final ResultSet resultSet;

Expand All @@ -576,12 +681,20 @@ final class RowsResponse extends Response {
]
: null;

final typeVector = switch (object[_UniqueFieldNames.typeVector]) {
final types? => (types as JSArrayBuffer).toDart.asUint8List(),
null => null,
};
final rows = <List<Object?>>[];
var i = 0;
for (final row in (object[_UniqueFieldNames.rows] as JSArray).toDart) {
final dartRow = <Object?>[];

for (final column in (row as JSArray).toDart) {
dartRow.add(column.dartify());
final typeCode =
typeVector != null ? TypeCode.of(typeVector[i]) : TypeCode.unknown;
dartRow.add(typeCode.decodeColumn(column));
i++;
}

rows.add(dartRow);
Expand All @@ -599,6 +712,29 @@ final class RowsResponse extends Response {
@override
void serialize(JSObject object, List<JSObject> transferred) {
super.serialize(object, transferred);
final jsRows = <JSArray>[];
final columns = resultSet.columnNames.length;
final typeVector = Uint8List(resultSet.length * columns);

for (var i = 0; i < resultSet.length; i++) {
final row = resultSet.rows[i];
assert(row.length == columns);
final jsRow = List<JSAny?>.filled(row.length, null);

for (var j = 0; j < columns; j++) {
final (code, value) = TypeCode.encodeValue(row[j]);

jsRow[j] = value;
typeVector[i * columns + j] = code.index;
}

jsRows.add(jsRow.toJS);
}

final jsTypes = typeVector.buffer.toJS;
object[_UniqueFieldNames.typeVector] = jsTypes;
transferred.add(jsTypes);

object[_UniqueFieldNames.rows] = <JSArray>[
for (final row in resultSet.rows)
<JSAny?>[
Expand Down
3 changes: 3 additions & 0 deletions sqlite3_web/test/integration_test.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
@TestOn('vm')
library;

import 'dart:async';
import 'dart:io';

Expand Down
167 changes: 167 additions & 0 deletions sqlite3_web/test/protocol_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
@TestOn('browser')
library;

import 'dart:async';
import 'dart:js_interop';
import 'dart:typed_data';

import 'package:sqlite3/common.dart';
import 'package:sqlite3_web/src/channel.dart';
import 'package:sqlite3_web/src/protocol.dart';
import 'package:test/test.dart';

void main() {
late TestServer server;
late TestClient client;

setUp(() async {
final (endpoint, channel) = await createChannel();

server = TestServer(channel);
client = TestClient(endpoint.connect());
});

tearDown(() async {
await server.close();
await client.close();
});

group('TypeCode', () {
test('is compatible with dartify()', () {
for (final value in [
1,
3.4,
true,
null,
{'custom': 'object'},
'string',
Uint8List(10),
]) {
final (_, jsified) = TypeCode.encodeValue(value);
expect(jsified.dartify(), value);
}
});
});

test('serializes types in request', () async {
server.handleRequestFunction = expectAsync1((request) async {
final run = request as RunQuery;
expect(run.sql, 'sql');
expect(run.parameters, [
1,
1.0,
true,
false,
'a string',
isA<Uint8List>().having((e) => e.length, 'length', 10),
isDart2Wasm ? 100 : BigInt.from(100),
null,
{'custom': 'object'},
]);
if (isDart2Wasm) {
// Make sure we don't loose type information in the js conversion across
// the message ports.
expect(run.parameters[0].runtimeType, int);
expect(run.parameters[1].runtimeType, double);
}

return SimpleSuccessResponse(
requestId: request.requestId,
response: null,
);
});

await client.sendRequest(
RunQuery(
requestId: 0,
databaseId: 0,
sql: 'sql',
parameters: [
1,
1.0,
true,
false,
'a string',
Uint8List(10),
BigInt.from(100),
null,
{'custom': 'object'},
],
returnRows: true,
),
MessageType.simpleSuccessResponse,
);
});

test('serializes types in response', () async {
server.handleRequestFunction = expectAsync1((request) async {
return RowsResponse(
requestId: request.requestId,
resultSet: ResultSet(
['a'],
null,
[
[1],
[Uint8List(10)],
[null],
['string value'],
],
),
);
});

final response = await client.sendRequest(
RunQuery(
requestId: 0,
databaseId: 0,
sql: 'sql',
parameters: [],
returnRows: true,
),
MessageType.rowsResponse,
);
final resultSet = response.resultSet;

expect(resultSet.length, 4);
expect(
resultSet.map((e) => e['a']),
[1, Uint8List(10), null, 'string value'],
);
});
}

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

final class TestServer extends ProtocolChannel {
final StreamController<Notification> _notifications = StreamController();
Future<Response> Function(Request request) handleRequestFunction =
(req) async {
throw 'unsupported';
};

TestServer(super.channel);

Stream<Notification> get notification => _notifications.stream;

@override
void handleNotification(Notification notification) {
_notifications.add(notification);
}

@override
Future<Response> handleRequest(Request request) {
return handleRequestFunction(request);
}
}

final class TestClient extends ProtocolChannel {
TestClient(super.channel);

@override
void handleNotification(Notification notification) {}

@override
Future<Response> handleRequest(Request request) {
throw UnimplementedError();
}
}

0 comments on commit 0b128d2

Please sign in to comment.