├── LICENSE ├── codecov.yml ├── CHANGELOG.md ├── dart_test.yaml ├── lib ├── websocket.dart └── src │ ├── websocket_status.dart │ ├── websocket_stub.dart │ ├── websocket_io.dart │ └── websocket_browser.dart ├── .metadata ├── pubspec.yaml ├── README.md ├── .cirrus.yml ├── tools └── server.dart ├── .gitignore └── test └── websocket_test.dart /LICENSE: -------------------------------------------------------------------------------- 1 | TODO: Add your license here. 2 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "**/*_stub.dart" 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.0.1] - TODO: Add release date. 2 | 3 | * TODO: Describe initial release. 4 | -------------------------------------------------------------------------------- /dart_test.yaml: -------------------------------------------------------------------------------- 1 | define_platforms: 2 | chromium: 3 | name: Chromium 4 | extends: chrome 5 | settings: 6 | arguments: --no-sandbox 7 | executable: 8 | linux: chromium 9 | -------------------------------------------------------------------------------- /lib/websocket.dart: -------------------------------------------------------------------------------- 1 | export 'src/websocket_stub.dart' 2 | if (dart.library.html) 'src/websocket_browser.dart' 3 | // ignore: uri_does_not_exist 4 | if (dart.library.io) 'src/websocket_io.dart'; 5 | export 'src/websocket_status.dart'; 6 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 7a4c33425ddd78c54aba07d86f3f9a4a0051769b 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: websocket 2 | description: Universal package for websocket, compatible with web, flutter, and vm. 3 | version: 0.0.5 4 | author: TruongSinh Tran-Nguyen 5 | homepage: https://github.com/truongsinh/dart-websocket 6 | 7 | environment: 8 | sdk: '>=2.0.0 <3.0.0' 9 | 10 | dev_dependencies: 11 | test: any 12 | test_coverage: any 13 | -------------------------------------------------------------------------------- /lib/src/websocket_status.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | abstract class WebSocketStatus { 4 | static const int normalClosure = 1000; 5 | static const int goingAway = 1001; 6 | static const int protocolError = 1002; 7 | static const int unsupportedData = 1003; 8 | static const int reserved1004 = 1004; 9 | static const int noStatusReceived = 1005; 10 | static const int abnormalClosure = 1006; 11 | static const int invalidFramePayloadData = 1007; 12 | static const int policyViolation = 1008; 13 | static const int messageTooBig = 1009; 14 | static const int missingMandatoryExtension = 1010; 15 | static const int internalServerError = 1011; 16 | static const int reserved1015 = 1015; 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![build][build-badge]][build-link] 2 | [![codecov][coverage-badge]][coverage-link] 3 | [![version][version-badge]][package-link] 4 | 5 | # Websocket 6 | 7 | Websocket universal depdency-free package for Dart, compatible with web, flutter, and vm. 8 | 9 | [build-badge]: https://api.cirrus-ci.com/github/truongsinh/dart-websocket.svg 10 | [build-link]: https://cirrus-ci.com/github/truongsinh/dart-websocket/master 11 | 12 | [coverage-badge]: https://codecov.io/gh/truongsinh/dart-websocket/branch/master/graph/badge.svg 13 | [coverage-link]: https://codecov.io/gh/truongsinh/dart-websocket 14 | 15 | [version-badge]: https://img.shields.io/pub/v/websocket.svg 16 | [package-link]: https://pub.dev/packages/websocket 17 | -------------------------------------------------------------------------------- /.cirrus.yml: -------------------------------------------------------------------------------- 1 | container: 2 | image: cirrusci/flutter:latest 3 | 4 | test_task: 5 | # pub_cache: 6 | # folder: $HOME/.pub-cache 7 | # fingerprint_script: cat pubspec.lock 8 | # populate_script: pub get 9 | pub_get_script: pub get 10 | format_script: dartfmt **/*.dart -n --set-exit-if-changed 11 | analyze_script: dartanalyzer --fatal-infos . 12 | # publishable_script: pub publish --dry-run 13 | environment: 14 | CODECOV_TOKEN: ENCRYPTED[64bbc39e3fd05e26073199c780e33a45609d3b2cc824dc11652e0c8c6bbf14db56ed5bd50153f67cc2a89a5d5185cd9a] 15 | test_script: 16 | - dart tools/server.dart & # start websocket server 17 | - pub run test_coverage 18 | # workaround until https://github.com/cirruslabs/docker-images-flutter/pull/18 19 | - sudo apt-get update && 20 | sudo apt-get install -y --allow-unauthenticated --no-install-recommends chromium 21 | - pub run test -p chromium 22 | - bash <(curl -s https://codecov.io/bash) 23 | -------------------------------------------------------------------------------- /tools/server.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | const PORT = 5600; 5 | void main() async { 6 | HttpServer server = await HttpServer.bind('localhost', PORT); 7 | server.transform( 8 | WebSocketTransformer(protocolSelector: (List protocols) { 9 | print('Requested protocols: $protocols'); 10 | return protocols?.elementAt(0) ?? ''; 11 | })).listen((WebSocket client) { 12 | print('a client just connected'); 13 | client.listen((data) { 14 | try { 15 | final parsed = jsonDecode(data); 16 | print('parsed: $parsed'); 17 | final command = parsed['command']; 18 | if (command == 'close') { 19 | client.close(parsed['code'], parsed['reason']); 20 | } 21 | } catch (_) { 22 | print('message: $data'); 23 | client.add(data); 24 | } 25 | }, onDone: () { 26 | print('close with ${client.closeCode} and ${client.closeReason}'); 27 | }); 28 | }); 29 | print('listening on port: $PORT'); 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/websocket_stub.dart: -------------------------------------------------------------------------------- 1 | library websocket; 2 | 3 | import 'dart:async'; 4 | 5 | final _unsupportedError = UnsupportedError( 6 | 'Cannot work with WebSocket without dart:html or dart:io.'); 7 | 8 | class WebSocket implements StreamConsumer*/ > { 9 | static Future connect( 10 | String url, { 11 | Iterable protocols, 12 | }) async => 13 | throw _unsupportedError; 14 | 15 | void add(/*String|List*/ data) => throw _unsupportedError; 16 | 17 | Future addStream(Stream stream) => throw _unsupportedError; 18 | 19 | void addUtf8Text(List bytes) => throw _unsupportedError; 20 | 21 | Future close([int code, String reason]) => throw _unsupportedError; 22 | 23 | int get closeCode => throw _unsupportedError; 24 | 25 | String get closeReason => throw _unsupportedError; 26 | 27 | String get extensions => throw _unsupportedError; 28 | 29 | String get protocol => throw _unsupportedError; 30 | 31 | int get readyState => throw _unsupportedError; 32 | 33 | Future get done => throw _unsupportedError; 34 | 35 | Stream*/ > get stream => throw _unsupportedError; 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/websocket_io.dart: -------------------------------------------------------------------------------- 1 | library websocket; 2 | 3 | import 'dart:async'; 4 | import 'dart:io' as io; 5 | import './websocket_stub.dart' as stub; 6 | 7 | class WebSocket implements stub.WebSocket { 8 | io.WebSocket _socket; 9 | 10 | WebSocket._(this._socket); 11 | 12 | static Future connect( 13 | String url, { 14 | Iterable protocols, 15 | }) async { 16 | return WebSocket._(await io.WebSocket.connect(url, protocols: protocols)); 17 | } 18 | 19 | @override 20 | void add(/*String|List*/ data) => _socket.add(data); 21 | 22 | @override 23 | Future addStream(Stream stream) => _socket.addStream(stream); 24 | 25 | @override 26 | void addUtf8Text(List bytes) => _socket.addUtf8Text(bytes); 27 | 28 | @override 29 | Future close([int code, String reason]) => _socket.close(code, reason); 30 | 31 | @override 32 | int get closeCode => _socket.closeCode; 33 | 34 | @override 35 | String get closeReason => _socket.closeReason; 36 | 37 | @override 38 | String get extensions => _socket.extensions; 39 | 40 | @override 41 | String get protocol => _socket.protocol; 42 | 43 | @override 44 | int get readyState => _socket.readyState; 45 | 46 | @override 47 | Future get done => _socket.done; 48 | 49 | Stream _stream; 50 | 51 | @override 52 | Stream*/ > get stream => 53 | _stream ??= _socket.asBroadcastStream(); 54 | } 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | test/.test_coverage.dart 12 | coverage/ 13 | 14 | # IntelliJ related 15 | *.iml 16 | *.ipr 17 | *.iws 18 | .idea/ 19 | 20 | # Visual Studio Code related 21 | .vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | build/ 31 | pubspec.lock 32 | 33 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | # iOS/XCode related 43 | **/ios/**/*.mode1v3 44 | **/ios/**/*.mode2v3 45 | **/ios/**/*.moved-aside 46 | **/ios/**/*.pbxuser 47 | **/ios/**/*.perspectivev3 48 | **/ios/**/*sync/ 49 | **/ios/**/.sconsign.dblite 50 | **/ios/**/.tags* 51 | **/ios/**/.vagrant/ 52 | **/ios/**/DerivedData/ 53 | **/ios/**/Icon? 54 | **/ios/**/Pods/ 55 | **/ios/**/.symlinks/ 56 | **/ios/**/profile 57 | **/ios/**/xcuserdata 58 | **/ios/.generated/ 59 | **/ios/Flutter/App.framework 60 | **/ios/Flutter/Flutter.framework 61 | **/ios/Flutter/Generated.xcconfig 62 | **/ios/Flutter/app.flx 63 | **/ios/Flutter/app.zip 64 | **/ios/Flutter/flutter_assets/ 65 | **/ios/ServiceDefinitions.json 66 | **/ios/Runner/GeneratedPluginRegistrant.* 67 | 68 | # Exceptions to above rules. 69 | !**/ios/**/default.mode1v3 70 | !**/ios/**/default.mode2v3 71 | !**/ios/**/default.pbxuser 72 | !**/ios/**/default.perspectivev3 73 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 74 | -------------------------------------------------------------------------------- /lib/src/websocket_browser.dart: -------------------------------------------------------------------------------- 1 | library websocket; 2 | 3 | import 'dart:async'; 4 | import 'dart:convert'; 5 | import 'dart:html' as html; 6 | import 'dart:typed_data'; 7 | import './websocket_stub.dart' as stub; 8 | 9 | class WebSocket implements stub.WebSocket { 10 | html.WebSocket _socket; 11 | final StreamController _streamConsumer = StreamController(); 12 | 13 | WebSocket._(this._socket) : done = _socket.onClose.first { 14 | _streamConsumer.stream.listen( 15 | (data) => _send(data), 16 | onError: (error) => _send(error.toString()), 17 | ); 18 | _socket.onClose.listen((html.CloseEvent event) { 19 | closeCode = event.code; 20 | closeReason = event.reason; 21 | _streamController.close(); 22 | }); 23 | _socket.onError.listen((html.Event error) { 24 | _streamController.addError(error); 25 | }); 26 | _socket.onMessage.listen((html.MessageEvent message) async { 27 | final data = message.data; 28 | if (data is String) { 29 | _streamController.add(data); 30 | return; 31 | } 32 | if (data is html.Blob) { 33 | final reader = html.FileReader(); 34 | reader.readAsArrayBuffer(data); 35 | await reader.onLoad.first; 36 | _streamController.add(reader.result); 37 | return; 38 | } 39 | 40 | throw UnsupportedError('unspported data type $data'); 41 | }); 42 | } 43 | 44 | static Future connect( 45 | String url, { 46 | Iterable protocols, 47 | }) async { 48 | final s = html.WebSocket(url, protocols); 49 | await s.onOpen.first; 50 | return WebSocket._(s); 51 | } 52 | 53 | void _send(/*String|List*/ data) { 54 | if (data is String) { 55 | return _socket.send(data); 56 | } 57 | if (data is List) { 58 | return _socket.sendByteBuffer(Uint8List.fromList(data).buffer); 59 | } 60 | 61 | throw UnsupportedError('unspported data type $data'); 62 | } 63 | 64 | @override 65 | void add(/*String|List*/ data) => _streamConsumer.add(data); 66 | 67 | @override 68 | Future addStream(Stream stream) => _streamConsumer.addStream(stream); 69 | 70 | @override 71 | void addUtf8Text(List bytes) => _streamConsumer.add(utf8.decode(bytes)); 72 | 73 | @override 74 | Future close([int code, String reason]) { 75 | _streamConsumer.close(); 76 | if (code != null) { 77 | _socket.close(code, reason); 78 | } else { 79 | _socket.close(); 80 | } 81 | return done; 82 | } 83 | 84 | @override 85 | int closeCode; 86 | 87 | @override 88 | String closeReason; 89 | 90 | @override 91 | String get extensions => _socket.extensions; 92 | 93 | @override 94 | String get protocol => _socket.protocol; 95 | 96 | @override 97 | int get readyState => _socket.readyState; 98 | 99 | @override 100 | final Future done; 101 | 102 | StreamController*/ > _streamController = 103 | StreamController.broadcast(); 104 | 105 | @override 106 | Stream*/ > get stream => _streamController.stream; 107 | } 108 | -------------------------------------------------------------------------------- /test/websocket_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:test/test.dart'; 5 | 6 | import 'package:websocket/websocket.dart' show WebSocket; 7 | 8 | void main() { 9 | final url = 'ws://localhost:5600'; 10 | group('#connect', () { 11 | test('open connection', () async { 12 | final socket = await WebSocket.connect(url); 13 | expect(socket.readyState, 1); 14 | await socket.close(); 15 | await socket.done; 16 | }); 17 | test('close connection from client side', () async { 18 | final socket = await WebSocket.connect(url); 19 | expect(socket.readyState, 1); 20 | await expectLater(socket.stream, emitsInOrder([])); 21 | socket.close(3001, 'reason 3001'); 22 | await expectLater(socket.stream, emitsDone); 23 | expect(socket.closeCode, 3001); 24 | expect(socket.closeReason, 'reason 3001'); 25 | await socket.done; 26 | }); 27 | test('close connection from server side', () async { 28 | final socket = await WebSocket.connect(url); 29 | expect(socket.readyState, 1); 30 | await socket.add(jsonEncode({ 31 | 'command': 'close', 32 | 'code': 3002, 33 | 'reason': 'reason 3002', 34 | })); 35 | await expectLater(socket.stream, emitsDone); 36 | expect(socket.closeCode, 3002); 37 | expect(socket.closeReason, 'reason 3002'); 38 | await socket.done; 39 | }); 40 | test('open connection with protocol', () async { 41 | final socket = await WebSocket.connect(url, 42 | protocols: ['weird-protocol', 'another-protocol']); 43 | expect(socket.readyState, 1); 44 | expect(socket.protocol, 'weird-protocol'); 45 | if (socket.extensions is String) { 46 | // in browser 47 | expect( 48 | socket.extensions, 'permessage-deflate; client_max_window_bits=15'); 49 | } else { 50 | expect(socket.extensions, null); 51 | } 52 | socket.close(); 53 | await socket.done; 54 | }); 55 | // test('error connection', () {}, skip: true); 56 | }); 57 | 58 | group('instance method', () { 59 | WebSocket socket; 60 | setUp(() async { 61 | socket = await WebSocket.connect(url); 62 | }); 63 | tearDown(() { 64 | socket?.close(); 65 | }); 66 | group('#add', () { 67 | test('String ASCII', () async { 68 | socket.add('string data'); 69 | await expectLater(socket.stream, emits('string data')); 70 | }); 71 | test('String Unicode/Emoji', () async { 72 | socket.add('string 👨‍👩‍👧‍👦'); 73 | await expectLater(socket.stream, emits('string 👨‍👩‍👧‍👦')); 74 | }); 75 | test('Bytes', () async { 76 | socket.add([0, 1, 195, 191]); 77 | // await expectLater(socket.stream, emits([0, 1, 195, 191])); 78 | expect(await socket.stream.first, [0, 1, 195, 191]); 79 | }); 80 | }); 81 | 82 | group('#addStream', () { 83 | StreamController stream1; 84 | StreamController stream2; 85 | setUp(() { 86 | stream1 = StreamController(); 87 | stream2 = StreamController(); 88 | }); 89 | tearDown(() { 90 | stream1.close(); 91 | stream2.close(); 92 | // otherwise socket?.close(); in other teadDown will throw error 93 | // Bad state: StreamSink is already bound to a stream 94 | socket = null; 95 | }); 96 | test('single-subscription stream', () async { 97 | stream1 98 | ..stream.pipe(socket) 99 | ..add('frame1') 100 | ..add('frame3'); 101 | 102 | await expectLater( 103 | socket.stream, 104 | emitsInOrder([ 105 | 'frame1', 106 | 'frame3', 107 | ])); 108 | }); 109 | test('multiple streams throw error', () async { 110 | stream1.stream.pipe(socket); 111 | await expect(() => stream2.stream.pipe(socket), throwsA(isStateError)); 112 | }); 113 | test('cannot send more data after addStream', () async { 114 | stream1.stream.pipe(socket); 115 | await expect(() => socket.add('a'), throwsA(isStateError)); 116 | await expect(() => socket.addUtf8Text([0, 1]), throwsA(isStateError)); 117 | await expect(() => socket.close(), throwsA(isStateError)); 118 | }); 119 | test('broadcast stream', () async { 120 | stream1 121 | ..stream.pipe(socket) 122 | ..add('frame1') 123 | ..add('frame3'); 124 | 125 | await expectLater( 126 | socket.stream, 127 | emitsInOrder([ 128 | 'frame1', 129 | 'frame3', 130 | ])); 131 | }); 132 | test('multiple broadcast streams throw error', () async { 133 | final stream1 = StreamController.broadcast(); 134 | stream1.stream.pipe(socket); 135 | final stream2 = StreamController.broadcast(); 136 | await expect(() => stream2.stream.pipe(socket), throwsA(isStateError)); 137 | stream1.close(); 138 | stream2.close(); 139 | }); 140 | }); 141 | group('#addUtf8Text', () { 142 | test('text', () async { 143 | socket.addUtf8Text([00, 1, 195, 191]); 144 | expect(await socket.stream.first, '\u{0}\u{1}\u{FF}'); 145 | }); 146 | }); 147 | }); 148 | } 149 | --------------------------------------------------------------------------------