Skip to content

Commit 3060b1a

Browse files
authored
[rfw] Support web (as JS) (flutter#4650)
Fixes flutter#129843
1 parent d72a5fe commit 3060b1a

File tree

9 files changed

+124
-27
lines changed

9 files changed

+124
-27
lines changed

packages/rfw/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
## NEXT
1+
## 1.0.12
22

3+
* Improves web compatibility.
34
* Updates minimum supported SDK version to Flutter 3.7/Dart 2.19.
45
* Adds more testing to restore coverage to 100%.
56
* Removes some dead code.

packages/rfw/dart_test.yaml

Lines changed: 0 additions & 3 deletions
This file was deleted.

packages/rfw/lib/src/dart/binary.dart

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,18 @@ Uint8List encodeLibraryBlob(RemoteWidgetLibrary value) {
245245
/// ([SetStateHandler.stateReference]), followed by the tagged value to which
246246
/// to set that state entry ([SetStateHandler.value]).
247247
///
248+
/// ## Limitations
249+
///
250+
/// JavaScript does not have a native integer type; all numbers are stored as
251+
/// [double]s. Data loss may therefore occur when handling integers that cannot
252+
/// be completely represented as a [binary64] floating point number.
253+
///
254+
/// Integers are used for two purposes in this format; as a length, for which it
255+
/// is extremely unlikely that numbers above 2^53 would be practical anyway, and
256+
/// for representing integer literals. Thus, when using RFW with JavaScript
257+
/// environments, it is recommended to use [double]s instead of [int]s whenever
258+
/// possible, to avoid accidental data loss.
259+
///
248260
/// See also:
249261
///
250262
/// * [encodeLibraryBlob], which encodes this format.
@@ -264,6 +276,10 @@ RemoteWidgetLibrary decodeLibraryBlob(Uint8List bytes) {
264276
// endianess used by this format
265277
const Endian _blobEndian = Endian.little;
266278

279+
// whether we can use 64 bit APIs on this platform
280+
// (on JS, we can only use 32 bit APIs and integers only go up to ~2^53)
281+
const bool _has64Bits = 0x1000000000000000 + 1 != 0x1000000000000000; // 2^60
282+
267283
// magic signatures
268284
const int _msFalse = 0x00;
269285
const int _msTrue = 0x01;
@@ -316,7 +332,14 @@ class _BlobDecoder {
316332
int _readInt64() {
317333
final int byteOffset = _cursor;
318334
_advance('int64', 8);
319-
return bytes.getInt64(byteOffset, _blobEndian);
335+
if (_has64Bits) {
336+
return bytes.getInt64(byteOffset, _blobEndian);
337+
}
338+
// We use multiplication rather than bit shifts because << truncates to 32 bits when compiled to JS:
339+
// https://dart.dev/guides/language/numbers#bitwise-operations
340+
final int a = bytes.getUint32(byteOffset, _blobEndian);
341+
final int b = bytes.getInt32(byteOffset + 4, _blobEndian);
342+
return a + (b * 0x100000000);
320343
}
321344

322345
double _readBinary64() {
@@ -516,7 +539,19 @@ class _BlobEncoder {
516539
final BytesBuilder bytes = BytesBuilder(); // copying builder -- we repeatedly add _scratchOut after changing it
517540

518541
void _writeInt64(int value) {
519-
_scratchIn.setInt64(0, value, _blobEndian);
542+
if (_has64Bits) {
543+
_scratchIn.setInt64(0, value, _blobEndian);
544+
} else {
545+
// We use division rather than bit shifts because >> truncates to 32 bits when compiled to JS:
546+
// https://dart.dev/guides/language/numbers#bitwise-operations
547+
if (value >= 0) {
548+
_scratchIn.setInt32(0, value, _blobEndian);
549+
_scratchIn.setInt32(4, value ~/ 0x100000000, _blobEndian);
550+
} else {
551+
_scratchIn.setInt32(0, value, _blobEndian);
552+
_scratchIn.setInt32(4, -((-value) ~/ 0x100000000 + 1), _blobEndian);
553+
}
554+
}
520555
bytes.add(_scratchOut);
521556
}
522557

@@ -551,7 +586,7 @@ class _BlobEncoder {
551586
bytes.addByte(_msFalse);
552587
} else if (value == true) {
553588
bytes.addByte(_msTrue);
554-
} else if (value is double) {
589+
} else if (value is double && value is! int) { // When compiled to JS, a Number can be both.
555590
bytes.addByte(_msBinary64);
556591
_scratchIn.setFloat64(0, value, _blobEndian);
557592
bytes.add(_scratchOut);

packages/rfw/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: rfw
22
description: "Remote Flutter widgets: a library for rendering declarative widget description files at runtime."
33
repository: https://github.com/flutter/packages/tree/main/packages/rfw
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+rfw%22
5-
version: 1.0.11
5+
version: 1.0.12
66

77
environment:
88
sdk: ">=3.0.0 <4.0.0"

packages/rfw/test/argument_decoders_test.dart

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,15 @@
44

55
// This file is hand-formatted.
66

7-
import 'dart:io' show Platform;
87
import 'dart:ui' as ui;
98

9+
import 'package:flutter/foundation.dart';
1010
import 'package:flutter/material.dart';
1111
import 'package:flutter_test/flutter_test.dart';
1212
import 'package:rfw/formats.dart' show parseLibraryFile;
1313
import 'package:rfw/rfw.dart';
1414

15-
final bool masterChannel =
16-
!Platform.environment.containsKey('CHANNEL') ||
17-
Platform.environment['CHANNEL'] == 'master';
18-
19-
// See Contributing section of README.md file.
20-
final bool runGoldens = Platform.isLinux && masterChannel;
15+
import 'utils.dart';
2116

2217
void main() {
2318
testWidgets('String example', (WidgetTester tester) async {
@@ -371,19 +366,23 @@ void main() {
371366
);
372367
'''));
373368
await tester.pump();
374-
expect(eventLog, hasLength(1));
375-
expect(eventLog.first, startsWith('image-error-event {exception: HTTP request failed, statusCode: 400, x-invalid:'));
376-
eventLog.clear();
369+
if (!kIsWeb) {
370+
expect(eventLog, hasLength(1));
371+
expect(eventLog.first, startsWith('image-error-event {exception: HTTP request failed, statusCode: 400, x-invalid:'));
372+
eventLog.clear();
373+
}
377374
await expectLater(
378375
find.byType(RemoteWidget),
379376
matchesGoldenFile('goldens/argument_decoders_test.containers.png'),
380377
skip: !runGoldens,
381378
);
382379
expect(find.byType(DecoratedBox), findsNWidgets(6));
380+
const String matrix = kIsWeb ? '1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1'
381+
: '1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0';
383382
expect(
384383
(tester.widgetList<DecoratedBox>(find.byType(DecoratedBox)).toList()[1].decoration as BoxDecoration).image.toString(),
385384
'DecorationImage(AssetImage(bundle: null, name: "asset"), ' // this just seemed like the easiest way to check all this...
386-
'ColorFilter.matrix([1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]), '
385+
'ColorFilter.matrix([$matrix]), '
387386
'Alignment.center, centerSlice: Rect.fromLTRB(5.0, 8.0, 105.0, 78.0), scale 1.0, opacity 1.0, FilterQuality.low)',
388387
);
389388
expect(
@@ -543,5 +542,5 @@ void main() {
543542
);
544543

545544
expect(eventLog, isEmpty);
546-
}, skip: !masterChannel); // https://github.com/flutter/flutter/pull/129851
545+
}, skip: kIsWeb || !isMainChannel); // https://github.com/flutter/flutter/pull/129851
547546
}

packages/rfw/test/binary_test.dart

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ import 'dart:typed_data';
99
import 'package:flutter_test/flutter_test.dart';
1010
import 'package:rfw/formats.dart';
1111

12+
// This is a number that requires more than 32 bits but less than 53 bits, so
13+
// that it works in a JS Number and tests the logic that parses 64 bit ints as
14+
// two separate 32 bit ints.
15+
const int largeNumber = 9007199254730661;
16+
1217
void main() {
1318
testWidgets('String example', (WidgetTester tester) async {
1419
final Uint8List bytes = encodeDataBlob('Hello');
@@ -18,6 +23,48 @@ void main() {
1823
expect(value, 'Hello');
1924
});
2025

26+
testWidgets('Big integer example', (WidgetTester tester) async {
27+
// This value is intentionally inside the JS Number range but above 2^32.
28+
final Uint8List bytes = encodeDataBlob(largeNumber);
29+
expect(bytes, <int>[ 0xfe, 0x52, 0x57, 0x44, 0x02, 0xa5, 0xd7, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00, ]);
30+
final Object value = decodeDataBlob(bytes);
31+
expect(value, isA<int>());
32+
expect(value, largeNumber);
33+
});
34+
35+
testWidgets('Big negative integer example', (WidgetTester tester) async {
36+
final Uint8List bytes = encodeDataBlob(-largeNumber);
37+
expect(bytes, <int>[ 0xfe, 0x52, 0x57, 0x44, 0x02, 0x5b, 0x28, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xff, ]);
38+
final Object value = decodeDataBlob(bytes);
39+
expect(value, isA<int>());
40+
expect(value, -largeNumber);
41+
});
42+
43+
testWidgets('Small integer example', (WidgetTester tester) async {
44+
final Uint8List bytes = encodeDataBlob(1);
45+
expect(bytes, <int>[ 0xfe, 0x52, 0x57, 0x44, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]);
46+
final Object value = decodeDataBlob(bytes);
47+
expect(value, isA<int>());
48+
expect(value, 1);
49+
});
50+
51+
testWidgets('Small negative integer example', (WidgetTester tester) async {
52+
final Uint8List bytes = encodeDataBlob(-1);
53+
expect(bytes, <int>[ 0xfe, 0x52, 0x57, 0x44, 0x02, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, ]);
54+
final Object value = decodeDataBlob(bytes);
55+
expect(value, isA<int>());
56+
expect(value, -1);
57+
});
58+
59+
testWidgets('Zero integer example', (WidgetTester tester) async {
60+
// This value is intentionally inside the JS Number range but above 2^32.
61+
final Uint8List bytes = encodeDataBlob(0);
62+
expect(bytes, <int>[ 0xfe, 0x52, 0x57, 0x44, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]);
63+
final Object value = decodeDataBlob(bytes);
64+
expect(value, isA<int>());
65+
expect(value, 0);
66+
});
67+
2168
testWidgets('Map example', (WidgetTester tester) async {
2269
final Uint8List bytes = encodeDataBlob(const <String, Object?>{ 'a': 15 });
2370
expect(bytes, <int>[

packages/rfw/test/material_widgets_test.dart

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,12 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import 'dart:io' show Platform;
6-
75
import 'package:flutter/material.dart';
86
import 'package:flutter_test/flutter_test.dart';
97
import 'package:rfw/formats.dart' show parseLibraryFile;
108
import 'package:rfw/rfw.dart';
119

12-
// See Contributing section of README.md file.
13-
final bool runGoldens = Platform.isLinux &&
14-
(!Platform.environment.containsKey('CHANNEL') ||
15-
Platform.environment['CHANNEL'] == 'master');
10+
import 'utils.dart';
1611

1712
void main() {
1813
testWidgets('Material widgets', (WidgetTester tester) async {

packages/rfw/test/text_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ void main() {
4343
test('', 'Expected symbol "{" but found <EOF> at line 1 column 0.');
4444
test('}', 'Expected symbol "{" but found } at line 1 column 1.');
4545
test('1', 'Expected symbol "{" but found 1 at line 1 column 1.');
46-
test('1.0', 'Expected symbol "{" but found 1.0 at line 1 column 3.');
46+
test('1.2', 'Expected symbol "{" but found 1.2 at line 1 column 3.');
4747
test('a', 'Expected symbol "{" but found a at line 1 column 1.');
4848
test('"a"', 'Expected symbol "{" but found "a" at line 1 column 3.');
4949
test('&', 'Unexpected character U+0026 ("&") at line 1 column 1.');

packages/rfw/test/utils.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:io' show Platform;
6+
7+
import 'package:flutter/foundation.dart';
8+
9+
// Detects if we're running the tests on the main channel.
10+
//
11+
// This is useful for _tests_ that depend on _Flutter_ features that have not
12+
// yet rolled to stable. Avoid using this to skip tests of _RFW_ features that
13+
// aren't compatible with stable. Those should wait until the stable release
14+
// channel is updated so that RFW can be compatible with it.
15+
bool get isMainChannel {
16+
assert(!kIsWeb, 'isMainChannel is not available on web');
17+
return !Platform.environment.containsKey('CHANNEL') ||
18+
Platform.environment['CHANNEL'] == 'main' ||
19+
Platform.environment['CHANNEL'] == 'master';
20+
}
21+
22+
// See Contributing section of README.md file.
23+
final bool runGoldens = !kIsWeb && Platform.isLinux && isMainChannel;

0 commit comments

Comments
 (0)