├── .gitignore ├── pubspec.yaml ├── test └── ddp_test.dart ├── CHANGELOG.md ├── lib ├── ddp.dart ├── ddp_messages.dart ├── ddp_collection.dart ├── ddp_stats.dart └── ddp_client.dart ├── LICENSE ├── README.md └── pubspec.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .dart_tool 3 | .packages -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: ddp 2 | description: MeteorJS DDP library for Dart. 3 | version: 0.0.13 4 | author: haoguo 5 | homepage: https://github.com/haoguo/ddp 6 | environment: 7 | sdk: ">=1.8.0 <3.0.0" 8 | 9 | dependencies: 10 | tuple: ^1.0.2 11 | 12 | dev_dependencies: 13 | test: ^1.3.0 -------------------------------------------------------------------------------- /test/ddp_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:ddp/ddp.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test('IdManager', () { 6 | print(16.toRadixString(16)); 7 | }); 8 | 9 | test('DdpClientTest', () { 10 | DdpClient client = DdpClient( 11 | 'DdpClientTest', 'ws://localhost:3000/websocket', 'http://localhost'); 12 | client.connect(); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.0.13] 2 | 3 | * Add missing type parameter 4 | 5 | ## [0.0.12] 6 | 7 | * Add removeStatusListener for DdpClient 8 | 9 | ## [0.0.11] 10 | 11 | * Remove ddp_ejson.dart file. 12 | 13 | ## [0.0.10] 14 | 15 | * Remove dependency [sprintf](https://pub.dev/packages/sprintf) now. Use int.**toRadixString** instead. 16 | * Remove dependency [mutex](https://pub.dev/packages/mutex) now. The previously use in sync code which is meaningless. 17 | * Merged [bobagold's pull request](https://github.com/haoguo/ddp/pull/4). 18 | 19 | ## [0.0.9] - Add NO sub 20 | 21 | * Add No Sub implementation of meteor DDP 22 | 23 | ## [0.0.1] - TODO: Add release date. 24 | 25 | * TODO: Describe initial release. 26 | -------------------------------------------------------------------------------- /lib/ddp.dart: -------------------------------------------------------------------------------- 1 | library ddp; 2 | 3 | import 'dart:async'; 4 | import 'dart:convert'; 5 | import 'dart:io'; 6 | 7 | import 'package:tuple/tuple.dart'; 8 | 9 | part 'ddp_client.dart'; 10 | part 'ddp_collection.dart'; 11 | part 'ddp_messages.dart'; 12 | part 'ddp_stats.dart'; 13 | 14 | class _IdManager { 15 | int _next = 0; 16 | 17 | String next() { 18 | final next = _next; 19 | _next++; 20 | return next.toRadixString(16); 21 | } 22 | } 23 | 24 | class _PingTracker { 25 | Function(Error) _handler; 26 | Duration _timeout; 27 | Timer _timer; 28 | } 29 | 30 | typedef void OnCallDone(Call call); 31 | 32 | class Call { 33 | String id; 34 | String serviceMethod; 35 | dynamic args; 36 | dynamic reply; 37 | Error error; 38 | DdpClient owner; 39 | List _handlers = []; 40 | 41 | void onceDone(OnCallDone fn) { 42 | this._handlers.add(fn); 43 | } 44 | 45 | void done() { 46 | owner._calls.remove(this.id); 47 | _handlers.forEach((handler) => handler(this)); 48 | _handlers.clear(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 haoguo 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 | -------------------------------------------------------------------------------- /lib/ddp_messages.dart: -------------------------------------------------------------------------------- 1 | part of ddp; 2 | 3 | class Message { 4 | String type; 5 | String id; 6 | 7 | Message(this.id, this.type); 8 | 9 | factory Message.ping(String id) => _Ping(id); 10 | 11 | factory Message.pong(String id) => _Pong(id); 12 | 13 | factory Message.method(String id, String serviceMethod, List args) => 14 | _Method(id, serviceMethod, args); 15 | 16 | factory Message.sub(String id, String subName, List args) => 17 | _Sub(id, subName, args); 18 | 19 | factory Message.unSub(String id) => _UnSub(id); 20 | 21 | factory Message.connect() => _Connect('1', ['1'], null); 22 | 23 | factory Message.reconnect(String session) => _Connect('1', ['1'], session); 24 | 25 | String toJson() { 26 | return json.encode(this._toMap()); 27 | } 28 | 29 | Map _toMap() { 30 | Map map = {}; 31 | if (this.id != null) { 32 | map['id'] = this.id; 33 | } 34 | map['msg'] = this.type; 35 | return map; 36 | } 37 | } 38 | 39 | class _Ping extends Message { 40 | _Ping(String id) : super(id, 'ping'); 41 | } 42 | 43 | class _Pong extends Message { 44 | _Pong(String id) : super(id, 'pong'); 45 | } 46 | 47 | class _Method extends Message { 48 | String serviceMethod; 49 | List args; 50 | 51 | _Method(String id, this.serviceMethod, this.args) : super(id, 'method'); 52 | 53 | @override 54 | Map _toMap() { 55 | final map = super._toMap(); 56 | map['method'] = this.serviceMethod; 57 | map['params'] = args; 58 | return map; 59 | } 60 | } 61 | 62 | class _Sub extends Message { 63 | String subName; 64 | List args; 65 | 66 | _Sub(String id, this.subName, this.args) : super(id, 'sub'); 67 | 68 | @override 69 | Map _toMap() { 70 | final map = super._toMap(); 71 | map['name'] = this.subName; 72 | map['params'] = args; 73 | return map; 74 | } 75 | } 76 | 77 | class _UnSub extends Message { 78 | _UnSub(String id) : super(id, 'unsub'); 79 | } 80 | 81 | class _Connect extends Message { 82 | String version; 83 | List support; 84 | String session; 85 | 86 | _Connect(this.version, this.support, this.session) : super(null, 'connect'); 87 | 88 | @override 89 | Map _toMap() { 90 | final map = super._toMap(); 91 | map['version'] = this.version; 92 | map['support'] = this.support; 93 | if (this.session != null) { 94 | map['session'] = this.session; 95 | } 96 | return map; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DDP 2 | 3 | This is a [DPP](https://github.com/meteor/meteor/blob/devel/packages/ddp/DDP.md) protocol implementation for Flutter/Dart. 4 | 5 | ## Getting Started 6 | ### Install 7 | Add the package to your project using these [instructions](https://pub.dartlang.org/packages/ddp#-installing-tab-). 8 | For help getting started with Flutter, view our online [documentation](https://flutter.io/). 9 | For help on editing package code, view the [documentation](https://flutter.io/developing-packages/). 10 | 11 | ### Connection 12 | ```dart 13 | import 'package:ddp/ddp.dart'; 14 | 15 | DdpClient client = DdpClient("meteor", "ws://localhost:3000/websocket", "meteor"); 16 | client.connect(); 17 | 18 | client.addStatusListener((status) { 19 | if (status == ConnectStatus.connected) { 20 | print('I am connected!'); 21 | } 22 | }); 23 | ``` 24 | 25 | ### Subscribe 26 | ```dart 27 | void myListener(String collectionName, String message, String docId, Map map) { 28 | // Do something great 29 | } 30 | 31 | client.addStatusListener((status) { 32 | if (status == ConnectStatus.connected) { 33 | client.subscribe("subscribe_name", (done) { 34 | Collection collection = done.owner.collectionByName("collection_name"); 35 | collection.addUpdateListener(myListener); 36 | }, []); 37 | } 38 | }); 39 | ``` 40 | 41 | ### Call method 42 | ```dart 43 | List tasks = []; 44 | 45 | client.addStatusListener((status) async { 46 | if (status == ConnectStatus.connected) { 47 | var data = await client.call('getTasks', []); 48 | data.reply.forEach((map) => tasks.add(map)); 49 | } 50 | }); 51 | ``` 52 | 53 | ## License 54 | 55 | Copyright (c) 2018 haoguo 56 | 57 | Permission is hereby granted, free of charge, to any person obtaining a copy 58 | of this software and associated documentation files (the "Software"), to deal 59 | in the Software without restriction, including without limitation the rights 60 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 61 | copies of the Software, and to permit persons to whom the Software is 62 | furnished to do so, subject to the following conditions: 63 | 64 | The above copyright notice and this permission notice shall be included in all 65 | copies or substantial portions of the Software. 66 | 67 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 68 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 69 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 70 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 71 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 72 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 73 | SOFTWARE. 74 | -------------------------------------------------------------------------------- /lib/ddp_collection.dart: -------------------------------------------------------------------------------- 1 | part of ddp; 2 | 3 | typedef void UpdateListener( 4 | String collection, 5 | String operation, 6 | String id, 7 | Map doc, 8 | ); 9 | 10 | Tuple2> _parseUpdate(Map update) { 11 | if (update.containsKey('id')) { 12 | final id = update['id']; 13 | if (id.runtimeType == String) { 14 | if (update.containsKey('fields')) { 15 | final updates = update['fields']; 16 | if (updates is Map) { 17 | return Tuple2(id, updates); 18 | } 19 | } 20 | return Tuple2(id, null); 21 | } 22 | } 23 | return Tuple2('', null); 24 | } 25 | 26 | abstract class Collection { 27 | Map findOne(String id); 28 | 29 | Map> findAll(); 30 | 31 | void addUpdateListener(UpdateListener listener); 32 | 33 | void _added(Map msg); 34 | 35 | void _changed(Map msg); 36 | 37 | void _removed(Map msg); 38 | 39 | void _addedBefore(Map msg); 40 | 41 | void _movedBefore(Map msg); 42 | 43 | void _init(); 44 | 45 | void _reset(); 46 | 47 | factory Collection.mock() => _MockCache(); 48 | 49 | factory Collection.key(String name) => KeyCache(name, {}, []); 50 | } 51 | 52 | class KeyCache implements Collection { 53 | String name; 54 | Map> _items; 55 | List _listeners; 56 | 57 | KeyCache(this.name, this._items, this._listeners); 58 | 59 | void _notify(String operation, String id, Map doc) { 60 | this._listeners.forEach((listener) { 61 | listener(this.name, operation, id, doc); 62 | }); 63 | } 64 | 65 | @override 66 | void _added(Map msg) { 67 | final pair = _parseUpdate(msg); 68 | if (pair.item2 != null) { 69 | this._items[pair.item1] = pair.item2; 70 | this._notify('create', pair.item1, pair.item2); 71 | } 72 | } 73 | 74 | @override 75 | void _addedBefore(Map msg) {} 76 | 77 | @override 78 | void _changed(Map msg) { 79 | final pair = _parseUpdate(msg); 80 | if (pair.item2 != null) { 81 | if (this._items.containsKey(pair.item1)) { 82 | final item = this._items[pair.item1]; 83 | pair.item2.forEach((key, value) => item[key] = value); 84 | this._items[pair.item1] = item; 85 | this._notify('update', pair.item1, item); 86 | } 87 | } 88 | } 89 | 90 | @override 91 | void _init() {} 92 | 93 | @override 94 | void _movedBefore(Map msg) {} 95 | 96 | @override 97 | void _removed(Map msg) { 98 | final pair = _parseUpdate(msg); 99 | if (pair.item1.isNotEmpty) { 100 | this._items.remove(pair.item1); 101 | this._notify('remove', pair.item1, null); 102 | } 103 | } 104 | 105 | @override 106 | void _reset() { 107 | this._notify('reset', '', null); 108 | } 109 | 110 | @override 111 | void addUpdateListener(UpdateListener listener) => 112 | this._listeners.add(listener); 113 | 114 | @override 115 | Map> findAll() => this._items; 116 | 117 | @override 118 | Map findOne(String id) => this._items[id]; 119 | } 120 | 121 | class _MockCache implements Collection { 122 | @override 123 | void _added(Map msg) {} 124 | 125 | @override 126 | void _addedBefore(Map msg) {} 127 | 128 | @override 129 | void _changed(Map msg) {} 130 | 131 | @override 132 | void _init() {} 133 | 134 | @override 135 | void _movedBefore(Map msg) {} 136 | 137 | @override 138 | void _removed(Map msg) {} 139 | 140 | @override 141 | void _reset() {} 142 | 143 | @override 144 | void addUpdateListener(UpdateListener listener) {} 145 | 146 | @override 147 | Map> findAll() => {}; 148 | 149 | @override 150 | Map findOne(String id) => null; 151 | } 152 | -------------------------------------------------------------------------------- /lib/ddp_stats.dart: -------------------------------------------------------------------------------- 1 | part of ddp; 2 | 3 | enum LoggingDataType { 4 | DataByte, 5 | DataText, 6 | } 7 | 8 | class ReaderProxy extends Stream { 9 | Stream _reader; 10 | 11 | ReaderProxy(this._reader); 12 | 13 | @override 14 | StreamSubscription listen( 15 | void Function(dynamic event) onData, { 16 | Function onError, 17 | void Function() onDone, 18 | bool cancelOnError, 19 | }) { 20 | return this._reader.listen( 21 | onData, 22 | onError: onError, 23 | onDone: onDone, 24 | cancelOnError: cancelOnError, 25 | ); 26 | } 27 | 28 | void setReader(Stream reader) { 29 | this._reader = reader; 30 | } 31 | } 32 | 33 | class WriterProxy implements StreamSink { 34 | StreamSink _writer; 35 | 36 | WriterProxy(this._writer); 37 | 38 | @override 39 | void add(event) { 40 | this._writer.add(event); 41 | } 42 | 43 | @override 44 | void addError(Object error, [StackTrace stackTrace]) { 45 | this._writer.addError(error, stackTrace); 46 | } 47 | 48 | @override 49 | Future addStream(Stream stream) { 50 | return this._writer.addStream(stream); 51 | } 52 | 53 | @override 54 | Future close() { 55 | return this._writer.close(); 56 | } 57 | 58 | @override 59 | Future get done { 60 | return this._writer.done; 61 | } 62 | 63 | void setWriter(StreamSink writer) { 64 | this._writer = writer; 65 | } 66 | } 67 | 68 | typedef void _Logger(Object object); 69 | 70 | class LoggerMixin { 71 | bool active; 72 | int truncate; 73 | LoggingDataType _dtype; 74 | _Logger _logger; 75 | 76 | log(dynamic p, int n) { 77 | if (this.active) { 78 | int limit = n; 79 | bool trancated = false; 80 | if (this.truncate > 0 && this.truncate < limit) { 81 | limit = this.truncate; 82 | trancated = true; 83 | } 84 | switch (this._dtype) { 85 | case LoggingDataType.DataText: 86 | if (trancated) { 87 | if (p is List) { 88 | this._logger('[${n}] ${utf8.decode(p.sublist(0, limit))}...'); 89 | } else if (p.runtimeType == String) { 90 | this._logger('[${n}] ${p.substring(0, limit)}...'); 91 | } 92 | } else { 93 | if (p is List) { 94 | this._logger('[${n}] ${utf8.decode(p.sublist(0, limit))}'); 95 | } else if (p.runtimeType == String) { 96 | this._logger('[${n}] ${p.substring(0, limit)}'); 97 | } 98 | } 99 | break; 100 | case LoggingDataType.DataByte: 101 | default: 102 | // Don't know what to do? maybe dataByte is not necessary to log? 103 | } 104 | } 105 | return n; 106 | } 107 | } 108 | 109 | class ReaderLogger extends ReaderProxy with LoggerMixin { 110 | ReaderLogger(Stream reader) : super(reader); 111 | 112 | factory ReaderLogger.text(Stream reader) => ReaderLogger(reader) 113 | .._logger = ((Object obj) => print('<- $obj')) 114 | ..active = true 115 | .._dtype = LoggingDataType.DataText 116 | ..truncate = 80; 117 | 118 | @override 119 | StreamSubscription listen( 120 | void Function(dynamic event) onData, { 121 | Function onError, 122 | void Function() onDone, 123 | bool cancelOnError, 124 | }) { 125 | return super.listen( 126 | (event) { 127 | this.log(event, event.length); 128 | onData(event); 129 | }, 130 | onError: onError, 131 | onDone: onDone, 132 | cancelOnError: cancelOnError, 133 | ); 134 | } 135 | } 136 | 137 | class WriterLogger extends WriterProxy with LoggerMixin { 138 | WriterLogger(StreamSink writer) : super(writer); 139 | 140 | factory WriterLogger.text(StreamSink writer) => WriterLogger(writer) 141 | .._logger = ((Object obj) => print('-> $obj')) 142 | ..active = true 143 | .._dtype = LoggingDataType.DataText 144 | ..truncate = 80; 145 | 146 | @override 147 | void add(event) { 148 | this.log(event, event.length); 149 | super.add(event); 150 | } 151 | } 152 | 153 | class Stats { 154 | int bytes; 155 | int ops; 156 | int errors; 157 | Duration runtime; 158 | } 159 | 160 | class ClientStats { 161 | Stats reads; 162 | Stats totalReads; 163 | Stats writes; 164 | Stats totalWrites; 165 | int reconnects; 166 | int pingsSent; 167 | int pingsRecv; 168 | } 169 | 170 | class CollectionStats { 171 | String name; 172 | int count; 173 | } 174 | 175 | class StatsTrackerMixin { 176 | int _bytes; 177 | int _ops; 178 | int _errors; 179 | DateTime _start; 180 | 181 | int op(int n) { 182 | this._ops++; 183 | this._bytes += n; 184 | 185 | return n; 186 | } 187 | 188 | Stats snapshot() { 189 | return this._snap(); 190 | } 191 | 192 | Stats reset() { 193 | final stats = this._snap(); 194 | this._bytes = 0; 195 | this._ops = 0; 196 | this._errors = 0; 197 | this._start = DateTime.now(); 198 | return stats; 199 | } 200 | 201 | Stats _snap() { 202 | return Stats() 203 | ..bytes = this._bytes 204 | ..ops = this._ops 205 | ..errors = this._errors 206 | ..runtime = DateTime.now().difference(this._start); 207 | } 208 | } 209 | 210 | class ReaderStats extends ReaderProxy with StatsTrackerMixin { 211 | ReaderStats(Stream reader) : super(reader) { 212 | this._bytes = 0; 213 | this._ops = 0; 214 | this._errors = 0; 215 | this._start = DateTime.now(); 216 | } 217 | 218 | @override 219 | StreamSubscription listen( 220 | void Function(dynamic event) onData, { 221 | Function onError, 222 | void Function() onDone, 223 | bool cancelOnError, 224 | }) { 225 | return super.listen( 226 | (event) { 227 | this.op(event.length); 228 | onData(event); 229 | }, 230 | onError: onError, 231 | onDone: onDone, 232 | cancelOnError: cancelOnError, 233 | ); 234 | } 235 | } 236 | 237 | class WriterStats extends WriterProxy with StatsTrackerMixin { 238 | WriterStats(StreamSink writer) : super(writer) { 239 | this._bytes = 0; 240 | this._ops = 0; 241 | this._errors = 0; 242 | this._start = DateTime.now(); 243 | } 244 | 245 | @override 246 | void add(dynamic event) { 247 | this.op(event.length); 248 | super.add(event); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://www.dartlang.org/tools/pub/glossary#lockfile 3 | packages: 4 | analyzer: 5 | dependency: transitive 6 | description: 7 | name: analyzer 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "0.32.5" 11 | args: 12 | dependency: transitive 13 | description: 14 | name: args 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "1.5.0" 18 | async: 19 | dependency: transitive 20 | description: 21 | name: async 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "2.0.8" 25 | boolean_selector: 26 | dependency: transitive 27 | description: 28 | name: boolean_selector 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.0.4" 32 | charcode: 33 | dependency: transitive 34 | description: 35 | name: charcode 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.1.2" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.14.11" 46 | convert: 47 | dependency: transitive 48 | description: 49 | name: convert 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "2.0.2" 53 | crypto: 54 | dependency: transitive 55 | description: 56 | name: crypto 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "2.0.6" 60 | csslib: 61 | dependency: transitive 62 | description: 63 | name: csslib 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "0.14.5" 67 | front_end: 68 | dependency: transitive 69 | description: 70 | name: front_end 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "0.1.4+1" 74 | glob: 75 | dependency: transitive 76 | description: 77 | name: glob 78 | url: "https://pub.dartlang.org" 79 | source: hosted 80 | version: "1.1.7" 81 | html: 82 | dependency: transitive 83 | description: 84 | name: html 85 | url: "https://pub.dartlang.org" 86 | source: hosted 87 | version: "0.13.3+3" 88 | http: 89 | dependency: transitive 90 | description: 91 | name: http 92 | url: "https://pub.dartlang.org" 93 | source: hosted 94 | version: "0.11.3+17" 95 | http_multi_server: 96 | dependency: transitive 97 | description: 98 | name: http_multi_server 99 | url: "https://pub.dartlang.org" 100 | source: hosted 101 | version: "2.0.5" 102 | http_parser: 103 | dependency: transitive 104 | description: 105 | name: http_parser 106 | url: "https://pub.dartlang.org" 107 | source: hosted 108 | version: "3.1.3" 109 | io: 110 | dependency: transitive 111 | description: 112 | name: io 113 | url: "https://pub.dartlang.org" 114 | source: hosted 115 | version: "0.3.3" 116 | js: 117 | dependency: transitive 118 | description: 119 | name: js 120 | url: "https://pub.dartlang.org" 121 | source: hosted 122 | version: "0.6.1+1" 123 | json_rpc_2: 124 | dependency: transitive 125 | description: 126 | name: json_rpc_2 127 | url: "https://pub.dartlang.org" 128 | source: hosted 129 | version: "2.0.9" 130 | kernel: 131 | dependency: transitive 132 | description: 133 | name: kernel 134 | url: "https://pub.dartlang.org" 135 | source: hosted 136 | version: "0.3.4+1" 137 | logging: 138 | dependency: transitive 139 | description: 140 | name: logging 141 | url: "https://pub.dartlang.org" 142 | source: hosted 143 | version: "0.11.3+2" 144 | matcher: 145 | dependency: transitive 146 | description: 147 | name: matcher 148 | url: "https://pub.dartlang.org" 149 | source: hosted 150 | version: "0.12.3+1" 151 | meta: 152 | dependency: transitive 153 | description: 154 | name: meta 155 | url: "https://pub.dartlang.org" 156 | source: hosted 157 | version: "1.1.6" 158 | mime: 159 | dependency: transitive 160 | description: 161 | name: mime 162 | url: "https://pub.dartlang.org" 163 | source: hosted 164 | version: "0.9.6+2" 165 | multi_server_socket: 166 | dependency: transitive 167 | description: 168 | name: multi_server_socket 169 | url: "https://pub.dartlang.org" 170 | source: hosted 171 | version: "1.0.2" 172 | node_preamble: 173 | dependency: transitive 174 | description: 175 | name: node_preamble 176 | url: "https://pub.dartlang.org" 177 | source: hosted 178 | version: "1.4.4" 179 | package_config: 180 | dependency: transitive 181 | description: 182 | name: package_config 183 | url: "https://pub.dartlang.org" 184 | source: hosted 185 | version: "1.0.5" 186 | package_resolver: 187 | dependency: transitive 188 | description: 189 | name: package_resolver 190 | url: "https://pub.dartlang.org" 191 | source: hosted 192 | version: "1.0.4" 193 | path: 194 | dependency: transitive 195 | description: 196 | name: path 197 | url: "https://pub.dartlang.org" 198 | source: hosted 199 | version: "1.6.2" 200 | plugin: 201 | dependency: transitive 202 | description: 203 | name: plugin 204 | url: "https://pub.dartlang.org" 205 | source: hosted 206 | version: "0.2.0+3" 207 | pool: 208 | dependency: transitive 209 | description: 210 | name: pool 211 | url: "https://pub.dartlang.org" 212 | source: hosted 213 | version: "1.3.6" 214 | pub_semver: 215 | dependency: transitive 216 | description: 217 | name: pub_semver 218 | url: "https://pub.dartlang.org" 219 | source: hosted 220 | version: "1.4.2" 221 | quiver: 222 | dependency: transitive 223 | description: 224 | name: quiver 225 | url: "https://pub.dartlang.org" 226 | source: hosted 227 | version: "2.0.0+1" 228 | shelf: 229 | dependency: transitive 230 | description: 231 | name: shelf 232 | url: "https://pub.dartlang.org" 233 | source: hosted 234 | version: "0.7.3+3" 235 | shelf_packages_handler: 236 | dependency: transitive 237 | description: 238 | name: shelf_packages_handler 239 | url: "https://pub.dartlang.org" 240 | source: hosted 241 | version: "1.0.4" 242 | shelf_static: 243 | dependency: transitive 244 | description: 245 | name: shelf_static 246 | url: "https://pub.dartlang.org" 247 | source: hosted 248 | version: "0.2.8" 249 | shelf_web_socket: 250 | dependency: transitive 251 | description: 252 | name: shelf_web_socket 253 | url: "https://pub.dartlang.org" 254 | source: hosted 255 | version: "0.2.2+4" 256 | source_map_stack_trace: 257 | dependency: transitive 258 | description: 259 | name: source_map_stack_trace 260 | url: "https://pub.dartlang.org" 261 | source: hosted 262 | version: "1.1.5" 263 | source_maps: 264 | dependency: transitive 265 | description: 266 | name: source_maps 267 | url: "https://pub.dartlang.org" 268 | source: hosted 269 | version: "0.10.7" 270 | source_span: 271 | dependency: transitive 272 | description: 273 | name: source_span 274 | url: "https://pub.dartlang.org" 275 | source: hosted 276 | version: "1.4.1" 277 | stack_trace: 278 | dependency: transitive 279 | description: 280 | name: stack_trace 281 | url: "https://pub.dartlang.org" 282 | source: hosted 283 | version: "1.9.3" 284 | stream_channel: 285 | dependency: transitive 286 | description: 287 | name: stream_channel 288 | url: "https://pub.dartlang.org" 289 | source: hosted 290 | version: "1.6.8" 291 | string_scanner: 292 | dependency: transitive 293 | description: 294 | name: string_scanner 295 | url: "https://pub.dartlang.org" 296 | source: hosted 297 | version: "1.0.4" 298 | term_glyph: 299 | dependency: transitive 300 | description: 301 | name: term_glyph 302 | url: "https://pub.dartlang.org" 303 | source: hosted 304 | version: "1.0.1" 305 | test: 306 | dependency: "direct dev" 307 | description: 308 | name: test 309 | url: "https://pub.dartlang.org" 310 | source: hosted 311 | version: "1.3.0" 312 | tuple: 313 | dependency: "direct main" 314 | description: 315 | name: tuple 316 | url: "https://pub.dartlang.org" 317 | source: hosted 318 | version: "1.0.2" 319 | typed_data: 320 | dependency: transitive 321 | description: 322 | name: typed_data 323 | url: "https://pub.dartlang.org" 324 | source: hosted 325 | version: "1.1.6" 326 | utf: 327 | dependency: transitive 328 | description: 329 | name: utf 330 | url: "https://pub.dartlang.org" 331 | source: hosted 332 | version: "0.9.0+5" 333 | vm_service_client: 334 | dependency: transitive 335 | description: 336 | name: vm_service_client 337 | url: "https://pub.dartlang.org" 338 | source: hosted 339 | version: "0.2.6" 340 | watcher: 341 | dependency: transitive 342 | description: 343 | name: watcher 344 | url: "https://pub.dartlang.org" 345 | source: hosted 346 | version: "0.9.7+10" 347 | web_socket_channel: 348 | dependency: transitive 349 | description: 350 | name: web_socket_channel 351 | url: "https://pub.dartlang.org" 352 | source: hosted 353 | version: "1.0.9" 354 | yaml: 355 | dependency: transitive 356 | description: 357 | name: yaml 358 | url: "https://pub.dartlang.org" 359 | source: hosted 360 | version: "2.1.15" 361 | sdks: 362 | dart: ">=2.0.0-dev.62.0 <3.0.0" 363 | -------------------------------------------------------------------------------- /lib/ddp_client.dart: -------------------------------------------------------------------------------- 1 | part of ddp; 2 | 3 | enum ConnectStatus { 4 | disconnected, 5 | dialing, 6 | connecting, 7 | connected, 8 | } 9 | 10 | typedef void _MessageHandler(Map message); 11 | 12 | typedef void ConnectionListener(); 13 | typedef void StatusListener(ConnectStatus status); 14 | 15 | abstract class ConnectionNotifier { 16 | void addConnectionListener(ConnectionListener listener); 17 | } 18 | 19 | abstract class StatusNotifier { 20 | void addStatusListener(StatusListener listener); 21 | void removeStatusListener(StatusListener listener); 22 | } 23 | 24 | class DdpClient implements ConnectionNotifier, StatusNotifier { 25 | String _name; 26 | Duration heartbeatInterval; 27 | Duration heartbeatTimeout; 28 | Duration reconnectInterval; 29 | 30 | WriterStats _writeSocketStats; 31 | WriterStats _writeStats; 32 | WriterLogger _writeLog; 33 | ReaderStats _readSocketStats; 34 | ReaderStats _readStats; 35 | ReaderLogger _readLog; 36 | 37 | int _reconnects; 38 | int _pingsIn; 39 | int _pingsOut; 40 | 41 | String _session; 42 | String _version; 43 | String _serverId; 44 | WebSocket _ws; 45 | String _url; 46 | String _origin; 47 | 48 | // Stream _inbox; 49 | // Stream _errors; 50 | Timer _pingTimer; 51 | 52 | Map> _pings; 53 | Map _calls; 54 | Map _subs; 55 | Map _unsubs; 56 | 57 | Map _collections; 58 | ConnectStatus _connectionStatus; 59 | Timer _reconnectTimer; 60 | 61 | List _statusListeners; 62 | List _connectionListener; 63 | 64 | _IdManager _idManager; 65 | 66 | Map _messageHandlers; 67 | 68 | DdpClient(this._name, String url, String origin) { 69 | this.heartbeatInterval = const Duration(minutes: 1); 70 | this.heartbeatTimeout = const Duration(seconds: 15); 71 | this.reconnectInterval = const Duration(seconds: 5); 72 | this._collections = {}; 73 | this._url = url; 74 | this._origin = origin; 75 | // this._inbox = Stream(); 76 | // this.errors = Stream(); 77 | this._pings = {}; 78 | this._calls = {}; 79 | this._subs = {}; 80 | this._unsubs = {}; 81 | this._connectionStatus = ConnectStatus.disconnected; 82 | 83 | this._writeSocketStats = WriterStats(null); 84 | this._writeStats = WriterStats(null); 85 | this._readSocketStats = ReaderStats(null); 86 | this._readStats = ReaderStats(null); 87 | 88 | this._writeLog = WriterLogger.text(null); 89 | this._readLog = ReaderLogger.text(null); 90 | 91 | this._idManager = _IdManager(); 92 | 93 | this._statusListeners = []; 94 | this._connectionListener = []; 95 | 96 | this._reconnects = 0; 97 | this._pingsIn = 0; 98 | this._pingsOut = 0; 99 | } 100 | 101 | void _log(String msg) { 102 | print('[DdpClient - ${_name}] $msg'); 103 | } 104 | 105 | String get session => _session; 106 | 107 | String get version => _version; 108 | 109 | @override 110 | void addConnectionListener(ConnectionListener listener) { 111 | this._connectionListener.add(listener); 112 | } 113 | 114 | @override 115 | void addStatusListener(StatusListener listener) { 116 | this._statusListeners.add(listener); 117 | } 118 | 119 | @override 120 | void removeStatusListener(StatusListener listener) { 121 | this._statusListeners.remove(listener); 122 | } 123 | 124 | void _status(ConnectStatus status) { 125 | if (this._connectionStatus == status) { 126 | return; 127 | } 128 | this._connectionStatus = status; 129 | this._statusListeners.forEach((l) => l(status)); 130 | } 131 | 132 | void connect() { 133 | this._status(ConnectStatus.dialing); 134 | WebSocket.connect(this._url).then((connection) { 135 | final ws = connection; 136 | this._start(ws, Message.connect()); 137 | }).catchError((error) { 138 | this.close(); 139 | this._reconnectLater(); 140 | }); 141 | } 142 | 143 | void reconnect() { 144 | if (this._reconnectTimer != null) { 145 | this._reconnectTimer.cancel(); 146 | this._reconnectTimer = null; 147 | } 148 | 149 | this.close(); 150 | this._reconnects++; 151 | this._status(ConnectStatus.dialing); 152 | WebSocket.connect(this._url).then((connection) { 153 | this._start(connection, Message.reconnect(this._session)); 154 | this._calls.values.forEach((call) => this.send( 155 | Message.method(call.id, call.serviceMethod, call.args).toJson())); 156 | this._subs.values.forEach((call) => this 157 | .send(Message.sub(call.id, call.serviceMethod, call.args).toJson())); 158 | }).catchError((error) { 159 | this.close(); 160 | this._reconnectLater(); 161 | }); 162 | } 163 | 164 | Call subscribe(String subName, OnCallDone done, List args) { 165 | if (args == null) { 166 | args = []; 167 | } 168 | 169 | final call = Call() 170 | ..id = _idManager.next() 171 | ..serviceMethod = subName 172 | ..args = args 173 | ..owner = this; 174 | 175 | if (done == null) { 176 | done = (c) {}; 177 | } 178 | call.onceDone(done); 179 | this._subs[call.id] = call; 180 | 181 | this.send(Message.sub(call.id, subName, args).toJson()); 182 | return call; 183 | } 184 | 185 | Future sub(String subName, List args) { 186 | final completer = Completer(); 187 | subscribe(subName, (call) => completer.complete(call), args); 188 | return completer.future; 189 | } 190 | 191 | Call unSubscribe(String id, OnCallDone done) { 192 | final call = Call() 193 | ..id = id 194 | ..owner = this; 195 | 196 | if (done == null) { 197 | done = (c) {}; 198 | } 199 | call.onceDone(done); 200 | this._unsubs[call.id] = call; 201 | this.send(Message.unSub(call.id).toJson()); 202 | return call; 203 | } 204 | 205 | Future unSub(String id) { 206 | final completer = Completer(); 207 | unSubscribe(id, (call) => completer.complete(call)); 208 | return completer.future; 209 | } 210 | 211 | Call go(String serviceMethod, OnCallDone done, List args) { 212 | if (args == null) { 213 | args = []; 214 | } 215 | final call = Call() 216 | ..id = this._idManager.next() 217 | ..serviceMethod = serviceMethod 218 | ..args = args 219 | ..owner = this; 220 | if (done == null) { 221 | done = (c) {}; 222 | } 223 | call.onceDone(done); 224 | this._calls[call.id] = call; 225 | this.send(Message.method(call.id, serviceMethod, args).toJson()); 226 | return call; 227 | } 228 | 229 | Future call(String serviceMethod, List args) { 230 | final completer = Completer(); 231 | go(serviceMethod, (call) => completer.complete(call), args); 232 | return completer.future; 233 | } 234 | 235 | void send(dynamic msg) { 236 | this._writeStats.add(msg); 237 | } 238 | 239 | void close() { 240 | if (this._pingTimer != null) { 241 | this._pingTimer.cancel(); 242 | this._pingTimer = null; 243 | } 244 | 245 | if (this._ws != null) { 246 | this._ws.close(WebSocketStatus.normalClosure); 247 | this._ws = null; 248 | } 249 | 250 | this._collections.values.forEach((collection) => collection._reset()); 251 | this._status(ConnectStatus.disconnected); 252 | } 253 | 254 | void resetStats() { 255 | this._readSocketStats.reset(); 256 | this._readStats.reset(); 257 | this._writeSocketStats.reset(); 258 | this._writeStats.reset(); 259 | this._reconnects = 0; 260 | this._pingsIn = 0; 261 | this._pingsOut = 0; 262 | } 263 | 264 | ClientStats stats() { 265 | return ClientStats() 266 | ..reads = this._readSocketStats.snapshot() 267 | ..totalReads = this._readStats.snapshot() 268 | ..writes = this._writeSocketStats.snapshot() 269 | ..totalWrites = this._writeStats.snapshot() 270 | ..reconnects = this._reconnects 271 | ..pingsSent = this._pingsOut 272 | ..pingsRecv = this._pingsIn; 273 | } 274 | 275 | bool socketLogActive() { 276 | return this._writeLog.active; 277 | } 278 | 279 | void setSocketLogActive(bool active) { 280 | assert(active != null); 281 | this._writeLog.active = active; 282 | this._readLog.active = active; 283 | } 284 | 285 | Collection collectionByName(String name) { 286 | if (!this._collections.containsKey(name)) { 287 | final collection = Collection.key(name); 288 | this._collections[name] = collection; 289 | } 290 | return this._collections[name]; 291 | } 292 | 293 | List collectionStats() { 294 | List stats = []; 295 | this._collections.forEach((name, collection) => stats.add(CollectionStats() 296 | ..name = name 297 | ..count = collection.findAll().length)); 298 | return stats; 299 | } 300 | 301 | void _start(WebSocket ws, _Connect connect) { 302 | this._status(ConnectStatus.connecting); 303 | 304 | this._initMessageHandlers(); 305 | this._ws = ws; 306 | 307 | this._writeLog.setWriter(ws); 308 | this._writeSocketStats = WriterStats(this._writeLog); 309 | this._writeStats.setWriter(this._writeSocketStats); 310 | this._readLog.setReader(ws); 311 | this._readSocketStats = ReaderStats(this._readLog); 312 | this._readStats.setReader(this._readSocketStats); 313 | 314 | this.inboxManager(); 315 | 316 | this.send(connect.toJson()); 317 | } 318 | 319 | void _reconnectLater() { 320 | this.close(); 321 | if (this._reconnectTimer == null) { 322 | this._reconnectTimer = Timer(this.reconnectInterval, this.reconnect); 323 | } 324 | } 325 | 326 | void ping() { 327 | this.pingPong(this._idManager.next(), this.heartbeatTimeout, (err) { 328 | if (err != null) { 329 | this._reconnectLater(); 330 | } 331 | }); 332 | } 333 | 334 | void pingPong(String id, Duration timeout, Function(Error) handler) { 335 | this.send(Message.ping(id).toJson()); 336 | this._pingsOut++; 337 | if (!this._pings.containsKey(id)) { 338 | this._pings[id] = []; 339 | } 340 | final pingTracker = _PingTracker() 341 | .._handler = handler 342 | .._timeout = timeout 343 | .._timer = Timer(timeout, () { 344 | handler(ArgumentError('ping timeout')); 345 | }); 346 | this._pings[id].add(pingTracker); 347 | } 348 | 349 | void _initMessageHandlers() { 350 | this._messageHandlers = {}; 351 | this._messageHandlers['connected'] = (msg) { 352 | this._status(ConnectStatus.connected); 353 | this._collections.values.forEach((c) => c._init()); 354 | this._version = '1'; 355 | this._session = msg['session'] as String; 356 | this._pingTimer = Timer.periodic(this.heartbeatInterval, (Timer timer) { 357 | this.ping(); 358 | }); 359 | this._connectionListener.forEach((l) => l()); 360 | }; 361 | this._messageHandlers['ping'] = (msg) { 362 | if (msg.containsKey('id')) { 363 | this.send(Message.pong(msg['id']).toJson()); 364 | } else { 365 | this.send(Message.pong(null).toJson()); 366 | } 367 | this._pingsIn++; 368 | }; 369 | this._messageHandlers['pong'] = (msg) { 370 | var key = ''; 371 | if (msg.containsKey('id')) { 372 | key = msg['id'] as String; 373 | } 374 | if (this._pings.containsKey(key)) { 375 | final pings = this._pings[key]; 376 | if (pings.isNotEmpty) { 377 | final ping = pings[0]; 378 | final newPings = pings.sublist(1); 379 | if (key.isEmpty || pings.isNotEmpty) { 380 | this._pings[key] = newPings; 381 | } 382 | ping._timer.cancel(); 383 | ping._handler(null); 384 | } 385 | } 386 | }; 387 | this._messageHandlers['nosub'] = (msg) { 388 | if (msg.containsKey('id')) { 389 | final id = msg['id'] as String; 390 | final runningSub = this._subs[id]; 391 | if (runningSub != null) { 392 | print(runningSub); 393 | this._log('Subscription returned a nosub error $msg'); 394 | runningSub.error = ArgumentError( 395 | 'Subscription returned a nosub error'); // TODO error type. 396 | runningSub.done(); 397 | this._subs.remove(id); 398 | } 399 | 400 | final runningUnSub = this._unsubs[id]; 401 | if (runningUnSub != null) { 402 | runningUnSub.done(); 403 | this._unsubs.remove(id); 404 | } 405 | } 406 | }; 407 | this._messageHandlers['ready'] = (msg) { 408 | if (msg.containsKey('subs')) { 409 | final subs = msg['subs'] as List; 410 | subs.forEach((sub) { 411 | if (this._subs.containsKey(sub)) { 412 | this._subs[sub].done(); 413 | this._subs.remove(sub); 414 | } 415 | }); 416 | } 417 | }; 418 | this._messageHandlers['added'] = 419 | (msg) => this._collectionBy(msg)._added(msg); 420 | this._messageHandlers['changed'] = 421 | (msg) => this._collectionBy(msg)._changed(msg); 422 | this._messageHandlers['removed'] = 423 | (msg) => this._collectionBy(msg)._removed(msg); 424 | this._messageHandlers['addedBefore'] = 425 | (msg) => this._collectionBy(msg)._addedBefore(msg); 426 | this._messageHandlers['movedBefore'] = 427 | (msg) => this._collectionBy(msg)._movedBefore(msg); 428 | this._messageHandlers['result'] = (msg) { 429 | if (msg.containsKey('id')) { 430 | final id = msg['id']; 431 | final call = this._calls[id]; 432 | this._calls.remove(id); 433 | if (msg.containsKey('error')) { 434 | final e = msg['error']; 435 | call.error = ArgumentError(json.encode(e)); // TODO Error Type 436 | call.reply = e; 437 | } else { 438 | call.reply = msg['result']; 439 | } 440 | call.done(); 441 | } 442 | }; 443 | this._messageHandlers['updated'] = (msg) {}; 444 | } 445 | 446 | void inboxManager() { 447 | this._readStats.listen((event) { 448 | final message = json.decode(event) as Map; 449 | if (message.containsKey('msg')) { 450 | final mtype = message['msg']; 451 | if (this._messageHandlers.containsKey(mtype)) { 452 | this._messageHandlers[mtype](message); 453 | } else { 454 | this._log('Server sent unexpected message ${message}'); 455 | } 456 | } else if (message.containsKey('server_id')) { 457 | final serverId = message['server_id']; 458 | if (serverId.runtimeType == String) { 459 | this._serverId = serverId; 460 | } else { 461 | this._log('Server cluster node ${serverId}'); 462 | } 463 | } else { 464 | this._log('Server sent message without `msg` field ${message}'); 465 | } 466 | }); 467 | } 468 | 469 | Collection _collectionBy(Map msg) { 470 | if (msg.containsKey('collection')) { 471 | final name = msg['collection']; 472 | if (name.runtimeType == String) { 473 | return this.collectionByName(name); 474 | } 475 | } 476 | return Collection.mock(); 477 | } 478 | } 479 | --------------------------------------------------------------------------------