├── test ├── config_test.yaml ├── packme │ └── test.json ├── modules │ └── test.dart ├── generated │ └── test.generated.dart └── serveme_test.dart ├── .metadata ├── .gitignore ├── pubspec.yaml ├── serveme.iml ├── lib ├── classes │ ├── module.dart │ └── config.dart ├── core │ ├── utils.dart │ ├── mongo.dart │ ├── scheduler.dart │ ├── events.dart │ ├── logger.dart │ ├── console.dart │ └── integrity.dart └── serveme.dart ├── packme ├── example-posts.json ├── example-users.json └── compile.dart ├── LICENSE ├── CHANGELOG.md ├── config.yaml ├── example ├── example.yaml ├── example.html ├── example.dart └── generated │ ├── example-posts.generated.dart │ └── example-users.generated.dart ├── analysis_options.yaml └── README.md /test/config_test.yaml: -------------------------------------------------------------------------------- 1 | port: 31337 2 | 3 | debug: true 4 | debug_log: test/debug_test.log 5 | error_log: test/error_test.log 6 | 7 | modules: 8 | - test 9 | -------------------------------------------------------------------------------- /test/packme/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": [ 3 | { 4 | "request_param": "double" 5 | }, 6 | { 7 | "response_param": "double" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /test/modules/test.dart: -------------------------------------------------------------------------------- 1 | import 'package:serveme/serveme.dart'; 2 | 3 | class TestModule extends Module { 4 | @override 5 | Future init() async { 6 | } 7 | 8 | @override 9 | void run() { 10 | } 11 | 12 | @override 13 | Future dispose() async { 14 | } 15 | } -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: b22742018b3edf16c6cadd7b76d9db5e7f9064b5 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you're building an application, you may want to check-in your pubspec.lock 2 | pubspec.lock 3 | 4 | # Miscellaneous 5 | *.class 6 | *.log 7 | *.pyc 8 | *.swp 9 | .DS_Store 10 | .atom/ 11 | .buildlog/ 12 | .history 13 | .svn/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # Flutter/Dart/Pub related 22 | **/doc/api/ 23 | .dart_tool/ 24 | .flutter-plugins 25 | .flutter-plugins-dependencies 26 | .packages 27 | .pub-cache/ 28 | .pub/ 29 | build/ -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: serveme 2 | description: Backend server framework designed for a quick development of modular WebSocket based server applications with MongoDB integration. 3 | version: 1.2.2 4 | homepage: https://github.com/sourcecaster/serveme 5 | repository: https://github.com/sourcecaster/serveme 6 | issue_tracker: https://github.com/sourcecaster/serveme/issues 7 | documentation: https://github.com/sourcecaster/serveme 8 | environment: 9 | sdk: ">=3.0.6 <4.0.0" 10 | dependencies: 11 | mongo_dart: ^0.9.1 12 | yaml: ^3.1.0 13 | connectme: ^2.2.0 14 | packme: ^2.0.1 15 | dev_dependencies: 16 | test: ^1.24.6 -------------------------------------------------------------------------------- /serveme.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /lib/classes/module.dart: -------------------------------------------------------------------------------- 1 | part of serveme; 2 | 3 | enum ModuleState { 4 | none, 5 | initialized, 6 | running, 7 | disposed, 8 | } 9 | 10 | abstract class Module { 11 | late ServeMe server; 12 | 13 | ModuleState _state = ModuleState.none; 14 | 15 | Config get config => server.config; 16 | Events get events => server._events; 17 | Scheduler get scheduler => server._scheduler; 18 | Console get console => server.console; 19 | Map> get modules => server._modules; 20 | Future get db { 21 | if (server._mongo == null) throw Exception('MongoDB is not initialized'); 22 | return server._mongo!.db; 23 | } 24 | Future Function(String, [String]) get log => server._logger.log; 25 | Future Function(String, [String]) get debug => server._logger.debug; 26 | Future Function(String, [StackTrace?]) get error => server._logger.error; 27 | 28 | Future init(); 29 | void run(); 30 | Future dispose(); 31 | } 32 | 33 | -------------------------------------------------------------------------------- /packme/example-posts.json: -------------------------------------------------------------------------------- 1 | { 2 | "get_all": [ 3 | {}, 4 | { 5 | "posts": [{ 6 | "id": ["uint8"], 7 | "author": { 8 | "id": ["uint8"], 9 | "nickname": "string", 10 | "avatar": "string" 11 | }, 12 | "title": "string", 13 | "short_content": "string", 14 | "posted": "datetime" 15 | }] 16 | } 17 | ], 18 | "get": [ 19 | { 20 | "post_id": ["uint8"] 21 | }, 22 | { 23 | "title": "string", 24 | "content": "string", 25 | "posted": "datetime", 26 | "author": { 27 | "id": ["uint8"], 28 | "nickname": "string", 29 | "avatar": "string", 30 | "?facebook_id": "string", 31 | "?twitter_id": "string", 32 | "?instagram_id": "string" 33 | }, 34 | "stats": { 35 | "likes": "uint32", 36 | "dislikes": "uint32" 37 | }, 38 | "comments": [{ 39 | "author": { 40 | "id": ["uint8"], 41 | "nickname": "string", 42 | "avatar": "string" 43 | }, 44 | "comment": "string", 45 | "posted": "datetime" 46 | }] 47 | } 48 | ], 49 | "delete": [ 50 | { 51 | "post_id": ["uint8"] 52 | }, 53 | { 54 | "?error": "string" 55 | } 56 | ] 57 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mikhail Kurilo (sourcecaster@gmail.com) 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/core/utils.dart: -------------------------------------------------------------------------------- 1 | part of serveme; 2 | 3 | const String RESET = '\x1b[0m'; 4 | const String BLACK = '\x1b[30m'; 5 | const String RED = '\x1b[31m'; 6 | const String GREEN = '\x1b[32m'; 7 | const String YELLOW = '\x1b[33m'; 8 | const String BLUE = '\x1b[34m'; 9 | const String MAGENTA = '\x1b[35m'; 10 | const String CYAN = '\x1b[36m'; 11 | const String WHITE = '\x1b[37m'; 12 | 13 | String dye(dynamic x) => x is String ? '$GREEN$x$RESET' 14 | : x is int || x is double || x is bool ? '$BLUE$x$RESET' 15 | : x is ObjectId ? '$MAGENTA${x.toHexString()}$RESET' 16 | : '$MAGENTA$x$RESET'; 17 | 18 | T cast(dynamic x, {T? fallback, String? errorMessage}) { 19 | final T? result = x is T ? x 20 | : x == null ? null 21 | : T == double ? ( 22 | x is int ? x.toDouble() as T 23 | : x is String ? double.tryParse(x) as T? 24 | : null 25 | ) 26 | : T == int ? ( 27 | x is double ? x.toInt() as T 28 | : x is String ? int.tryParse(x) as T? 29 | : null 30 | ) 31 | : T == String ? x.toString() as T? 32 | : null; 33 | if (result == null && null is! T) { 34 | if (fallback != null) return fallback; 35 | else throw Exception(errorMessage ?? 'Unable to cast from ${x.runtimeType} to $T'); 36 | } 37 | return result as T; 38 | } -------------------------------------------------------------------------------- /packme/example-users.json: -------------------------------------------------------------------------------- 1 | { 2 | "get_all": [ 3 | {}, 4 | { 5 | "users": [{ 6 | "id": ["uint8"], 7 | "nickname": "string", 8 | "?first_name": "string", 9 | "?last_name": "string", 10 | "?age": "uint8" 11 | }] 12 | } 13 | ], 14 | "get": [ 15 | { 16 | "user_id": ["uint8"] 17 | }, 18 | { 19 | "email": "string", 20 | "nickname": "string", 21 | "hidden": "bool", 22 | "created": "datetime", 23 | "info": { 24 | "?first_name": "string", 25 | "?last_name": "string", 26 | "?male": "uint8", 27 | "?age": "uint8", 28 | "?birth_date": "datetime" 29 | }, 30 | "social": { 31 | "?facebook_id": "string", 32 | "?twitter_id": "string", 33 | "?instagram_id": "string" 34 | }, 35 | "stats": { 36 | "posts": "uint32", 37 | "comments": "uint32", 38 | "likes": "uint32", 39 | "dislikes": "uint32", 40 | "rating": "float" 41 | }, 42 | "?last_active": { 43 | "datetime": "datetime", 44 | "ip": "string" 45 | }, 46 | "sessions": [{ 47 | "created": "datetime", 48 | "ip": "string", 49 | "active": "bool" 50 | }] 51 | } 52 | ], 53 | "delete": [ 54 | { 55 | "user_id": ["uint8"] 56 | }, 57 | { 58 | "?error": "string" 59 | } 60 | ] 61 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.2.2 2 | * HTTP server type added. Method server.on(route, handler) added for HTTP routes implementation. 3 | 4 | ## v1.2.1 5 | * PackMe upgraded to v2.0.1: object inheritance implemented, nested arrays support added. 6 | * IMPORTANT: PackMe objects and enumerations from other JSON files are now referenced using filename: "some_user": "@filename:user". No changes required for references within the same file. 7 | 8 | ## v1.2.0 9 | * Added support for binary type (uses Uint8List). Format: binary12, binary64 etc. - any buffer length in bytes. 10 | * Example file is up to date now. (Message constructors require non-optional parameters). 11 | 12 | ## v1.1.3 13 | * Bugfix: concurrent modifications during iteration occurred in some cases while processing events. 14 | 15 | ## v1.1.2 16 | * Bugfix: expireAfterSeconds option for IndexDescriptor of CollectionDescriptor was ignored. 17 | 18 | ## v1.1.1 19 | * Maximum data length increased to 2^63 for messages sent over TCP socket. 20 | * Bugfix: data messages sent over TCP socket could stall in some cases. 21 | 22 | ## v1.1.0 23 | * TCP sockets support implemented (breaking changes). 24 | * ServeMeClient constructor now takes single ServeMeSocket argument. 25 | * Future ServeMe.connect() method added allowing to create WebSocket or TCP client connections (with the same functionality as server client connections). 26 | 27 | ## v1.0.2 28 | * Bugfix: ESC key press caused ServeMe to crash on Linux. 29 | 30 | ## v1.0.1 31 | * Small fixes in ReadMe file. 32 | 33 | ## v1.0.0 34 | * Finally released. -------------------------------------------------------------------------------- /lib/core/mongo.dart: -------------------------------------------------------------------------------- 1 | part of serveme; 2 | 3 | class MongoDbConnection { 4 | MongoDbConnection._internal(this._db, this._config, this._server); 5 | 6 | final Db _db; 7 | final MongoConfig _config; 8 | final ServeMe _server; 9 | 10 | Future get db async { 11 | if (!_db.isConnected && _db.state != State.OPENING) { 12 | _server.error('MongoDB connection is lost, reconnecting...'); 13 | try { 14 | await _db.close(); 15 | await Future.delayed(const Duration(seconds: 1)); 16 | await _db.open(secure: _config.secure); 17 | _server.log('MongoDB connection is reestablished'); 18 | } 19 | catch (err) { 20 | _server.error('Unable to establish MongoDB connection: $err'); 21 | } 22 | } 23 | return _db; 24 | } 25 | 26 | static Future connect(MongoConfig config, ServeMe server) async { 27 | server.log('Connecting to MongoDB...'); 28 | final String connectionString = 29 | 'mongodb://' 30 | '${config.user != null ? config.user! + ':' + (config.password ?? '') + '@' : ''}' 31 | '${config.hosts != null ? config.hosts!.join(',') : config.host}' 32 | '/${config.database}' 33 | '${config.replica != null ? '?replicaSet=${config.replica}' : ''}'; 34 | final Db db = Db(connectionString); 35 | await db.open(secure: config.secure); 36 | server.log('MongoDB connection is established'); 37 | return MongoDbConnection._internal(db, config, server); 38 | } 39 | 40 | Future close() async { 41 | if (_db.state == State.OPEN) await _db.close(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/core/scheduler.dart: -------------------------------------------------------------------------------- 1 | part of serveme; 2 | 3 | class Task { 4 | Task(this.time, this.handler, {this.period, this.skip = false}); 5 | 6 | late Scheduler _scheduler; 7 | DateTime time; 8 | final Future Function(DateTime time) handler; 9 | final Duration? period; 10 | final bool skip; 11 | bool busy = false; 12 | 13 | bool check(DateTime now) { 14 | return now.millisecondsSinceEpoch >= time.millisecondsSinceEpoch; 15 | } 16 | 17 | Future execute() async { 18 | if (period != null) { 19 | time = time.add(period!); 20 | if (skip) busy = true; 21 | } 22 | else _scheduler.discard(this); 23 | try { 24 | await handler(time); 25 | } 26 | catch (err, stack) { 27 | _scheduler._server._logger.error('Scheduled task execution error: $err', stack); 28 | } 29 | if (period != null && skip) { 30 | while (check(DateTime.now().toUtc())) time = time.add(period!); 31 | busy = false; 32 | } 33 | } 34 | } 35 | 36 | class Scheduler { 37 | Scheduler(this._server) { 38 | _server._events.listen(_process); 39 | } 40 | 41 | final ServeMe _server; 42 | final List _tasks = []; 43 | 44 | void schedule(Task task) { 45 | task._scheduler = this; 46 | if (!_tasks.contains(task)) _tasks.add(task); 47 | } 48 | 49 | void discard(Task task) { 50 | _tasks.remove(task); 51 | } 52 | 53 | Future _process(TickEvent _) async { 54 | final DateTime now = DateTime.now().toUtc(); 55 | for (int i = _tasks.length - 1; i >= 0; i--) { 56 | if (_tasks[i].check(now) && !_tasks[i].busy) _tasks[i].execute(); 57 | } 58 | } 59 | 60 | void dispose() { 61 | _server._events.cancel(_process); 62 | } 63 | } 64 | 65 | 66 | -------------------------------------------------------------------------------- /test/generated/test.generated.dart: -------------------------------------------------------------------------------- 1 | import 'package:packme/packme.dart'; 2 | 3 | class TestRequest extends PackMeMessage { 4 | TestRequest({ 5 | required this.requestParam, 6 | }); 7 | TestRequest.$empty(); 8 | 9 | late double requestParam; 10 | 11 | TestResponse $response({ 12 | required double responseParam, 13 | }) { 14 | final TestResponse message = TestResponse(responseParam: responseParam); 15 | message.$request = this; 16 | return message; 17 | } 18 | 19 | @override 20 | int $estimate() { 21 | $reset(); 22 | return 16; 23 | } 24 | 25 | @override 26 | void $pack() { 27 | $initPack(764832169); 28 | $packDouble(requestParam); 29 | } 30 | 31 | @override 32 | void $unpack() { 33 | $initUnpack(); 34 | requestParam = $unpackDouble(); 35 | } 36 | 37 | @override 38 | String toString() { 39 | return 'TestRequest\x1b[0m(requestParam: ${PackMe.dye(requestParam)})'; 40 | } 41 | } 42 | 43 | class TestResponse extends PackMeMessage { 44 | TestResponse({ 45 | required this.responseParam, 46 | }); 47 | TestResponse.$empty(); 48 | 49 | late double responseParam; 50 | 51 | @override 52 | int $estimate() { 53 | $reset(); 54 | return 16; 55 | } 56 | 57 | @override 58 | void $pack() { 59 | $initPack(216725115); 60 | $packDouble(responseParam); 61 | } 62 | 63 | @override 64 | void $unpack() { 65 | $initUnpack(); 66 | responseParam = $unpackDouble(); 67 | } 68 | 69 | @override 70 | String toString() { 71 | return 'TestResponse\x1b[0m(responseParam: ${PackMe.dye(responseParam)})'; 72 | } 73 | } 74 | 75 | final Map testMessageFactory = { 76 | 764832169: () => TestRequest.$empty(), 77 | 216725115: () => TestResponse.$empty(), 78 | }; -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | # If you want server to accept WebSocket connections you need to specify "socket" or "port" 2 | # parameter. If both are set then "socket" setting will have higher priority. 3 | # 4 | # Note that unix domain sockets are only available on Linux, Android and MacOS. 5 | 6 | socket: /tmp/serveme.sock 7 | port: 8080 8 | 9 | # MongoDB connection configuration (optional). If no mongo configuration is provided then server 10 | # will skip MongoDB initialization step. Note that in this case trying to access db object from 11 | # modules will cause an exception. 12 | # 13 | # If "replica" parameter is set then "host" parameter must be a list: 14 | # 15 | # mongo: 16 | # host: 17 | # - replica1.database.domain 18 | # - replica2.database.domain 19 | # - replica3.database.domain 20 | # replica: replicaSetName 21 | # database: dataBaseName 22 | # 23 | # User/password can be specified as well (optional): 24 | # 25 | # mongo: 26 | # host: 127.0.0.1 27 | # database: dataBaseName 28 | # user: dbUser 29 | # password: dbUserPassword 30 | 31 | mongo: 32 | host: 127.0.0.1 33 | database: serveme_test 34 | 35 | # Set "debug" parameter to true if you want to show and log all debug messages (see Module.debug() 36 | # method). 37 | # 38 | # If no debug and logs parameters are specified then server will use default configuration: 39 | # 40 | # debug: false 41 | # debug_log: debug.log 42 | # error_log: error.log 43 | 44 | debug: true 45 | debug_log: debug.log 46 | error_log: error.log 47 | 48 | # Server will initialize and run only modules listed in "modules" parameter: 49 | # 50 | # modules: 51 | # - some_module_1 52 | # - some_module_2 53 | # - some_module_3 54 | # 55 | # Note that if some module is implemented and passed to ServeMe constructor but not listed in 56 | # configuration file then it will be ignored. 57 | 58 | modules: -------------------------------------------------------------------------------- /example/example.yaml: -------------------------------------------------------------------------------- 1 | # If you want server to accept WebSocket connections you need to specify "socket" or "port" 2 | # parameter. If both are set then "socket" setting will have higher priority. 3 | # 4 | # Note that unix domain sockets are only available on Linux, Android and MacOS. 5 | 6 | port: 8080 7 | 8 | # MongoDB connection configuration (optional). If no mongo configuration is provided then server 9 | # will skip MongoDB initialization step. Note that in this case trying to access db object from 10 | # modules will cause an exception. 11 | # 12 | # If "replica" parameter is set then "host" parameter must be a list: 13 | # 14 | # mongo: 15 | # host: 16 | # - replica1.database.domain 17 | # - replica2.database.domain 18 | # - replica3.database.domain 19 | # replica: replicaSetName 20 | # database: dataBaseName 21 | # 22 | # User/password can be specified as well (optional): 23 | # 24 | # mongo: 25 | # host: 127.0.0.1 26 | # database: dataBaseName 27 | # user: dbUser 28 | # password: dbUserPassword 29 | 30 | 31 | # Set "debug" parameter to true if you want to show and log all debug messages (see Module.debug() 32 | # method). 33 | # 34 | # If no debug and logs parameters are specified then server will use default configuration: 35 | # 36 | # debug: false 37 | # debug_log: debug.log 38 | # error_log: error.log 39 | 40 | debug: true 41 | debug_log: debug.log 42 | error_log: error.log 43 | 44 | # Server will initialize and run only modules listed in "modules" parameter: 45 | # 46 | # modules: 47 | # - some_module_1 48 | # - some_module_2 49 | # - some_module_3 50 | # 51 | # Note that if some module is implemented and passed to ServeMe constructor but not listed in 52 | # configuration file then it will be ignored. 53 | 54 | modules: 55 | - meh 56 | 57 | # Since this is example config file we're extending it with our custom data which will be loaded by 58 | # our own Config class. 59 | 60 | meh_messages: 61 | alive_notification: MehModule is alive! 62 | spam_message: Cheese for everyone! -------------------------------------------------------------------------------- /lib/core/events.dart: -------------------------------------------------------------------------------- 1 | part of serveme; 2 | 3 | abstract class Event { 4 | } 5 | 6 | class ReadyEvent extends Event { 7 | ReadyEvent(); 8 | } 9 | 10 | class TickEvent extends Event { 11 | TickEvent(this.counter); 12 | final int counter; 13 | } 14 | 15 | class StopEvent extends Event { 16 | StopEvent(this.signal, this.code); 17 | final ProcessSignal signal; 18 | final int code; 19 | } 20 | 21 | class LogEvent extends Event { 22 | LogEvent(this.message); 23 | final String message; 24 | } 25 | 26 | class ErrorEvent extends Event { 27 | ErrorEvent(this.message, [this.stack]); 28 | final String message; 29 | final StackTrace? stack; 30 | } 31 | 32 | class ConnectEvent extends Event { 33 | ConnectEvent(this.client); 34 | final C client; 35 | } 36 | 37 | class DisconnectEvent extends Event { 38 | DisconnectEvent(this.client); 39 | final C client; 40 | } 41 | 42 | class Events { 43 | Events(this._server) { 44 | int counter = 0; 45 | _timer = Timer.periodic(const Duration(seconds: 1), (_) { 46 | dispatch(TickEvent(++counter)); 47 | }); 48 | } 49 | 50 | late final Timer _timer; 51 | final ServeMe _server; 52 | final Map> _eventHandlers = >{}; 53 | 54 | void listen(Future Function(T) handler) { 55 | if (_eventHandlers[T] == null) _eventHandlers[T] = []; 56 | if (!_eventHandlers[T]!.contains(handler)) _eventHandlers[T]!.add(handler); 57 | } 58 | 59 | void cancel(Future Function(T) handler) { 60 | _eventHandlers[T]?.remove(handler); 61 | } 62 | 63 | Future _tryExecute(Function handler, Event event) async { 64 | try { 65 | await handler(event); 66 | } 67 | catch (err, stack) { 68 | _server._logger.error('Event handler execution error: $err', stack); 69 | } 70 | } 71 | 72 | Future dispatch(Event event) async { 73 | final List> futures = >[]; 74 | if (_eventHandlers[event.runtimeType] != null) { 75 | for (final Function handler in _eventHandlers[event.runtimeType]!) { 76 | futures.add(_tryExecute(handler, event)); 77 | } 78 | } 79 | await Future.wait(futures); 80 | } 81 | 82 | void dispose() { 83 | _timer.cancel(); 84 | } 85 | } -------------------------------------------------------------------------------- /packme/compile.dart: -------------------------------------------------------------------------------- 1 | /// This file allows you to generate Dart source code files for PackMe data 2 | /// protocol using JSON manifest files. 3 | /// 4 | /// Usage: dart compile.dart 5 | /// 6 | /// JSON Manifest file represents a set of commands, each command consists of 7 | /// one (single message) or two (request and response) messages. In your server 8 | /// code you mostly listen for request messages from client and reply with 9 | /// response messages. However it totally depends on your architecture: server 10 | /// may as well send request messages and in some cases client may process those 11 | /// requests without reply. Though using single messages are preferred in such 12 | /// cases. 13 | /// 14 | /// The reason why each command is strictly divided on two messages (instead of 15 | /// just using raw messages) is to make manifest structure as clear as possible. 16 | /// I.e. when you look at some command you already know how it is supposed to 17 | /// work, not just some random message which will be used by server or client in 18 | /// unobvious ways. 19 | /// 20 | /// Another thing worth mentioning is that it is not possible to separately 21 | /// declare a message (like in FlatBuffers or ProtoBuffers) and then reuse it in 22 | /// different commands. Here's why: if you look carefully in .json examples you 23 | /// will see that the same entities (like user) in different commands have 24 | /// different set of parameters. You don't want to encode the whole user's 25 | /// profile when you need to send a list of friends. Or when you need to show 26 | /// short user info on the post etc. Reusing declared messages firstly leads to 27 | /// encoding and transferring unused data, and secondly makes it hard to 28 | /// refactor your data protocol when different parts of your application are 29 | /// being changed. 30 | /// 31 | /// Nested object in command request or response will be represented with class 32 | /// SomeCommandResponsNested. For example compiling example-posts.json will 33 | /// result in creating class GetResponseCommentAuthor which will contain three 34 | /// fields: List id, String nickname and String avatar. 35 | /// 36 | /// Prefix "?" in field declaration means it is optional (Null by default). 37 | 38 | import 'package:packme/compiler.dart' as compiler; 39 | 40 | void main(List args) { 41 | compiler.main(args); 42 | } -------------------------------------------------------------------------------- /example/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | LocalHost Echo Client 5 | 37 | 38 | 39 |
40 |

LocalHost Echo Client

41 |

This example page will try connect to 127.0.0.1:8080 and display every incoming message from WebSocket connection.

42 |

Additionally it will echo back PackMeMessage messages in order to test its' reception and decoding on server side.

43 |

44 | 	
45 | 81 | 82 | -------------------------------------------------------------------------------- /lib/classes/config.dart: -------------------------------------------------------------------------------- 1 | part of serveme; 2 | 3 | class MongoConfig { 4 | MongoConfig({ 5 | this.host, 6 | this.hosts, 7 | this.replica, 8 | required this.database, 9 | this.user, 10 | this.password, 11 | this.secure = false, 12 | }) { 13 | if (replica != null && hosts == null) throw Exception('Using MongoDB replica requires host list to be set'); 14 | if (replica == null && host == null) throw Exception('MongoDB host parameter is not set'); 15 | } 16 | 17 | final String? host; 18 | final List? hosts; 19 | final String? replica; 20 | final String database; 21 | final String? user; 22 | final String? password; 23 | final bool secure; 24 | } 25 | 26 | class Config { 27 | Config(String filename) { 28 | final String yaml = File(filename).readAsStringSync(); 29 | _map = loadYaml(yaml) as YamlMap?; 30 | if (_map == null) throw Exception('"$filename" is not valid YAML file'); 31 | } 32 | 33 | YamlMap? _map = YamlMap(); 34 | MongoConfig? _mongo; 35 | String? _socket; 36 | String? _host; 37 | int? _port; 38 | bool _debug = false; 39 | String _debugLog = 'debug.log'; 40 | String _errorLog = 'error.log'; 41 | final List modules = []; 42 | 43 | YamlMap get map => _map!; 44 | bool get debug => _debug; 45 | 46 | static Config _instantiate(String filename, {Config Function(String)? factory}) { 47 | Config config; 48 | try { 49 | config = factory != null ? factory(filename) : Config(filename); 50 | if (config.map['mongo'] is YamlMap) { 51 | config._mongo = MongoConfig( 52 | host: cast(config.map['mongo']['host']), 53 | hosts: config.map['mongo']['host'] is YamlList ? (config.map['mongo']['host'] as YamlList).cast() : null, 54 | replica: cast(config.map['mongo']['replica']), 55 | database: cast(config.map['mongo']['database'], errorMessage: 'MongoDB database is not specified'), 56 | user: cast(config.map['mongo']['user']), 57 | password: cast(config.map['mongo']['password']), 58 | secure: cast(config.map['mongo']['secure'], fallback: false), 59 | ); 60 | } 61 | config._socket = cast(config.map['socket']); 62 | config._host = cast(config.map['host']); 63 | config._port = cast(config.map['port']); 64 | config._debug = cast(config.map['debug'], fallback: false); 65 | config._debugLog = cast(config.map['debug_log'], fallback: 'debug.log'); 66 | config._errorLog = cast(config.map['error_log'], fallback: 'error.log'); 67 | if (config.map['modules'] is YamlList) config.modules.addAll((config.map['modules'] as YamlList).cast()); 68 | } 69 | catch (err) { 70 | throw Exception('Unable to load configuration file "$filename": $err'); 71 | } 72 | return config; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/core/logger.dart: -------------------------------------------------------------------------------- 1 | part of serveme; 2 | 3 | class Logger { 4 | Logger(this._server) { 5 | try { 6 | _debugFile = File(_server.config._debugLog).openSync(mode: FileMode.writeOnlyAppend); 7 | } 8 | catch (err) { 9 | error('Unable to write debug log file: $err'); 10 | } 11 | try { 12 | _errorFile = File(_server.config._errorLog).openSync(mode: FileMode.writeOnlyAppend); 13 | } 14 | catch (err) { 15 | error('Unable to write error log file: $err'); 16 | } 17 | errorsCountReduceTimer = Timer.periodic(const Duration(minutes: 1), (_) { 18 | if (errorsCount > 0) errorsCount -= min(errorsCount, 100); 19 | }); 20 | } 21 | 22 | static const String _reset = '\x1b[0m'; 23 | static const String _red = '\x1b[31m'; 24 | static const String _green = '\x1b[32m'; 25 | static const String _clear = '\x1b[999D\x1b[K'; 26 | 27 | final ServeMe _server; 28 | Future _debugPromise = Future.value(null); 29 | Future _errorPromise = Future.value(null); 30 | RandomAccessFile? _debugFile; 31 | RandomAccessFile? _errorFile; 32 | late Timer errorsCountReduceTimer; 33 | int errorsCount = 0; 34 | 35 | Future log(String message, [String color = _green]) async { 36 | final String time = DateTime.now().toUtc().toString().replaceFirst(RegExp(r'\..*'), ''); 37 | _server._events.dispatch(LogEvent(message)); 38 | stdout.write(_clear); 39 | print('$color${time.replaceFirst(RegExp('.* '), '')}: $message$_reset'); 40 | _server.console.update(); 41 | if (_debugFile != null) { 42 | Future func(void _) => _debugFile!.writeString('$time - $message\n'); 43 | _debugPromise = _debugPromise.then(func); 44 | await _debugPromise; 45 | } 46 | } 47 | 48 | Future debug(String message, [String color = _reset]) async { 49 | if (_server.config._debug) await log(message, color); 50 | } 51 | 52 | Future error(String message, [StackTrace? stack]) async { 53 | errorsCount++; 54 | if (errorsCount >= 1000) { 55 | if (errorsCount == 1000) { 56 | message = 'Too many errors, error processing is suspended'; 57 | stack = null; 58 | } 59 | else return; 60 | } 61 | final String time = DateTime.now().toUtc().toString().replaceFirst(RegExp(r'\..*'), ''); 62 | _server._events.dispatch(ErrorEvent(message, stack)); 63 | stdout.write(_clear); 64 | print('$_red${time.replaceFirst(RegExp('.* '), '')}: $message$_reset'); 65 | _server.console.update(); 66 | if (_errorFile != null) { 67 | Future func(void _) => _errorFile!.writeString('$time - $message\n${stack.toString()}\n'); 68 | _errorPromise = _errorPromise.then(func); 69 | await _errorPromise; 70 | } 71 | if (_server.config.debug && _debugFile != null) { 72 | Future func(void _) => _debugFile!.writeString('$time - $message\n'); 73 | _debugPromise = _debugPromise.then(func); 74 | await _debugPromise; 75 | } 76 | } 77 | 78 | Future dispose() async { 79 | errorsCountReduceTimer.cancel(); 80 | await Future.wait(>[_debugPromise, _errorPromise]); 81 | if (_debugFile != null) _debugFile!.closeSync(); 82 | if (_errorFile != null) _errorFile!.closeSync(); 83 | } 84 | } -------------------------------------------------------------------------------- /lib/core/console.dart: -------------------------------------------------------------------------------- 1 | part of serveme; 2 | 3 | final Stream> stdinStream = stdin.asBroadcastStream(); 4 | 5 | class CommandHandler { 6 | CommandHandler({required this.function, this.validator, this.usage}); 7 | 8 | final Function(String, List) function; 9 | final RegExp? validator; 10 | final String? usage; 11 | } 12 | 13 | class Console { 14 | Console(this._server) { 15 | stdin.echoMode = false; 16 | stdin.lineMode = false; 17 | _listener = stdinStream.listen(_key); 18 | } 19 | 20 | final ServeMe _server; 21 | late final StreamSubscription> _listener; 22 | final Map handlers = {}; 23 | final Map similar = {}; 24 | final List history = ['']; 25 | 26 | String line = ''; 27 | int pos = 0; 28 | int row = 0; 29 | RegExp? search; 30 | int index = 0; 31 | final List matches = []; 32 | 33 | void on(String command, Function(String string, List args) handler, {RegExp? validator, String? usage, List? aliases, List? similar}) { 34 | final CommandHandler commandHandler = CommandHandler( 35 | function: handler, 36 | validator: validator, 37 | usage: usage, 38 | ); 39 | handlers[command] = commandHandler; 40 | if (aliases != null) for (final String alias in aliases) handlers[alias] = commandHandler; 41 | if (similar != null) for (final String str in similar) this.similar[str] = command; 42 | } 43 | 44 | void process(String line) { 45 | // TODO: Exceptions are not handled as "UNHANDLED" for some reason. Need to figure that out. 46 | line = line.trim(); 47 | if (line.isEmpty) return; 48 | _server.log('> $line', YELLOW); 49 | final List args = line.split(RegExp(r'\s+')); 50 | final String command = args[0]; 51 | final CommandHandler? handler = handlers[command]; 52 | if (handler != null) { 53 | args.removeAt(0); 54 | line = args.join(' '); 55 | if (args.isNotEmpty && ['-h', '--help', '-?', '/?'].contains(args[0])) { 56 | if (handler.usage != null) _server.log('Usage: ${handler.usage}', CYAN); 57 | else _server.log('No usage info available for: $command', CYAN); 58 | } 59 | else { 60 | final bool valid = handler.validator == null || handler.validator!.hasMatch(line); 61 | if (valid) handler.function(line, args); 62 | else if (handler.usage != null) _server.log('Usage: ${handler.usage}', CYAN); 63 | } 64 | } 65 | else _server.log('Unknown command: $command' + (similar[command] != null ? '. Did you mean "${similar[command]}"?' : ''), CYAN); 66 | } 67 | 68 | void previous() { 69 | if (row > 0) { 70 | line = history[--row]; 71 | pos = line.length; 72 | } 73 | } 74 | 75 | void next() { 76 | if (row < history.length - 1) { 77 | line = history[++row]; 78 | pos = line.length; 79 | } 80 | } 81 | 82 | void reset() { 83 | history..last = line..add(line = ''); 84 | pos = 0; 85 | if (history.length > 100) history.removeAt(0); 86 | row = history.length - 1; 87 | search = null; 88 | } 89 | 90 | void _key(List input) { 91 | if (input[0] == 10 || input[0] == 13) { 92 | final String cmd = line; 93 | reset(); 94 | update(); 95 | process(cmd); 96 | } 97 | else if (input[0] == 9) { 98 | if (search == null) { 99 | search = RegExp('^${RegExp.escape(line)}'); 100 | matches.clear(); 101 | for (final String cmd in handlers.keys) { 102 | if (search!.hasMatch(cmd)) matches.add(cmd); 103 | } 104 | matches.sort(); 105 | index = 0; 106 | } 107 | if (matches.isNotEmpty) { 108 | line = matches[index++] + ' '; 109 | pos = line.length; 110 | if (index >= matches.length) index = 0; 111 | } 112 | } 113 | else if (input[0] == 5) previous(); 114 | else if (input[0] == 24) next(); 115 | else if (input[0] == 27) { 116 | if (input.length > 1) { 117 | if (input[1] == 91 && input.length > 2) { 118 | if (input[2] == 68) if (pos > 0) pos--; 119 | if (input[2] == 67) if (pos < line.length) pos++; 120 | if (input[2] == 49) pos = 0; 121 | if (input[2] == 52) pos = line.length; 122 | if (input[2] == 51) { 123 | if (pos < line.length) line = line.replaceRange(pos, pos + 1, ''); 124 | search = null; 125 | } 126 | if (input[2] == 65) previous(); 127 | if (input[2] == 66) next(); 128 | } 129 | } 130 | else { 131 | line = ''; 132 | pos = 0; 133 | row = history.length - 1; 134 | search = null; 135 | } 136 | } 137 | else if (input[0] == 8 || input[0] == 127) { 138 | if (pos > 0) { 139 | line = line.replaceRange(pos - 1, pos, ''); 140 | pos--; 141 | search = null; 142 | } 143 | } 144 | else if (input[0] >= 32) { 145 | final String char = String.fromCharCodes(input); 146 | line = line.replaceRange(pos, pos, char); 147 | pos += char.length; 148 | search = null; 149 | } 150 | update(); 151 | } 152 | 153 | void update() { 154 | final int maxLen = stdout.terminalColumns - 3; 155 | final int offset = min(pos, max(0, line.length - maxLen)); 156 | final String lineCapped = line.substring(offset, min(line.length, offset + maxLen)); 157 | stdout.write('\r> $lineCapped' + ' ' * (maxLen - line.length) + '\r\x1b[${pos - offset + 2}C'); 158 | } 159 | 160 | Future dispose() async { 161 | await _listener.cancel(); 162 | } 163 | } -------------------------------------------------------------------------------- /lib/core/integrity.dart: -------------------------------------------------------------------------------- 1 | part of serveme; 2 | 3 | enum IndexImplementationStatus { 4 | implemented, 5 | invalid, 6 | none, 7 | } 8 | 9 | class IndexDescriptor { 10 | IndexDescriptor({required this.key, this.unique = false, this.sparse = false, this.expireAfterSeconds}); 11 | 12 | final Map key; 13 | final bool unique; 14 | final bool sparse; 15 | final int? expireAfterSeconds; 16 | 17 | IndexImplementationStatus _implementationStatus(String name, List> indexes) { 18 | for (final Map index in indexes) { 19 | bool equal = key.toString() == index['key'].toString(); 20 | equal &= (unique && index['unique'] == true) || (!unique && index['unique'] != true); 21 | equal &= (sparse && index['sparse'] == true) || (!sparse && index['sparse'] != true); 22 | equal &= expireAfterSeconds == index['expireAfterSeconds']; 23 | if (equal) return IndexImplementationStatus.implemented; 24 | else if (index['name'] == name) return IndexImplementationStatus.invalid; 25 | } 26 | return IndexImplementationStatus.none; 27 | } 28 | } 29 | 30 | class CollectionDescriptor { 31 | CollectionDescriptor({this.indexes = const {}, this.capped = false, this.cappedSize, this.cappedLength, this.documents = const >[]}); 32 | 33 | final Map indexes; 34 | final bool capped; 35 | final int? cappedSize; 36 | final int? cappedLength; 37 | final List> documents; 38 | 39 | CreateCollectionOptions get _options { 40 | final CreateCollectionOptions data = CreateCollectionOptions( 41 | capped: capped, 42 | size: cappedSize, 43 | max: cappedLength, 44 | ); 45 | return data; 46 | } 47 | } 48 | 49 | Future _checkCollections(ServeMe server, Map collections) async { 50 | final List names = await (await server.db).getCollectionNames(); 51 | for (final String name in collections.keys) { 52 | if (names.contains(name)) continue; 53 | server.error('Collection "$name" is missing: fixing...'); 54 | await (await server.db).createCollection(name, createCollectionOptions: collections[name]!._options); 55 | server.log('Collection "$name" is created'); 56 | } 57 | } 58 | 59 | Future _checkIndexes(ServeMe server, Map collections) async { 60 | for (final String name in collections.keys) { 61 | final CollectionDescriptor collection = collections[name]!; 62 | if (collection.indexes.isEmpty) continue; 63 | final List> actualIndexes = await (await server.db).collection(name).getIndexes(); 64 | for (final String indexName in collection.indexes.keys) { 65 | final IndexDescriptor index = collection.indexes[indexName]!; 66 | final IndexImplementationStatus implementation = index._implementationStatus(indexName, actualIndexes); 67 | if (implementation == IndexImplementationStatus.implemented) continue; 68 | server.error('Index "$indexName" is missing for collection "$name": fixing...'); 69 | if (implementation == IndexImplementationStatus.invalid) { 70 | server.error('Name "$indexName" is used by another index: MANUAL FIX REQUIRED'); 71 | continue; 72 | } 73 | // creteIndex doesn't support expireAfterSeconds, so we do it long way 74 | // await (await server.db).collection(name).createIndex(name: indexName, keys: index.key, unique: index.unique, sparse: index.sparse); 75 | final Db db = await server.db; 76 | final DbCollection dbCollection = db.collection(name); 77 | final CreateIndexOptions indexOptions = CreateIndexOptions(dbCollection, 78 | uniqueIndex: index.unique, 79 | sparseIndex: index.sparse, 80 | indexName: indexName 81 | ); 82 | final Map? rawOptions = index.expireAfterSeconds != null ? {'expireAfterSeconds': index.expireAfterSeconds!} : null; 83 | final CreateIndexOperation indexOperation = CreateIndexOperation(db, dbCollection, index.key, indexOptions, rawOptions: rawOptions); 84 | final Map result = await indexOperation.execute(); 85 | if (result['ok'] == 1.0) server.log('Index "$indexName" is created'); 86 | else server.error('Unable to create index "$indexName": ${result['errmsg']}'); 87 | } 88 | } 89 | } 90 | 91 | Future _checkData(ServeMe server, Map collections) async { 92 | for (final String name in collections.keys) { 93 | final CollectionDescriptor collection = collections[name]!; 94 | if (collection.documents.isEmpty) continue; 95 | for (final Map document in collection.documents) { 96 | final Map? found = await (await server.db).collection(name).findOne({'_id': document['_id']}); 97 | if (found == null) { 98 | server.error('Mandatory document is missing in collection "$name": fixing...'); 99 | await (await server.db).collection(name).insertOne(document); 100 | server.log('Document ID "${document['_id']}" added'); 101 | } 102 | } 103 | } 104 | } 105 | 106 | Future _checkMongoIntegrity(ServeMe server, Map collections) async { 107 | server.log('Checking database integrity...'); 108 | await _checkCollections(server, collections); 109 | await _checkIndexes(server, collections); 110 | await _checkData(server, collections); 111 | server.log('Database integrity OK'); 112 | } -------------------------------------------------------------------------------- /lib/serveme.dart: -------------------------------------------------------------------------------- 1 | library serveme; 2 | 3 | import 'dart:async'; 4 | import 'dart:io'; 5 | import 'dart:math'; 6 | import 'package:connectme/connectme.dart'; 7 | import 'package:mongo_dart/mongo_dart.dart' hide Type; 8 | import 'package:packme/packme.dart'; 9 | import 'package:yaml/yaml.dart'; 10 | 11 | part 'classes/module.dart'; 12 | part 'classes/config.dart'; 13 | part 'core/console.dart'; 14 | part 'core/events.dart'; 15 | part 'core/integrity.dart'; 16 | part 'core/logger.dart'; 17 | part 'core/mongo.dart'; 18 | part 'core/scheduler.dart'; 19 | part 'core/utils.dart'; 20 | 21 | typedef ServeMeClient = ConnectMeClient; 22 | typedef ServeMeSocket = ConnectMeSocket; 23 | typedef ServeMeType = ConnectMeType; 24 | 25 | final bool _unixSocketsAvailable = Platform.isLinux || Platform.isAndroid || Platform.isMacOS; 26 | 27 | class ServeMe { 28 | ServeMe({ 29 | String configFile = 'config.yaml', 30 | ServeMeType type = ServeMeType.ws, 31 | Config Function(String filename)? configFactory, 32 | C Function(ServeMeSocket)? clientFactory, 33 | Map>? modules, 34 | Map? dbIntegrityDescriptor, 35 | }) : _type = type, _clientFactory = clientFactory, _dbIntegrityDescriptor = dbIntegrityDescriptor { 36 | config = Config._instantiate(configFile, factory: configFactory); 37 | console = Console(this); 38 | _logger = Logger(this); 39 | _events = Events(this); 40 | _scheduler = Scheduler(this); 41 | if (config != null && modules != null) { 42 | _modules.addEntries(modules.entries.where((MapEntry> entry) { 43 | if (!config.modules.contains(entry.key)) return false; 44 | entry.value.server = this; 45 | return true; 46 | })); 47 | } 48 | final InternetAddress address = _unixSocketsAvailable && config._socket != null 49 | ? InternetAddress(config._socket!, type: InternetAddressType.unix) 50 | : InternetAddress(config._host ?? '127.0.0.1', type: InternetAddressType.IPv4); 51 | _cmServer = ConnectMe.server(address, 52 | port: config._port ?? 0, 53 | type: _type, 54 | clientFactory: _clientFactory, 55 | onLog: log, 56 | onError: error, 57 | onConnect: (C client) => _events.dispatch(ConnectEvent(client)), 58 | onDisconnect: (C client) => _events.dispatch(DisconnectEvent(client)) 59 | ); 60 | } 61 | 62 | bool _running = false; 63 | final List> _processSignalListeners = >[]; 64 | late final Config config; 65 | late final Console console; 66 | late final Events _events; 67 | late final Logger _logger; 68 | late final Scheduler _scheduler; 69 | late final ConnectMeServer _cmServer; 70 | final ServeMeType _type; 71 | MongoDbConnection? _mongo; 72 | final Map> _modules = >{}; 73 | final C Function(ServeMeSocket)? _clientFactory; 74 | final Map? _dbIntegrityDescriptor; 75 | ProcessSignal? _signalReceived; 76 | Timer? _signalTimer; 77 | 78 | List get clients => _cmServer.clients; 79 | Module? operator [](String module) => _modules[module]; 80 | Future get db { 81 | if (_mongo == null) throw Exception('MongoDB is not initialized'); 82 | return _mongo!.db; 83 | } 84 | Future Function(String, [String]) get log => _logger.log; 85 | Future Function(String, [String]) get debug => _logger.debug; 86 | Future Function(String, [StackTrace?]) get error => _logger.error; 87 | 88 | Future _initMongoDB() async { 89 | if (config._mongo == null) return; 90 | _mongo = await MongoDbConnection.connect(config._mongo!, this); 91 | if (_dbIntegrityDescriptor != null) await _checkMongoIntegrity(this, _dbIntegrityDescriptor!); 92 | } 93 | 94 | Future _initModules() async { 95 | if (_modules.isEmpty) return; 96 | log('Initializing modules...'); 97 | for (final String name in _modules.keys) { 98 | log('Initializing module: $name'); 99 | await _modules[name]!.init(); 100 | _modules[name]!._state = ModuleState.initialized; 101 | } 102 | log('Modules initialization complete'); 103 | } 104 | 105 | void _runModules() { 106 | if (_modules.isEmpty) return; 107 | log('Running modules...'); 108 | for (final String name in _modules.keys) { 109 | log('Running module: $name'); 110 | _modules[name]!.run(); 111 | _modules[name]!._state = ModuleState.running; 112 | } 113 | log('All modules are running'); 114 | } 115 | 116 | bool _confirm(ProcessSignal signal, String message) { 117 | if (_signalTimer != null && _signalTimer!.isActive) _signalTimer!.cancel(); 118 | if (signal == _signalReceived) return true; 119 | else log(message, CYAN); 120 | _signalReceived = signal; 121 | _signalTimer = Timer(const Duration(seconds: 2), () { 122 | _signalReceived = null; 123 | }); 124 | return false; 125 | } 126 | 127 | Future _disposeModules() async { 128 | for (final String name in _modules.keys) { 129 | if (_modules[name]!._state == ModuleState.none) continue; 130 | try { 131 | await _modules[name]!.dispose(); 132 | } 133 | catch(err, stack) { 134 | error('An error has occurred while disposing module "$name": $err', stack); 135 | } 136 | _modules[name]!._state = ModuleState.disposed; 137 | } 138 | } 139 | 140 | void register(Map messageFactory) { 141 | _cmServer.register(messageFactory); 142 | } 143 | 144 | void broadcast(dynamic data, {bool Function(C)? where}) { 145 | _cmServer.broadcast(data, where: where); 146 | } 147 | 148 | void on(String route, Function(HttpRequest request) handler) { 149 | _cmServer.on(route, handler); 150 | } 151 | 152 | void listen(Future Function(T, C) handler) { 153 | _cmServer.listen(handler); 154 | } 155 | 156 | void cancel(Future Function(T, C) handler) { 157 | _cmServer.cancel(handler); 158 | } 159 | 160 | Future connect(dynamic address, { 161 | Map headers = const {}, 162 | int port = 0, 163 | bool autoReconnect = true, 164 | int queryTimeout = 30, 165 | Function()? onConnect, 166 | Function()? onDisconnect, 167 | }) { 168 | return ConnectMe.connect(address, 169 | port: port, 170 | autoReconnect: autoReconnect, 171 | queryTimeout: queryTimeout, 172 | onLog: log, 173 | onError: error, 174 | onConnect: onConnect, 175 | onDisconnect: onDisconnect, 176 | ); 177 | } 178 | 179 | Future run() async { 180 | if (_running) return false; 181 | log('Server start initiated'); 182 | _running = true; 183 | await runZonedGuarded( 184 | () async { 185 | try { 186 | _processSignalListeners.add(ProcessSignal.sighup.watch().listen((_) => _shutdown(_, 1))); 187 | _processSignalListeners.add(ProcessSignal.sigint.watch().listen((_) { 188 | if (_confirm(ProcessSignal.sigint, 'Press ^C again shortly to stop the server')) _shutdown(_, 2); 189 | })); 190 | if (!Platform.isWindows) { 191 | _processSignalListeners.add(ProcessSignal.sigterm.watch().listen((_) => _shutdown(_, 15))); 192 | _processSignalListeners.add(ProcessSignal.sigusr1.watch().listen((_) => _shutdown(_, 10))); 193 | _processSignalListeners.add(ProcessSignal.sigusr2.watch().listen((_) => _shutdown(_, 12))); 194 | } 195 | console.on('stop', (_, __) => _shutdown(ProcessSignal.sigquit, 0), 196 | validator: RegExp(r'^$'), 197 | usage: 'stop', 198 | similar: ['exit', 'quit', 'shutdown'], 199 | ); 200 | await _initMongoDB(); 201 | await _initModules(); 202 | _events.dispatch(ReadyEvent()); 203 | _runModules(); 204 | await _cmServer.serve(); 205 | } 206 | catch (err, stack) { 207 | await error('Server initialization failed: $err', stack); 208 | await _shutdown(ProcessSignal.sigabrt, 6); 209 | } 210 | }, 211 | // Handle all unhandled exceptions in order to prevent application crash 212 | (Object err, StackTrace stack) async { 213 | error('UNHANDLED: $err', stack); 214 | } 215 | ); 216 | return _running; 217 | } 218 | 219 | Future stop() async { 220 | await _shutdown(ProcessSignal.sigquit, 100500); 221 | } 222 | 223 | Future _shutdown(ProcessSignal event, [int code = 100500]) async { 224 | await log('Server shutdown initiated: $event'); 225 | await _events.dispatch(StopEvent(event, code)); 226 | if (_unixSocketsAvailable && config._socket != null) { 227 | final File socketFile = File(config._socket!); 228 | if (socketFile.existsSync()) socketFile.deleteSync(recursive: true); 229 | } 230 | for (final StreamSubscription listener in _processSignalListeners) { 231 | listener.cancel(); 232 | } 233 | _processSignalListeners.clear(); 234 | await _cmServer.close(); 235 | await _disposeModules(); 236 | _scheduler.dispose(); 237 | _events.dispose(); 238 | if (_mongo != null) await _mongo!.close(); 239 | await log('Server stopped'); 240 | await console.dispose(); 241 | await _logger.dispose(); 242 | if (code != 100500) exit(code); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:math' show Random; 3 | import 'package:serveme/serveme.dart'; 4 | 5 | /// This file is generated by packme/compile.dart using 6 | /// packme/example-users.json manifest file. 7 | import 'generated/example-users.generated.dart'; 8 | 9 | /// Some helper functions. 10 | 11 | int randomInt() { 12 | final Random rand = Random(); 13 | return rand.nextInt(255); 14 | } 15 | 16 | String randomString({int min = 3, int max = 9, bool spaces = false}) { 17 | final Random rand = Random(); 18 | const String characters = ' qwertyuiopasdfghjklzcvbnm'; 19 | String result = ''; 20 | final int len = rand.nextInt(max - min) + min; 21 | final int from = spaces ? 0 : 1; 22 | const int to = characters.length - 1; 23 | for (int i = 0; i < len; i++) result += characters[rand.nextInt(to - from) + from]; 24 | return result; 25 | } 26 | 27 | ListmessageToStrings(GetResponse message) { 28 | final List result = []; 29 | result.add('email: ${message.email}'); 30 | result.add('nickname: ${message.nickname}'); 31 | result.add('hidden: ${message.hidden}'); 32 | result.add('created: ${message.created}'); 33 | result.add('firstName: ${message.info.firstName}'); 34 | result.add('lastName: ${message.info.lastName}'); 35 | result.add('age: ${message.info.age}'); 36 | result.add('facebookId: ${message.social.facebookId}'); 37 | result.add('twitterId: ${message.social.twitterId}'); 38 | result.add('instagramId: ${message.social.instagramId}'); 39 | result.add('posts: ${message.stats.posts}'); 40 | result.add('comments: ${message.stats.comments}'); 41 | result.add('likes: ${message.stats.likes}'); 42 | result.add('dislikes: ${message.stats.dislikes}'); 43 | result.add('rating: ${message.stats.rating}'); 44 | return result; 45 | } 46 | 47 | /// Implementing our own config class since we want to add some custom fields 48 | /// to our configuration and load some custom data from main config file. 49 | 50 | class MehConfig extends Config { 51 | MehConfig(String filename) : super(filename) { 52 | /// At this point we can access to Map map field. 53 | aliveNotification = map['meh_messages']['alive_notification'] as String; 54 | spamMessage = map['meh_messages']['spam_message'] as String; 55 | } 56 | 57 | late String aliveNotification; /// Should be final but we will modify it. 58 | late final String spamMessage; 59 | } 60 | 61 | /// Implementing our own client class since we might want to add some custom 62 | /// functionality such as user authorization etc. 63 | 64 | class MehClient extends ServeMeClient { 65 | MehClient(ServeMeSocket socket) : super(socket) { 66 | userIsLocal = socket.httpRequest!.headers.host == '127.0.0.1'; 67 | } 68 | 69 | late final bool userIsLocal; 70 | bool userEnteredPassword = false; 71 | } 72 | 73 | /// Implementing our main example module. Note that in order to make server run 74 | /// this module it must be enabled in configuration file. 75 | 76 | class MehModule extends Module { 77 | late Task _periodicShout; 78 | late Task _webSocketSpam; 79 | Future Function(ConnectEvent)? onConnectListener; 80 | 81 | /// Since we're using custom Config class then we better override it a bit 82 | /// just to make sure it returns MehConfig, not default Config. 83 | 84 | @override 85 | MehConfig get config => super.config as MehConfig; 86 | 87 | /// Method init() will be called once after MongoDB initialized. Server will 88 | /// await each module init() completion. 89 | 90 | @override 91 | Future init() async { 92 | /// Declare tasks for scheduler. 93 | _periodicShout = Task(DateTime.now(), (DateTime _) async { 94 | log(config.aliveNotification, CYAN); 95 | }, period: const Duration(seconds: 3)); 96 | /// This task will spam all connected WebSocket clients. 97 | _webSocketSpam = Task(DateTime.now(), (DateTime _) async { 98 | /// We can pass PackMeMessage instance, Uint8List or String. 99 | server.broadcast(config.spamMessage); 100 | }, period: const Duration(seconds: 5)); 101 | 102 | /// Once scheduled tasks will be processed until completed or discarded. 103 | scheduler.schedule(_periodicShout); 104 | } 105 | 106 | /// Method run() will be called once after all modules are initialized. 107 | /// Server will call run() method for all modules simultaneously. 108 | 109 | @override 110 | void run() { 111 | /// For the sake of example let's add some custom console commands. 112 | console.on('setMessage', 113 | (String line, __) { 114 | /// Don't. It is a bad practice to modify config like this :) 115 | config.aliveNotification = line; 116 | log('MehModule message is set to "$line"'); 117 | }, 118 | /// Using regular expression for command line format verification. 119 | validator: RegExp(r'^.*\S+.*$'), /// At least 1 printable character. 120 | /// Message to be used to show command help. 121 | usage: 'setMessage ', 122 | /// These commands will work the same way as setMessage. 123 | aliases: ['setMsg', 'setNotification'], 124 | /// These commands will not be executed but a hint will be given. 125 | similar: ['set', 'message'], 126 | ); 127 | 128 | /// Now let's see how to use messages encoded with PackMe. 129 | /// It allows to use JSON manifest to describe data protocols and 130 | /// exchange any data between client and server. 131 | /// 132 | /// See packme/compile.dart and example .json manifest files. We 133 | /// imported 'generated/example-users.generated.dart' which was created 134 | /// with compile.dart script. 135 | 136 | /// First we need to register our messages 137 | server.register(exampleUsersMessageFactory); 138 | 139 | /// Now let's add console command which will broadcast some message. 140 | console.on('sendPackedMessage', (_, __) { 141 | /// GetResponse is just the name of response message of command Get. 142 | /// It's declared in example-users.generated.dart. 143 | final GetResponse message = GetResponse( 144 | email: '${randomString()}@${randomString()}.com', 145 | nickname: 'Mr. ${randomString()}', 146 | hidden: false, 147 | created: DateTime.now(), 148 | info: GetResponseInfo( 149 | firstName: randomString(), 150 | lastName: randomString(), 151 | age: randomInt(), 152 | ), 153 | social: GetResponseSocial( 154 | facebookId: randomInt() < 128 ? 'fbID_${randomString()}' : null, 155 | twitterId: randomInt() < 128 ? 'twID_${randomString()}' : null, 156 | instagramId: randomInt() < 128 ? 'inID_${randomString()}' : null, 157 | ), 158 | stats: GetResponseStats( 159 | posts: randomInt(), 160 | comments: randomInt(), 161 | likes: randomInt() * 10, 162 | dislikes: randomInt(), 163 | rating: randomInt() / 25.5, 164 | ), 165 | sessions: [], 166 | ); 167 | server.broadcast(message); 168 | log('Here is the message I sent:', MAGENTA); 169 | messageToStrings(message).forEach(log); 170 | log('Now waiting for echo response from client...', MAGENTA); 171 | }); 172 | 173 | /// And finally we will start listening for messages from clients. 174 | events.listen>(onConnectListener = (ConnectEvent event) async { 175 | /// Listen to GetResponse message only. 176 | event.client.listen((GetResponse data) async { 177 | log('Got response, decoded message:', MAGENTA); 178 | messageToStrings(data).forEach(log); 179 | }); 180 | /// Listen to String data only 181 | event.client.listen((String data) async { 182 | log('Got a string from client: "$data"', MAGENTA); 183 | }); 184 | }); 185 | 186 | log("MehModule is started. Apparently. Now let's spam them all.", MAGENTA); 187 | scheduler.schedule(_webSocketSpam); 188 | } 189 | 190 | /// Method dispose() will be called during server shutdown/restart process. 191 | /// Please do not forget to cancel your timers or subscriptions and release 192 | /// other resources in order to avoid memory leaks. 193 | 194 | @override 195 | Future dispose() async { 196 | if (onConnectListener != null) events.cancel>(onConnectListener!); 197 | scheduler.discard(_periodicShout); 198 | scheduler.discard(_webSocketSpam); 199 | } 200 | } 201 | 202 | /// Note that server.run() method returns Future which might be handy in 203 | /// some cases. It will return true if server initialization was successful and 204 | /// false if initialization failed. 205 | 206 | Future main() async { 207 | final ServeMe server = ServeMe( 208 | /// Main configuration file extended with our own custom data. 209 | configFile: 'example/example.yaml', 210 | /// Tell server to use our own Config class. 211 | configFactory: (_) => MehConfig(_), 212 | /// Tell server to use our own Client class. 213 | clientFactory: (_) => MehClient(_), 214 | /// Pass our modules to server (don't forget to enable them in config). 215 | modules: >{ 216 | 'meh': MehModule() 217 | }, 218 | ); 219 | 220 | final bool initializationResult = await server.run(); 221 | print('Server initialization status: $initializationResult'); 222 | } -------------------------------------------------------------------------------- /test/serveme_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:typed_data'; 4 | import 'package:connectme/connectme.dart'; 5 | import 'package:serveme/serveme.dart'; 6 | import 'package:test/test.dart'; 7 | import 'generated/test.generated.dart'; 8 | import 'modules/test.dart'; 9 | 10 | const Utf8Codec _utf8 = Utf8Codec(); 11 | 12 | void main() { 13 | late ServeMe server; 14 | late ConnectMeClient client; 15 | late Timer timer; 16 | late TestModule module; 17 | 18 | group('Connection tests', () { 19 | test('ServeMe.run() and ConnectMe.connect()', () async { 20 | timer = Timer(const Duration(seconds: 2), () => fail('Operation timed out')); 21 | server = ServeMe( 22 | configFile: 'test/config_test.yaml', 23 | modules: >{ 24 | 'test': module = TestModule(), 25 | }, 26 | ); 27 | await server.run(); 28 | module.events.listen>(expectAsync1, dynamic>((dynamic event) async { 29 | expect(event.client, isA()); 30 | })); 31 | module.events.listen>(expectAsync1, dynamic>((dynamic event) async { 32 | expect(event.client, isA()); 33 | })); 34 | client = await ConnectMe.connect('ws://127.0.0.1:31337', 35 | onConnect: expectAsync0(() {}), 36 | ); 37 | await client.close(); 38 | await server.stop(); 39 | timer.cancel(); 40 | }); 41 | }); 42 | 43 | group('ServeMe WebSocket data exchange tests', () { 44 | setUp(() async { 45 | timer = Timer(const Duration(seconds: 2), () => fail('Operation timed out')); 46 | server = ServeMe( 47 | configFile: 'test/config_test.yaml', 48 | modules: >{ 49 | 'test': module = TestModule(), 50 | }, 51 | ); 52 | await server.run(); 53 | client = await ConnectMe.connect('ws://127.0.0.1:31337'); 54 | await Future.delayed(const Duration(milliseconds: 100)); 55 | }); 56 | 57 | tearDown(() async { 58 | await client.close(); 59 | await server.stop(); 60 | timer.cancel(); 61 | }); 62 | 63 | test('Client sends String to server', () async { 64 | final Completer completer = Completer(); 65 | server.listen((String message, ConnectMeClient client) async { 66 | completer.complete(message); 67 | }); 68 | client.send('Test message from client'); 69 | expect(await completer.future, 'Test message from client'); 70 | }); 71 | 72 | test('Server broadcasts String to clients', () async { 73 | final Completer completer = Completer(); 74 | client.listen((String message) { 75 | completer.complete(message); 76 | }); 77 | server.broadcast('Test message from server'); 78 | expect(await completer.future, 'Test message from server'); 79 | }); 80 | 81 | test('Client sends Uint8List to server', () async { 82 | final Completer completer = Completer(); 83 | server.listen((Uint8List message, ConnectMeClient client) async { 84 | completer.complete(message); 85 | }); 86 | client.send(Uint8List.fromList([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5])); 87 | expect(await completer.future, Uint8List.fromList([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5])); 88 | }); 89 | 90 | test('Server broadcasts Uint8List to clients', () async { 91 | final Completer completer = Completer(); 92 | client.listen((Uint8List message) { 93 | completer.complete(message); 94 | }); 95 | server.broadcast(Uint8List.fromList([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5])); 96 | expect(await completer.future, Uint8List.fromList([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5])); 97 | }); 98 | 99 | test('Client sends TestResponse query to server', () async { 100 | server.register(testMessageFactory); 101 | server.listen((TestRequest request, ConnectMeClient client) async { 102 | client.send(request.$response(responseParam: request.requestParam)); 103 | }); 104 | client.register(testMessageFactory); 105 | final TestResponse response = await client.query(TestRequest(requestParam: 3.1415926535)); 106 | expect(response.responseParam, 3.1415926535); 107 | }); 108 | 109 | test('Server sends TestResponse query to client', () async { 110 | client.register(testMessageFactory); 111 | client.listen((TestRequest request) { 112 | client.send(request.$response(responseParam: request.requestParam)); 113 | }); 114 | server.register(testMessageFactory); 115 | final TestResponse response = await server.clients.first.query(TestRequest(requestParam: 3.1415926535)); 116 | expect(response.responseParam, 3.1415926535); 117 | }); 118 | }); 119 | 120 | group('ServeMe TCP data exchange tests', () { 121 | setUp(() async { 122 | timer = Timer(const Duration(seconds: 2), () => fail('Operation timed out')); 123 | server = ServeMe( 124 | type: ServeMeType.tcp, 125 | configFile: 'test/config_test.yaml', 126 | modules: >{ 127 | 'test': module = TestModule(), 128 | }, 129 | ); 130 | await server.run(); 131 | client = await ConnectMe.connect('127.0.0.1', port: 31337); 132 | await Future.delayed(const Duration(milliseconds: 100)); 133 | }); 134 | 135 | tearDown(() async { 136 | await client.close(); 137 | await server.stop(); 138 | timer.cancel(); 139 | }); 140 | 141 | test('Client sends String to server', () async { 142 | final Completer completer = Completer(); 143 | server.listen((Uint8List message, ConnectMeClient client) async { 144 | completer.complete(message); 145 | }); 146 | client.send('Test message from client'); 147 | final List expected = _utf8.encode('Test message from client'); 148 | expect(await completer.future, expected); 149 | }); 150 | 151 | test('Server broadcasts String to clients', () async { 152 | final Completer completer = Completer(); 153 | client.listen((Uint8List message) { 154 | completer.complete(message); 155 | }); 156 | server.broadcast('Test message from server'); 157 | final List expected = _utf8.encode('Test message from server'); 158 | expect(await completer.future, expected); 159 | }); 160 | 161 | test('Client sends Uint8List to server', () async { 162 | final Completer completer = Completer(); 163 | server.listen((Uint8List message, ConnectMeClient client) async { 164 | completer.complete(message); 165 | }); 166 | client.send(Uint8List.fromList([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5])); 167 | expect(await completer.future, Uint8List.fromList([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5])); 168 | }); 169 | 170 | test('Server broadcasts Uint8List to clients', () async { 171 | final Completer completer = Completer(); 172 | client.listen((Uint8List message) { 173 | completer.complete(message); 174 | }); 175 | server.broadcast(Uint8List.fromList([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5])); 176 | expect(await completer.future, Uint8List.fromList([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5])); 177 | }); 178 | 179 | test('Client sends TestResponse query to server', () async { 180 | server.register(testMessageFactory); 181 | server.listen((TestRequest request, ConnectMeClient client) async { 182 | client.send(request.$response(responseParam: request.requestParam)); 183 | }); 184 | client.register(testMessageFactory); 185 | final TestResponse response = await client.query(TestRequest(requestParam: 3.1415926535)); 186 | expect(response.responseParam, 3.1415926535); 187 | }); 188 | 189 | test('Server sends TestResponse query to client', () async { 190 | client.register(testMessageFactory); 191 | client.listen((TestRequest request) { 192 | client.send(request.$response(responseParam: request.requestParam)); 193 | }); 194 | server.register(testMessageFactory); 195 | final TestResponse response = await server.clients.first.query(TestRequest(requestParam: 3.1415926535)); 196 | expect(response.responseParam, 3.1415926535); 197 | }); 198 | }); 199 | 200 | group('ServeMe API tests', () { 201 | setUp(() async { 202 | timer = Timer(const Duration(seconds: 2), () => fail('Operation timed out')); 203 | server = ServeMe( 204 | configFile: 'test/config_test.yaml', 205 | modules: >{ 206 | 'test': module = TestModule(), 207 | }, 208 | ); 209 | await server.run(); 210 | }); 211 | 212 | tearDown(() async { 213 | await server.stop(); 214 | timer.cancel(); 215 | }); 216 | 217 | test('TickEvent dispatch and handle', () async { 218 | module.events.listen(expectAsync1, dynamic>((dynamic event) async { 219 | expect(event, isA()); 220 | })); 221 | }); 222 | 223 | test('Scheduler', () async { 224 | final Task task = Task(DateTime.now(), expectAsync1, dynamic>((dynamic time) async { 225 | expect(time, isA()); 226 | })); 227 | module.scheduler.schedule(task); 228 | }); 229 | }); 230 | } -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Specify analysis options. 2 | # 3 | # Until there are meta linter rules, each desired lint must be explicitly enabled. 4 | # See: https://github.com/dart-lang/linter/issues/288 5 | # 6 | # For a list of lints, see: http://dart-lang.github.io/linter/lints/ 7 | # See the configuration guide for more 8 | # https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer 9 | # 10 | # There are other similar analysis options files in the flutter repos, 11 | # which should be kept in sync with this file: 12 | # 13 | # - analysis_options.yaml (this file) 14 | # - https://github.com/flutter/plugins/blob/master/analysis_options.yaml 15 | # - https://github.com/flutter/engine/blob/master/analysis_options.yaml 16 | # - https://github.com/flutter/packages/blob/master/analysis_options.yaml 17 | # 18 | # This file contains the analysis options used by Flutter tools, such as IntelliJ, 19 | # Android Studio, and the `flutter analyze` command. 20 | 21 | analyzer: 22 | strong-mode: 23 | implicit-casts: false 24 | implicit-dynamic: false 25 | errors: 26 | # treat missing required parameters as a warning (not a hint) 27 | missing_required_param: warning 28 | # treat missing returns as a warning (not a hint) 29 | missing_return: warning 30 | # allow having TODOs in the code 31 | todo: ignore 32 | # allow self-reference to deprecated members (we do this because otherwise we have 33 | # to annotate every member in every test, assert, etc, when we deprecate something) 34 | deprecated_member_use_from_same_package: ignore 35 | # Ignore analyzer hints for updating pubspecs when using Future or 36 | # Stream and not importing dart:async 37 | # Please see https://github.com/flutter/flutter/pull/24528 for details. 38 | sdk_version_async_exported_from_core: ignore 39 | # Turned off until null-safe rollout is complete. 40 | unnecessary_null_comparison: ignore 41 | exclude: 42 | - "bin/cache/**" 43 | # Ignore protoc generated files 44 | - "dev/tools/lib/proto/*" 45 | 46 | linter: 47 | rules: 48 | # these rules are documented on and in the same order as 49 | # the Dart Lint rules page to make maintenance easier 50 | # https://github.com/dart-lang/linter/blob/master/example/all.yaml 51 | - always_declare_return_types 52 | # - always_put_control_body_on_new_line 53 | # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 54 | - always_require_non_null_named_parameters 55 | - always_specify_types 56 | # - always_use_package_imports # we do this commonly 57 | - annotate_overrides 58 | # - avoid_annotating_with_dynamic # conflicts with always_specify_types 59 | - avoid_bool_literals_in_conditional_expressions 60 | # - avoid_catches_without_on_clauses # we do this commonly 61 | # - avoid_catching_errors # we do this commonly 62 | - avoid_classes_with_only_static_members 63 | # - avoid_double_and_int_checks # only useful when targeting JS runtime 64 | # - avoid_dynamic_calls # not yet tested 65 | - avoid_empty_else 66 | - avoid_equals_and_hash_code_on_mutable_classes 67 | - avoid_escaping_inner_quotes 68 | - avoid_field_initializers_in_const_classes 69 | - avoid_function_literals_in_foreach_calls 70 | # - avoid_implementing_value_types # not yet tested 71 | - avoid_init_to_null 72 | # - avoid_js_rounded_ints # only useful when targeting JS runtime 73 | - avoid_null_checks_in_equality_operators 74 | # - avoid_positional_boolean_parameters # not yet tested 75 | # - avoid_print # not yet tested 76 | # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) 77 | # - avoid_redundant_argument_values # not yet tested 78 | - avoid_relative_lib_imports 79 | - avoid_renaming_method_parameters 80 | - avoid_return_types_on_setters 81 | # - avoid_returning_null # there are plenty of valid reasons to return null 82 | # - avoid_returning_null_for_future # not yet tested 83 | - avoid_returning_null_for_void 84 | # - avoid_returning_this # there are plenty of valid reasons to return this 85 | # - avoid_setters_without_getters # not yet tested 86 | - avoid_shadowing_type_parameters 87 | - avoid_single_cascade_in_expression_statements 88 | - avoid_slow_async_io 89 | - avoid_type_to_string 90 | - avoid_types_as_parameter_names 91 | # - avoid_types_on_closure_parameters # conflicts with always_specify_types 92 | - avoid_unnecessary_containers 93 | - avoid_unused_constructor_parameters 94 | - avoid_void_async 95 | # - avoid_web_libraries_in_flutter # not yet tested 96 | - await_only_futures 97 | - camel_case_extensions 98 | - camel_case_types 99 | - cancel_subscriptions 100 | # - cascade_invocations # not yet tested 101 | - cast_nullable_to_non_nullable 102 | # - close_sinks # not reliable enough 103 | # - comment_references # blocked on https://github.com/dart-lang/linter/issues/1142 104 | # - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204 105 | - control_flow_in_finally 106 | # - curly_braces_in_flow_control_structures # not required by flutter style 107 | - deprecated_consistency 108 | # - diagnostic_describe_all_properties # not yet tested 109 | - directives_ordering 110 | # - do_not_use_environment # we do this commonly 111 | - empty_catches 112 | - empty_constructor_bodies 113 | - empty_statements 114 | - exhaustive_cases 115 | - file_names 116 | - flutter_style_todos 117 | - hash_and_equals 118 | - implementation_imports 119 | # - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811 120 | - iterable_contains_unrelated_type 121 | # - join_return_with_assignment # not required by flutter style 122 | - leading_newlines_in_multiline_strings 123 | - library_names 124 | - library_prefixes 125 | # - lines_longer_than_80_chars # not required by flutter style 126 | - list_remove_unrelated_type 127 | # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/sdk/issues/34181 128 | - missing_whitespace_between_adjacent_strings 129 | - no_adjacent_strings_in_list 130 | # - no_default_cases # too many false positives 131 | - no_duplicate_case_values 132 | - no_logic_in_create_state 133 | # - no_runtimeType_toString # ok in tests; we enable this only in packages/ 134 | - non_constant_identifier_names 135 | - null_check_on_nullable_type_parameter 136 | - null_closures 137 | # - omit_local_variable_types # opposite of always_specify_types 138 | # - one_member_abstracts # too many false positives 139 | # - only_throw_errors # https://github.com/flutter/flutter/issues/5792 140 | - overridden_fields 141 | - package_api_docs 142 | - package_names 143 | - package_prefixed_library_names 144 | # - parameter_assignments # we do this commonly 145 | - prefer_adjacent_string_concatenation 146 | - prefer_asserts_in_initializer_lists 147 | # - prefer_asserts_with_message # not required by flutter style 148 | - prefer_collection_literals 149 | - prefer_conditional_assignment 150 | - prefer_const_constructors 151 | - prefer_const_constructors_in_immutables 152 | - prefer_const_declarations 153 | - prefer_const_literals_to_create_immutables 154 | # - prefer_constructors_over_static_methods # far too many false positives 155 | - prefer_contains 156 | # - prefer_double_quotes # opposite of prefer_single_quotes 157 | - prefer_equal_for_default_values 158 | # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods 159 | - prefer_final_fields 160 | - prefer_final_in_for_each 161 | - prefer_final_locals 162 | - prefer_for_elements_to_map_fromIterable 163 | - prefer_foreach 164 | - prefer_function_declarations_over_variables 165 | - prefer_generic_function_type_aliases 166 | - prefer_if_elements_to_conditional_expressions 167 | - prefer_if_null_operators 168 | - prefer_initializing_formals 169 | - prefer_inlined_adds 170 | # - prefer_int_literals # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#use-double-literals-for-double-constants 171 | # - prefer_interpolation_to_compose_strings # doesn't work with raw strings, see https://github.com/dart-lang/linter/issues/2490 172 | - prefer_is_empty 173 | - prefer_is_not_empty 174 | - prefer_is_not_operator 175 | - prefer_iterable_whereType 176 | # - prefer_mixin # https://github.com/dart-lang/language/issues/32 177 | - prefer_null_aware_operators 178 | # - prefer_relative_imports # incompatible with sub-package imports 179 | - prefer_single_quotes 180 | - prefer_spread_collections 181 | - prefer_typing_uninitialized_variables 182 | - prefer_void_to_null 183 | - provide_deprecation_message 184 | # - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml 185 | - recursive_getters 186 | - sized_box_for_whitespace 187 | - slash_for_doc_comments 188 | # - sort_child_properties_last # not yet tested 189 | - sort_constructors_first 190 | # - sort_pub_dependencies # prevents separating pinned transitive dependencies 191 | - sort_unnamed_constructors_first 192 | - test_types_in_equals 193 | - throw_in_finally 194 | - tighten_type_of_initializing_formals 195 | # - type_annotate_public_apis # subset of always_specify_types 196 | - type_init_formals 197 | # - unawaited_futures # too many false positives 198 | - unnecessary_await_in_return 199 | - unnecessary_brace_in_string_interps 200 | - unnecessary_const 201 | # - unnecessary_final # conflicts with prefer_final_locals 202 | - unnecessary_getters_setters 203 | # - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498 204 | - unnecessary_new 205 | - unnecessary_null_aware_assignments 206 | - unnecessary_null_checks 207 | - unnecessary_null_in_if_null_operators 208 | - unnecessary_nullable_for_final_variable_declarations 209 | - unnecessary_overrides 210 | - unnecessary_parenthesis 211 | # - unnecessary_raw_strings # not yet tested 212 | - unnecessary_statements 213 | - unnecessary_string_escapes 214 | - unnecessary_string_interpolations 215 | - unnecessary_this 216 | - unrelated_type_equality_checks 217 | # - unsafe_html # not yet tested 218 | - use_full_hex_values_for_flutter_colors 219 | - use_function_type_syntax_for_parameters 220 | # - use_if_null_to_convert_nulls_to_bools # not yet tested 221 | - use_is_even_rather_than_modulo 222 | - use_key_in_widget_constructors 223 | - use_late_for_private_fields_and_variables 224 | - use_named_constants 225 | - use_raw_strings 226 | - use_rethrow_when_possible 227 | # - use_setters_to_change_properties # not yet tested 228 | # - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182 229 | # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review 230 | - valid_regexps 231 | - void_checks -------------------------------------------------------------------------------- /example/generated/example-posts.generated.dart: -------------------------------------------------------------------------------- 1 | import 'package:packme/packme.dart'; 2 | 3 | class GetAllResponsePost extends PackMeMessage { 4 | GetAllResponsePost({ 5 | required this.id, 6 | required this.author, 7 | required this.title, 8 | required this.shortContent, 9 | required this.posted, 10 | }); 11 | GetAllResponsePost.$empty(); 12 | 13 | late List id; 14 | late GetAllResponsePostAuthor author; 15 | late String title; 16 | late String shortContent; 17 | late DateTime posted; 18 | 19 | @override 20 | int $estimate() { 21 | $reset(); 22 | int _bytes = 8; 23 | _bytes += 4 + id.length * 1; 24 | _bytes += author.$estimate(); 25 | _bytes += $stringBytes(title); 26 | _bytes += $stringBytes(shortContent); 27 | return _bytes; 28 | } 29 | 30 | @override 31 | void $pack() { 32 | $packUint32(id.length); 33 | for (int _i2 = 0; _i2 < id.length; _i2++) { 34 | $packUint8(id[_i2]); 35 | } 36 | $packMessage(author); 37 | $packString(title); 38 | $packString(shortContent); 39 | $packDateTime(posted); 40 | } 41 | 42 | @override 43 | void $unpack() { 44 | id = List.generate($unpackUint32(), (int i) { 45 | return $unpackUint8(); 46 | }); 47 | author = $unpackMessage(GetAllResponsePostAuthor.$empty()); 48 | title = $unpackString(); 49 | shortContent = $unpackString(); 50 | posted = $unpackDateTime(); 51 | } 52 | 53 | @override 54 | String toString() { 55 | return 'GetAllResponsePost\x1b[0m(id: ${PackMe.dye(id)}, author: ${PackMe.dye(author)}, title: ${PackMe.dye(title)}, shortContent: ${PackMe.dye(shortContent)}, posted: ${PackMe.dye(posted)})'; 56 | } 57 | } 58 | 59 | class GetAllResponsePostAuthor extends PackMeMessage { 60 | GetAllResponsePostAuthor({ 61 | required this.id, 62 | required this.nickname, 63 | required this.avatar, 64 | }); 65 | GetAllResponsePostAuthor.$empty(); 66 | 67 | late List id; 68 | late String nickname; 69 | late String avatar; 70 | 71 | @override 72 | int $estimate() { 73 | $reset(); 74 | int _bytes = 0; 75 | _bytes += 4 + id.length * 1; 76 | _bytes += $stringBytes(nickname); 77 | _bytes += $stringBytes(avatar); 78 | return _bytes; 79 | } 80 | 81 | @override 82 | void $pack() { 83 | $packUint32(id.length); 84 | for (int _i2 = 0; _i2 < id.length; _i2++) { 85 | $packUint8(id[_i2]); 86 | } 87 | $packString(nickname); 88 | $packString(avatar); 89 | } 90 | 91 | @override 92 | void $unpack() { 93 | id = List.generate($unpackUint32(), (int i) { 94 | return $unpackUint8(); 95 | }); 96 | nickname = $unpackString(); 97 | avatar = $unpackString(); 98 | } 99 | 100 | @override 101 | String toString() { 102 | return 'GetAllResponsePostAuthor\x1b[0m(id: ${PackMe.dye(id)}, nickname: ${PackMe.dye(nickname)}, avatar: ${PackMe.dye(avatar)})'; 103 | } 104 | } 105 | 106 | class GetResponseAuthor extends PackMeMessage { 107 | GetResponseAuthor({ 108 | required this.id, 109 | required this.nickname, 110 | required this.avatar, 111 | this.facebookId, 112 | this.twitterId, 113 | this.instagramId, 114 | }); 115 | GetResponseAuthor.$empty(); 116 | 117 | late List id; 118 | late String nickname; 119 | late String avatar; 120 | String? facebookId; 121 | String? twitterId; 122 | String? instagramId; 123 | 124 | @override 125 | int $estimate() { 126 | $reset(); 127 | int _bytes = 1; 128 | _bytes += 4 + id.length * 1; 129 | _bytes += $stringBytes(nickname); 130 | _bytes += $stringBytes(avatar); 131 | $setFlag(facebookId != null); 132 | if (facebookId != null) _bytes += $stringBytes(facebookId!); 133 | $setFlag(twitterId != null); 134 | if (twitterId != null) _bytes += $stringBytes(twitterId!); 135 | $setFlag(instagramId != null); 136 | if (instagramId != null) _bytes += $stringBytes(instagramId!); 137 | return _bytes; 138 | } 139 | 140 | @override 141 | void $pack() { 142 | for (int i = 0; i < 1; i++) $packUint8($flags[i]); 143 | $packUint32(id.length); 144 | for (int _i2 = 0; _i2 < id.length; _i2++) { 145 | $packUint8(id[_i2]); 146 | } 147 | $packString(nickname); 148 | $packString(avatar); 149 | if (facebookId != null) $packString(facebookId!); 150 | if (twitterId != null) $packString(twitterId!); 151 | if (instagramId != null) $packString(instagramId!); 152 | } 153 | 154 | @override 155 | void $unpack() { 156 | for (int i = 0; i < 1; i++) $flags.add($unpackUint8()); 157 | id = List.generate($unpackUint32(), (int i) { 158 | return $unpackUint8(); 159 | }); 160 | nickname = $unpackString(); 161 | avatar = $unpackString(); 162 | if ($getFlag()) facebookId = $unpackString(); 163 | if ($getFlag()) twitterId = $unpackString(); 164 | if ($getFlag()) instagramId = $unpackString(); 165 | } 166 | 167 | @override 168 | String toString() { 169 | return 'GetResponseAuthor\x1b[0m(id: ${PackMe.dye(id)}, nickname: ${PackMe.dye(nickname)}, avatar: ${PackMe.dye(avatar)}, facebookId: ${PackMe.dye(facebookId)}, twitterId: ${PackMe.dye(twitterId)}, instagramId: ${PackMe.dye(instagramId)})'; 170 | } 171 | } 172 | 173 | class GetResponseComment extends PackMeMessage { 174 | GetResponseComment({ 175 | required this.author, 176 | required this.comment, 177 | required this.posted, 178 | }); 179 | GetResponseComment.$empty(); 180 | 181 | late GetResponseCommentAuthor author; 182 | late String comment; 183 | late DateTime posted; 184 | 185 | @override 186 | int $estimate() { 187 | $reset(); 188 | int _bytes = 8; 189 | _bytes += author.$estimate(); 190 | _bytes += $stringBytes(comment); 191 | return _bytes; 192 | } 193 | 194 | @override 195 | void $pack() { 196 | $packMessage(author); 197 | $packString(comment); 198 | $packDateTime(posted); 199 | } 200 | 201 | @override 202 | void $unpack() { 203 | author = $unpackMessage(GetResponseCommentAuthor.$empty()); 204 | comment = $unpackString(); 205 | posted = $unpackDateTime(); 206 | } 207 | 208 | @override 209 | String toString() { 210 | return 'GetResponseComment\x1b[0m(author: ${PackMe.dye(author)}, comment: ${PackMe.dye(comment)}, posted: ${PackMe.dye(posted)})'; 211 | } 212 | } 213 | 214 | class GetResponseCommentAuthor extends PackMeMessage { 215 | GetResponseCommentAuthor({ 216 | required this.id, 217 | required this.nickname, 218 | required this.avatar, 219 | }); 220 | GetResponseCommentAuthor.$empty(); 221 | 222 | late List id; 223 | late String nickname; 224 | late String avatar; 225 | 226 | @override 227 | int $estimate() { 228 | $reset(); 229 | int _bytes = 0; 230 | _bytes += 4 + id.length * 1; 231 | _bytes += $stringBytes(nickname); 232 | _bytes += $stringBytes(avatar); 233 | return _bytes; 234 | } 235 | 236 | @override 237 | void $pack() { 238 | $packUint32(id.length); 239 | for (int _i2 = 0; _i2 < id.length; _i2++) { 240 | $packUint8(id[_i2]); 241 | } 242 | $packString(nickname); 243 | $packString(avatar); 244 | } 245 | 246 | @override 247 | void $unpack() { 248 | id = List.generate($unpackUint32(), (int i) { 249 | return $unpackUint8(); 250 | }); 251 | nickname = $unpackString(); 252 | avatar = $unpackString(); 253 | } 254 | 255 | @override 256 | String toString() { 257 | return 'GetResponseCommentAuthor\x1b[0m(id: ${PackMe.dye(id)}, nickname: ${PackMe.dye(nickname)}, avatar: ${PackMe.dye(avatar)})'; 258 | } 259 | } 260 | 261 | class GetResponseStats extends PackMeMessage { 262 | GetResponseStats({ 263 | required this.likes, 264 | required this.dislikes, 265 | }); 266 | GetResponseStats.$empty(); 267 | 268 | late int likes; 269 | late int dislikes; 270 | 271 | @override 272 | int $estimate() { 273 | $reset(); 274 | return 8; 275 | } 276 | 277 | @override 278 | void $pack() { 279 | $packUint32(likes); 280 | $packUint32(dislikes); 281 | } 282 | 283 | @override 284 | void $unpack() { 285 | likes = $unpackUint32(); 286 | dislikes = $unpackUint32(); 287 | } 288 | 289 | @override 290 | String toString() { 291 | return 'GetResponseStats\x1b[0m(likes: ${PackMe.dye(likes)}, dislikes: ${PackMe.dye(dislikes)})'; 292 | } 293 | } 294 | 295 | class GetAllRequest extends PackMeMessage { 296 | GetAllRequest(); 297 | GetAllRequest.$empty(); 298 | 299 | 300 | GetAllResponse $response({ 301 | required List posts, 302 | }) { 303 | final GetAllResponse message = GetAllResponse(posts: posts); 304 | message.$request = this; 305 | return message; 306 | } 307 | 308 | @override 309 | int $estimate() { 310 | $reset(); 311 | return 8; 312 | } 313 | 314 | @override 315 | void $pack() { 316 | $initPack(63570112); 317 | } 318 | 319 | @override 320 | void $unpack() { 321 | $initUnpack(); 322 | } 323 | 324 | @override 325 | String toString() { 326 | return 'GetAllRequest\x1b[0m()'; 327 | } 328 | } 329 | 330 | class GetAllResponse extends PackMeMessage { 331 | GetAllResponse({ 332 | required this.posts, 333 | }); 334 | GetAllResponse.$empty(); 335 | 336 | late List posts; 337 | 338 | @override 339 | int $estimate() { 340 | $reset(); 341 | int _bytes = 8; 342 | _bytes += 4 + posts.fold(0, (int a, GetAllResponsePost b) => a + b.$estimate()); 343 | return _bytes; 344 | } 345 | 346 | @override 347 | void $pack() { 348 | $initPack(280110613); 349 | $packUint32(posts.length); 350 | for (int _i5 = 0; _i5 < posts.length; _i5++) { 351 | $packMessage(posts[_i5]); 352 | } 353 | } 354 | 355 | @override 356 | void $unpack() { 357 | $initUnpack(); 358 | posts = List.generate($unpackUint32(), (int i) { 359 | return $unpackMessage(GetAllResponsePost.$empty()); 360 | }); 361 | } 362 | 363 | @override 364 | String toString() { 365 | return 'GetAllResponse\x1b[0m(posts: ${PackMe.dye(posts)})'; 366 | } 367 | } 368 | 369 | class GetRequest extends PackMeMessage { 370 | GetRequest({ 371 | required this.postId, 372 | }); 373 | GetRequest.$empty(); 374 | 375 | late List postId; 376 | 377 | GetResponse $response({ 378 | required String title, 379 | required String content, 380 | required DateTime posted, 381 | required GetResponseAuthor author, 382 | required GetResponseStats stats, 383 | required List comments, 384 | }) { 385 | final GetResponse message = GetResponse(title: title, content: content, posted: posted, author: author, stats: stats, comments: comments); 386 | message.$request = this; 387 | return message; 388 | } 389 | 390 | @override 391 | int $estimate() { 392 | $reset(); 393 | int _bytes = 8; 394 | _bytes += 4 + postId.length * 1; 395 | return _bytes; 396 | } 397 | 398 | @override 399 | void $pack() { 400 | $initPack(187698222); 401 | $packUint32(postId.length); 402 | for (int _i6 = 0; _i6 < postId.length; _i6++) { 403 | $packUint8(postId[_i6]); 404 | } 405 | } 406 | 407 | @override 408 | void $unpack() { 409 | $initUnpack(); 410 | postId = List.generate($unpackUint32(), (int i) { 411 | return $unpackUint8(); 412 | }); 413 | } 414 | 415 | @override 416 | String toString() { 417 | return 'GetRequest\x1b[0m(postId: ${PackMe.dye(postId)})'; 418 | } 419 | } 420 | 421 | class GetResponse extends PackMeMessage { 422 | GetResponse({ 423 | required this.title, 424 | required this.content, 425 | required this.posted, 426 | required this.author, 427 | required this.stats, 428 | required this.comments, 429 | }); 430 | GetResponse.$empty(); 431 | 432 | late String title; 433 | late String content; 434 | late DateTime posted; 435 | late GetResponseAuthor author; 436 | late GetResponseStats stats; 437 | late List comments; 438 | 439 | @override 440 | int $estimate() { 441 | $reset(); 442 | int _bytes = 16; 443 | _bytes += $stringBytes(title); 444 | _bytes += $stringBytes(content); 445 | _bytes += author.$estimate(); 446 | _bytes += stats.$estimate(); 447 | _bytes += 4 + comments.fold(0, (int a, GetResponseComment b) => a + b.$estimate()); 448 | return _bytes; 449 | } 450 | 451 | @override 452 | void $pack() { 453 | $initPack(244485545); 454 | $packString(title); 455 | $packString(content); 456 | $packDateTime(posted); 457 | $packMessage(author); 458 | $packMessage(stats); 459 | $packUint32(comments.length); 460 | for (int _i8 = 0; _i8 < comments.length; _i8++) { 461 | $packMessage(comments[_i8]); 462 | } 463 | } 464 | 465 | @override 466 | void $unpack() { 467 | $initUnpack(); 468 | title = $unpackString(); 469 | content = $unpackString(); 470 | posted = $unpackDateTime(); 471 | author = $unpackMessage(GetResponseAuthor.$empty()); 472 | stats = $unpackMessage(GetResponseStats.$empty()); 473 | comments = List.generate($unpackUint32(), (int i) { 474 | return $unpackMessage(GetResponseComment.$empty()); 475 | }); 476 | } 477 | 478 | @override 479 | String toString() { 480 | return 'GetResponse\x1b[0m(title: ${PackMe.dye(title)}, content: ${PackMe.dye(content)}, posted: ${PackMe.dye(posted)}, author: ${PackMe.dye(author)}, stats: ${PackMe.dye(stats)}, comments: ${PackMe.dye(comments)})'; 481 | } 482 | } 483 | 484 | class DeleteRequest extends PackMeMessage { 485 | DeleteRequest({ 486 | required this.postId, 487 | }); 488 | DeleteRequest.$empty(); 489 | 490 | late List postId; 491 | 492 | DeleteResponse $response({ 493 | String? error, 494 | }) { 495 | final DeleteResponse message = DeleteResponse(error: error); 496 | message.$request = this; 497 | return message; 498 | } 499 | 500 | @override 501 | int $estimate() { 502 | $reset(); 503 | int _bytes = 8; 504 | _bytes += 4 + postId.length * 1; 505 | return _bytes; 506 | } 507 | 508 | @override 509 | void $pack() { 510 | $initPack(486637631); 511 | $packUint32(postId.length); 512 | for (int _i6 = 0; _i6 < postId.length; _i6++) { 513 | $packUint8(postId[_i6]); 514 | } 515 | } 516 | 517 | @override 518 | void $unpack() { 519 | $initUnpack(); 520 | postId = List.generate($unpackUint32(), (int i) { 521 | return $unpackUint8(); 522 | }); 523 | } 524 | 525 | @override 526 | String toString() { 527 | return 'DeleteRequest\x1b[0m(postId: ${PackMe.dye(postId)})'; 528 | } 529 | } 530 | 531 | class DeleteResponse extends PackMeMessage { 532 | DeleteResponse({ 533 | this.error, 534 | }); 535 | DeleteResponse.$empty(); 536 | 537 | String? error; 538 | 539 | @override 540 | int $estimate() { 541 | $reset(); 542 | int _bytes = 9; 543 | $setFlag(error != null); 544 | if (error != null) _bytes += $stringBytes(error!); 545 | return _bytes; 546 | } 547 | 548 | @override 549 | void $pack() { 550 | $initPack(788388804); 551 | for (int i = 0; i < 1; i++) $packUint8($flags[i]); 552 | if (error != null) $packString(error!); 553 | } 554 | 555 | @override 556 | void $unpack() { 557 | $initUnpack(); 558 | for (int i = 0; i < 1; i++) $flags.add($unpackUint8()); 559 | if ($getFlag()) error = $unpackString(); 560 | } 561 | 562 | @override 563 | String toString() { 564 | return 'DeleteResponse\x1b[0m(error: ${PackMe.dye(error)})'; 565 | } 566 | } 567 | 568 | final Map examplePostsMessageFactory = { 569 | 63570112: () => GetAllRequest.$empty(), 570 | 187698222: () => GetRequest.$empty(), 571 | 486637631: () => DeleteRequest.$empty(), 572 | 280110613: () => GetAllResponse.$empty(), 573 | 244485545: () => GetResponse.$empty(), 574 | 788388804: () => DeleteResponse.$empty(), 575 | }; -------------------------------------------------------------------------------- /example/generated/example-users.generated.dart: -------------------------------------------------------------------------------- 1 | import 'package:packme/packme.dart'; 2 | 3 | class GetAllResponseUser extends PackMeMessage { 4 | GetAllResponseUser({ 5 | required this.id, 6 | required this.nickname, 7 | this.firstName, 8 | this.lastName, 9 | this.age, 10 | }); 11 | GetAllResponseUser.$empty(); 12 | 13 | late List id; 14 | late String nickname; 15 | String? firstName; 16 | String? lastName; 17 | int? age; 18 | 19 | @override 20 | int $estimate() { 21 | $reset(); 22 | int _bytes = 1; 23 | _bytes += 4 + id.length * 1; 24 | _bytes += $stringBytes(nickname); 25 | $setFlag(firstName != null); 26 | if (firstName != null) _bytes += $stringBytes(firstName!); 27 | $setFlag(lastName != null); 28 | if (lastName != null) _bytes += $stringBytes(lastName!); 29 | $setFlag(age != null); 30 | if (age != null) _bytes += 1; 31 | return _bytes; 32 | } 33 | 34 | @override 35 | void $pack() { 36 | for (int i = 0; i < 1; i++) $packUint8($flags[i]); 37 | $packUint32(id.length); 38 | for (int _i2 = 0; _i2 < id.length; _i2++) { 39 | $packUint8(id[_i2]); 40 | } 41 | $packString(nickname); 42 | if (firstName != null) $packString(firstName!); 43 | if (lastName != null) $packString(lastName!); 44 | if (age != null) $packUint8(age!); 45 | } 46 | 47 | @override 48 | void $unpack() { 49 | for (int i = 0; i < 1; i++) $flags.add($unpackUint8()); 50 | id = List.generate($unpackUint32(), (int i) { 51 | return $unpackUint8(); 52 | }); 53 | nickname = $unpackString(); 54 | if ($getFlag()) firstName = $unpackString(); 55 | if ($getFlag()) lastName = $unpackString(); 56 | if ($getFlag()) age = $unpackUint8(); 57 | } 58 | 59 | @override 60 | String toString() { 61 | return 'GetAllResponseUser\x1b[0m(id: ${PackMe.dye(id)}, nickname: ${PackMe.dye(nickname)}, firstName: ${PackMe.dye(firstName)}, lastName: ${PackMe.dye(lastName)}, age: ${PackMe.dye(age)})'; 62 | } 63 | } 64 | 65 | class GetResponseInfo extends PackMeMessage { 66 | GetResponseInfo({ 67 | this.firstName, 68 | this.lastName, 69 | this.male, 70 | this.age, 71 | this.birthDate, 72 | }); 73 | GetResponseInfo.$empty(); 74 | 75 | String? firstName; 76 | String? lastName; 77 | int? male; 78 | int? age; 79 | DateTime? birthDate; 80 | 81 | @override 82 | int $estimate() { 83 | $reset(); 84 | int _bytes = 1; 85 | $setFlag(firstName != null); 86 | if (firstName != null) _bytes += $stringBytes(firstName!); 87 | $setFlag(lastName != null); 88 | if (lastName != null) _bytes += $stringBytes(lastName!); 89 | $setFlag(male != null); 90 | if (male != null) _bytes += 1; 91 | $setFlag(age != null); 92 | if (age != null) _bytes += 1; 93 | $setFlag(birthDate != null); 94 | if (birthDate != null) _bytes += 8; 95 | return _bytes; 96 | } 97 | 98 | @override 99 | void $pack() { 100 | for (int i = 0; i < 1; i++) $packUint8($flags[i]); 101 | if (firstName != null) $packString(firstName!); 102 | if (lastName != null) $packString(lastName!); 103 | if (male != null) $packUint8(male!); 104 | if (age != null) $packUint8(age!); 105 | if (birthDate != null) $packDateTime(birthDate!); 106 | } 107 | 108 | @override 109 | void $unpack() { 110 | for (int i = 0; i < 1; i++) $flags.add($unpackUint8()); 111 | if ($getFlag()) firstName = $unpackString(); 112 | if ($getFlag()) lastName = $unpackString(); 113 | if ($getFlag()) male = $unpackUint8(); 114 | if ($getFlag()) age = $unpackUint8(); 115 | if ($getFlag()) birthDate = $unpackDateTime(); 116 | } 117 | 118 | @override 119 | String toString() { 120 | return 'GetResponseInfo\x1b[0m(firstName: ${PackMe.dye(firstName)}, lastName: ${PackMe.dye(lastName)}, male: ${PackMe.dye(male)}, age: ${PackMe.dye(age)}, birthDate: ${PackMe.dye(birthDate)})'; 121 | } 122 | } 123 | 124 | class GetResponseLastActive extends PackMeMessage { 125 | GetResponseLastActive({ 126 | required this.datetime, 127 | required this.ip, 128 | }); 129 | GetResponseLastActive.$empty(); 130 | 131 | late DateTime datetime; 132 | late String ip; 133 | 134 | @override 135 | int $estimate() { 136 | $reset(); 137 | int _bytes = 8; 138 | _bytes += $stringBytes(ip); 139 | return _bytes; 140 | } 141 | 142 | @override 143 | void $pack() { 144 | $packDateTime(datetime); 145 | $packString(ip); 146 | } 147 | 148 | @override 149 | void $unpack() { 150 | datetime = $unpackDateTime(); 151 | ip = $unpackString(); 152 | } 153 | 154 | @override 155 | String toString() { 156 | return 'GetResponseLastActive\x1b[0m(datetime: ${PackMe.dye(datetime)}, ip: ${PackMe.dye(ip)})'; 157 | } 158 | } 159 | 160 | class GetResponseSession extends PackMeMessage { 161 | GetResponseSession({ 162 | required this.created, 163 | required this.ip, 164 | required this.active, 165 | }); 166 | GetResponseSession.$empty(); 167 | 168 | late DateTime created; 169 | late String ip; 170 | late bool active; 171 | 172 | @override 173 | int $estimate() { 174 | $reset(); 175 | int _bytes = 9; 176 | _bytes += $stringBytes(ip); 177 | return _bytes; 178 | } 179 | 180 | @override 181 | void $pack() { 182 | $packDateTime(created); 183 | $packString(ip); 184 | $packBool(active); 185 | } 186 | 187 | @override 188 | void $unpack() { 189 | created = $unpackDateTime(); 190 | ip = $unpackString(); 191 | active = $unpackBool(); 192 | } 193 | 194 | @override 195 | String toString() { 196 | return 'GetResponseSession\x1b[0m(created: ${PackMe.dye(created)}, ip: ${PackMe.dye(ip)}, active: ${PackMe.dye(active)})'; 197 | } 198 | } 199 | 200 | class GetResponseSocial extends PackMeMessage { 201 | GetResponseSocial({ 202 | this.facebookId, 203 | this.twitterId, 204 | this.instagramId, 205 | }); 206 | GetResponseSocial.$empty(); 207 | 208 | String? facebookId; 209 | String? twitterId; 210 | String? instagramId; 211 | 212 | @override 213 | int $estimate() { 214 | $reset(); 215 | int _bytes = 1; 216 | $setFlag(facebookId != null); 217 | if (facebookId != null) _bytes += $stringBytes(facebookId!); 218 | $setFlag(twitterId != null); 219 | if (twitterId != null) _bytes += $stringBytes(twitterId!); 220 | $setFlag(instagramId != null); 221 | if (instagramId != null) _bytes += $stringBytes(instagramId!); 222 | return _bytes; 223 | } 224 | 225 | @override 226 | void $pack() { 227 | for (int i = 0; i < 1; i++) $packUint8($flags[i]); 228 | if (facebookId != null) $packString(facebookId!); 229 | if (twitterId != null) $packString(twitterId!); 230 | if (instagramId != null) $packString(instagramId!); 231 | } 232 | 233 | @override 234 | void $unpack() { 235 | for (int i = 0; i < 1; i++) $flags.add($unpackUint8()); 236 | if ($getFlag()) facebookId = $unpackString(); 237 | if ($getFlag()) twitterId = $unpackString(); 238 | if ($getFlag()) instagramId = $unpackString(); 239 | } 240 | 241 | @override 242 | String toString() { 243 | return 'GetResponseSocial\x1b[0m(facebookId: ${PackMe.dye(facebookId)}, twitterId: ${PackMe.dye(twitterId)}, instagramId: ${PackMe.dye(instagramId)})'; 244 | } 245 | } 246 | 247 | class GetResponseStats extends PackMeMessage { 248 | GetResponseStats({ 249 | required this.posts, 250 | required this.comments, 251 | required this.likes, 252 | required this.dislikes, 253 | required this.rating, 254 | }); 255 | GetResponseStats.$empty(); 256 | 257 | late int posts; 258 | late int comments; 259 | late int likes; 260 | late int dislikes; 261 | late double rating; 262 | 263 | @override 264 | int $estimate() { 265 | $reset(); 266 | return 20; 267 | } 268 | 269 | @override 270 | void $pack() { 271 | $packUint32(posts); 272 | $packUint32(comments); 273 | $packUint32(likes); 274 | $packUint32(dislikes); 275 | $packFloat(rating); 276 | } 277 | 278 | @override 279 | void $unpack() { 280 | posts = $unpackUint32(); 281 | comments = $unpackUint32(); 282 | likes = $unpackUint32(); 283 | dislikes = $unpackUint32(); 284 | rating = $unpackFloat(); 285 | } 286 | 287 | @override 288 | String toString() { 289 | return 'GetResponseStats\x1b[0m(posts: ${PackMe.dye(posts)}, comments: ${PackMe.dye(comments)}, likes: ${PackMe.dye(likes)}, dislikes: ${PackMe.dye(dislikes)}, rating: ${PackMe.dye(rating)})'; 290 | } 291 | } 292 | 293 | class GetAllRequest extends PackMeMessage { 294 | GetAllRequest(); 295 | GetAllRequest.$empty(); 296 | 297 | 298 | GetAllResponse $response({ 299 | required List users, 300 | }) { 301 | final GetAllResponse message = GetAllResponse(users: users); 302 | message.$request = this; 303 | return message; 304 | } 305 | 306 | @override 307 | int $estimate() { 308 | $reset(); 309 | return 8; 310 | } 311 | 312 | @override 313 | void $pack() { 314 | $initPack(12982278); 315 | } 316 | 317 | @override 318 | void $unpack() { 319 | $initUnpack(); 320 | } 321 | 322 | @override 323 | String toString() { 324 | return 'GetAllRequest\x1b[0m()'; 325 | } 326 | } 327 | 328 | class GetAllResponse extends PackMeMessage { 329 | GetAllResponse({ 330 | required this.users, 331 | }); 332 | GetAllResponse.$empty(); 333 | 334 | late List users; 335 | 336 | @override 337 | int $estimate() { 338 | $reset(); 339 | int _bytes = 8; 340 | _bytes += 4 + users.fold(0, (int a, GetAllResponseUser b) => a + b.$estimate()); 341 | return _bytes; 342 | } 343 | 344 | @override 345 | void $pack() { 346 | $initPack(242206268); 347 | $packUint32(users.length); 348 | for (int _i5 = 0; _i5 < users.length; _i5++) { 349 | $packMessage(users[_i5]); 350 | } 351 | } 352 | 353 | @override 354 | void $unpack() { 355 | $initUnpack(); 356 | users = List.generate($unpackUint32(), (int i) { 357 | return $unpackMessage(GetAllResponseUser.$empty()); 358 | }); 359 | } 360 | 361 | @override 362 | String toString() { 363 | return 'GetAllResponse\x1b[0m(users: ${PackMe.dye(users)})'; 364 | } 365 | } 366 | 367 | class GetRequest extends PackMeMessage { 368 | GetRequest({ 369 | required this.userId, 370 | }); 371 | GetRequest.$empty(); 372 | 373 | late List userId; 374 | 375 | GetResponse $response({ 376 | required String email, 377 | required String nickname, 378 | required bool hidden, 379 | required DateTime created, 380 | required GetResponseInfo info, 381 | required GetResponseSocial social, 382 | required GetResponseStats stats, 383 | GetResponseLastActive? lastActive, 384 | required List sessions, 385 | }) { 386 | final GetResponse message = GetResponse(email: email, nickname: nickname, hidden: hidden, created: created, info: info, social: social, stats: stats, lastActive: lastActive, sessions: sessions); 387 | message.$request = this; 388 | return message; 389 | } 390 | 391 | @override 392 | int $estimate() { 393 | $reset(); 394 | int _bytes = 8; 395 | _bytes += 4 + userId.length * 1; 396 | return _bytes; 397 | } 398 | 399 | @override 400 | void $pack() { 401 | $initPack(781905656); 402 | $packUint32(userId.length); 403 | for (int _i6 = 0; _i6 < userId.length; _i6++) { 404 | $packUint8(userId[_i6]); 405 | } 406 | } 407 | 408 | @override 409 | void $unpack() { 410 | $initUnpack(); 411 | userId = List.generate($unpackUint32(), (int i) { 412 | return $unpackUint8(); 413 | }); 414 | } 415 | 416 | @override 417 | String toString() { 418 | return 'GetRequest\x1b[0m(userId: ${PackMe.dye(userId)})'; 419 | } 420 | } 421 | 422 | class GetResponse extends PackMeMessage { 423 | GetResponse({ 424 | required this.email, 425 | required this.nickname, 426 | required this.hidden, 427 | required this.created, 428 | required this.info, 429 | required this.social, 430 | required this.stats, 431 | this.lastActive, 432 | required this.sessions, 433 | }); 434 | GetResponse.$empty(); 435 | 436 | late String email; 437 | late String nickname; 438 | late bool hidden; 439 | late DateTime created; 440 | late GetResponseInfo info; 441 | late GetResponseSocial social; 442 | late GetResponseStats stats; 443 | GetResponseLastActive? lastActive; 444 | late List sessions; 445 | 446 | @override 447 | int $estimate() { 448 | $reset(); 449 | int _bytes = 18; 450 | _bytes += $stringBytes(email); 451 | _bytes += $stringBytes(nickname); 452 | _bytes += info.$estimate(); 453 | _bytes += social.$estimate(); 454 | _bytes += stats.$estimate(); 455 | $setFlag(lastActive != null); 456 | if (lastActive != null) _bytes += lastActive!.$estimate(); 457 | _bytes += 4 + sessions.fold(0, (int a, GetResponseSession b) => a + b.$estimate()); 458 | return _bytes; 459 | } 460 | 461 | @override 462 | void $pack() { 463 | $initPack(430536944); 464 | for (int i = 0; i < 1; i++) $packUint8($flags[i]); 465 | $packString(email); 466 | $packString(nickname); 467 | $packBool(hidden); 468 | $packDateTime(created); 469 | $packMessage(info); 470 | $packMessage(social); 471 | $packMessage(stats); 472 | if (lastActive != null) $packMessage(lastActive!); 473 | $packUint32(sessions.length); 474 | for (int _i8 = 0; _i8 < sessions.length; _i8++) { 475 | $packMessage(sessions[_i8]); 476 | } 477 | } 478 | 479 | @override 480 | void $unpack() { 481 | $initUnpack(); 482 | for (int i = 0; i < 1; i++) $flags.add($unpackUint8()); 483 | email = $unpackString(); 484 | nickname = $unpackString(); 485 | hidden = $unpackBool(); 486 | created = $unpackDateTime(); 487 | info = $unpackMessage(GetResponseInfo.$empty()); 488 | social = $unpackMessage(GetResponseSocial.$empty()); 489 | stats = $unpackMessage(GetResponseStats.$empty()); 490 | if ($getFlag()) lastActive = $unpackMessage(GetResponseLastActive.$empty()); 491 | sessions = List.generate($unpackUint32(), (int i) { 492 | return $unpackMessage(GetResponseSession.$empty()); 493 | }); 494 | } 495 | 496 | @override 497 | String toString() { 498 | return 'GetResponse\x1b[0m(email: ${PackMe.dye(email)}, nickname: ${PackMe.dye(nickname)}, hidden: ${PackMe.dye(hidden)}, created: ${PackMe.dye(created)}, info: ${PackMe.dye(info)}, social: ${PackMe.dye(social)}, stats: ${PackMe.dye(stats)}, lastActive: ${PackMe.dye(lastActive)}, sessions: ${PackMe.dye(sessions)})'; 499 | } 500 | } 501 | 502 | class DeleteRequest extends PackMeMessage { 503 | DeleteRequest({ 504 | required this.userId, 505 | }); 506 | DeleteRequest.$empty(); 507 | 508 | late List userId; 509 | 510 | DeleteResponse $response({ 511 | String? error, 512 | }) { 513 | final DeleteResponse message = DeleteResponse(error: error); 514 | message.$request = this; 515 | return message; 516 | } 517 | 518 | @override 519 | int $estimate() { 520 | $reset(); 521 | int _bytes = 8; 522 | _bytes += 4 + userId.length * 1; 523 | return _bytes; 524 | } 525 | 526 | @override 527 | void $pack() { 528 | $initPack(808423104); 529 | $packUint32(userId.length); 530 | for (int _i6 = 0; _i6 < userId.length; _i6++) { 531 | $packUint8(userId[_i6]); 532 | } 533 | } 534 | 535 | @override 536 | void $unpack() { 537 | $initUnpack(); 538 | userId = List.generate($unpackUint32(), (int i) { 539 | return $unpackUint8(); 540 | }); 541 | } 542 | 543 | @override 544 | String toString() { 545 | return 'DeleteRequest\x1b[0m(userId: ${PackMe.dye(userId)})'; 546 | } 547 | } 548 | 549 | class DeleteResponse extends PackMeMessage { 550 | DeleteResponse({ 551 | this.error, 552 | }); 553 | DeleteResponse.$empty(); 554 | 555 | String? error; 556 | 557 | @override 558 | int $estimate() { 559 | $reset(); 560 | int _bytes = 9; 561 | $setFlag(error != null); 562 | if (error != null) _bytes += $stringBytes(error!); 563 | return _bytes; 564 | } 565 | 566 | @override 567 | void $pack() { 568 | $initPack(69897231); 569 | for (int i = 0; i < 1; i++) $packUint8($flags[i]); 570 | if (error != null) $packString(error!); 571 | } 572 | 573 | @override 574 | void $unpack() { 575 | $initUnpack(); 576 | for (int i = 0; i < 1; i++) $flags.add($unpackUint8()); 577 | if ($getFlag()) error = $unpackString(); 578 | } 579 | 580 | @override 581 | String toString() { 582 | return 'DeleteResponse\x1b[0m(error: ${PackMe.dye(error)})'; 583 | } 584 | } 585 | 586 | final Map exampleUsersMessageFactory = { 587 | 12982278: () => GetAllRequest.$empty(), 588 | 781905656: () => GetRequest.$empty(), 589 | 808423104: () => DeleteRequest.$empty(), 590 | 242206268: () => GetAllResponse.$empty(), 591 | 430536944: () => GetResponse.$empty(), 592 | 69897231: () => DeleteResponse.$empty(), 593 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## What is ServeMe 2 | ServeMe is a simple and powerful modular server framework. It allows to easily create backend services for both mobile and web applications. Here are some of the features provided by ServeMe framework: 3 | * modular architecture allows to easily implement separate parts of the server using ServeMe Modular API; 4 | * both WebSockets and TCP sockets are supported (TCP sockets support implemented in v1.1.0); 5 | * MongoDB support out of the box, automatic database integrity validation for easy server deployment; 6 | * events API allows to dispatch and listen to any built-in or custom events in your application; 7 | * scheduler API allows to create different tasks and schedule its' execution time and period; 8 | * logging, debug and error handling tools; 9 | * console API enables to handle custom server console commands (with autocomplete, command line format validation, command info etc.); 10 | * using build-in or custom configuration files using Config API; 11 | * client connections management, broadcasting messages by criteria, listening to data from clients globally or individually; 12 | * it's integrated with [PackMe](https://pub.dev/packages/packme) binary serialization library for data transfer: it is very fast; 13 | * possibility to implement complex data transfer protocols using JSON (compiled to [PackMe](https://pub.dev/packages/packme) messages .dart files); 14 | * support of different message data types: String, Uint8List or [PackMe](https://pub.dev/packages/packme) messages; 15 | * asynchronously query data using [PackMe](https://pub.dev/packages/packme) messages: SomeResponse response = await client.query(SomeRequest()); 16 | 17 | ## Usage 18 | Here the simplest example code of a server application based on ServeMe: 19 | ```dart 20 | import 'package:serveme/serveme.dart'; 21 | 22 | Future main() async { 23 | final ServeMe server = ServeMe(); 24 | await server.run(); 25 | } 26 | ``` 27 | You should provide config.yaml (configuration file by default) in order to start the server. 28 | ```yaml 29 | port: 8080 30 | debug: true 31 | debug_log: debug.log 32 | error_log: error.log 33 | ``` 34 | It's ready to run! Though it does nothing at this point. Now we need to implement at least one Module file where something will actually happen. It's recommended to keep your project file structure clean and put all your module files in a separate "modules" directory. 35 | Let's create some module which will listen for String messages from connected clients and echo them back: 36 | ```dart 37 | class MyModule extends Module { 38 | @override 39 | Future init() async { 40 | await Future.delayed(const Duration(seconds: 1)); // let's imitate some initialization process i.e. loading some data from Db 41 | server.log('Module initialized'); // logs message to console and debug.log file 42 | } 43 | 44 | @override 45 | void run() { 46 | server.listen((String message, ServeMeClient client) async { 47 | log('Got a message: $message'); 48 | client.send(message); 49 | }); 50 | } 51 | 52 | @override 53 | Future dispose() async { 54 | await Future.delayed(const Duration(seconds: 1)); // doing all necessary cleanup before server shutdown 55 | server.log('Module disposed'); 56 | } 57 | } 58 | ``` 59 | Now we have a module but we also need to enable it in our configuration file: 60 | ```yaml 61 | modules: 62 | - mymodule 63 | ``` 64 | Let's update our main function: 65 | ```dart 66 | Future main() async { 67 | final ServeMe server = ServeMe( 68 | modules: >{ 69 | 'mymodule': MyModule(), 70 | }, 71 | ); 72 | await server.run(); 73 | } 74 | ``` 75 | And it's ready! You can now connect to the server using your browser and test it out: 76 | ```javascript 77 | let ws = new WebSocket('ws:// 127.0.0.1:8080'); 78 | ws.onmessage = console.log; 79 | ws.send('Something'); 80 | ``` 81 | It's possible to access one module from another via [] operator applied to server object: 82 | ```dart 83 | class AnotherModule extends Module { 84 | MyModule get myModule => server['mymodule']! as MyModule; 85 | 86 | @override 87 | Future init() async { 88 | log('Here is our main module: $myModule'); 89 | } 90 | 91 | // ... 92 | } 93 | ``` 94 | And don't forget to enable your newly implemented modules in configuration file. 95 | 96 | ## WebSockets and TCP sockets 97 | ServeMe is using WebSockets by default. However it can handle pure TCP sockets as well: 98 | ```dart 99 | Future main() async { 100 | final ServeMe server = ServeMe( 101 | type: ServeMeType.tcp, 102 | modules: >{ 103 | 'mymodule': MyModule(), 104 | }, 105 | ); 106 | await server.run(); 107 | } 108 | ``` 109 | Keep in mind that String messages you will try to send via TCP sockets will be converted to Uint8List. So in order to receive String via TCP socket you need to use listen() instead of listen(). 110 | 111 | ## Configuration files 112 | By default ServeMe uses config.yaml file and ServeMeConfig class for instantiating config object accessible from any module. However it is possible to implement and use custom configuration class. 113 | ```dart 114 | class MyConfig extends Config { 115 | MyConfig(String filename) : super(filename) { 116 | optionalNumber = cast(map['optional'], fallback: null); 117 | greetingMessage = cast(map['greeting'], 118 | errorMessage: 'Failed to load config: greeting message is not set' 119 | ); 120 | } 121 | 122 | late final int? optionalNumber; 123 | late final String greetingMessage; 124 | } 125 | ``` 126 | Method cast<T>() allows to easily cast dynamic variable into typed one with specified fallback value or exception error message. Now let's update our configuration file and see hot to use custom configuration class instead of default one. 127 | ```yaml 128 | port: 8080 129 | debug: true 130 | debug_log: debug.log 131 | error_log: error.log 132 | 133 | optional: 42 134 | greeting: Welcome, friend! 135 | 136 | modules: 137 | - mymodule 138 | ``` 139 | ```dart 140 | Future main() async { 141 | final ServeMe server = ServeMe( 142 | configFile: 'config.yaml', 143 | configFactory: (String filename) => MyConfig(filename), 144 | modules: >{ 145 | 'mymodule': MyModule(), 146 | }, 147 | ); 148 | await server.run(); 149 | } 150 | ``` 151 | Here's how to access custom config from the module: 152 | ```dart 153 | class MyModule extends Module { 154 | // ... 155 | 156 | @override 157 | MyConfig get config => super.config as MyConfig; 158 | 159 | void printConfig() { 160 | log('optionalNumber: ${config.optionalNumber}, greetingMessage: ${config.greetingMessage}'); 161 | } 162 | 163 | // ... 164 | } 165 | ``` 166 | 167 | ## Establishing client connection to a remote server 168 | ServeMe instance allows you to create client connection to a remote WebSocket or TCP server. 169 | ```dart 170 | @override 171 | Future init() async { 172 | // Establish WebSocket connection to localhost 173 | final ServeMeClient wsConnectionClient = await server.connect( 174 | 'ws://127.0.0.1:8080', 175 | onConnect: () => log('WebSocket connection established'), 176 | onDisconnect: () => log('Disconnected from WebSocket server'), 177 | ); 178 | 179 | // Establish TCP socket connection to localhost 180 | final ServeMeClient tcpConnectionClient = await server.connect( 181 | InternetAddress('127.0.0.1', type: InternetAddressType.IPv4), 182 | port: 8177, 183 | onConnect: () => log('TCP connection established'), 184 | onDisconnect: () => log('Disconnected from TCP server'), 185 | ); 186 | } 187 | ``` 188 | Since server.connect() method returns an instance of ServeMeClient, all features such as sending/receiving PackMe messages and using asynchronous queries are available. 189 | 190 | ## Generic client class type 191 | You probably already noticed that both classes ServeMe and Module have generic client class (<ServeMeClient> by default). It's used in some server properties and methods and it is possible to implement custom client class. Here's an example: 192 | ```dart 193 | import 'dart:io'; 194 | 195 | class MyClient extends ServeMeClient { 196 | MyClient(ServeMeSocket socket) : super(socket) { 197 | authToken = socket.httpRequest!.headers.value('x-auth-token'); 198 | } 199 | 200 | late final String? authToken; 201 | } 202 | ``` 203 | We've added some custom property authToken and in order to use this class instead of default it's necessary to set clientFactory property in ServeMe constructor: 204 | ```dart 205 | Future main() async { 206 | final ServeMe server = ServeMe( 207 | clientFactory: (_, __) => MyClient(_, __), 208 | modules: >{ 209 | 'mymodule': MyModule(), 210 | }, 211 | ); 212 | await server.run(); 213 | } 214 | ``` 215 | Keep in mind that in this case all modules should be declared with the same generic class type. 216 | ```dart 217 | class MyModule extends Module { 218 | // ... 219 | 220 | void echoAuthenticatedClients() { 221 | server.listen((String message, MyClient client) async { 222 | if (client.authToken != 'some-valid-token') return; 223 | clent.send(message); 224 | }); 225 | } 226 | 227 | // ... 228 | } 229 | ``` 230 | 231 | ## Modules 232 | 233 | Every module has three mandatory methods: init(), run() and dispose(). 234 | ```dart 235 | Future init(); 236 | ``` 237 | Asynchronous method init() is invoked on server start and usually used to preload all necessary data for module to be ready to run. 238 | ```dart 239 | void run(); 240 | ``` 241 | Method run() is invoked after all modules have been successfully initialized. It's where modules start processing things and do its' job. 242 | ```dart 243 | Future dispose(); 244 | ``` 245 | Asynchronous method dispose() is used on server shutdown to finish modules operation properly (when it's necessary). 246 | 247 | ## Logs and errors 248 | Every ServeMe module has access to three methods: log(), debug() and error(). 249 | ```dart 250 | Future log(String message, [String color = _green]); 251 | ``` 252 | Method log() writes message to console and saves it to debug.log file (specified in configuration file). 253 | ```dart 254 | Future debug(String message, [String color = _reset]); 255 | ``` 256 | If debug is enabled in config then debug() writes message to console and saves it to log file. 257 | ```dart 258 | Future error(String message, [StackTrace? stack]); 259 | ``` 260 | Method error() logs error to console and writes it to error.log file (specified in configuration file). 261 | 262 | ## Console commands 263 | By default there is a single command you can use in server console: stop - which shuts down the server. However it is possible to implement any other commands using console object accessible from modules: 264 | ```dart 265 | @override 266 | void run() { 267 | console.on('echo', (String line, List args) async => log(line), 268 | aliases: ['say'], // optional 269 | similar: ['repeat', 'tell', 'speak'], // optional 270 | usage: 'echo \nEchoes specified string (max 20 characters length)', // optional 271 | validator: RegExp(r'^.{1,20}$'), // optional 272 | ); 273 | } 274 | ``` 275 | This code will add echo command which allows to echo specified string no longer that 20 characters length. 276 | * String line - command arguments string (without command itself); 277 | * List<String> args - list of arguments 278 | * aliases - use it if you need to assign multiple commands to the same command handler; 279 | * similar - list of commands which won't be recognized as valid but a suggestion of original command will be displayed; 280 | * usage - command format hint and/or short description which will be displayed if command format is invalid or command is used with --help key (or -h, -?, /?); 281 | * validator - regular expression for arguments string validation. 282 | 283 | ## Events 284 | ServeMe supports some built-in events: 285 | * ReadyEvent - dispatched once all modules are initialized, right before invoking modules run() methods; 286 | * TickEvent - dispatched every second; 287 | * StopEvent - dispatched once server shutdown initiated (either by stop command or POSIX signal); 288 | * LogEvent - dispatched on every message logging event; 289 | * ErrorEvent - dispatched on errors; 290 | * ConnectEvent - dispatched when incoming client connection established; 291 | * DisconnectEvent - dispatched when client connection is closed. 292 | 293 | You can subscribe to events using events object accessible from modules: 294 | ```dart 295 | @override 296 | void run() { 297 | events.listen((TickEvent event) async { 298 | log('${event.counter} seconds passed since server start'); 299 | }); 300 | } 301 | ``` 302 | It is also possible to implement own events and dispatch them when necessary. It's often very useful for interaction between different modules. 303 | ```dart 304 | class AnnouncementEvent extends Event { 305 | AnnouncementEvent(this.message) : super(); 306 | 307 | final String message; 308 | } 309 | ``` 310 | Now you can dispatch AnnouncementEvent in one module and listen for it in another module. 311 | ```dart 312 | // implemented in some module 313 | void makeAnnouncement() { 314 | events.dispatch(AnnouncementEvent('Cheese for everyone!')); 315 | } 316 | 317 | // implemented in some another module 318 | @override 319 | void run() { 320 | events.listen((AnnouncementEvent event) async { 321 | server.broadcast(event.message); // sends data to all connected clients 322 | }); 323 | } 324 | ``` 325 | 326 | ## Scheduler 327 | ServeMe allows to create and schedule tasks. There's a scheduler object accessible from modules: 328 | ```dart 329 | class SomeModule extends Module { 330 | late final Task task; 331 | 332 | @override 333 | Future init() async { 334 | task = Task( 335 | DateTime.now()..add(const Duration(minutes: 1)), 336 | (DateTime time) async { 337 | log('Current time is $time'); 338 | }, 339 | period: const Duration(seconds: 10), // optional 340 | skip: false, // optional 341 | ); 342 | } 343 | 344 | @override 345 | void run() { 346 | scheduler.schedule(task); 347 | } 348 | 349 | @override 350 | Future dispose() async { 351 | scheduler.cancel(task); 352 | } 353 | } 354 | ``` 355 | This module creates periodic Task which will be started in 1 minute. Note that task is cancelled on dispose. 356 | * skip - if true then periodic task will be skipped till next time if previously returned Future is not resolved yet. Default value: false. 357 | 358 | ## Connections and data transfer 359 | You can access all of your current client connections via clients object implemented in Module class: 360 | ```dart 361 | @override 362 | void run() { 363 | for (final ServeMeClient in server.clients) { 364 | // do something, don't use it for broadcasting however, use server.broadcast() instead 365 | } 366 | } 367 | ``` 368 | It's always recommended to use [PackMe](https://pub.dev/packages/packme) messages for data exchange since it gives some important benefits such as clear communication protocol described in JSON, asynchronous queries support out of the box and small data packets size. 369 | Here's a simple protocol.json file (located in packme directory) for some hypothetical client-server application (see PackMe JSON manifest format documentation [here](https://pub.dev/packages/packme)): 370 | ```json 371 | { 372 | "get_user": [ 373 | { 374 | "id": "string" 375 | }, 376 | { 377 | "first_name": "string", 378 | "last_name": "string", 379 | "age": "uint8" 380 | } 381 | ] 382 | } 383 | ``` 384 | Generate dart files: 385 | ```bash 386 | # Usage: dart run packme 387 | dart run packme packme generated 388 | ``` 389 | Before listening for any PackMe message from clients it is necessary to register message factory (which is created automatically and declared in generated dart file). 390 | ```dart 391 | @override 392 | void run() { 393 | // necessary for ServeMe to know how to parse incoming binary data 394 | server.register(protocolMessageFactory); 395 | server.listen((GetUserRequest request, ServeMeClient client) { 396 | // GetUserRequest.$response method returns GetUserResponse associated with current request. 397 | final GetUserResponse response = request.$response( 398 | firstName: 'Alyx', 399 | lastName: 'Vance', 400 | age: 19, 401 | ); 402 | }); 403 | } 404 | ``` 405 | This code listens for GetUserRequest message from clients and replies with GetUserResponse message. However sometimes it is useful to be able to add message listeners for some specific clients only, for example, logged in users only: 406 | ```dart 407 | bool _isAuthorizedToDoSomething(String codePhrase) { 408 | return codePhrase == "I am Iron Man."; 409 | } 410 | 411 | @override 412 | void run() { 413 | // Listen for some authorization request from connected clients. 414 | server.listen((AuthorizeRequest request, ServeMeClient client) { 415 | if (_isAuthorizedToDoSomething(request.codePhrase)) { 416 | client.listen(_handleGodModeRequest); 417 | client.listen(_handleAllWeaponsRequest); 418 | client.listen(_handleKillEveryoneRequest); 419 | client.send(request.$response( 420 | allowed: true, 421 | reason: 'Welcome on board!', 422 | )); 423 | } 424 | else { 425 | client.send(request.$response( 426 | allowed: false, 427 | reason: 'You are not Iron Man.', 428 | )); 429 | // Close client connection. 430 | client.close(); 431 | } 432 | }); 433 | } 434 | ``` 435 | You could see in previous examples that request.$response() method is used instead of just instantiating corresponding ResponseMessage. It's made for assigning the response to this particular request which allows us to use .query() on client side (or server side, it doesn't matter once implementation is valid on the opposite side): 436 | ```dart 437 | // let's for example obtain some data from clients once server is going offline 438 | @override 439 | Future dispose() async { 440 | int ok = 0, notOk = 0; 441 | for (final ServeMeClient client in server.clients) { 442 | // in real life situation you probably want to use asynchronous calls in parallel 443 | final AreYouOkResponse response = await client.query(AreYouOkRequest()); 444 | if (response.ok) ok++; 445 | else notOk++; 446 | } 447 | server.log('$ok clients are OK and $notOk clients are not. Now ready for shutting down.'); 448 | } 449 | ``` 450 | Method broadcast() allows to send a message to all connected clients or to some clients filtered by some criteria: 451 | ```dart 452 | // say good bye to all clients on server shut down 453 | @override 454 | Future dispose() async { 455 | // Send a String message to all connected clients. 456 | server.broadcast( 457 | 'See you later!', 458 | (ServeMeClient client) => true // optional criteria filter 459 | ); 460 | } 461 | ``` 462 | 463 | ## MongoDB 464 | ServeMe uses [mongo_dart](https://pub.dev/packages/mongo_dart) package for MongoDB support. In order to use MongoDB in modules it's necessary to specify mongo config section of your configuration file: 465 | ```yaml 466 | mongo: 467 | host: 127.0.0.1 468 | database: test_db 469 | ``` 470 | Or in case of using replica set: 471 | ```yaml 472 | mongo: 473 | host: 474 | - 192.160.1.101:27017 475 | - 192.160.1.102:27017 476 | - 192.160.1.103:27017 477 | database: test_db 478 | replica: myReplicaSet 479 | ``` 480 | There's an object db accessible from modules. This object is actually Future<Db>. Future is used to ensure that connection to database is alive and Db object is valid. 481 | ```dart 482 | import 'package:mongo_dart/mongo_dart.dart'; 483 | ``` 484 | ```dart 485 | late final List> items; 486 | 487 | @override 488 | Future init() async { 489 | // load all items within price range specified in config file 490 | items = await (await db).collection('users') 491 | .find(where.gte('price', config.minPrice).lte('price', config.maxPrice)) 492 | .toList(); 493 | } 494 | ``` 495 | 496 | ## Database integrity validation 497 | Sometimes it's necessary to ensure that server database contains all necessary collections, indexes and data for server to work properly. For this purpose ServeMe provides special integrity descriptor. It allows you to automatically create missing indexes and create mandatory documents in database on server start. Aside from validation it also allows to deploy servers with ease without extra steps for setting up database. 498 | ```dart 499 | Future main() async { 500 | final ServeMe server = ServeMe( 501 | dbIntegrityDescriptor: { 502 | 'users': CollectionDescriptor( 503 | indexes: { 504 | 'login_unique': IndexDescriptor(key: {'login': 1}, unique: true), 505 | 'email_unique': IndexDescriptor(key: {'email': 1}, unique: true), 506 | 'session': IndexDescriptor(key: {'sessions.key': 1}, unique: true), 507 | } 508 | ), 509 | 'settings': CollectionDescriptor( 510 | indexes: { 511 | 'param_unique': IndexDescriptor(key: {'param': 1}, unique: true), 512 | }, 513 | documents: >[ 514 | { 515 | 'param': 'online_users_limit', 516 | 'value': 5000, 517 | }, 518 | { 519 | 'param': 'disable_email_login', 520 | 'value': false, 521 | }, 522 | ] 523 | ), 524 | }, 525 | modules: >{ 526 | 'mymodule': MyModule(), 527 | }, 528 | ); 529 | await server.run(); 530 | } 531 | ``` 532 | 533 | ## Supported platforms 534 | It's available for Dart only. Currently there are no plans to implement it for any other language. However if developers will find this package useful then it may be implemented for Node.JS and C++ in the future. 535 | 536 | ## P.S. 537 | I hope you enjoy it ;) --------------------------------------------------------------------------------