├── test ├── config │ ├── websocket.cfg │ ├── nkey.cfg │ ├── jwt2.cfg │ ├── jwt.cfg │ └── wss.cfg ├── readme.md ├── model │ └── student.dart ├── verbose_test.dart ├── z_continuous_test.dart ├── tls_connect_test.dart ├── nkey_test.dart ├── authen_test.dart ├── defect │ └── 0000_0100_test.dart ├── chrome_test.dart ├── message_data_test.dart ├── structure_test.dart ├── message_test.dart ├── request_test.dart ├── connect_test.dart └── nats_client_test.dart ├── analysis_options.yaml ├── .gitignore ├── lib ├── dart_nats.dart └── src │ ├── subscription.dart │ ├── inbox.dart │ ├── message.dart │ ├── common.dart │ ├── nkeys.dart │ └── client.dart ├── example ├── main.dart ├── .vscode │ └── launch.json └── flutter │ └── main_dart ├── .vscode └── launch.json ├── pubspec.yaml ├── LICENSE ├── docker-compose.yml ├── CHANGELOG.md └── README.md /test/config/websocket.cfg: -------------------------------------------------------------------------------- 1 | websocket { 2 | port: 8080 3 | no_tls: true 4 | } -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | linter: 2 | rules: 3 | - public_member_api_docs 4 | - unrelated_type_equality_checks -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool 2 | pubspec.lock 3 | .packages 4 | test/go.sum 5 | build/ 6 | doc/api/ 7 | *.pem 8 | *.iml 9 | *.idea/ 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /test/config/nkey.cfg: -------------------------------------------------------------------------------- 1 | websocket { 2 | port: 8080 3 | no_tls: true 4 | } 5 | authorization: { 6 | users: [ 7 | { nkey: UDXU4RCSJNZOIQHZNWXHXORDPRTGNJAHAHFRGZNEEJCPQTT2M7NLCNF4 } 8 | ] 9 | } -------------------------------------------------------------------------------- /lib/dart_nats.dart: -------------------------------------------------------------------------------- 1 | library dart_nats; 2 | 3 | export 'src/client.dart'; 4 | export 'src/common.dart'; 5 | export 'src/inbox.dart'; 6 | export 'src/message.dart'; 7 | export 'src/nkeys.dart'; 8 | export 'src/subscription.dart'; 9 | -------------------------------------------------------------------------------- /test/readme.md: -------------------------------------------------------------------------------- 1 | start nats servers before test 2 | ``` 3 | openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -out test/config/server-cert.pem -keyout test/config/server-key.pem 4 | docker-compose up 5 | ``` 6 | 7 | tests should be run with 1 thread to avoid multiple listeners to the same subjects -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nats/dart_nats.dart'; 2 | 3 | void main() async { 4 | var client = Client(); 5 | await client.connect(Uri.parse('ws://localhost:80')); 6 | var sub = client.sub('subject1'); 7 | client.pubString('subject1', 'message1'); 8 | var data = await sub.stream.first; 9 | 10 | print(data.string); 11 | client.unSub(sub); 12 | await client.close(); 13 | } 14 | -------------------------------------------------------------------------------- /example/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Dart & Flutter", 9 | "request": "launch", 10 | "type": "dart" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Dart", 9 | "program": "bin/main.dart", 10 | "request": "launch", 11 | "type": "dart" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /test/model/student.dart: -------------------------------------------------------------------------------- 1 | class Student { 2 | String id; 3 | String name; 4 | int score; 5 | 6 | Student(this.id, this.name, this.score); 7 | 8 | factory Student.fromJson(Map json) => Student( 9 | json['id'] as String, 10 | json['name'] as String, 11 | json['score'] as int, 12 | ); 13 | 14 | Map toJson() => { 15 | 'id': id, 16 | 'name': name, 17 | 'score': score, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dart_nats 2 | description: A Dart client for the NATS messaging system. Design to use with Dart and flutter. 3 | version: 0.6.5 4 | homepage: https://github.com/chartchuo 5 | repository: https://github.com/chartchuo/dart-nats 6 | 7 | environment: 8 | sdk: '>=2.15.0 <4.0.0' 9 | 10 | # dependencies: 11 | 12 | dev_dependencies: 13 | lints: ^2.0.1 14 | test: ^1.9.4 15 | # pedantic: ^1.11.0 16 | dependencies: 17 | base32: ^2.1.3 18 | # cryptography: ^2.0.5 19 | ed25519_edwards: ^0.3.1 20 | mutex: ^3.0.0 21 | web_socket_channel: ^2.2.0 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 chartchuo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/verbose_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_nats/dart_nats.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('all', () { 6 | test('verbose', () async { 7 | var client = Client(); 8 | client.seed = 9 | 'SUADD2T2KGHBWPJ4OUN4WXEFVEKJSOHRMZFPQPO5F5EBYXMM5VLWJWL6DY'; 10 | await client.connect( 11 | Uri.parse('nats://localhost:4227'), 12 | retryInterval: 1, 13 | connectOption: ConnectOption( 14 | verbose: true, 15 | jwt: 16 | 'eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJLQUE3TU5NRTNHQVZRNUlZUTNFTk5PNk1SVFJHSVlTWk9ESFNYS01TU1lJTFBPWkc3WFZBIiwiaWF0IjoxNjY3NzE0ODEzLCJpc3MiOiJBQVhGT1pVNFVIS1lCTTVCUVZPQ01LNUZYSkVCTlFSU0NZUTJKTTNUNjdSVEpRTlhVNlFKS0tGQyIsIm5hbWUiOiJ0ZXN0Iiwic3ViIjoiVUNHVkMzVkY1SVhBQk9DV0w3Tkw3WFFTR1lMVzRBT0lNWEdNWUpJSVlCSlgyVjZUN1JWMzJYVVEiLCJuYXRzIjp7InB1YiI6eyJhbGxvdyI6WyJ0ZXN0Il19LCJzdWIiOnt9LCJzdWJzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJ0eXBlIjoidXNlciIsInZlcnNpb24iOjJ9fQ.Sa27RTijMuACL7EhPwxYwmGPINRXqaQtlrODQaPYf-JtrJo43CTKQ_I1jryG_VK7il61Y-LjxAiJJZxehNO7Cw', 17 | ), 18 | ); 19 | var result = await client.pubString('test', 'message1'); 20 | expect(result, equals(true)); 21 | result = await client.pubString('other', 'message2'); 22 | expect(result, equals(false)); 23 | await client.close(); 24 | }); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/subscription.dart: -------------------------------------------------------------------------------- 1 | ///subscription model 2 | import 'dart:async'; 3 | 4 | import 'client.dart'; 5 | import 'message.dart'; 6 | 7 | /// subscription class 8 | class Subscription { 9 | ///subscriber id (audo generate) 10 | final int sid; 11 | 12 | ///subject and queuegroup of this subscription 13 | final String? subject, queueGroup; 14 | 15 | final Client _client; 16 | 17 | late StreamController> _controller; 18 | 19 | late Stream> _stream; 20 | 21 | ///convert from json string to T for structure data 22 | T Function(String)? jsonDecoder; 23 | 24 | ///constructure 25 | Subscription(this.sid, this.subject, this._client, 26 | {this.queueGroup, this.jsonDecoder}) { 27 | _controller = StreamController>(); 28 | _stream = _controller.stream.asBroadcastStream(); 29 | } 30 | 31 | /// 32 | void unSub() { 33 | _client.unSub(this); 34 | } 35 | 36 | ///Stream output when server publish message 37 | Stream> get stream => _stream; 38 | 39 | ///sink messat to listener 40 | void add(Message raw) { 41 | if (_controller.isClosed) return; 42 | _controller.sink.add(Message( 43 | raw.subject, 44 | raw.sid, 45 | raw.byte, 46 | _client, 47 | replyTo: raw.replyTo, 48 | jsonDecoder: jsonDecoder, 49 | header: raw.header, 50 | )); 51 | } 52 | 53 | ///close the stream 54 | Future close() async { 55 | await _controller.close(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | services: 3 | nats: 4 | image: nats 5 | ports: 6 | - "4222:4222" 7 | - "8080:8080" 8 | volumes: 9 | - ./test/config:/config 10 | command: "-V -D -c /config/websocket.cfg" 11 | 12 | nats-jwt: 13 | image: nats 14 | ports: 15 | - "4223:4222" 16 | - "8083:8080" 17 | volumes: 18 | - ./test/config:/config 19 | command: "-V -D -c /config/jwt.cfg" 20 | 21 | nats-jwt2: 22 | image: nats 23 | ports: 24 | - "4227:4222" 25 | volumes: 26 | - ./test/config:/config 27 | command: "-V -D -c /config/jwt2.cfg" 28 | 29 | nats-token: 30 | image: nats 31 | ports: 32 | - "4224:4222" 33 | - "8084:8080" 34 | volumes: 35 | - ./test/config:/config 36 | command: "-V -D -c /config/websocket.cfg --auth mytoken" 37 | 38 | nats-user: 39 | image: nats 40 | ports: 41 | - "4225:4222" 42 | - "8085:8080" 43 | volumes: 44 | - ./test/config:/config 45 | command: "-V -D -c /config/websocket.cfg --user foo --pass bar" 46 | 47 | nats-nkey: 48 | image: nats 49 | ports: 50 | - "4226:4222" 51 | - "8086:8080" 52 | volumes: 53 | - ./test/config:/config 54 | command: "-V -D -c /config/nkey.cfg" 55 | 56 | nats-tls: 57 | image: nats 58 | ports: 59 | - "4443:4222" 60 | - "8443:443" 61 | volumes: 62 | - ./test/config:/config 63 | command: "-V -D -c /config/wss.cfg --tls --tlscert=/config/server-cert.pem --tlskey=/config/server-key.pem" 64 | 65 | -------------------------------------------------------------------------------- /lib/src/inbox.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'dart:typed_data'; 3 | 4 | var _nuid = Nuid(); 5 | 6 | ///generate inbox 7 | String newInbox({String inboxPrefix = '_INBOX', bool secure = true}) { 8 | if (secure) { 9 | _nuid = Nuid(); 10 | } 11 | return inboxPrefix + _nuid.next(); 12 | } 13 | 14 | ///nuid port from go nats 15 | class Nuid { 16 | static const _digits = 17 | '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; 18 | static const _base = 62; 19 | static const _maxSeq = 839299365868340224; // base^seqLen == 62^10; 20 | static const _minInc = 33; 21 | static const _maxInc = 333; 22 | static const _preLen = 12; 23 | static const _seqLen = 10; 24 | static const _totalLen = _preLen + _seqLen; 25 | 26 | late Uint8List _pre; // check initial 27 | late int _seq; 28 | late int _inc; 29 | 30 | static final Nuid _nuid = Nuid._createInstance(); 31 | 32 | Nuid._createInstance() { 33 | randomizePrefix(); 34 | resetSequential(); 35 | } 36 | 37 | ///constructure 38 | Nuid() { 39 | randomizePrefix(); 40 | resetSequential(); 41 | } 42 | 43 | /// get instance 44 | static getInstance() { 45 | return _nuid; 46 | } 47 | 48 | ///generate next nuid 49 | String next() { 50 | _seq = _seq + _inc; 51 | if (_seq >= _maxSeq) { 52 | randomizePrefix(); 53 | resetSequential(); 54 | } 55 | var s = _seq; 56 | var b = List.from(_pre); 57 | b.addAll(Uint8List(_seqLen)); 58 | for (int? i = _totalLen, l = s; i! > _preLen; l = l ~/ _base) { 59 | i -= 1; 60 | b[i] = _digits.codeUnits[l! % _base]; 61 | } 62 | return String.fromCharCodes(b); 63 | } 64 | 65 | ///reset sequential 66 | void resetSequential() { 67 | Random(); 68 | var _rng = Random.secure(); 69 | 70 | _seq = _rng.nextInt(1 << 31) << 32 | _rng.nextInt(1 << 31); 71 | if (_seq > _maxSeq) { 72 | _seq = _seq % _maxSeq; 73 | } 74 | _inc = _minInc + _rng.nextInt(_maxInc - _minInc); 75 | } 76 | 77 | ///random new prefix 78 | void randomizePrefix() { 79 | _pre = Uint8List(_preLen); 80 | var _rng = Random.secure(); 81 | for (var i = 0; i < _preLen; i++) { 82 | var n = _rng.nextInt(255) % _base; 83 | _pre[i] = _digits.codeUnits[n]; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /test/z_continuous_test.dart: -------------------------------------------------------------------------------- 1 | @Timeout(Duration(seconds: 300)) 2 | import 'dart:isolate'; 3 | 4 | import 'package:dart_nats/dart_nats.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | const iteration = 1000; 8 | void run(SendPort sendPort) async { 9 | var client = Client(); 10 | await client.connect(Uri.parse('ws://localhost:8080')); 11 | for (var i = 0; i < iteration; i++) { 12 | client.pubString('iso', i.toString()); 13 | //commend out for reproduce issue#4 14 | await Future.delayed(Duration(milliseconds: 1)); 15 | } 16 | await client.ping(); 17 | await client.close(); 18 | sendPort.send('finish'); 19 | } 20 | 21 | void main() { 22 | group('all', () { 23 | test('continuous', () async { 24 | var client = Client(); 25 | await client.connect(Uri.parse('nats://localhost:4222')); 26 | var sub = client.sub('iso'); 27 | var r = 0; 28 | 29 | sub.stream.listen((msg) { 30 | if (r % 1000 == 0) { 31 | // print(msg.string); 32 | } 33 | r++; 34 | }); 35 | 36 | var receivePort = ReceivePort(); 37 | var iso = await Isolate.spawn(run, receivePort.sendPort); 38 | var out = await receivePort.first; 39 | // print(out); 40 | expect(out, equals('finish')); 41 | iso.kill(); 42 | //wait for last message send round trip to server 43 | await Future.delayed(Duration(seconds: 3)); 44 | 45 | await sub.close(); 46 | await client.close(); 47 | 48 | expect(r, equals(iteration)); 49 | }); 50 | test('continuous with ack', () async { 51 | var client = Client(); 52 | await client.connect(Uri.parse('nats://localhost:4222'), 53 | connectOption: ConnectOption(verbose: true)); 54 | var sub = client.sub('iso'); 55 | var r = 0; 56 | 57 | sub.stream.listen((msg) { 58 | if (r % 1000 == 0) { 59 | // print(msg.string); 60 | } 61 | r++; 62 | }); 63 | 64 | var receivePort = ReceivePort(); 65 | var iso = await Isolate.spawn(run, receivePort.sendPort); 66 | var out = await receivePort.first; 67 | // print(out); 68 | expect(out, equals('finish')); 69 | iso.kill(); 70 | //wait for last message send round trip to server 71 | await Future.delayed(Duration(seconds: 3)); 72 | 73 | await sub.close(); 74 | await client.close(); 75 | 76 | expect(r, equals(iteration)); 77 | }); 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /test/tls_connect_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:dart_nats/dart_nats.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | // mkcert -install 8 | // mkcert -cert-file server-cert.pem -key-file server-key.pem localhost ::1 9 | // nats-server --tls --tlscert=server-cert.pem --tlskey=server-key.pem -ms 8222 10 | // move both files to test/config 11 | 12 | void main() { 13 | group('all', () { 14 | test('nats:', () async { 15 | var client = Client(); 16 | client.acceptBadCert = true; 17 | await client.connect(Uri.parse('nats://localhost:4443')); 18 | var sub = client.sub('subject1'); 19 | var result = await client.pub( 20 | 'subject1', Uint8List.fromList('message1'.codeUnits), 21 | buffer: false); 22 | expect(result, true); 23 | 24 | var msg = await sub.stream.first; 25 | await client.close(); 26 | expect(String.fromCharCodes(msg.byte), equals('message1')); 27 | }); 28 | test('tls:', () async { 29 | var client = Client(); 30 | client.acceptBadCert = true; 31 | await client.connect(Uri.parse('tls://localhost:4443')); 32 | var sub = client.sub('subject1'); 33 | var result = await client.pub( 34 | 'subject1', Uint8List.fromList('message1'.codeUnits), 35 | buffer: false); 36 | expect(result, true); 37 | 38 | var msg = await sub.stream.first; 39 | await client.close(); 40 | expect(String.fromCharCodes(msg.byte), equals('message1')); 41 | }); 42 | test('wss:', () async { 43 | HttpOverrides.global = MyHttpOverrides(); 44 | 45 | var client = Client(); 46 | client.acceptBadCert = true; 47 | await client.connect(Uri.parse('wss://localhost:8443')); 48 | var sub = client.sub('subject1'); 49 | var result = await client.pub( 50 | 'subject1', Uint8List.fromList('message1'.codeUnits), 51 | buffer: false); 52 | expect(result, true); 53 | 54 | var msg = await sub.stream.first; 55 | await client.close(); 56 | expect(String.fromCharCodes(msg.byte), equals('message1')); 57 | }); 58 | }); 59 | } 60 | 61 | class MyHttpOverrides extends HttpOverrides { 62 | @override 63 | HttpClient createHttpClient(SecurityContext? context) { 64 | return super.createHttpClient(context) 65 | ..badCertificateCallback = 66 | (X509Certificate cert, String host, int port) => 67 | true; // add your localhost detection logic here if you want 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/nkey_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dart_nats/dart_nats.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | var port = 8084; 7 | 8 | void main() { 9 | group('all', () { 10 | test('decode', () async { 11 | var nkeys = Nkeys.fromSeed( 12 | 'SUAKJESHKJ5POJJINJFMCVYAASA7LQTL5ZOMMTYOWRZCM3JRZRS3OIVKZA'); 13 | var p = nkeys.publicKey(); 14 | print(p); 15 | var r = Nkeys.decode(PrefixByteUser, p); 16 | print(r); 17 | // no exception should be OK 18 | }); 19 | test('seed', () async { 20 | var nkeys = Nkeys.fromSeed( 21 | 'SUAKJESHKJ5POJJINJFMCVYAASA7LQTL5ZOMMTYOWRZCM3JRZRS3OIVKZA'); 22 | var s = nkeys.seed; 23 | expect(s, 24 | equals('SUAKJESHKJ5POJJINJFMCVYAASA7LQTL5ZOMMTYOWRZCM3JRZRS3OIVKZA')); 25 | }); 26 | test('public key', () async { 27 | var nkeys = Nkeys.fromSeed( 28 | 'SUAKJESHKJ5POJJINJFMCVYAASA7LQTL5ZOMMTYOWRZCM3JRZRS3OIVKZA'); 29 | var p = nkeys.publicKey(); 30 | expect(p, 31 | equals('UBYKMUQEJ7U2KFHB37IUOX6NBTJAWGY6SDO3DFRVOBNXVDUPPOTNWXD5')); 32 | 33 | //second test case 34 | nkeys = Nkeys.fromSeed( 35 | 'SUACSSL3UAHUDXKFSNVUZRF5UHPMWZ6BFDTJ7M6USDXIEDNPPQYYYCU3VY'); 36 | p = nkeys.publicKey(); 37 | expect(p, 38 | equals('UDXU4RCSJNZOIQHZNWXHXORDPRTGNJAHAHFRGZNEEJCPQTT2M7NLCNF4')); 39 | }); 40 | test('private key', () async { 41 | var nkeys = Nkeys.fromSeed( 42 | 'SUAKJESHKJ5POJJINJFMCVYAASA7LQTL5ZOMMTYOWRZCM3JRZRS3OIVKZA'); 43 | var p = nkeys.privateKey(); 44 | 45 | expect( 46 | p, 47 | equals( 48 | 'PCSJER2SPL3SKKDKJLAVOAAEQH24E27OLTDE6DVUOITG2MOMMW3SE4FGKICE72NFCTQ57UKHL7GQZUQLDMPJBXNRSY2XAW32R2HXXJW3A6AQ')); 49 | }); 50 | test('create', () async { 51 | var userPK = Nkeys.createUser(); 52 | expect(userPK.publicKey()[0], equals('U')); 53 | var accountPK = Nkeys.createAccount(); 54 | expect(accountPK.publicKey()[0], equals('A')); 55 | var operatorPK = Nkeys.createOperator(); 56 | expect(operatorPK.publicKey()[0], equals('O')); 57 | }); 58 | test('verify', () async { 59 | var sig = base64Decode( 60 | 'WosANJXgeyxerXFo0twRiMG+/ZjYp1K/46bFeFax705yFTCTjM18jWl01gGYk4KKbadiHd+hP3WgUQ2iLZUAAA=='); 61 | var nonce = 'DhXdTMAeiHhLDig'; 62 | var seed = 'SUACSSL3UAHUDXKFSNVUZRF5UHPMWZ6BFDTJ7M6USDXIEDNPPQYYYCU3VY'; 63 | var nkeys = Nkeys.fromSeed(seed); 64 | 65 | var result = Nkeys.verify(nkeys.publicKey(), utf8.encode(nonce), sig); 66 | expect(result, equals(true)); 67 | }); 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /example/flutter/main_dart: -------------------------------------------------------------------------------- 1 | // Please rename from main_dart to main.dart and replace in flutter lib/ folder 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:dart_nats/dart_nats.dart' as nats; 5 | 6 | void main() => runApp(MyApp()); 7 | 8 | class MyApp extends StatelessWidget { 9 | @override 10 | Widget build(BuildContext context) { 11 | return MaterialApp( 12 | home: MyHomePage(), 13 | ); 14 | } 15 | } 16 | 17 | class MyHomePage extends StatefulWidget { 18 | _MyHomePageState createState() => _MyHomePageState(); 19 | } 20 | 21 | class _MyHomePageState extends State { 22 | TextEditingController _controller = TextEditingController(); 23 | nats.Client natsClient; 24 | nats.Subscription fooSub, barSub; 25 | 26 | void initState() { 27 | super.initState(); 28 | connect(); 29 | } 30 | 31 | void connect() { 32 | natsClient = nats.Client(); 33 | //change to ws://localhost:80 or wss://localhost:443 if you connect using futter web to localhost 34 | natsClient.connect(Uri.parse('ws://10.0.2.2:80'); 35 | fooSub = natsClient.sub('foo'); 36 | barSub = natsClient.sub('bar'); 37 | } 38 | 39 | Widget build(BuildContext context) { 40 | return Scaffold( 41 | appBar: AppBar(), 42 | body: Column( 43 | crossAxisAlignment: CrossAxisAlignment.start, 44 | children: [ 45 | Form( 46 | child: TextFormField( 47 | controller: _controller, 48 | decoration: InputDecoration(labelText: 'publish'), 49 | ), 50 | ), 51 | Text('foo message:'), 52 | StreamBuilder( 53 | stream: fooSub.stream, 54 | builder: (context, AsyncSnapshot snapshot) { 55 | return Text(snapshot.hasData ? '${snapshot.data.string}' : ''); 56 | }, 57 | ), 58 | Text('bar message:'), 59 | StreamBuilder( 60 | stream: barSub.stream, 61 | builder: (context, AsyncSnapshot snapshot) { 62 | return Text(snapshot.hasData ? '${snapshot.data.string}' : ''); 63 | }, 64 | ), 65 | Row( 66 | children: [ 67 | RaisedButton( 68 | child: Text('pub to foo'), 69 | onPressed: () => _publishMessage('foo')), 70 | RaisedButton( 71 | child: Text('pub to bar'), 72 | onPressed: () => _publishMessage('bar')), 73 | ], 74 | ), 75 | ], 76 | ), 77 | ); 78 | } 79 | 80 | void _publishMessage(String subject) { 81 | if (_controller.text.isNotEmpty) { 82 | natsClient.pubString(subject, _controller.text); 83 | } 84 | } 85 | 86 | void dispose() { 87 | natsClient.close(); 88 | super.dispose(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /test/authen_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:dart_nats/dart_nats.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | var port = 8084; 7 | 8 | void main() { 9 | group('all', () { 10 | test('token', () async { 11 | var client = Client(); 12 | await client.connect(Uri.parse('ws://localhost:8084'), 13 | connectOption: ConnectOption(authToken: 'mytoken')); 14 | var sub = client.sub('subject1'); 15 | var result = await client.pub( 16 | 'subject1', Uint8List.fromList('message1'.codeUnits), 17 | buffer: false); 18 | expect(result, true); 19 | 20 | var msg = await sub.stream.first; 21 | await client.close(); 22 | expect(String.fromCharCodes(msg.byte), equals('message1')); 23 | }); 24 | test('user', () async { 25 | var client = Client(); 26 | await client.connect(Uri.parse('ws://localhost:8085'), 27 | connectOption: ConnectOption(user: 'foo', pass: 'bar')); 28 | var sub = client.sub('subject1'); 29 | var result = await client.pub( 30 | 'subject1', Uint8List.fromList('message1'.codeUnits), 31 | buffer: false); 32 | expect(result, true); 33 | 34 | var msg = await sub.stream.first; 35 | await client.close(); 36 | expect(String.fromCharCodes(msg.byte), equals('message1')); 37 | }); 38 | test('jwt', () async { 39 | var client = Client(); 40 | client.seed = 41 | 'SUAJGSBAKQHGYI7ZVKVR6WA7Z5U52URHKGGT6ZICUJXMG4LCTC2NTLQSF4'; 42 | await client.connect( 43 | Uri.parse('nats://localhost:4223'), 44 | retryInterval: 1, 45 | connectOption: ConnectOption( 46 | jwt: 47 | 'eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJBU1pFQVNGMzdKS0dPTFZLTFdKT1hOM0xZUkpHNURJUFczUEpVT0s0WUlDNFFENlAyVFlRIiwiaWF0IjoxNjY0NTI0OTU5LCJpc3MiOiJBQUdTSkVXUlFTWFRDRkUzRVE3RzVPQldSVUhaVVlDSFdSM0dRVERGRldaSlM1Q1JLTUhOTjY3SyIsIm5hbWUiOiJzaWdudXAiLCJzdWIiOiJVQzZCUVY1Tlo1V0pQRUVZTTU0UkZBNU1VMk5NM0tON09WR01DU1VaV1dORUdZQVBNWEM0V0xZUCIsIm5hdHMiOnsicHViIjp7fSwic3ViIjp7fSwic3VicyI6LTEsImRhdGEiOi0xLCJwYXlsb2FkIjotMSwidHlwZSI6InVzZXIiLCJ2ZXJzaW9uIjoyfX0.8Q0HiN0h2tBvgpF2cAaz2E3WLPReKEnSmUWT43NSlXFNRpsCWpmkikxGgFn86JskEN4yast1uSj306JdOhyJBA', 48 | ), 49 | ); 50 | var sub = client.sub('subject.foo'); 51 | client.pubString('subject.foo', 'message1'); 52 | var msg = await sub.stream.first; 53 | await client.close(); 54 | expect(String.fromCharCodes(msg.byte), equals('message1')); 55 | }); 56 | test('nkey', () async { 57 | var client = Client(); 58 | client.seed = 59 | 'SUACSSL3UAHUDXKFSNVUZRF5UHPMWZ6BFDTJ7M6USDXIEDNPPQYYYCU3VY'; 60 | await client.connect( 61 | Uri.parse('ws://localhost:8086'), 62 | retryInterval: 1, 63 | connectOption: ConnectOption( 64 | nkey: 'UDXU4RCSJNZOIQHZNWXHXORDPRTGNJAHAHFRGZNEEJCPQTT2M7NLCNF4', 65 | ), 66 | ); 67 | var sub = client.sub('subject.foo'); 68 | client.pubString('subject.foo', 'message1'); 69 | var msg = await sub.stream.first; 70 | await client.close(); 71 | expect(String.fromCharCodes(msg.byte), equals('message1')); 72 | }); 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /test/config/jwt2.cfg: -------------------------------------------------------------------------------- 1 | // Operator "wonderful_ramanujan" 2 | operator: eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJNVlFLUU1JNjJHTFRCVFBZQVZURUlTNkk3RFRYUlc1MlBaQ0lHM0ZQWUpIQkVQMktTQ0hRIiwiaWF0IjoxNjY3NzE0NTE3LCJpc3MiOiJPQ1VQTTZVNUxZQURaQUxDM0QzUUZKSEFPU0UyUUdHMkhBMjdQVlREN1FGQ01JNEhONVdYQ1VCNiIsIm5hbWUiOiJ3b25kZXJmdWxfcmFtYW51amFuIiwic3ViIjoiT0NVUE02VTVMWUFEWkFMQzNEM1FGSkhBT1NFMlFHRzJIQTI3UFZURDdRRkNNSTRITjVXWENVQjYiLCJuYXRzIjp7Im9wZXJhdG9yX3NlcnZpY2VfdXJscyI6WyJuYXRzOi8vbG9jYWxob3N0OjQyMjIiXSwic3lzdGVtX2FjY291bnQiOiJBQVgzRjZZVUZOQjU3RzZBWlhXWE01NFdPNE5MQVQyNEJDU1RVUEVFSDJTUFJPRllDUFNJM09aNiIsInR5cGUiOiJvcGVyYXRvciIsInZlcnNpb24iOjJ9fQ.HMNeDnNSl4nlhkZlj-7o14Tbj1Sjz2fDNpiGjcCBAjKMyfrgQKfS_Tg8UftJnA0ZGG5UQNPGdG0Ihk9BDTgUCg 3 | 4 | system_account: AAX3F6YUFNB57G6AZXWXM54WO4NLAT24BCSTUPEEH2SPROFYCPSI3OZ6 5 | 6 | resolver: MEMORY 7 | 8 | resolver_preload: { 9 | // Account "SYS" 10 | AAX3F6YUFNB57G6AZXWXM54WO4NLAT24BCSTUPEEH2SPROFYCPSI3OZ6: eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJDTkNGRVlVRjdYS0NRS0NCU1g2TUU0UVg3SzRMSkZTUldGTlVYN01PUkxLWjNMT01QT09RIiwiaWF0IjoxNjY3NzE0NTE3LCJpc3MiOiJPQ1VQTTZVNUxZQURaQUxDM0QzUUZKSEFPU0UyUUdHMkhBMjdQVlREN1FGQ01JNEhONVdYQ1VCNiIsIm5hbWUiOiJTWVMiLCJzdWIiOiJBQVgzRjZZVUZOQjU3RzZBWlhXWE01NFdPNE5MQVQyNEJDU1RVUEVFSDJTUFJPRllDUFNJM09aNiIsIm5hdHMiOnsiZXhwb3J0cyI6W3sibmFtZSI6ImFjY291bnQtbW9uaXRvcmluZy1zdHJlYW1zIiwic3ViamVjdCI6IiRTWVMuQUNDT1VOVC4qLlx1MDAzZSIsInR5cGUiOiJzdHJlYW0iLCJhY2NvdW50X3Rva2VuX3Bvc2l0aW9uIjozLCJkZXNjcmlwdGlvbiI6IkFjY291bnQgc3BlY2lmaWMgbW9uaXRvcmluZyBzdHJlYW0iLCJpbmZvX3VybCI6Imh0dHBzOi8vZG9jcy5uYXRzLmlvL25hdHMtc2VydmVyL2NvbmZpZ3VyYXRpb24vc3lzX2FjY291bnRzIn0seyJuYW1lIjoiYWNjb3VudC1tb25pdG9yaW5nLXNlcnZpY2VzIiwic3ViamVjdCI6IiRTWVMuUkVRLkFDQ09VTlQuKi4qIiwidHlwZSI6InNlcnZpY2UiLCJyZXNwb25zZV90eXBlIjoiU3RyZWFtIiwiYWNjb3VudF90b2tlbl9wb3NpdGlvbiI6NCwiZGVzY3JpcHRpb24iOiJSZXF1ZXN0IGFjY291bnQgc3BlY2lmaWMgbW9uaXRvcmluZyBzZXJ2aWNlcyBmb3I6IFNVQlNaLCBDT05OWiwgTEVBRlosIEpTWiBhbmQgSU5GTyIsImluZm9fdXJsIjoiaHR0cHM6Ly9kb2NzLm5hdHMuaW8vbmF0cy1zZXJ2ZXIvY29uZmlndXJhdGlvbi9zeXNfYWNjb3VudHMifV0sImxpbWl0cyI6eyJzdWJzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJpbXBvcnRzIjotMSwiZXhwb3J0cyI6LTEsIndpbGRjYXJkcyI6dHJ1ZSwiY29ubiI6LTEsImxlYWYiOi0xfSwic2lnbmluZ19rZXlzIjpbIkFBVUU3VlpJV0xBWkZaWVdBRUpSVk0zVFVBWVdGS1FaMzZHWTMzUURIM0hORzJFTUxPQlNMNlVSIl0sImRlZmF1bHRfcGVybWlzc2lvbnMiOnsicHViIjp7fSwic3ViIjp7fX0sInR5cGUiOiJhY2NvdW50IiwidmVyc2lvbiI6Mn19.ZqEo41X-rAuQJMHpH0cc4VKy8zQzaEqNi9DtubGS7u48uBECZHyQrjzcxsuBpuoqrSBn_lw0NgITaGaDkHAgDA 11 | 12 | // Account "wonderful_ramanujan" 13 | AAXFOZU4UHKYBM5BQVOCMK5FXJEBNQRSCYQ2JM3T67RTJQNXU6QJKKFC: eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJEUUNKMzJHSjI3Mk5OWlNNSUNaVlBDSlFJUE81SE1aT1hCN1NGUDdGQ1haTDVSRTJUUlRBIiwiaWF0IjoxNjY3NzE0NTE3LCJpc3MiOiJPQ1VQTTZVNUxZQURaQUxDM0QzUUZKSEFPU0UyUUdHMkhBMjdQVlREN1FGQ01JNEhONVdYQ1VCNiIsIm5hbWUiOiJ3b25kZXJmdWxfcmFtYW51amFuIiwic3ViIjoiQUFYRk9aVTRVSEtZQk01QlFWT0NNSzVGWEpFQk5RUlNDWVEySk0zVDY3UlRKUU5YVTZRSktLRkMiLCJuYXRzIjp7ImxpbWl0cyI6eyJzdWJzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJpbXBvcnRzIjotMSwiZXhwb3J0cyI6LTEsIndpbGRjYXJkcyI6dHJ1ZSwiY29ubiI6LTEsImxlYWYiOi0xfSwiZGVmYXVsdF9wZXJtaXNzaW9ucyI6eyJwdWIiOnt9LCJzdWIiOnt9fSwidHlwZSI6ImFjY291bnQiLCJ2ZXJzaW9uIjoyfX0.QWCXfW-BAxDM0KNUMXYLLSTS3rY9cCesWt17_8ozlqzvhKdEoppgg-Tdy503GfvRitc7XPIbi2VPdeoPaPIDDQ 14 | 15 | } 16 | -------------------------------------------------------------------------------- /test/defect/0000_0100_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:dart_nats/dart_nats.dart'; 4 | import 'package:test/test.dart'; 5 | // larger MSG payloads not always working, check if full payload present in buffer #20 6 | // while (_receiveState == _ReceiveState.idle && _buffer.contains(13)) { 7 | 8 | // var n13 = _buffer.indexOf(13); 9 | // var msgFull = String.fromCharCodes(_buffer.take(n13)).toLowerCase().trim(); 10 | // var msgList = msgFull.split(' '); 11 | // var msgType = msgList[0]; 12 | // //print('... process $msgType ${_buffer.length}'); 13 | 14 | // if (msgType == 'msg') { 15 | // var len = int.parse((msgList.length == 4 ? msgList[3] : msgList[4])); 16 | // if (len > 0 && _buffer.length < (msgFull.length + len + 4)) { 17 | // break; // not a full payload, go around again 18 | // } 19 | // } 20 | 21 | // _processOp(); 22 | // } 23 | void main() { 24 | group('all', () { 25 | // test('0016 ws: Connect to invalid ws connection does not give error', 26 | // () async { 27 | // var client = Client(); 28 | // var gotit = false; 29 | // client.statusStream.listen( 30 | // (s) { 31 | // print(s); 32 | // }, 33 | // onError: (e) { 34 | // gotit = true; 35 | // }, 36 | // ); 37 | // try { 38 | // await client.connect(Uri.parse('ws://localhost:1234'), 39 | // retry: false, retryInterval: 1); 40 | // } on NatsException { 41 | // gotit = true; 42 | // } on WebSocketChannelException { 43 | // gotit = true; 44 | // } catch (e) { 45 | // gotit = true; 46 | // } 47 | // await client.close(); 48 | // expect(gotit, equals(true)); 49 | // }); 50 | test('0016 nats: Connect to invalid ws connection does not give error', 51 | () async { 52 | var client = Client(); 53 | var gotit = false; 54 | try { 55 | await client.connect(Uri.parse('nats://localhost:1234'), 56 | retry: false, retryInterval: 1); 57 | } on NatsException { 58 | gotit = true; 59 | } 60 | expect(gotit, equals(true)); 61 | }); 62 | test( 63 | '0020 larger MSG payloads not always working, check if full payload present in buffer', 64 | () async { 65 | var client = Client(); 66 | unawaited(client.connect(Uri.parse('nats://localhost'))); 67 | var sub = client.sub('subject1'); 68 | var str21k = ''; 69 | for (var i = 0; i < 21000; i++) { 70 | str21k += '${i % 10}'; 71 | } 72 | client.pubString('subject1', str21k); 73 | var msg = await sub.stream.first; 74 | await client.close(); 75 | expect(msg.string, equals(str21k)); 76 | }); 77 | test('0022 Connection to nats with macos and mobile:', () async { 78 | var client = Client(); 79 | unawaited( 80 | client.connect(Uri.parse('nats://demo.nats.io'), retryInterval: 1)); 81 | var sub = client.sub('subject1'); 82 | client.pubString('subject1', 'message1'); 83 | var msg = await sub.stream.first; 84 | await client.close(); 85 | expect(String.fromCharCodes(msg.byte), equals('message1')); 86 | }); 87 | }); 88 | } 89 | -------------------------------------------------------------------------------- /lib/src/message.dart: -------------------------------------------------------------------------------- 1 | ///message model sending from NATS server 2 | import 'dart:convert'; 3 | import 'dart:typed_data'; 4 | 5 | import 'client.dart'; 6 | 7 | /// Message Header 8 | class Header { 9 | /// header version 10 | String version; 11 | 12 | /// headers key value 13 | Map? headers; 14 | 15 | /// constructor 16 | Header({this.headers, this.version = 'NATS/1.0'}) { 17 | this.headers ??= {}; 18 | } 19 | 20 | /// add key, value 21 | Header add(String key, String value) { 22 | headers![key] = value; 23 | return this; 24 | } 25 | 26 | /// get value from key 27 | /// return null if notfound 28 | String? get(String key) { 29 | return headers![key]; 30 | } 31 | 32 | /// construct from bytes 33 | static Header fromBytes(Uint8List b) { 34 | var str = utf8.decode(b); 35 | Map m = {}; 36 | var strList = str.split('\r\n'); 37 | var version = strList[0]; 38 | strList.removeAt(0); 39 | for (var h in strList) { 40 | /// values of headers can contain ':' so find the first index for the 41 | /// correct split index 42 | var splitIndex = h.indexOf(':'); 43 | 44 | /// if the index is <= to 0 it means there was either no ':' or its the 45 | /// first character. In either case its not a valid header to split. 46 | if (splitIndex <= 0) { 47 | continue; 48 | } 49 | var key = h.substring(0, splitIndex); 50 | var value = h.substring(splitIndex + 1); 51 | m[key] = value; 52 | } 53 | 54 | return Header(headers: m, version: version); 55 | } 56 | 57 | /// convert to bytes 58 | Uint8List toBytes() { 59 | var str = '${this.version}\r\n'; 60 | 61 | headers?.forEach((k, v) { 62 | str = str + '$k:$v\r\n'; 63 | }); 64 | 65 | return Uint8List.fromList(utf8.encode(str)); 66 | } 67 | } 68 | 69 | /// Message class 70 | class Message { 71 | ///subscriber id auto generate by client 72 | final int sid; 73 | 74 | /// subject and replyto 75 | final String? subject, replyTo; 76 | final Client _client; 77 | 78 | /// message header 79 | final Header? header; 80 | 81 | ///payload of data in byte 82 | final Uint8List byte; 83 | 84 | ///convert from json string to T for structure data 85 | T Function(String)? jsonDecoder; 86 | 87 | ///payload of data in byte 88 | T get data { 89 | // if (jsonDecoder == null) throw Exception('no converter. can not convert. use msg.byte instead'); 90 | if (jsonDecoder == null) { 91 | return byte as T; 92 | } 93 | return jsonDecoder!(string); 94 | } 95 | 96 | ///constructor 97 | Message(this.subject, this.sid, this.byte, this._client, 98 | {this.replyTo, this.jsonDecoder, this.header}); 99 | 100 | ///payload in string 101 | String get string => utf8.decode(byte); 102 | 103 | ///Respond to message 104 | bool respond(Uint8List data) { 105 | if (replyTo == null || replyTo == '') return false; 106 | _client.pub(replyTo, data); 107 | return true; 108 | } 109 | 110 | ///Respond to string message 111 | bool respondString(String str) { 112 | return respond(Uint8List.fromList(utf8.encode(str))); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /test/chrome_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:dart_nats/dart_nats.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | //dart test test\chrome_test.dart -p chrome 8 | 9 | void main() { 10 | group('all', () { 11 | test('ws', () async { 12 | var client = Client(); 13 | unawaited( 14 | client.connect(Uri.parse('ws://localhost:8080'), retryInterval: 1)); 15 | var sub = client.sub('subject1'); 16 | await client.pub('subject1', Uint8List.fromList('message1'.codeUnits)); 17 | var msg = await sub.stream.first; 18 | await client.close(); 19 | expect(String.fromCharCodes(msg.byte), equals('message1')); 20 | }); 21 | test('await', () async { 22 | var client = Client(); 23 | await client.connect(Uri.parse('ws://localhost:8080')); 24 | var sub = client.sub('subject1'); 25 | var result = await client.pub( 26 | 'subject1', Uint8List.fromList('message1'.codeUnits), 27 | buffer: false); 28 | expect(result, true); 29 | 30 | var msg = await sub.stream.first; 31 | await client.close(); 32 | expect(String.fromCharCodes(msg.byte), equals('message1')); 33 | }); 34 | test('reconnect', () async { 35 | var client = Client(); 36 | await client.connect(Uri.parse('ws://localhost:8080')); 37 | var sub = client.sub('subject1'); 38 | var result = await client.pub( 39 | 'subject1', Uint8List.fromList('message1'.codeUnits), 40 | buffer: false); 41 | expect(result, true); 42 | 43 | var msg = await sub.stream.first; 44 | await client.close(); 45 | expect(String.fromCharCodes(msg.byte), equals('message1')); 46 | 47 | await client.connect(Uri.parse('ws://localhost:8080')); 48 | result = await client.pub( 49 | 'subject1', Uint8List.fromList('message2'.codeUnits), 50 | buffer: false); 51 | expect(result, true); 52 | msg = await sub.stream.first; 53 | expect(String.fromCharCodes(msg.byte), equals('message2')); 54 | }); 55 | test('status stream', () async { 56 | var client = Client(); 57 | var statusHistory = []; 58 | client.statusStream.listen((s) { 59 | // print(s); 60 | statusHistory.add(s); 61 | }); 62 | await client.connect(Uri.parse('ws://localhost:8080')); 63 | await client.close(); 64 | 65 | // no runtime error should be fine 66 | // expect only first and last status 67 | expect(statusHistory.first, equals(Status.connecting)); 68 | expect(statusHistory.last, equals(Status.closed)); 69 | }); 70 | test('status stream detail', () async { 71 | var client = Client(); 72 | var statusHistory = []; 73 | client.statusStream.listen((s) { 74 | // print(s); 75 | statusHistory.add(s); 76 | }); 77 | await client.connect( 78 | Uri.parse('ws://localhost:8080'), 79 | retry: true, 80 | retryCount: 3, 81 | retryInterval: 1, 82 | ); 83 | await client.close(); 84 | 85 | // no runtime error should be fine 86 | // expect only first and last status 87 | expect(statusHistory.length, equals(4)); 88 | expect(statusHistory[0], equals(Status.connecting)); 89 | expect(statusHistory[1], equals(Status.infoHandshake)); 90 | expect(statusHistory[2], equals(Status.connected)); 91 | expect(statusHistory[3], equals(Status.closed)); 92 | }); 93 | }); 94 | } 95 | -------------------------------------------------------------------------------- /test/config/jwt.cfg: -------------------------------------------------------------------------------- 1 | // Operator "schedoOperator" 2 | operator: eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJZRVBITVg0SlFMRFJXVUdSQU9VR1pUUjIzTVhZSVlEWEdBVVZBNDcyVFpHTkY3RTNPWkxRIiwiaWF0IjoxNjY0NTEwMTAwLCJpc3MiOiJPQVE3WjM2NFNZUUMyU1dESktDRkZERjJHWDZRRFpEQ0xYRVZXT1hJSkpLR0RIV0lQWlpPRVNRUyIsIm5hbWUiOiJzY2hlZG9PcGVyYXRvciIsInN1YiI6Ik9BUTdaMzY0U1lRQzJTV0RKS0NGRkRGMkdYNlFEWkRDTFhFVldPWElKSktHREhXSVBaWk9FU1FTIiwibmF0cyI6eyJzaWduaW5nX2tleXMiOlsiT0RHT0lNR09JVEdTMkVXQUU0T1pJVTREWTcyUDZVU040QlE0SVVKM1VQNTdFWTRGNlBNSVBETEEiXSwiYWNjb3VudF9zZXJ2ZXJfdXJsIjoibmF0czovL2xvY2FsaG9zdDo0MjIyIiwib3BlcmF0b3Jfc2VydmljZV91cmxzIjpbIm5hdHM6Ly9sb2NhbGhvc3Q6NDIyMiJdLCJzeXN0ZW1fYWNjb3VudCI6IkFDVjNaNVEzTTdXVFFUTEhIRFZKWlFOUEFQWDVVWjdFNE1MSk5SWFlIUkFXQ0RTR0M3UDVOTEFFIiwidHlwZSI6Im9wZXJhdG9yIiwidmVyc2lvbiI6Mn19.ueHJ2vX18s9cRop9zBCCcxyuPaV3Cp9f35mxbKCGaIckxntL8Z7V24VqqUAF28WKLRD6i24sb0LqFYRzgherAw 3 | 4 | system_account: ACV3Z5Q3M7WTQTLHHDVJZQNPAPX5UZ7E4MLJNRXYHRAWCDSGC7P5NLAE 5 | 6 | resolver: MEMORY 7 | 8 | resolver_preload: { 9 | // Account "schedoAccount" 10 | AAGSJEWRQSXTCFE3EQ7G5OBWRUHZUYCHWR3GQTDFFWZJS5CRKMHNN67K: eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJFMlhTSVVZSzMyUEZEMldURllLNlRVRlVUWlVDM0FGQldFQldYRkpNTzdINVdUWlFLUkhBIiwiaWF0IjoxNjY0NTEwMTE4LCJpc3MiOiJPQVE3WjM2NFNZUUMyU1dESktDRkZERjJHWDZRRFpEQ0xYRVZXT1hJSkpLR0RIV0lQWlpPRVNRUyIsIm5hbWUiOiJzY2hlZG9BY2NvdW50Iiwic3ViIjoiQUFHU0pFV1JRU1hUQ0ZFM0VRN0c1T0JXUlVIWlVZQ0hXUjNHUVRERkZXWkpTNUNSS01ITk42N0siLCJuYXRzIjp7ImxpbWl0cyI6eyJzdWJzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJpbXBvcnRzIjotMSwiZXhwb3J0cyI6LTEsIndpbGRjYXJkcyI6dHJ1ZSwiY29ubiI6LTEsImxlYWYiOi0xfSwic2lnbmluZ19rZXlzIjpbIkFEQVRCSDNXUDZGUExNT1VVRUszNVEyM1NBR1FOSlRCTzUzVkVWQUJaQldPN05NM1BWS01ZWlJHIl0sImRlZmF1bHRfcGVybWlzc2lvbnMiOnsicHViIjp7fSwic3ViIjp7fX0sInR5cGUiOiJhY2NvdW50IiwidmVyc2lvbiI6Mn19.qVAC1r5pa-Q-6Y7ILoOqwncqJAp5iM3_e1JVKcAX-t4b4eUJAo4YC3Q7dNDD2v4lx7FaSWcpnQo3JaemxyeJBA 11 | 12 | // Account "SYS" 13 | ACV3Z5Q3M7WTQTLHHDVJZQNPAPX5UZ7E4MLJNRXYHRAWCDSGC7P5NLAE: eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJUWExPVlhDUjNNVkVDVTRFQU41VFQzR1FDRzZHMkJZTU1FSE4yV0xKR1ZTVTc0S01CTExBIiwiaWF0IjoxNjY0NTEwMDczLCJpc3MiOiJPREdPSU1HT0lUR1MyRVdBRTRPWklVNERZNzJQNlVTTjRCUTRJVUozVVA1N0VZNEY2UE1JUERMQSIsIm5hbWUiOiJTWVMiLCJzdWIiOiJBQ1YzWjVRM003V1RRVExISERWSlpRTlBBUFg1VVo3RTRNTEpOUlhZSFJBV0NEU0dDN1A1TkxBRSIsIm5hdHMiOnsiZXhwb3J0cyI6W3sibmFtZSI6ImFjY291bnQtbW9uaXRvcmluZy1zdHJlYW1zIiwic3ViamVjdCI6IiRTWVMuQUNDT1VOVC4qLlx1MDAzZSIsInR5cGUiOiJzdHJlYW0iLCJhY2NvdW50X3Rva2VuX3Bvc2l0aW9uIjozLCJkZXNjcmlwdGlvbiI6IkFjY291bnQgc3BlY2lmaWMgbW9uaXRvcmluZyBzdHJlYW0iLCJpbmZvX3VybCI6Imh0dHBzOi8vZG9jcy5uYXRzLmlvL25hdHMtc2VydmVyL2NvbmZpZ3VyYXRpb24vc3lzX2FjY291bnRzIn0seyJuYW1lIjoiYWNjb3VudC1tb25pdG9yaW5nLXNlcnZpY2VzIiwic3ViamVjdCI6IiRTWVMuUkVRLkFDQ09VTlQuKi4qIiwidHlwZSI6InNlcnZpY2UiLCJyZXNwb25zZV90eXBlIjoiU3RyZWFtIiwiYWNjb3VudF90b2tlbl9wb3NpdGlvbiI6NCwiZGVzY3JpcHRpb24iOiJSZXF1ZXN0IGFjY291bnQgc3BlY2lmaWMgbW9uaXRvcmluZyBzZXJ2aWNlcyBmb3I6IFNVQlNaLCBDT05OWiwgTEVBRlosIEpTWiBhbmQgSU5GTyIsImluZm9fdXJsIjoiaHR0cHM6Ly9kb2NzLm5hdHMuaW8vbmF0cy1zZXJ2ZXIvY29uZmlndXJhdGlvbi9zeXNfYWNjb3VudHMifV0sImxpbWl0cyI6eyJzdWJzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJpbXBvcnRzIjotMSwiZXhwb3J0cyI6LTEsIndpbGRjYXJkcyI6dHJ1ZSwiY29ubiI6LTEsImxlYWYiOi0xfSwic2lnbmluZ19rZXlzIjpbIkFDVUZPRVRXSVJQREk2NEkyWkZTTlBNUllYS0hWWDVIMkNHWlFOUUc0SUVZSVkySENQUzVNU0I3Il0sImRlZmF1bHRfcGVybWlzc2lvbnMiOnsicHViIjp7fSwic3ViIjp7fX0sInR5cGUiOiJhY2NvdW50IiwidmVyc2lvbiI6Mn19.PXkwxTPHLVL-lsKAJwx6OT5BLfRqrpfiGCYdzmZqRwBEjEgtSuWBQmg8ji44CjgX8TMMTPPakIpLm20wx0fWCQ 14 | 15 | } 16 | 17 | websocket { 18 | port: 8080 19 | no_tls: true 20 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.6.5 2 | * Issue resolved when app is in background. Thanks nileshsoni97 for contribution. 3 | 4 | ## 0.6.4 5 | 6 | * Fix Uint8List and List inconsistency. Thank myxzlpltk for contribution. 7 | 8 | ## 0.6.3 9 | 10 | * fix Exposing Security Context#31. 11 | * Fix missing headers in request response. 12 | 13 | 14 | ## 0.6.2 15 | 16 | * fix a bug that does not correctly parse headers containing the ':' character. Thank https://github.com/CoryHagerman for contribution. 17 | 18 | ## 0.6.1 19 | 20 | * fix nkeys decode issue 21 | 22 | ## 0.6.0 23 | 24 | * Support verbos acknowledge 25 | * Chang pub, pubString to async 26 | * Header support (hpub and hmsg) 27 | 28 | ## 0.5.1 29 | 30 | * Add retry 31 | 32 | ## 0.5.0 33 | 34 | * Add nkeys publicKey privateKey functions 35 | * Revamp rqeust 36 | * Custom inbox prefix 37 | * Inbound structure data 38 | 39 | ## 0.4.9 40 | 41 | * fix #22 error when connect to nats://demo.nats.io 42 | 43 | ## 0.4.8 44 | 45 | * fix #23 Unsupported operation: Platform._version with WebSocket 46 | 47 | ## 0.4.7 48 | 49 | * add generic type to client.request() 50 | * fix reconnect issue 51 | * fix retry issue 52 | 53 | ## 0.4.6 54 | 55 | * fix bug #16 Connect to invalid ws connection does not give error 56 | 57 | ## 0.4.5 58 | 59 | * fix bug wss: connecting bug 60 | 61 | ## 0.4.4 62 | 63 | * fix bug #20 larger MSG payloads not always working, check if full payload present in buffer 64 | 65 | ## 0.4.2 66 | 67 | * TLS support 68 | 69 | ## 0.4.1 70 | 71 | * fix wss://host:port 72 | 73 | ## 0.4.0 74 | 75 | * client.connect() support with url schema example ws://host:port or nats://nost:port 76 | * tls:// not support yet 77 | * discontinue client.tcpConnect() 78 | * add nkey authentication 79 | * add jwt authentication 80 | 81 | ## 0.3.5 82 | 83 | * Update readme 84 | 85 | ## 0.3.4 86 | 87 | * Support TCP socket as 0.2.x by client.tcpConnect() 88 | 89 | ## 0.3.3 90 | 91 | * Update package dependencies 92 | 93 | ## 0.3.2 94 | 95 | * Fix flutter web Nuid() error 96 | 97 | ## 0.3.1 98 | 99 | * Add statusStream 100 | * Add request timeout 101 | 102 | ## 0.3.0 103 | 104 | * Change transport from socket to WebSock 105 | * Support Flutter Web 106 | 107 | ## 0.2.0 108 | 109 | * Add user passwor authentication 110 | * Add token authentication 111 | * Convert to Null safety 112 | * Dart SDK version 2.12.0 - 3.0.0 113 | * fix inbox security 114 | 115 | ## 0.1.8 116 | 117 | * fix request error on second request 118 | 119 | ## 0.1.7 120 | 121 | * add async support for ping() 122 | * add message.respondString 123 | 124 | ## 0.1.6+1 125 | 126 | * Improve receive buffer handling 127 | 128 | ## 0.1.6 129 | 130 | * async connect 131 | * fix defect message delay when sub receive continuous message 132 | 133 | ## 0.1.5+1 134 | 135 | * fix defect 136 | 137 | ## 0.1.5 138 | 139 | * request/respond function 140 | * change some wording from payload to data 141 | 142 | ## 0.1.4+1 143 | 144 | * add inbox to generate unique inbox subject 145 | * add nuid to generate unique id 146 | 147 | ## 0.1.3+4 148 | 149 | * refactor code 150 | * add commend 151 | 152 | ## 0.1.3+1 153 | 154 | * add string api client.pubString and message.string 155 | * fix defect: pub sub non ascii 156 | * fix defect: message include \r or \n 157 | * revamp message decoding 158 | 159 | ## 0.1.2 160 | 161 | * change api from string to byte array 162 | 163 | ## 0.1.1 164 | 165 | * publish can be buffered. 166 | 167 | ## 0.1.0+4 168 | 169 | * Update sample code 170 | 171 | ## 0.1.0+3 172 | 173 | * Update sample code 174 | 175 | ## 0.1.0+2 176 | 177 | * Update readme 178 | 179 | ## 0.1.0+1 180 | 181 | * Add readme 182 | 183 | ## 0.0.2+1 184 | 185 | * Add change log 186 | 187 | ## 0.0.2 188 | 189 | * Refactor code 190 | 191 | ## 0.0.1 192 | 193 | * Initial experimental version 194 | -------------------------------------------------------------------------------- /test/message_data_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:dart_nats/dart_nats.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | group('all', () { 9 | test('simple', () async { 10 | var client = Client(); 11 | await client.connect(Uri.parse('ws://localhost:8080'), retryInterval: 1); 12 | var sub = client.sub('subject1'); 13 | client.pub('subject1', Uint8List.fromList('message1'.codeUnits)); 14 | var msg = await sub.stream.first; 15 | await client.close(); 16 | expect(String.fromCharCodes(msg.data), equals('message1')); 17 | }); 18 | test('respond', () async { 19 | var server = Client(); 20 | await server.connect(Uri.parse('ws://localhost:8080')); 21 | var service = server.sub('service'); 22 | service.stream.listen((m) { 23 | m.respondString('respond'); 24 | }); 25 | 26 | var requester = Client(); 27 | await requester.connect(Uri.parse('ws://localhost:8080')); 28 | var inbox = newInbox(); 29 | var inboxSub = requester.sub(inbox); 30 | 31 | requester.pubString('service', 'request', replyTo: inbox); 32 | 33 | var receive = await inboxSub.stream.first; 34 | 35 | await requester.close(); 36 | await service.close(); 37 | expect(receive.string, equals('respond')); 38 | }); 39 | test('request', () async { 40 | var server = Client(); 41 | await server.connect(Uri.parse('ws://localhost:8080')); 42 | var service = server.sub('service'); 43 | unawaited(service.stream.first.then((m) { 44 | m.respond(Uint8List.fromList('respond'.codeUnits)); 45 | })); 46 | 47 | var client = Client(); 48 | await client.connect(Uri.parse('ws://localhost:8080')); 49 | var receive = await client.request( 50 | 'service', Uint8List.fromList('request'.codeUnits)); 51 | 52 | await client.close(); 53 | await service.close(); 54 | expect(receive.string, equals('respond')); 55 | }); 56 | test('long message', () async { 57 | var txt = 58 | '12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890'; 59 | var client = Client(); 60 | await client.connect(Uri.parse('ws://localhost:8080'), retryInterval: 1); 61 | var sub = client.sub('subject1'); 62 | client.pub('subject1', Uint8List.fromList(txt.codeUnits)); 63 | client.pub('subject1', Uint8List.fromList(txt.codeUnits)); 64 | var msg = await sub.stream.first; 65 | msg = await sub.stream.first; 66 | await client.close(); 67 | expect(String.fromCharCodes(msg.data), equals(txt)); 68 | }); 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /test/config/wss.cfg: -------------------------------------------------------------------------------- 1 | websocket { 2 | # Specify a host and port to listen for websocket connections 3 | # 4 | # listen: "host:port" 5 | 6 | # It can also be configured with individual parameters, 7 | # namely host and port. 8 | # 9 | # host: "hostname" 10 | port: 443 11 | 12 | # This will optionally specify what host:port for websocket 13 | # connections to be advertised in the cluster. 14 | # 15 | # advertise: "host:port" 16 | 17 | # TLS configuration is required by default 18 | # 19 | tls { 20 | cert_file: "/config/server-cert.pem" 21 | key_file: "/config/server-key.pem" 22 | } 23 | 24 | # For test environments, you can disable the need for TLS 25 | # by explicitly setting this option to `true` 26 | # 27 | # no_tls: true 28 | 29 | # [Cross-origin resource sharing option](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). 30 | # 31 | # IMPORTANT! This option is used only when the http request presents an Origin 32 | # header, which is the case for web browsers. If no Origin header is present, 33 | # this check will not be performed. 34 | # 35 | # When set to `true`, the HTTP origin header must match the request’s hostname. 36 | # The default is `false`. 37 | # 38 | # same_origin: true 39 | 40 | # [Cross-origin resource sharing option](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). 41 | # 42 | # IMPORTANT! This option is used only when the http request presents an Origin 43 | # header, which is the case for web browsers. If no Origin header is present, 44 | # this check will not be performed. 45 | # 46 | # List of accepted origins. When empty, and `same_origin` is `false`, clients from any origin are allowed to connect. 47 | # This list specifies the only accepted values for the client's request Origin header. The scheme, 48 | # host and port must match. By convention, the absence of TCP port in the URL will be port 80 49 | # for an "http://" scheme, and 443 for "https://". 50 | # 51 | # allowed_origins [ 52 | # "http://www.example.com" 53 | # "https://www.other-example.com" 54 | # ] 55 | 56 | # This enables support for compressed websocket frames 57 | # in the server. For compression to be used, both server 58 | # and client have to support it. 59 | # 60 | # compression: true 61 | 62 | # This is the total time allowed for the server to 63 | # read the client request and write the response back 64 | # to the client. This includes the time needed for the 65 | # TLS handshake. 66 | # 67 | # handshake_timeout: "2s" 68 | 69 | # Name for an HTTP cookie, that if present will be used as a client JWT. 70 | # If the client specifies a JWT in the CONNECT protocol, this option is ignored. 71 | # The cookie should be set by the HTTP server as described [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies). 72 | # This setting is useful when generating NATS `Bearer` client JWTs as the 73 | # result of some authentication mechanism. The HTTP server after correct 74 | # authentication can issue a JWT for the user, that is set securely preventing 75 | # access by unintended scripts. Note these JWTs must be [NATS JWTs](https://docs.nats.io/nats-server/configuration/securing_nats/jwt). 76 | # 77 | # jwt_cookie: "my_jwt_cookie_name" 78 | 79 | # If no user name is provided when a websocket client connects, will default 80 | # this user name in the authentication phase. If specified, this will 81 | # override, for websocket clients, any `no_auth_user` value defined in the 82 | # main configuration file. 83 | # Note that this is not compatible with running the server in operator mode. 84 | # 85 | # no_auth_user: "my_username_for_apps_not_providing_credentials" 86 | 87 | # See below to know what is the normal way of limiting websocket clients 88 | # to specific users. 89 | # If there are no users specified in the configuration, this simple authorization 90 | # block allows you to override the values that would be configured in the 91 | # equivalent block in the main section. 92 | # 93 | # authorization { 94 | # # If this is specified, the client has to provide the same username 95 | # # and password to be able to connect. 96 | # # username: "my_user_name" 97 | # # password: "my_password" 98 | # 99 | # # If this is specified, the password field in the CONNECT has to 100 | # # match this token. 101 | # # token: "my_token" 102 | # 103 | # # This overrides the main's authorization timeout. For consistency 104 | # # with the main's authorization configuration block, this is expressed 105 | # # as a number of seconds. 106 | # # timeout: 2.0 107 | #} 108 | } 109 | -------------------------------------------------------------------------------- /test/structure_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:typed_data'; 4 | 5 | import 'package:dart_nats/dart_nats.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | import 'model/student.dart'; 9 | 10 | void main() { 11 | group('all', () { 12 | test('string to string', () async { 13 | var client = Client(); 14 | 15 | await client.connect(Uri.parse('nats://localhost:4222'), 16 | retryInterval: 1); 17 | var sub = client.sub('subject1', jsonDecoder: string2string); 18 | client.pub('subject1', Uint8List.fromList('message1'.codeUnits)); 19 | var msg = await sub.stream.first; 20 | await client.close(); 21 | expect(msg.data, equals('message1')); 22 | }); 23 | 24 | test('sub', () async { 25 | var client = Client(); 26 | await client.connect(Uri.parse('nats://localhost:4222'), 27 | retryInterval: 1); 28 | var sub = client.sub('subject1', jsonDecoder: json2Student); 29 | var student = Student('id', 'name', 1); 30 | client.pubString('subject1', jsonEncode(student.toJson())); 31 | var msg = await sub.stream.first; 32 | await client.close(); 33 | expect(msg.data.id, student.id); 34 | expect(msg.data.name, student.name); 35 | expect(msg.data.score, student.score); 36 | }); 37 | test('sub register jsonDecoder', () async { 38 | var client = Client(); 39 | await client.connect(Uri.parse('nats://localhost:4222'), 40 | retryInterval: 1); 41 | client.registerJsonDecoder(json2Student); 42 | var sub = client.sub('subject1'); 43 | var student = Student('id', 'name', 1); 44 | client.pubString('subject1', jsonEncode(student.toJson())); 45 | var msg = await sub.stream.first; 46 | await client.close(); 47 | expect(msg.data.id, student.id); 48 | expect(msg.data.name, student.name); 49 | expect(msg.data.score, student.score); 50 | }); 51 | test('sub no type', () async { 52 | var client = Client(); 53 | await client.connect(Uri.parse('nats://localhost:4222'), 54 | retryInterval: 1); 55 | var sub = client.sub('subject1', jsonDecoder: json2Student); 56 | var student = Student('id', 'name', 1); 57 | client.pubString('subject1', jsonEncode(student.toJson())); 58 | var msg = await sub.stream.first; 59 | await client.close(); 60 | expect(msg.data.id, student.id); 61 | expect(msg.data.name, student.name); 62 | expect(msg.data.score, student.score); 63 | }); 64 | test('sub no type no jsonDecoder', () async { 65 | var client = Client(); 66 | await client.connect(Uri.parse('nats://localhost:4222'), 67 | retryInterval: 1); 68 | var sub = client.sub('subject1'); 69 | var student = Student('id', 'name', 1); 70 | client.pubString('subject1', jsonEncode(student.toJson())); 71 | var msg = await sub.stream.first; 72 | await client.close(); 73 | if (!(msg.data is Uint8List)) { 74 | throw Exception('missing data type'); 75 | } 76 | }); 77 | test('request', () async { 78 | var server = Client(); 79 | await server.connect(Uri.parse('nats://localhost:4222')); 80 | server.registerJsonDecoder(json2Student); 81 | var service = server.sub('service'); 82 | unawaited(service.stream.first.then((m) { 83 | m.respondString(jsonEncode(m.data.toJson())); 84 | })); 85 | 86 | var client = Client(); 87 | var s1 = Student('id', 'name', 1); 88 | await client.connect(Uri.parse('ws://localhost:8080')); 89 | var receive = 90 | await client.requestString('service', jsonEncode(s1.toJson())); 91 | var s2 = Student.fromJson(jsonDecode(receive.string)); 92 | await client.close(); 93 | await server.close(); 94 | expect(s1.score, equals(s2.score)); 95 | }); 96 | test('request register jsonDecoder', () async { 97 | var server = Client(); 98 | server.registerJsonDecoder(json2Student); 99 | await server.connect(Uri.parse('nats://localhost:4222')); 100 | var service = server.sub('service'); 101 | unawaited(service.stream.first.then((m) { 102 | m.respondString(jsonEncode(m.data.toJson())); 103 | })); 104 | 105 | var client = Client(); 106 | client.registerJsonDecoder(json2Student); 107 | var s1 = Student('id', 'name', 1); 108 | await client.connect(Uri.parse('ws://localhost:8080')); 109 | var receive = await client.requestString( 110 | 'service', jsonEncode(s1.toJson())); 111 | var s2 = receive.data; 112 | await client.close(); 113 | await server.close(); 114 | expect(s1.score, equals(s2.score)); 115 | }); 116 | }); 117 | } 118 | 119 | String string2string(String input) { 120 | return input; 121 | } 122 | 123 | Student json2Student(String json) { 124 | var map = jsonDecode(json); 125 | return Student.fromJson(map); 126 | } 127 | 128 | String student2json(Student student) { 129 | return jsonEncode(student.toJson()); 130 | } 131 | -------------------------------------------------------------------------------- /test/message_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:dart_nats/dart_nats.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | group('all', () { 9 | test('simple', () async { 10 | var client = Client(); 11 | await client.connect(Uri.parse('ws://localhost:8080'), retryInterval: 1); 12 | var sub = client.sub('subject1'); 13 | client.pub('subject1', Uint8List.fromList('message1'.codeUnits)); 14 | var msg = await sub.stream.first; 15 | await client.close(); 16 | expect(String.fromCharCodes(msg.byte), equals('message1')); 17 | }); 18 | test('respond', () async { 19 | var server = Client(); 20 | await server.connect(Uri.parse('ws://localhost:8080')); 21 | var service = server.sub('service'); 22 | service.stream.listen((m) { 23 | m.respondString('respond'); 24 | }); 25 | 26 | var requester = Client(); 27 | await requester.connect(Uri.parse('ws://localhost:8080')); 28 | var inbox = newInbox(); 29 | var inboxSub = requester.sub(inbox); 30 | 31 | requester.pubString('service', 'request', replyTo: inbox); 32 | 33 | var receive = await inboxSub.stream.first; 34 | 35 | await requester.close(); 36 | await service.close(); 37 | expect(receive.string, equals('respond')); 38 | }); 39 | test('request', () async { 40 | var server = Client(); 41 | await server.connect(Uri.parse('ws://localhost:8080')); 42 | var service = server.sub('service'); 43 | unawaited(service.stream.first.then((m) { 44 | m.respond(Uint8List.fromList('respond'.codeUnits)); 45 | })); 46 | 47 | var client = Client(); 48 | await client.connect(Uri.parse('ws://localhost:8080')); 49 | var receive = await client.request( 50 | 'service', Uint8List.fromList('request'.codeUnits)); 51 | 52 | await client.close(); 53 | await service.close(); 54 | expect(receive.string, equals('respond')); 55 | }); 56 | test('long message', () async { 57 | var txt = 58 | '12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890'; 59 | var client = Client(); 60 | await client.connect(Uri.parse('ws://localhost:8080'), retryInterval: 1); 61 | var sub = client.sub('subject1'); 62 | client.pub('subject1', Uint8List.fromList(txt.codeUnits)); 63 | // client.pub('subject1', Uint8List.fromList(txt.codeUnits)); 64 | var msg = await sub.stream.first; 65 | // msg = await sub.stream.first; 66 | await client.close(); 67 | expect(String.fromCharCodes(msg.byte), equals(txt)); 68 | }); 69 | 70 | test('pub with header', () async { 71 | var txt = 'this is text'; 72 | var client = Client(); 73 | await client.connect(Uri.parse('ws://localhost:8080'), retryInterval: 1); 74 | var sub = client.sub('subject1'); 75 | var header = Header(); 76 | header.add('key1', 'value1'); 77 | header.add('key2', 'value2'); 78 | header.add('key3', 'value3'); 79 | header.add('key4', 'value:with:colon'); 80 | client.pub('subject1', Uint8List.fromList(txt.codeUnits), header: header); 81 | var msg = await sub.stream.first; 82 | await client.close(); 83 | expect(String.fromCharCodes(msg.byte), equals(txt)); 84 | expect(msg.header?.get('key1'), equals('value1')); 85 | expect(msg.header?.get('key4'), equals('value:with:colon')); 86 | expect(msg.header?.toBytes(), equals(header.toBytes())); 87 | }); 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /test/request_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | import 'dart:typed_data'; 4 | 5 | import 'package:dart_nats/dart_nats.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | group('all', () { 10 | test('simple', () async { 11 | var client = Client(); 12 | await client.connect(Uri.parse('ws://localhost:8080'), retryInterval: 1); 13 | var sub = client.sub('subject1'); 14 | client.pub('subject1', Uint8List.fromList('message1'.codeUnits)); 15 | var msg = await sub.stream.first; 16 | await client.close(); 17 | expect(String.fromCharCodes(msg.byte), equals('message1')); 18 | }); 19 | test('respond', () async { 20 | var server = Client(); 21 | await server.connect(Uri.parse('ws://localhost:8080')); 22 | var service = server.sub('service'); 23 | service.stream.listen((m) { 24 | m.respondString('respond'); 25 | }); 26 | 27 | var requester = Client(); 28 | await requester.connect(Uri.parse('ws://localhost:8080')); 29 | var inbox = newInbox(); 30 | var inboxSub = requester.sub(inbox); 31 | 32 | requester.pubString('service', 'request', replyTo: inbox); 33 | 34 | var receive = await inboxSub.stream.first; 35 | 36 | await requester.close(); 37 | await server.close(); 38 | expect(receive.string, equals('respond')); 39 | }); 40 | test('request', () async { 41 | var server = Client(); 42 | await server.connect(Uri.parse('ws://localhost:8080')); 43 | var service = server.sub('service'); 44 | unawaited(service.stream.first.then((m) { 45 | m.respond(Uint8List.fromList('respond'.codeUnits)); 46 | })); 47 | 48 | var client = Client(); 49 | await client.connect(Uri.parse('ws://localhost:8080')); 50 | var receive = await client.request( 51 | 'service', Uint8List.fromList('request'.codeUnits)); 52 | 53 | await client.close(); 54 | await server.close(); 55 | expect(receive.string, equals('respond')); 56 | }); 57 | test('custom inbox', () async { 58 | var server = Client(); 59 | await server.connect(Uri.parse('ws://localhost:8080')); 60 | var service = server.sub('service'); 61 | unawaited(service.stream.first.then((m) { 62 | m.respond(Uint8List.fromList('respond'.codeUnits)); 63 | })); 64 | 65 | var client = Client(); 66 | client.inboxPrefix = '_INBOX.test_test'; 67 | await client.connect(Uri.parse('ws://localhost:8080')); 68 | var receive = await client.request( 69 | 'service', Uint8List.fromList('request'.codeUnits)); 70 | 71 | await client.close(); 72 | await server.close(); 73 | expect(receive.string, equals('respond')); 74 | }); 75 | test('request with timeout', () async { 76 | var server = Client(); 77 | await server.connect(Uri.parse('ws://localhost:8080')); 78 | var service = server.sub('service'); 79 | unawaited(service.stream.first.then((m) { 80 | sleep(Duration(seconds: 1)); 81 | m.respond(Uint8List.fromList('respond'.codeUnits)); 82 | })); 83 | 84 | var client = Client(); 85 | await client.connect(Uri.parse('ws://localhost:8080')); 86 | var receive = await client.request( 87 | 'service', Uint8List.fromList('request'.codeUnits), 88 | timeout: Duration(seconds: 3)); 89 | 90 | await client.close(); 91 | await server.close(); 92 | expect(receive.string, equals('respond')); 93 | }); 94 | test('request with timeout exception', () async { 95 | var server = Client(); 96 | await server.connect(Uri.parse('ws://localhost:8080')); 97 | var service = server.sub('service'); 98 | unawaited(service.stream.first.then((m) { 99 | sleep(Duration(seconds: 5)); 100 | m.respond(Uint8List.fromList('respond'.codeUnits)); 101 | })); 102 | 103 | var client = Client(); 104 | var gotit = false; 105 | await client.connect(Uri.parse('ws://localhost:8080')); 106 | try { 107 | await client.request('service', Uint8List.fromList('request'.codeUnits), 108 | timeout: Duration(seconds: 2)); 109 | } on TimeoutException { 110 | gotit = true; 111 | } 112 | await client.close(); 113 | await service.close(); 114 | await server.close(); 115 | expect(gotit, equals(true)); 116 | }); 117 | test('future request to 2 service', () async { 118 | var server = Client(); 119 | await server.connect(Uri.parse('ws://localhost:8080')); 120 | var service1 = server.sub('service1'); 121 | service1.stream.listen((m) { 122 | m.respond(Uint8List.fromList('respond1'.codeUnits)); 123 | }); 124 | var service2 = server.sub('service2'); 125 | service2.stream.listen((m) { 126 | m.respond(Uint8List.fromList('respond2'.codeUnits)); 127 | }); 128 | 129 | var client = Client(); 130 | await client.connect(Uri.parse('ws://localhost:8080')); 131 | Future receive1; 132 | Future receive2; 133 | unawaited(receive2 = 134 | client.request('service2', Uint8List.fromList('request'.codeUnits))); 135 | unawaited(receive1 = 136 | client.request('service1', Uint8List.fromList('request'.codeUnits))); 137 | var r1 = await receive1; 138 | var r2 = await receive2; 139 | await client.close(); 140 | await server.close(); 141 | expect(r1.string, equals('respond1')); 142 | expect(r2.string, equals('respond2')); 143 | }); 144 | }); 145 | } 146 | -------------------------------------------------------------------------------- /lib/src/common.dart: -------------------------------------------------------------------------------- 1 | Map _removeNull(Map data) { 2 | var data2 = {}; 3 | 4 | data.forEach((s, d) { 5 | if (d != null) data2[s] = d; 6 | }); 7 | return data2; 8 | } 9 | 10 | ///NATS Server Info 11 | class Info { 12 | /// sever id 13 | String? serverId; 14 | 15 | /// server name 16 | String? serverName; 17 | 18 | /// server version 19 | String? version; 20 | 21 | /// protocol 22 | int? proto; 23 | 24 | /// server go version 25 | String? go; 26 | 27 | /// host 28 | String? host; 29 | 30 | /// port number 31 | int? port; 32 | 33 | /// TLS Required 34 | bool? tlsRequired; 35 | 36 | /// max payload 37 | int? maxPayload; 38 | 39 | /// nounce 40 | String? nonce; 41 | 42 | ///client id assigned by server 43 | int? clientId; 44 | 45 | //todo 46 | //authen required 47 | //tls_required 48 | //tls_verify 49 | //connect_url 50 | 51 | ///constructure 52 | Info( 53 | {this.serverId, 54 | this.serverName, 55 | this.version, 56 | this.proto, 57 | this.go, 58 | this.host, 59 | this.port, 60 | this.tlsRequired, 61 | this.maxPayload, 62 | this.nonce, 63 | this.clientId}); 64 | 65 | ///constructure from json 66 | Info.fromJson(Map json) { 67 | serverId = json['server_id']; 68 | serverName = json['server_name']; 69 | version = json['version']; 70 | proto = json['proto']; 71 | go = json['go']; 72 | host = json['host']; 73 | port = json['port']; 74 | tlsRequired = json['tls_required']; 75 | maxPayload = json['max_payload']; 76 | nonce = json['nonce']; 77 | clientId = json['client_id']; 78 | } 79 | 80 | ///convert to json 81 | Map toJson() { 82 | final data = {}; 83 | data['server_id'] = serverId; 84 | data['server_name'] = serverName; 85 | data['version'] = version; 86 | data['proto'] = proto; 87 | data['go'] = go; 88 | data['host'] = host; 89 | data['port'] = port; 90 | data['tls_required'] = tlsRequired; 91 | data['max_payload'] = maxPayload; 92 | data['nonce'] = nonce; 93 | data['client_id'] = clientId; 94 | 95 | return _removeNull(data); 96 | } 97 | } 98 | 99 | ///connection option to send to server 100 | class ConnectOption { 101 | ///NATS server send +OK or not (default nats server is turn on) this client will auto tuen off as after connect 102 | bool? verbose; 103 | 104 | /// 105 | bool? pedantic; 106 | 107 | /// TLS require or not //not implement yet 108 | bool? tlsRequired; 109 | 110 | /// Auehtnticatio Token 111 | String? authToken; 112 | 113 | /// JWT 114 | String? jwt; 115 | 116 | /// NKEY 117 | String? nkey; 118 | 119 | /// signature jwt.sig = sign(hash(jwt.header + jwt.body), private-key(jwt.issuer))(jwt.issuer is part of jwt.body) 120 | String? sig; 121 | 122 | /// username 123 | String? user; 124 | 125 | /// password 126 | String? pass; 127 | 128 | ///server name 129 | String? name; 130 | 131 | /// lang?? 132 | String? lang; 133 | 134 | /// sever version 135 | String? version; 136 | 137 | /// headers 138 | bool? headers; 139 | 140 | ///protocol 141 | int? protocol; 142 | 143 | ///construcure 144 | ConnectOption( 145 | {this.verbose = false, 146 | this.pedantic, 147 | this.authToken, 148 | this.jwt, 149 | this.nkey, 150 | this.user, 151 | this.pass, 152 | this.tlsRequired, 153 | this.name, 154 | this.lang = 'dart', 155 | this.version = '0.6.0', 156 | this.headers = true, 157 | this.protocol = 1}); 158 | 159 | ///constructure from json 160 | ConnectOption.fromJson(Map json) { 161 | verbose = json['verbose']; 162 | pedantic = json['pedantic']; 163 | tlsRequired = json['tls_required']; 164 | authToken = json['auth_token']; 165 | jwt = json['jwt']; 166 | nkey = json['nkey']; 167 | sig = json['sig']; 168 | user = json['user']; 169 | pass = json['pass']; 170 | name = json['name']; 171 | lang = json['lang']; 172 | version = json['version']; 173 | headers = json['headers']; 174 | protocol = json['protocol']; 175 | } 176 | 177 | ///export to json 178 | Map toJson() { 179 | final data = {}; 180 | data['verbose'] = verbose; 181 | data['pedantic'] = pedantic; 182 | data['tls_required'] = tlsRequired; 183 | data['auth_token'] = authToken; 184 | data['jwt'] = jwt; 185 | data['nkey'] = nkey; 186 | data['sig'] = sig; 187 | data['user'] = user; 188 | data['pass'] = pass; 189 | data['name'] = name; 190 | data['lang'] = lang; 191 | data['version'] = version; 192 | data['headers'] = headers; 193 | data['protocol'] = protocol; 194 | 195 | return _removeNull(data); 196 | } 197 | } 198 | 199 | /// Nats Exception 200 | class NatsException implements Exception { 201 | /// Description of the cause of the timeout. 202 | final String? message; 203 | 204 | /// NatsException 205 | NatsException(this.message); 206 | 207 | @override 208 | String toString() { 209 | var result = 'NatsException'; 210 | if (message != null) result = '$result: $message'; 211 | return result; 212 | } 213 | } 214 | 215 | /// nkeys Exception 216 | class NkeysException implements Exception { 217 | /// Description of the cause of the timeout. 218 | final String? message; 219 | 220 | /// NkeysException 221 | NkeysException(this.message); 222 | 223 | @override 224 | String toString() { 225 | var result = 'NkeysException'; 226 | if (message != null) result = '$result: $message'; 227 | return result; 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | 3 | 4 | 5 | # Dart-NATS 6 | A Dart client for the [NATS](https://nats.io) messaging system. Design to use with Dart and flutter. 7 | 8 | ### Flutter Web Support by WebSocket 9 | ```dart 10 | client.connect(Uri.parse('ws://localhost:80')); 11 | client.connect(Uri.parse('wss://localhost:443')); 12 | ``` 13 | 14 | 15 | ### Flutter Other Platform Support both TCP Socket and WebSocket 16 | ```dart 17 | client.connect(Uri.parse('nats://localhost:4222')); 18 | client.connect(Uri.parse('tls://localhost:4222')); 19 | client.connect(Uri.parse('ws://localhost:80')); 20 | client.connect(Uri.parse('wss://localhost:443')); 21 | ``` 22 | 23 | ### background retry 24 | ```dart 25 | // unawait 26 | client.connect(Uri.parse('nats://localhost:4222'), retry: true, retryCount: -1); 27 | 28 | // await for connect if need 29 | await client.wait4Connected(); 30 | 31 | // listen to status stream 32 | client.statusStream.lesten((status){ 33 | // 34 | print(status); 35 | }); 36 | ``` 37 | 38 | ### Turn off retry and catch exception 39 | ```dart 40 | try { 41 | await client.connect(Uri.parse('nats://localhost:1234'), retry: false); 42 | } on NatsException { 43 | //Error handle 44 | } 45 | ``` 46 | 47 | ## Dart Examples: 48 | 49 | Run the `example/main.dart`: 50 | 51 | ``` 52 | dart example/main.dart 53 | ``` 54 | 55 | ```dart 56 | import 'package:dart_nats/dart_nats.dart'; 57 | 58 | void main() async { 59 | var client = Client(); 60 | client.connect(Uri.parse('nats://localhost')); 61 | var sub = client.sub('subject1'); 62 | await client.pubString('subject1', 'message1'); 63 | var msg = await sub.stream.first; 64 | 65 | print(msg.string); 66 | client.unSub(sub); 67 | client.close(); 68 | } 69 | ``` 70 | 71 | ## Flutter Examples: 72 | 73 | Import and Declare object 74 | ```dart 75 | import 'package:dart_nats/dart_nats.dart' as nats; 76 | 77 | nats.Client natsClient; 78 | nats.Subscription fooSub, barSub; 79 | ``` 80 | 81 | Simply connect to server and subscribe to subject 82 | ```dart 83 | void connect() { 84 | natsClient = nats.Client(); 85 | natsClient.connect(Uri.parse('nats://hostname'); 86 | fooSub = natsClient.sub('foo'); 87 | barSub = natsClient.sub('bar'); 88 | } 89 | ``` 90 | Use as Stream in StreamBuilder 91 | ```dart 92 | StreamBuilder( 93 | stream: fooSub.stream, 94 | builder: (context, AsyncSnapshot snapshot) { 95 | return Text(snapshot.hasData ? '${snapshot.data.string}' : ''); 96 | }, 97 | ), 98 | ``` 99 | 100 | Publish Message 101 | ```dart 102 | await natsClient.pubString('subject','message string'); 103 | ``` 104 | 105 | Request 106 | ```dart 107 | var client = Client(); 108 | client.inboxPrefix = '_INBOX.test_test'; 109 | await client.connect(Uri.parse('nats://localhost:4222')); 110 | var receive = await client.request( 111 | 'service', Uint8List.fromList('request'.codeUnits)); 112 | ``` 113 | 114 | Structure Request 115 | ```dart 116 | var client = Client(); 117 | await client.connect(Uri.parse('nats://localhost:4222')); 118 | client.registerJsonDecoder(json2Student); 119 | var receive = await client.requestString('service', ''); 120 | var student = receive.data; 121 | 122 | 123 | Student json2Student(String json) { 124 | return Student.fromJson(jsonDecode(json)); 125 | } 126 | ``` 127 | 128 | Dispose 129 | ```dart 130 | void dispose() { 131 | natsClient.close(); 132 | super.dispose(); 133 | } 134 | ``` 135 | 136 | ## Authentication 137 | 138 | Token Authtication 139 | ```dart 140 | var client = Client(); 141 | client.connect(Uri.parse('nats://localhost'), 142 | connectOption: ConnectOption(authToken: 'mytoken')); 143 | ``` 144 | 145 | User/Passwore Authentication 146 | ```dart 147 | var client = Client(); 148 | client.connect(Uri.parse('nats://localhost'), 149 | connectOption: ConnectOption(user: 'foo', pass: 'bar')); 150 | ``` 151 | 152 | NKEY Authentication 153 | ```dart 154 | var client = Client(); 155 | client.seed = 156 | 'SUACSSL3UAHUDXKFSNVUZRF5UHPMWZ6BFDTJ7M6USDXIEDNPPQYYYCU3VY'; 157 | client.connect( 158 | Uri.parse('nats://localhost'), 159 | connectOption: ConnectOption( 160 | nkey: 'UDXU4RCSJNZOIQHZNWXHXORDPRTGNJAHAHFRGZNEEJCPQTT2M7NLCNF4', 161 | ), 162 | ); 163 | ``` 164 | 165 | JWT Authentication 166 | ```dart 167 | var client = Client(); 168 | client.seed = 169 | 'SUAJGSBAKQHGYI7ZVKVR6WA7Z5U52URHKGGT6ZICUJXMG4LCTC2NTLQSF4'; 170 | client.connect( 171 | Uri.parse('nats://localhost'), 172 | connectOption: ConnectOption( 173 | jwt: 174 | '''eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJBU1pFQVNGMzdKS0dPTFZLTFdKT1hOM0xZUkpHNURJUFczUEpVT0s0WUlDNFFENlAyVFlRIiwiaWF0IjoxNjY0NTI0OTU5LCJpc3MiOiJBQUdTSkVXUlFTWFRDRkUzRVE3RzVPQldSVUhaVVlDSFdSM0dRVERGRldaSlM1Q1JLTUhOTjY3SyIsIm5hbWUiOiJzaWdudXAiLCJzdWIiOiJVQzZCUVY1Tlo1V0pQRUVZTTU0UkZBNU1VMk5NM0tON09WR01DU1VaV1dORUdZQVBNWEM0V0xZUCIsIm5hdHMiOnsicHViIjp7fSwic3ViIjp7fSwic3VicyI6LTEsImRhdGEiOi0xLCJwYXlsb2FkIjotMSwidHlwZSI6InVzZXIiLCJ2ZXJzaW9uIjoyfX0.8Q0HiN0h2tBvgpF2cAaz2E3WLPReKEnSmUWT43NSlXFNRpsCWpmkikxGgFn86JskEN4yast1uSj306JdOhyJBA''', 175 | ), 176 | ); 177 | ``` 178 | 179 | 180 | 181 | 182 | Full Flutter sample code [example/flutter/main.dart](https://github.com/chartchuo/dart-nats/blob/master/example/flutter/main_dart) 183 | 184 | 185 | ## Features 186 | The following is a list of features currently supported: 187 | 188 | - [x] - Publish 189 | - [x] - Subscribe, unsubscribe 190 | - [x] - NUID, Inbox 191 | - [x] - Reconnect to single server when connection lost and resume subscription 192 | - [x] - Unsubscribe after N message 193 | - [x] - Request, Respond 194 | - [x] - Queue subscribe 195 | - [x] - Request timeout 196 | - [x] - Events/status 197 | - [x] - Buffering message during reconnect atempts 198 | - [x] - All authentication models, including NATS 2.0 JWT and nkey 199 | - [x] - NATS 2.x 200 | - [x] - TLS 201 | 202 | Planned: 203 | - [ ] - Connect to list of servers 204 | -------------------------------------------------------------------------------- /test/connect_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:dart_nats/dart_nats.dart'; 5 | import 'package:test/test.dart'; 6 | import 'package:web_socket_channel/web_socket_channel.dart'; 7 | 8 | void main() { 9 | group('all', () { 10 | test('ws', () async { 11 | var client = Client(); 12 | unawaited( 13 | client.connect(Uri.parse('ws://localhost:8080'), retryInterval: 1)); 14 | var sub = client.sub('subject1'); 15 | client.pub('subject1', Uint8List.fromList('message1'.codeUnits)); 16 | var msg = await sub.stream.first; 17 | await client.close(); 18 | expect(String.fromCharCodes(msg.byte), equals('message1')); 19 | }); 20 | test('nats:', () async { 21 | var client = Client(); 22 | unawaited( 23 | client.connect(Uri.parse('nats://localhost:4222'), retryInterval: 1)); 24 | var sub = client.sub('subject1'); 25 | client.pub('subject1', Uint8List.fromList('message1'.codeUnits)); 26 | var msg = await sub.stream.first; 27 | await client.close(); 28 | expect(String.fromCharCodes(msg.byte), equals('message1')); 29 | }); 30 | test('await', () async { 31 | var client = Client(); 32 | await client.connect(Uri.parse('nats://localhost')); 33 | var sub = client.sub('subject1'); 34 | var result = await client.pub( 35 | 'subject1', Uint8List.fromList('message1'.codeUnits), 36 | buffer: false); 37 | expect(result, true); 38 | 39 | var msg = await sub.stream.first; 40 | await client.close(); 41 | expect(String.fromCharCodes(msg.byte), equals('message1')); 42 | }); 43 | test('retry false', () async { 44 | var client = Client(); 45 | await client.connect(Uri.parse('nats://localhost'), retry: false); 46 | var sub = client.sub('subject1'); 47 | var result = await client.pub( 48 | 'subject1', Uint8List.fromList('message1'.codeUnits), 49 | buffer: false); 50 | expect(result, true); 51 | 52 | var msg = await sub.stream.first; 53 | await client.close(); 54 | expect(String.fromCharCodes(msg.byte), equals('message1')); 55 | }); 56 | test('reconnect', () async { 57 | var client = Client(); 58 | await client.connect(Uri.parse('nats://localhost')); 59 | var sub = client.sub('subject1'); 60 | var result = await client.pub( 61 | 'subject1', Uint8List.fromList('message1'.codeUnits), 62 | buffer: false); 63 | expect(result, true); 64 | 65 | var msg = await sub.stream.first; 66 | await client.close(); 67 | expect(String.fromCharCodes(msg.byte), equals('message1')); 68 | 69 | await client.connect(Uri.parse('nats://localhost')); 70 | result = await client.pub( 71 | 'subject1', Uint8List.fromList('message2'.codeUnits), 72 | buffer: false); 73 | expect(result, true); 74 | msg = await sub.stream.first; 75 | expect(String.fromCharCodes(msg.byte), equals('message2')); 76 | }); 77 | test('retry background', () async { 78 | var client = Client(); 79 | unawaited(client.connect( 80 | Uri.parse('nats://localhost'), 81 | retry: true, 82 | retryCount: -1, 83 | timeout: 2, 84 | retryInterval: 2, 85 | )); 86 | 87 | await client.waitUntilConnected(); 88 | var sub = client.sub('subject1'); 89 | var result = await client.pub( 90 | 'subject1', Uint8List.fromList('message1'.codeUnits), 91 | buffer: false); 92 | expect(result, true); 93 | 94 | var msg = await sub.stream.first; 95 | expect(String.fromCharCodes(msg.byte), equals('message1')); 96 | 97 | await client.tcpClose(); 98 | await client.waitUntilConnected(); 99 | 100 | result = await client.pub( 101 | 'subject1', Uint8List.fromList('message2'.codeUnits), 102 | buffer: false); 103 | expect(result, true); 104 | msg = await sub.stream.first; 105 | expect(String.fromCharCodes(msg.byte), equals('message2')); 106 | }); 107 | test('status stream', () async { 108 | var client = Client(); 109 | var statusHistory = []; 110 | client.statusStream.listen((s) { 111 | // print(s); 112 | statusHistory.add(s); 113 | }); 114 | await client.connect(Uri.parse('ws://localhost:8080')); 115 | await client.close(); 116 | 117 | // no runtime error should be fine 118 | // expect only first and last status 119 | expect(statusHistory.first, equals(Status.connecting)); 120 | expect(statusHistory.last, equals(Status.closed)); 121 | }); 122 | test('status stream detail', () async { 123 | var client = Client(); 124 | var statusHistory = []; 125 | client.statusStream.listen((s) { 126 | // print(s); 127 | statusHistory.add(s); 128 | }); 129 | await client.connect( 130 | Uri.parse('ws://localhost:8080'), 131 | retry: true, 132 | retryCount: 3, 133 | retryInterval: 1, 134 | ); 135 | await client.close(); 136 | 137 | // no runtime error should be fine 138 | // expect only first and last status 139 | expect(statusHistory.length, equals(4)); 140 | expect(statusHistory[0], equals(Status.connecting)); 141 | expect(statusHistory[1], equals(Status.infoHandshake)); 142 | expect(statusHistory[2], equals(Status.connected)); 143 | expect(statusHistory[3], equals(Status.closed)); 144 | }); 145 | test('status stream retry fail', () async { 146 | var client = Client(); 147 | var statusHistory = []; 148 | try { 149 | client.statusStream.listen((s) { 150 | print(s); 151 | statusHistory.add(s); 152 | }); 153 | await client.connect( 154 | Uri.parse('nats://localhost:1234'), 155 | retry: true, 156 | retryCount: 3, 157 | retryInterval: 1, 158 | ); 159 | print('after connect'); 160 | } on NatsException { 161 | // 162 | } on WebSocketChannelException { 163 | // 164 | } catch (e) { 165 | // 166 | } 167 | await client.close(); 168 | 169 | // no runtime error should be fine 170 | // expect only first and last status 171 | expect(statusHistory.length, equals(4)); 172 | expect(statusHistory[0], equals(Status.connecting)); 173 | expect(statusHistory[1], equals(Status.reconnecting)); 174 | expect(statusHistory[2], equals(Status.reconnecting)); 175 | expect(statusHistory[3], equals(Status.closed)); 176 | }); 177 | }); 178 | } 179 | -------------------------------------------------------------------------------- /lib/src/nkeys.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:base32/base32.dart'; 4 | import 'package:dart_nats/src/common.dart'; 5 | import 'package:ed25519_edwards/ed25519_edwards.dart' as ed; 6 | 7 | /// PrefixByteSeed is the version byte used for encoded NATS Seeds 8 | const PrefixByteSeed = 18 << 3; // Base32-encodes to 'S...' 9 | 10 | /// PrefixBytePrivate is the version byte used for encoded NATS Private keys 11 | const PrefixBytePrivate = 15 << 3; // Base32-encodes to 'P...' 12 | 13 | /// PrefixByteServer is the version byte used for encoded NATS Servers 14 | const PrefixByteServer = 13 << 3; // Base32-encodes to 'N...' 15 | 16 | /// PrefixByteCluster is the version byte used for encoded NATS Clusters 17 | const PrefixByteCluster = 2 << 3; // Base32-encodes to 'C...' 18 | 19 | /// PrefixByteOperator is the version byte used for encoded NATS Operators 20 | const PrefixByteOperator = 14 << 3; // Base32-encodes to 'O...' 21 | 22 | /// PrefixByteAccount is the version byte used for encoded NATS Accounts 23 | const PrefixByteAccount = 0; // Base32-encodes to 'A...' 24 | 25 | /// PrefixByteUser is the version byte used for encoded NATS Users 26 | const PrefixByteUser = 20 << 3; // Base32-encodes to 'U...' 27 | 28 | /// PrefixByteUnknown is for unknown prefixes. 29 | const PrefixByteUnknown = 23 << 3; // Base32-encodes to 'X...' 30 | 31 | ///Nkeys 32 | class Nkeys { 33 | /// key pair 34 | ed.KeyPair keyPair; 35 | 36 | /// seed string 37 | Uint8List get rawSeed { 38 | return ed.seed(keyPair.privateKey); 39 | } 40 | 41 | /// prefixByte 42 | int prefixByte; 43 | 44 | ///create nkeys by keypair 45 | Nkeys(this.prefixByte, this.keyPair) { 46 | if (!_checkValidPrefixByte(prefixByte)) { 47 | throw NkeysException('invalid prefix byte $prefixByte'); 48 | } 49 | } 50 | 51 | /// generate new nkeys 52 | static Nkeys newNkeys(int prefixByte) { 53 | var kp = ed.generateKey(); 54 | 55 | return Nkeys(prefixByte, kp); 56 | } 57 | 58 | /// new nkeys from seed 59 | static Nkeys fromSeed(String seed) { 60 | var raw = base32.decode(seed); 61 | 62 | // Need to do the reverse here to get back to internal representation. 63 | var b1 = raw[0] & 248; // 248 = 11111000 64 | var b2 = ((raw[0] & 7) << 5) | ((raw[1] & 248) >> 3); // 7 = 00000111 65 | 66 | if (b1 != PrefixByteSeed) { 67 | throw Exception(NkeysException('not seed prefix byte')); 68 | } 69 | if (_checkValidPublicPrefixByte(b2) == PrefixByteUnknown) { 70 | throw Exception(NkeysException('not public prefix byte')); 71 | } 72 | 73 | var rawSeed = raw.sublist(2, 34); 74 | var key = ed.newKeyFromSeed(rawSeed); 75 | var kp = ed.KeyPair(key, ed.public(key)); 76 | 77 | return Nkeys(b2, kp); 78 | } 79 | 80 | /// Create new pair 81 | static Nkeys createPair(int prefix) { 82 | var kp = ed.generateKey(); 83 | return Nkeys(prefix, kp); 84 | } 85 | 86 | /// Create new User type KeyPair 87 | static Nkeys createUser() { 88 | return createPair(PrefixByteUser); 89 | } 90 | 91 | /// Create new Account type KeyPair 92 | static Nkeys createAccount() { 93 | return createPair(PrefixByteAccount); 94 | } 95 | 96 | /// Create new Operator type KeyPair 97 | static Nkeys createOperator() { 98 | return createPair(PrefixByteOperator); 99 | } 100 | 101 | /// get public key 102 | String get seed { 103 | return _encodeSeed(prefixByte, rawSeed); 104 | } 105 | 106 | /// get public key 107 | String publicKey() { 108 | return _encode(prefixByte, keyPair.publicKey.bytes); 109 | } 110 | 111 | /// raw public key 112 | List rawPublicKey() { 113 | return keyPair.publicKey.bytes; 114 | } 115 | 116 | /// get private key 117 | String privateKey() { 118 | return _encode(PrefixBytePrivate, keyPair.privateKey.bytes); 119 | } 120 | 121 | /// get raw private key 122 | List rawPrivateKey() { 123 | return keyPair.privateKey.bytes; 124 | } 125 | 126 | /// Sign message 127 | List sign(List message) { 128 | var msg = Uint8List.fromList(message); 129 | var r = List.from(ed.sign(keyPair.privateKey, msg)); 130 | return r; 131 | } 132 | 133 | /// verify 134 | static bool verify(String publicKey, List message, List signature) { 135 | var r = _decode(publicKey); 136 | var prefix = r[0][0]; 137 | if (!_checkValidPrefixByte(prefix)) { 138 | throw NkeysException('Ivalid Public key'); 139 | } 140 | 141 | var pub = r[1].toList(); 142 | if (pub.length < ed.PublicKeySize) { 143 | throw NkeysException('Ivalid Public key'); 144 | } 145 | while (pub.length > ed.PublicKeySize) { 146 | pub.removeLast(); 147 | } 148 | return ed.verify(ed.PublicKey(pub), Uint8List.fromList(message), 149 | Uint8List.fromList(signature)); 150 | } 151 | 152 | /// decide public expect prefix 153 | /// throw exception if error 154 | static Uint8List decode(int expectPrefix, String src) { 155 | var res = _decode(src); 156 | if (res[0][0] != expectPrefix) { 157 | throw NkeysException('encode invalid prefix'); 158 | } 159 | return res[1]; 160 | } 161 | } 162 | 163 | /// return [0]=prefix [1]=byte data [2]=type if prefix is 'S' seed 164 | List _decode(String src) { 165 | var b = base32.decode(src).toList(); 166 | var ret = []; 167 | 168 | var prefix = b[0]; 169 | if (_checkValidPrefixByte(prefix)) { 170 | ret.add(Uint8List.fromList([prefix])); 171 | b.removeAt(0); 172 | ret.add(Uint8List.fromList(b)); 173 | return ret; 174 | } 175 | 176 | // Might be a seed. 177 | // Need to do the reverse here to get back to internal representation. 178 | var b1 = b[0] & 248; // 248 = 11111000 179 | var b2 = ((b[0] & 7) << 5) | ((b[1] & 248) >> 3); // 7 = 00000111 180 | 181 | if (b1 == PrefixByteSeed) { 182 | ret.add(Uint8List.fromList([PrefixByteSeed])); 183 | b.removeAt(0); 184 | b.removeAt(0); 185 | ret.add(Uint8List.fromList(b)); 186 | ret.add(Uint8List.fromList([b2])); 187 | return ret; 188 | } 189 | 190 | ret.add(Uint8List.fromList([PrefixByteUnknown])); 191 | b.removeAt(0); 192 | ret.add(Uint8List.fromList(b)); 193 | return ret; 194 | } 195 | 196 | int _checkValidPublicPrefixByte(int prefix) { 197 | switch (prefix) { 198 | case PrefixByteServer: 199 | case PrefixByteCluster: 200 | case PrefixByteOperator: 201 | case PrefixByteAccount: 202 | case PrefixByteUser: 203 | return prefix; 204 | } 205 | return PrefixByteUnknown; 206 | } 207 | 208 | bool _checkValidPrefixByte(int prefix) { 209 | switch (prefix) { 210 | case PrefixByteOperator: 211 | case PrefixByteServer: 212 | case PrefixByteCluster: 213 | case PrefixByteAccount: 214 | case PrefixByteUser: 215 | case PrefixByteSeed: 216 | case PrefixBytePrivate: 217 | return true; 218 | } 219 | return false; 220 | } 221 | 222 | String _encode(int prefix, List src) { 223 | if (!_checkValidPrefixByte(prefix)) { 224 | throw NkeysException('encode invalid prefix'); 225 | } 226 | 227 | var raw = [prefix]; 228 | raw.addAll(src); 229 | 230 | // Calculate and write crc16 checksum 231 | raw.addAll(_crc16(raw)); 232 | var bytes = Uint8List.fromList(raw); 233 | 234 | return _b32Encode(bytes); 235 | } 236 | 237 | Uint8List _crc16(List bytes) { 238 | // CCITT 239 | const POLYNOMIAL = 0x1021; 240 | // XMODEM 241 | const INIT_VALUE = 0x0000; 242 | 243 | final bitRange = Iterable.generate(8); 244 | 245 | var crc = INIT_VALUE; 246 | for (var byte in bytes) { 247 | crc ^= (byte << 8); 248 | // ignore: unused_local_variable 249 | for (var i in bitRange) { 250 | crc = (crc & 0x8000) != 0 ? (crc << 1) ^ POLYNOMIAL : crc << 1; 251 | } 252 | } 253 | var byteData = ByteData(2)..setUint16(0, crc, Endian.little); 254 | return byteData.buffer.asUint8List(); 255 | } 256 | 257 | // EncodeSeed will encode a raw key with the prefix and then seed prefix and crc16 and then base32 encoded. 258 | String _encodeSeed(int public, List src) { 259 | if (_checkValidPublicPrefixByte(public) == PrefixByteUnknown) { 260 | throw NkeysException('Invalid public prefix byte'); 261 | } 262 | 263 | if (src.length != 32) { 264 | throw NkeysException('Invalid src langth'); 265 | } 266 | 267 | // In order to make this human printable for both bytes, we need to do a little 268 | // bit manipulation to setup for base32 encoding which takes 5 bits at a time. 269 | var b1 = PrefixByteSeed | ((public) >> 5); 270 | var b2 = ((public) & 31) << 3; // 31 = 00011111 271 | 272 | var raw = [b1, b2]; 273 | 274 | raw.addAll(src); 275 | 276 | // Calculate and write crc16 checksum 277 | raw.addAll(_crc16(raw)); 278 | 279 | return _b32Encode(raw); 280 | } 281 | 282 | String _b32Encode(List bytes) { 283 | var b = Uint8List.fromList(bytes); 284 | var str = base32.encode(b).replaceAll(RegExp('='), ''); 285 | return str; 286 | } 287 | -------------------------------------------------------------------------------- /test/nats_client_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:dart_nats/dart_nats.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | group('all', () { 9 | test('simple', () async { 10 | var client = Client(); 11 | await client.connect(Uri.parse('ws://localhost:8080')); 12 | var sub = client.sub('subject1'); 13 | client.pub('subject1', Uint8List.fromList('message1'.codeUnits)); 14 | var msg = await sub.stream.first; 15 | await client.close(); 16 | expect(String.fromCharCodes(msg.byte), equals('message1')); 17 | }); 18 | test('newInbox', () { 19 | //just loop generate with out error 20 | var i = 0; 21 | for (i = 0; i < 10000; i++) { 22 | newInbox(); 23 | } 24 | expect(i, 10000); 25 | }); 26 | test('nuid not dup', () { 27 | var dup = false; 28 | var nuid1 = Nuid(); 29 | var nuid2 = Nuid(); 30 | for (var i = 0; i < 10000; i++) { 31 | var n1 = nuid1.next(); 32 | var n2 = nuid2.next(); 33 | if (n1 == n2) dup = true; 34 | } 35 | for (var i = 0; i < 10000; i++) { 36 | var nuid1 = Nuid(); 37 | var nuid2 = Nuid(); 38 | var n1 = nuid1.next(); 39 | var n2 = nuid2.next(); 40 | if (n1 == n2) dup = true; 41 | } 42 | expect(dup, false); 43 | }); 44 | test('pub with Uint8List', () async { 45 | var client = Client(); 46 | await client.connect(Uri.parse('ws://localhost:8080')); 47 | var sub = client.sub('subject1'); 48 | var msgByte = Uint8List.fromList([1, 2, 3, 129, 130]); 49 | client.pub('subject1', msgByte); 50 | var msg = await sub.stream.first; 51 | await client.close(); 52 | expect(msg.byte, equals(msgByte)); 53 | }); 54 | test('pub with Uint8List include return and new line', () async { 55 | var client = Client(); 56 | await client.connect(Uri.parse('ws://localhost:8080')); 57 | var sub = client.sub('subject1'); 58 | var msgByte = Uint8List.fromList( 59 | [1, 10, 3, 13, 10, 13, 130, 1, 10, 3, 13, 10, 13, 130]); 60 | client.pub('subject1', msgByte); 61 | var msg = await sub.stream.first; 62 | await client.close(); 63 | expect(msg.byte, equals(msgByte)); 64 | }); 65 | test('byte huge data', () async { 66 | var client = Client(); 67 | await client.connect(Uri.parse('ws://localhost:8080')); 68 | var sub = client.sub('subject1'); 69 | var msgByte = Uint8List.fromList( 70 | List.generate(1024 + 1024 * 4, (i) => i % 256)); 71 | client.pub('subject1', msgByte); 72 | var msg = await sub.stream.first; 73 | await client.close(); 74 | expect(msg.byte, equals(msgByte)); 75 | }); 76 | test('UTF8', () async { 77 | var client = Client(); 78 | await client.connect(Uri.parse('ws://localhost:8080')); 79 | var sub = client.sub('subject1'); 80 | var thaiString = utf8.encode('ทดสอบ'); 81 | client.pub('subject1', Uint8List.fromList(thaiString)); 82 | var msg = await sub.stream.first; 83 | await client.close(); 84 | expect(msg.byte, equals(thaiString)); 85 | }); 86 | test('pubString ascii', () async { 87 | var client = Client(); 88 | await client.connect(Uri.parse('ws://localhost:8080')); 89 | var sub = client.sub('subject1'); 90 | client.pubString('subject1', 'testtesttest'); 91 | var msg = await sub.stream.first; 92 | await client.close(); 93 | expect(msg.string, equals('testtesttest')); 94 | }); 95 | test('pubString Thai', () async { 96 | var client = Client(); 97 | await client.connect(Uri.parse('ws://localhost:8080')); 98 | var sub = client.sub('subject1'); 99 | client.pubString('subject1', 'ทดสอบ'); 100 | var msg = await sub.stream.first; 101 | await client.close(); 102 | expect(msg.string, equals('ทดสอบ')); 103 | }); 104 | test('delay connect', () async { 105 | var client = Client(); 106 | var sub = client.sub('subject1'); 107 | client.pubString('subject1', 'message1'); 108 | await client.connect(Uri.parse('ws://localhost:8080')); 109 | var msg = await sub.stream.first; 110 | await client.close(); 111 | expect(msg.string, equals('message1')); 112 | }); 113 | test('pub with no buffer ', () async { 114 | var client = Client(); 115 | await client.connect(Uri.parse('ws://localhost:8080')); 116 | var sub = client.sub('subject1'); 117 | await Future.delayed(Duration(seconds: 1)); 118 | client.pubString('subject1', 'message1', buffer: false); 119 | var msg = await sub.stream.first; 120 | await client.close(); 121 | expect(msg.string, equals('message1')); 122 | }); 123 | test('multiple sub ', () async { 124 | var client = Client(); 125 | await client.connect(Uri.parse('ws://localhost:8080')); 126 | var sub1 = client.sub('subject1'); 127 | var sub2 = client.sub('subject2'); 128 | await Future.delayed(Duration(seconds: 1)); 129 | client.pubString('subject1', 'message1'); 130 | client.pubString('subject2', 'message2'); 131 | var msg1 = await sub1.stream.first; 132 | var msg2 = await sub2.stream.first; 133 | await client.close(); 134 | expect(msg1.string, equals('message1')); 135 | expect(msg2.string, equals('message2')); 136 | }); 137 | test('Wildcard sub * ', () async { 138 | var client = Client(); 139 | await client.connect(Uri.parse('ws://localhost:8080')); 140 | var sub = client.sub('subject1.*'); 141 | client.pubString('subject1.1', 'message1'); 142 | client.pubString('subject1.2', 'message2'); 143 | var msgStream = sub.stream.asBroadcastStream(); 144 | var msg1 = await msgStream.first; 145 | var msg2 = await msgStream.first; 146 | await client.close(); 147 | expect(msg1.string, equals('message1')); 148 | expect(msg2.string, equals('message2')); 149 | }); 150 | test('Wildcard sub > ', () async { 151 | var client = Client(); 152 | await client.connect(Uri.parse('ws://localhost:8080')); 153 | var sub = client.sub('subject1.>'); 154 | client.pubString('subject1.a.1', 'message1'); 155 | client.pubString('subject1.b.2', 'message2'); 156 | var msgStream = sub.stream.asBroadcastStream(); 157 | var msg1 = await msgStream.first; 158 | var msg2 = await msgStream.first; 159 | await client.close(); 160 | expect(msg1.string, equals('message1')); 161 | expect(msg2.string, equals('message2')); 162 | }); 163 | test('unsub after connect', () async { 164 | var client = Client(); 165 | await client.connect(Uri.parse('ws://localhost:8080')); 166 | var sub = client.sub('subject1'); 167 | client.pubString('subject1', 'message1'); 168 | var msg = await sub.stream.first; 169 | client.unSub(sub); 170 | expect(msg.string, equals('message1')); 171 | 172 | sub = client.sub('subject1'); 173 | client.pubString('subject1', 'message1'); 174 | msg = await sub.stream.first; 175 | sub.unSub(); 176 | expect(msg.string, equals('message1')); 177 | 178 | await client.close(); 179 | }); 180 | test('unsub before connect', () async { 181 | var client = Client(); 182 | await client.connect(Uri.parse('ws://localhost:8080')); 183 | var sub = client.sub('subject1'); 184 | client.unSub(sub); 185 | 186 | sub = client.sub('subject1'); 187 | sub.unSub(); 188 | await client.close(); 189 | expect(1, 1); 190 | }); 191 | test('get max payload', () async { 192 | var client = Client(); 193 | await client.connect(Uri.parse('ws://localhost:8080')); 194 | 195 | //todo wait for connected 196 | await Future.delayed(Duration(seconds: 2)); 197 | var max = client.maxPayload(); 198 | await client.close(); 199 | 200 | expect(max, isNotNull); 201 | }); 202 | test('sub continuous msg', () async { 203 | var client = Client(); 204 | await client.connect(Uri.parse('ws://localhost:8080')); 205 | var sub = client.sub('sub'); 206 | var r = 0; 207 | var iteration = 100; 208 | sub.stream.listen((msg) { 209 | r++; 210 | }); 211 | for (var i = 0; i < iteration; i++) { 212 | client.pubString('sub', i.toString()); 213 | // await Future.delayed(Duration(milliseconds: 10)); 214 | } 215 | await Future.delayed(Duration(seconds: 1)); 216 | await client.close(); 217 | expect(r, equals(iteration)); 218 | }); 219 | test('sub defect 13 binary', () async { 220 | var client = Client(); 221 | await client.connect(Uri.parse('ws://localhost:8080')); 222 | var sub = client.sub('sub'); 223 | var r = 0; 224 | var iteration = 100; 225 | sub.stream.listen((msg) { 226 | r++; 227 | }); 228 | for (var i = 0; i < iteration; i++) { 229 | client.pub('sub', Uint8List.fromList([10, 13, 10])); 230 | // await Future.delayed(Duration(milliseconds: 10)); 231 | } 232 | await Future.delayed(Duration(seconds: 1)); 233 | await client.close(); 234 | expect(r, equals(iteration)); 235 | }); 236 | }); 237 | } 238 | -------------------------------------------------------------------------------- /lib/src/client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | import 'dart:typed_data'; 5 | 6 | import 'package:mutex/mutex.dart'; 7 | import 'package:web_socket_channel/web_socket_channel.dart'; 8 | 9 | import 'common.dart'; 10 | import 'inbox.dart'; 11 | import 'message.dart'; 12 | import 'nkeys.dart'; 13 | import 'subscription.dart'; 14 | 15 | enum _ReceiveState { 16 | idle, //op=msg -> msg 17 | msg, //newline -> idle 18 | } 19 | 20 | ///status of the nats client 21 | enum Status { 22 | /// disconnected or not connected 23 | disconnected, 24 | 25 | /// tlsHandshake 26 | tlsHandshake, 27 | 28 | /// channel layer connect wait for info connect handshake 29 | infoHandshake, 30 | 31 | ///connected to server ready 32 | connected, 33 | 34 | ///already close by close or server 35 | closed, 36 | 37 | ///automatic reconnection to server 38 | reconnecting, 39 | 40 | ///connecting by connect() method 41 | connecting, 42 | 43 | // draining_subs, 44 | // draining_pubs, 45 | } 46 | 47 | enum _ClientStatus { 48 | init, 49 | used, 50 | closed, 51 | } 52 | 53 | class _Pub { 54 | final String? subject; 55 | final List data; 56 | final String? replyTo; 57 | 58 | _Pub(this.subject, this.data, this.replyTo); 59 | } 60 | 61 | ///NATS client 62 | class Client { 63 | var _ackStream = StreamController.broadcast(); 64 | _ClientStatus _clientStatus = _ClientStatus.init; 65 | WebSocketChannel? _wsChannel; 66 | Socket? _tcpSocket; 67 | SecureSocket? _secureSocket; 68 | bool _tlsRequired = false; 69 | bool _retry = false; 70 | 71 | Info _info = Info(); 72 | late Completer _pingCompleter; 73 | late Completer _connectCompleter; 74 | 75 | /// Error handler for websocket errors 76 | Function(dynamic) wsErrorHandler = (e) { 77 | throw NatsException('listen ws error: $e'); 78 | }; 79 | 80 | var _status = Status.disconnected; 81 | 82 | /// true if connected 83 | bool get connected => _status == Status.connected; 84 | 85 | final _statusController = StreamController.broadcast(); 86 | 87 | var _channelStream = StreamController(); 88 | 89 | ///status of the client 90 | Status get status => _status; 91 | 92 | /// accept bad certificate NOT recomend to use in production 93 | bool acceptBadCert = false; 94 | 95 | /// Stream status for status update 96 | Stream get statusStream => _statusController.stream; 97 | 98 | var _connectOption = ConnectOption(); 99 | 100 | ///SecurityContext 101 | SecurityContext? securityContext; 102 | 103 | Nkeys? _nkeys; 104 | 105 | /// Nkeys seed 106 | String? get seed => _nkeys?.seed; 107 | set seed(String? newseed) { 108 | if (newseed == null) { 109 | _nkeys = null; 110 | return; 111 | } 112 | _nkeys = Nkeys.fromSeed(newseed); 113 | } 114 | 115 | final _jsonDecoder = {}; 116 | // final _jsonEncoder = {}; 117 | 118 | /// add json decoder for type 119 | void registerJsonDecoder(T Function(String) f) { 120 | if (T == dynamic) { 121 | NatsException('can not register dyname type'); 122 | } 123 | _jsonDecoder[T] = f; 124 | } 125 | 126 | /// add json encoder for type 127 | // void registerJsonEncoder(String Function(T) f) { 128 | // if (T == dynamic) { 129 | // NatsException('can not register dyname type'); 130 | // } 131 | // _jsonEncoder[T] = f as String Function(Type); 132 | // } 133 | 134 | ///server info 135 | Info? get info => _info; 136 | 137 | final _subs = {}; 138 | final _backendSubs = {}; 139 | final _pubBuffer = <_Pub>[]; 140 | 141 | int _ssid = 0; 142 | 143 | List _buffer = []; 144 | _ReceiveState _receiveState = _ReceiveState.idle; 145 | String _receiveLine1 = ''; 146 | Future _sign() async { 147 | if (_info.nonce != null && _nkeys != null) { 148 | var sig = _nkeys?.sign(utf8.encode(_info.nonce!)); 149 | 150 | _connectOption.sig = base64.encode(sig!); 151 | } 152 | } 153 | 154 | ///NATS Client Constructor 155 | Client() { 156 | _steamHandle(); 157 | } 158 | 159 | void _steamHandle() { 160 | _channelStream.stream.listen((d) { 161 | _buffer.addAll(d); 162 | // org code 163 | // while ( 164 | // _receiveState == _ReceiveState.idle && _buffer.contains(13)) { 165 | // _processOp(); 166 | // } 167 | 168 | //Thank aktxyz for contribution 169 | while (_receiveState == _ReceiveState.idle && _buffer.contains(13)) { 170 | var n13 = _buffer.indexOf(13); 171 | var msgFull = 172 | String.fromCharCodes(_buffer.take(n13)).toLowerCase().trim(); 173 | var msgList = msgFull.split(' '); 174 | var msgType = msgList[0]; 175 | //print('... process $msgType ${_buffer.length}'); 176 | 177 | if (msgType == 'msg' || msgType == 'hmsg') { 178 | var len = int.parse(msgList.last); 179 | if (len > 0 && _buffer.length < (msgFull.length + len + 4)) { 180 | break; // not a full payload, go around again 181 | } 182 | } 183 | 184 | _processOp(); 185 | } 186 | // }, onDone: () { 187 | // _setStatus(Status.disconnected); 188 | // close(); 189 | // }, onError: (err) { 190 | // _setStatus(Status.disconnected); 191 | // close(); 192 | }); 193 | } 194 | 195 | /// Connect to NATS server 196 | Future connect( 197 | Uri uri, { 198 | ConnectOption? connectOption, 199 | int timeout = 5, 200 | bool retry = true, 201 | int retryInterval = 10, 202 | int retryCount = 3, 203 | SecurityContext? securityContext, 204 | }) async { 205 | this._retry = retry; 206 | this.securityContext = securityContext; 207 | _connectCompleter = Completer(); 208 | if (_clientStatus == _ClientStatus.used) { 209 | throw Exception( 210 | NatsException('client in use. must close before call connect')); 211 | } 212 | if (status != Status.disconnected && status != Status.closed) { 213 | return Future.error('Error: status not disconnected and not closed'); 214 | } 215 | _clientStatus = _ClientStatus.used; 216 | if (connectOption != null) _connectOption = connectOption; 217 | do { 218 | _connectLoop( 219 | uri, 220 | timeout: timeout, 221 | retryInterval: retryInterval, 222 | retryCount: retryCount, 223 | ); 224 | 225 | if (_clientStatus == _ClientStatus.closed || status == Status.closed) { 226 | if (!_connectCompleter.isCompleted) { 227 | _connectCompleter.complete(); 228 | } 229 | close(); 230 | _clientStatus = _ClientStatus.closed; 231 | return; 232 | } 233 | if (!this._retry || retryCount != -1) { 234 | return _connectCompleter.future; 235 | } 236 | await for (var s in statusStream) { 237 | if (s == Status.disconnected) { 238 | break; 239 | } 240 | if (s == Status.closed) { 241 | return; 242 | } 243 | } 244 | } while (this._retry && retryCount == -1); 245 | return _connectCompleter.future; 246 | } 247 | 248 | void _connectLoop(Uri uri, 249 | {int timeout = 5, 250 | required int retryInterval, 251 | required int retryCount}) async { 252 | for (var count = 0; 253 | count == 0 || ((count < retryCount || retryCount == -1) && this._retry); 254 | count++) { 255 | if (count == 0) { 256 | _setStatus(Status.connecting); 257 | } else { 258 | _setStatus(Status.reconnecting); 259 | } 260 | 261 | try { 262 | if (_channelStream.isClosed) { 263 | _channelStream = StreamController(); 264 | } 265 | var sucess = await _connectUri(uri, timeout: timeout); 266 | if (!sucess) { 267 | await Future.delayed(Duration(seconds: retryInterval)); 268 | continue; 269 | } 270 | 271 | _buffer = []; 272 | 273 | return; 274 | } catch (err) { 275 | await close(); 276 | if (!_connectCompleter.isCompleted) { 277 | _connectCompleter.completeError(err); 278 | } 279 | _setStatus(Status.disconnected); 280 | } 281 | } 282 | if (!_connectCompleter.isCompleted) { 283 | _clientStatus = _ClientStatus.closed; 284 | _connectCompleter 285 | .completeError(NatsException('can not connect ${uri.toString()}')); 286 | } 287 | } 288 | 289 | Future _connectUri(Uri uri, {int timeout = 5}) async { 290 | try { 291 | if (uri.scheme == '') { 292 | throw Exception(NatsException('No scheme in uri')); 293 | } 294 | switch (uri.scheme) { 295 | case 'wss': 296 | case 'ws': 297 | try { 298 | _wsChannel = WebSocketChannel.connect(uri); 299 | } catch (e) { 300 | return false; 301 | } 302 | if (_wsChannel == null) { 303 | return false; 304 | } 305 | _setStatus(Status.infoHandshake); 306 | _wsChannel?.stream.listen((event) { 307 | if (_channelStream.isClosed) return; 308 | _channelStream.add(event); 309 | }, onDone: () { 310 | _setStatus(Status.disconnected); 311 | }, onError: (e) { 312 | close(); 313 | wsErrorHandler(e); 314 | }); 315 | return true; 316 | case 'nats': 317 | var port = uri.port; 318 | if (port == 0) { 319 | port = 4222; 320 | } 321 | _tcpSocket = await Socket.connect( 322 | uri.host, 323 | port, 324 | timeout: Duration(seconds: timeout), 325 | ); 326 | if (_tcpSocket == null) { 327 | return false; 328 | } 329 | _setStatus(Status.infoHandshake); 330 | _tcpSocket!.listen((event) { 331 | if (_secureSocket == null) { 332 | if (_channelStream.isClosed) return; 333 | _channelStream.add(event); 334 | } 335 | }).onDone(() { 336 | _setStatus(Status.disconnected); 337 | }); 338 | return true; 339 | case 'tls': 340 | _tlsRequired = true; 341 | var port = uri.port; 342 | if (port == 0) { 343 | port = 4443; 344 | } 345 | _tcpSocket = await Socket.connect(uri.host, port, 346 | timeout: Duration(seconds: timeout)); 347 | if (_tcpSocket == null) break; 348 | _setStatus(Status.infoHandshake); 349 | _tcpSocket!.listen((event) { 350 | if (_secureSocket == null) { 351 | if (_channelStream.isClosed) return; 352 | _channelStream.add(event); 353 | } 354 | }); 355 | return true; 356 | default: 357 | throw Exception(NatsException('schema ${uri.scheme} not support')); 358 | } 359 | } catch (e) { 360 | return false; 361 | } 362 | return false; 363 | } 364 | 365 | void _backendSubscriptAll() { 366 | _backendSubs.clear(); 367 | _subs.forEach((sid, s) async { 368 | _sub(s.subject, sid, queueGroup: s.queueGroup); 369 | // s.backendSubscription = true; 370 | _backendSubs[sid] = true; 371 | }); 372 | } 373 | 374 | void _flushPubBuffer() { 375 | _pubBuffer.forEach((p) { 376 | _pub(p); 377 | }); 378 | } 379 | 380 | void _processOp() async { 381 | ///find endline 382 | var nextLineIndex = _buffer.indexWhere((c) { 383 | if (c == 13) { 384 | return true; 385 | } 386 | return false; 387 | }); 388 | if (nextLineIndex == -1) return; 389 | var line = 390 | String.fromCharCodes(_buffer.sublist(0, nextLineIndex)); // retest 391 | if (_buffer.length > nextLineIndex + 2) { 392 | _buffer.removeRange(0, nextLineIndex + 2); 393 | } else { 394 | _buffer = []; 395 | } 396 | 397 | ///decode operation 398 | var i = line.indexOf(' '); 399 | String op, data; 400 | if (i != -1) { 401 | op = line.substring(0, i).trim().toLowerCase(); 402 | data = line.substring(i).trim(); 403 | } else { 404 | op = line.trim().toLowerCase(); 405 | data = ''; 406 | } 407 | 408 | ///process operation 409 | switch (op) { 410 | case 'msg': 411 | _receiveState = _ReceiveState.msg; 412 | _receiveLine1 = line; 413 | _processMsg(); 414 | _receiveLine1 = ''; 415 | _receiveState = _ReceiveState.idle; 416 | break; 417 | case 'hmsg': 418 | _receiveState = _ReceiveState.msg; 419 | _receiveLine1 = line; 420 | _processHMsg(); 421 | _receiveLine1 = ''; 422 | _receiveState = _ReceiveState.idle; 423 | break; 424 | case 'info': 425 | _info = Info.fromJson(jsonDecode(data)); 426 | if (_tlsRequired && !(_info.tlsRequired ?? false)) { 427 | throw Exception(NatsException('require TLS but server not required')); 428 | } 429 | 430 | if ((_info.tlsRequired ?? false) && _tcpSocket != null) { 431 | _setStatus(Status.tlsHandshake); 432 | var secureSocket = await SecureSocket.secure( 433 | _tcpSocket!, 434 | context: this.securityContext, 435 | onBadCertificate: (certificate) { 436 | if (acceptBadCert) return true; 437 | return false; 438 | }, 439 | ); 440 | 441 | _secureSocket = secureSocket; 442 | secureSocket.listen((event) { 443 | if (_channelStream.isClosed) return; 444 | _channelStream.add(event); 445 | }, onError: (error) { 446 | print('Socket error: $error'); 447 | _setStatus(Status.disconnected); 448 | 449 | if (error is TlsException) { 450 | this._retry = false; 451 | this.close(); 452 | throw Exception(NatsException(error.message)); 453 | } 454 | }); 455 | } 456 | 457 | await _sign(); 458 | _addConnectOption(_connectOption); 459 | if (_connectOption.verbose == true) { 460 | var ack = await _ackStream.stream.first; 461 | if (ack) { 462 | _setStatus(Status.connected); 463 | } else { 464 | _setStatus(Status.disconnected); 465 | } 466 | } else { 467 | _setStatus(Status.connected); 468 | } 469 | _backendSubscriptAll(); 470 | _flushPubBuffer(); 471 | if (!_connectCompleter.isCompleted) { 472 | _connectCompleter.complete(); 473 | } 474 | break; 475 | case 'ping': 476 | if (status == Status.connected) { 477 | _add('pong'); 478 | } 479 | break; 480 | case '-err': 481 | // _processErr(data); 482 | if (_connectOption.verbose == true) { 483 | _ackStream.sink.add(false); 484 | } 485 | break; 486 | case 'pong': 487 | _pingCompleter.complete(); 488 | break; 489 | case '+ok': 490 | //do nothing 491 | if (_connectOption.verbose == true) { 492 | _ackStream.sink.add(true); 493 | } 494 | break; 495 | } 496 | } 497 | 498 | void _processMsg() { 499 | var s = _receiveLine1.split(' '); 500 | var subject = s[1]; 501 | var sid = int.parse(s[2]); 502 | String? replyTo; 503 | int length; 504 | if (s.length == 4) { 505 | length = int.parse(s[3]); 506 | } else { 507 | replyTo = s[3]; 508 | length = int.parse(s[4]); 509 | } 510 | if (_buffer.length < length) return; 511 | var payload = Uint8List.fromList(_buffer.sublist(0, length)); 512 | // _buffer = _buffer.sublist(length + 2); 513 | if (_buffer.length > length + 2) { 514 | _buffer.removeRange(0, length + 2); 515 | } else { 516 | _buffer = []; 517 | } 518 | 519 | if (_subs[sid] != null) { 520 | _subs[sid]?.add(Message(subject, sid, payload, this, replyTo: replyTo)); 521 | } 522 | } 523 | 524 | void _processHMsg() { 525 | var s = _receiveLine1.split(' '); 526 | var subject = s[1]; 527 | var sid = int.parse(s[2]); 528 | String? replyTo; 529 | int length; 530 | int headerLength; 531 | if (s.length == 5) { 532 | headerLength = int.parse(s[3]); 533 | length = int.parse(s[4]); 534 | } else { 535 | replyTo = s[3]; 536 | headerLength = int.parse(s[4]); 537 | length = int.parse(s[5]); 538 | } 539 | if (_buffer.length < length) return; 540 | var header = Uint8List.fromList(_buffer.sublist(0, headerLength)); 541 | var payload = Uint8List.fromList(_buffer.sublist(headerLength, length)); 542 | // _buffer = _buffer.sublist(length + 2); 543 | if (_buffer.length > length + 2) { 544 | _buffer.removeRange(0, length + 2); 545 | } else { 546 | _buffer = []; 547 | } 548 | 549 | if (_subs[sid] != null) { 550 | var msg = Message(subject, sid, payload, this, 551 | replyTo: replyTo, header: Header.fromBytes(header)); 552 | _subs[sid]?.add(msg); 553 | } 554 | } 555 | 556 | /// get server max payload 557 | int? maxPayload() => _info.maxPayload; 558 | 559 | ///ping server current not implement pong verification 560 | Future ping() { 561 | _pingCompleter = Completer(); 562 | _add('ping'); 563 | return _pingCompleter.future; 564 | } 565 | 566 | void _addConnectOption(ConnectOption c) { 567 | _add('connect ' + jsonEncode(c.toJson())); 568 | } 569 | 570 | ///default buffer action for pub 571 | var defaultPubBuffer = true; 572 | 573 | ///publish by byte (Uint8List) return true if sucess sending or buffering 574 | ///return false if not connect 575 | Future pub(String? subject, Uint8List data, 576 | {String? replyTo, bool? buffer, Header? header}) async { 577 | buffer ??= defaultPubBuffer; 578 | if (status != Status.connected) { 579 | if (buffer) { 580 | _pubBuffer.add(_Pub(subject, data, replyTo)); 581 | return true; 582 | } else { 583 | return false; 584 | } 585 | } 586 | 587 | String cmd; 588 | var headerByte = header?.toBytes(); 589 | if (header == null) { 590 | cmd = 'pub'; 591 | } else { 592 | cmd = 'hpub'; 593 | } 594 | cmd += ' $subject'; 595 | if (replyTo != null) { 596 | cmd += ' $replyTo'; 597 | } 598 | if (headerByte != null) { 599 | cmd += ' ${headerByte.length} ${headerByte.length + data.length}'; 600 | _add(cmd); 601 | var dataWithHeader = headerByte.toList(); 602 | dataWithHeader.addAll(data.toList()); 603 | _addByte(dataWithHeader); 604 | } else { 605 | cmd += ' ${data.length}'; 606 | _add(cmd); 607 | _addByte(data); 608 | } 609 | 610 | if (_connectOption.verbose == true) { 611 | var ack = await _ackStream.stream.first; 612 | return ack; 613 | } 614 | return true; 615 | } 616 | 617 | ///publish by string 618 | Future pubString(String subject, String str, 619 | {String? replyTo, bool buffer = true, Header? header}) async { 620 | return pub(subject, Uint8List.fromList(utf8.encode(str)), replyTo: replyTo, buffer: buffer); 621 | } 622 | 623 | Future _pub(_Pub p) async { 624 | if (p.replyTo == null) { 625 | _add('pub ${p.subject} ${p.data.length}'); 626 | } else { 627 | _add('pub ${p.subject} ${p.replyTo} ${p.data.length}'); 628 | } 629 | _addByte(p.data); 630 | if (_connectOption.verbose == true) { 631 | var ack = await _ackStream.stream.first; 632 | return ack; 633 | } 634 | return true; 635 | } 636 | 637 | T Function(String) _getJsonDecoder() { 638 | var c = _jsonDecoder[T]; 639 | if (c == null) { 640 | throw NatsException('no decoder for type $T'); 641 | } 642 | return c as T Function(String); 643 | } 644 | 645 | // String Function(dynamic) _getJsonEncoder(Type T) { 646 | // var c = _jsonDecoder[T]; 647 | // if (c == null) { 648 | // throw NatsException('no encoder for type $T'); 649 | // } 650 | // return c as String Function(dynamic); 651 | // } 652 | 653 | ///subscribe to subject option with queuegroup 654 | Subscription sub( 655 | String subject, { 656 | String? queueGroup, 657 | T Function(String)? jsonDecoder, 658 | }) { 659 | _ssid++; 660 | 661 | //get registered json decoder 662 | if (T != dynamic && jsonDecoder == null) { 663 | jsonDecoder = _getJsonDecoder(); 664 | } 665 | 666 | var s = Subscription(_ssid, subject, this, 667 | queueGroup: queueGroup, jsonDecoder: jsonDecoder); 668 | _subs[_ssid] = s; 669 | if (status == Status.connected) { 670 | _sub(subject, _ssid, queueGroup: queueGroup); 671 | _backendSubs[_ssid] = true; 672 | } 673 | return s; 674 | } 675 | 676 | void _sub(String? subject, int sid, {String? queueGroup}) { 677 | if (queueGroup == null) { 678 | _add('sub $subject $sid'); 679 | } else { 680 | _add('sub $subject $queueGroup $sid'); 681 | } 682 | } 683 | 684 | ///unsubscribe 685 | bool unSub(Subscription s) { 686 | var sid = s.sid; 687 | 688 | if (_subs[sid] == null) return false; 689 | _unSub(sid); 690 | _subs.remove(sid); 691 | s.close(); 692 | _backendSubs.remove(sid); 693 | return true; 694 | } 695 | 696 | ///unsubscribe by id 697 | bool unSubById(int sid) { 698 | if (_subs[sid] == null) return false; 699 | return unSub(_subs[sid]!); 700 | } 701 | 702 | //todo unsub with max msgs 703 | 704 | void _unSub(int sid, {String? maxMsgs}) { 705 | if (maxMsgs == null) { 706 | _add('unsub $sid'); 707 | } else { 708 | _add('unsub $sid $maxMsgs'); 709 | } 710 | } 711 | 712 | void _add(String str) { 713 | if (status == Status.closed || status == Status.disconnected) { 714 | return; 715 | } 716 | if (_wsChannel != null) { 717 | // if (_wsChannel?.closeCode == null) return; 718 | _wsChannel?.sink.add(utf8.encode(str + '\r\n')); 719 | return; 720 | } else if (_secureSocket != null) { 721 | _secureSocket!.add(utf8.encode(str + '\r\n')); 722 | return; 723 | } else if (_tcpSocket != null) { 724 | _tcpSocket!.add(utf8.encode(str + '\r\n')); 725 | return; 726 | } 727 | throw Exception(NatsException('no connection')); 728 | } 729 | 730 | void _addByte(List msg) { 731 | if (_wsChannel != null) { 732 | _wsChannel?.sink.add(msg); 733 | _wsChannel?.sink.add(utf8.encode('\r\n')); 734 | return; 735 | } else if (_secureSocket != null) { 736 | _secureSocket?.add(msg); 737 | _secureSocket?.add(utf8.encode('\r\n')); 738 | return; 739 | } else if (_tcpSocket != null) { 740 | _tcpSocket?.add(msg); 741 | _tcpSocket?.add(utf8.encode('\r\n')); 742 | return; 743 | } 744 | throw Exception(NatsException('no connection')); 745 | } 746 | 747 | var _inboxPrefix = '_INBOX'; 748 | 749 | /// get Inbox prefix default '_INBOX' 750 | set inboxPrefix(String i) { 751 | if (_clientStatus == _ClientStatus.used) { 752 | throw NatsException('inbox prefix can not change when connection in use'); 753 | } 754 | _inboxPrefix = i; 755 | _inboxSubPrefix = null; 756 | } 757 | 758 | /// set Inbox prefix default '_INBOX' 759 | String get inboxPrefix => _inboxPrefix; 760 | 761 | final _inboxs = {}; 762 | final _mutex = Mutex(); 763 | String? _inboxSubPrefix; 764 | Subscription? _inboxSub; 765 | 766 | /// Request will send a request payload and deliver the response message, 767 | /// TimeoutException on timeout. 768 | /// 769 | /// Example: 770 | /// ```dart 771 | /// try { 772 | /// await client.request('service', Uint8List.fromList('request'.codeUnits), 773 | /// timeout: Duration(seconds: 2)); 774 | /// } on TimeoutException { 775 | /// timeout = true; 776 | /// } 777 | /// ``` 778 | Future> request( 779 | String subj, 780 | Uint8List data, { 781 | Duration timeout = const Duration(seconds: 2), 782 | T Function(String)? jsonDecoder, 783 | }) async { 784 | if (!connected) { 785 | throw NatsException("request error: client not connected"); 786 | } 787 | Message resp; 788 | //ensure no other request 789 | await _mutex.acquire(); 790 | //get registered json decoder 791 | if (T != dynamic && jsonDecoder == null) { 792 | jsonDecoder = _getJsonDecoder(); 793 | } 794 | 795 | if (_inboxSubPrefix == null) { 796 | if (inboxPrefix == '_INBOX') { 797 | _inboxSubPrefix = inboxPrefix + '.' + Nuid().next(); 798 | } else { 799 | _inboxSubPrefix = inboxPrefix; 800 | } 801 | _inboxSub = sub(_inboxSubPrefix! + '.>', jsonDecoder: jsonDecoder); 802 | } 803 | var inbox = _inboxSubPrefix! + '.' + Nuid().next(); 804 | var stream = _inboxSub!.stream; 805 | 806 | pub(subj, data, replyTo: inbox); 807 | 808 | try { 809 | do { 810 | resp = await stream.take(1).single.timeout(timeout); 811 | } while (resp.subject != inbox); 812 | } on TimeoutException { 813 | throw TimeoutException('request time > $timeout'); 814 | } finally { 815 | _mutex.release(); 816 | } 817 | var msg = Message( 818 | resp.subject, 819 | resp.sid, 820 | resp.byte, 821 | this, 822 | header: resp.header, 823 | jsonDecoder: jsonDecoder, 824 | ); 825 | return msg; 826 | } 827 | 828 | /// requestString() helper to request() 829 | Future> requestString( 830 | String subj, 831 | String data, { 832 | Duration timeout = const Duration(seconds: 2), 833 | T Function(String)? jsonDecoder, 834 | }) { 835 | return request( 836 | subj, 837 | Uint8List.fromList(data.codeUnits), 838 | timeout: timeout, 839 | jsonDecoder: jsonDecoder, 840 | ); 841 | } 842 | 843 | void _setStatus(Status newStatus) { 844 | _status = newStatus; 845 | _statusController.add(newStatus); 846 | } 847 | 848 | /// close connection and cancel all future retries 849 | Future forceClose() async { 850 | this._retry = false; 851 | this.close(); 852 | } 853 | 854 | ///close connection to NATS server unsub to server but still keep subscription list at client 855 | Future close() async { 856 | _setStatus(Status.closed); 857 | _backendSubs.forEach((_, s) => s = false); 858 | _inboxs.clear(); 859 | await _wsChannel?.sink.close(); 860 | _wsChannel = null; 861 | await _secureSocket?.close(); 862 | _secureSocket = null; 863 | await _tcpSocket?.close(); 864 | _tcpSocket = null; 865 | await _inboxSub?.close(); 866 | _inboxSub = null; 867 | _inboxSubPrefix = null; 868 | _buffer = []; 869 | _clientStatus = _ClientStatus.closed; 870 | } 871 | 872 | /// discontinue tcpConnect. use connect(uri) instead 873 | ///Backward compatible with 0.2.x version 874 | Future tcpConnect(String host, 875 | {int port = 4222, 876 | ConnectOption? connectOption, 877 | int timeout = 5, 878 | bool retry = true, 879 | int retryInterval = 10}) { 880 | return connect( 881 | Uri(scheme: 'nats', host: host, port: port), 882 | retry: retry, 883 | retryInterval: retryInterval, 884 | timeout: timeout, 885 | connectOption: connectOption, 886 | ); 887 | } 888 | 889 | /// close tcp connect Only for testing 890 | Future tcpClose() async { 891 | await _tcpSocket?.close(); 892 | _setStatus(Status.disconnected); 893 | } 894 | 895 | /// wait until client connected 896 | Future waitUntilConnected() async { 897 | await waitUntil(Status.connected); 898 | } 899 | 900 | /// wait untril status 901 | Future waitUntil(Status s) async { 902 | if (status == s) { 903 | return; 904 | } 905 | await for (var st in statusStream) { 906 | if (st == s) { 907 | break; 908 | } 909 | } 910 | } 911 | } 912 | --------------------------------------------------------------------------------