| // Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'package:dart_service_protocol_shared/src/named_lookup.dart'; | |
| import 'package:meta/meta.dart'; | |
| /// Represents a client that is connected to a service. | |
| abstract class Client { | |
| /// Sends [data] to the client on the provided [stream]. | |
| /// | |
| /// This method should do any formatting needed on [data], then send it to | |
| /// the [Client]. | |
| void streamNotify(String stream, Object data); | |
| /// Called if the connection to the client should be closed. | |
| Future<void> close(); | |
| /// Sends a request to the client. | |
| /// | |
| /// This method should forward [method] with [parameters] to the client. | |
| Future<Object?> sendRequest({required String method, Object? parameters}); | |
| /// A map of services that are handled by this client. | |
| /// | |
| /// The key is the service name and the value is a class containing | |
| /// information about each service method. | |
| final Map<String, ClientServiceInfo> services = {}; | |
| static int _idCounter = 0; | |
| final int _id = ++_idCounter; | |
| /// The name given to the client upon its creation. | |
| String get defaultClientName => 'client$_id'; | |
| /// The current name associated with this client. | |
| String? get name => _name; | |
| // NOTE: this should not be called directly except from: | |
| // - `ClientManager._clearClientName` | |
| // - `ClientManager._setClientNameHelper` | |
| void _setName(String? n) => _name = n ?? defaultClientName; | |
| String? _name; | |
| } | |
| /// Information about a service provided by a client. | |
| class ClientServiceInfo { | |
| ClientServiceInfo(this.name, [Map<String, ClientServiceMethodInfo>? methods]) | |
| : methods = methods ?? {}; | |
| /// Deserializes a [json] object to create a [ClientServiceInfo] object. | |
| static ClientServiceInfo fromJson(Map<String, Object?> json) { | |
| if (json case {_kName: final String name, _kMethods: final List methods}) { | |
| return ClientServiceInfo( | |
| name, | |
| <String, ClientServiceMethodInfo>{ | |
| for (final method in methods | |
| .cast<Map<String, Object?>>() | |
| .map(ClientServiceMethodInfo.fromJson)) | |
| method.name: method | |
| }, | |
| ); | |
| } | |
| throw ArgumentError('Unexpected JSON format: $json'); | |
| } | |
| static const _kName = 'name'; | |
| static const _kMethods = 'methods'; | |
| /// The name of the service. | |
| /// | |
| /// A client can register multiple services each with multiple methods. | |
| /// | |
| /// Only one client can register services for a given name (this is enforced | |
| /// by the implementation of `registerService`). | |
| final String name; | |
| /// The service methods registered for this service. | |
| final Map<String, ClientServiceMethodInfo> methods; | |
| /// Serializes this [ClientServiceInfo] object to JSON. | |
| Map<String, Object?> toJson() => { | |
| _kName: name, | |
| _kMethods: methods.values.map((m) => m.toJson()).toList(), | |
| }; | |
| } | |
| /// Information about an individual method of a service provided by a | |
| /// client. | |
| class ClientServiceMethodInfo { | |
| ClientServiceMethodInfo(this.name, [this.capabilities]); | |
| /// Deserializes a [json] object to create a [ClientServiceMethodInfo] object. | |
| static ClientServiceMethodInfo fromJson(Map<String, Object?> json) { | |
| try { | |
| return ClientServiceMethodInfo( | |
| json[_kName] as String, | |
| json[_kCapabilities] as Map<String, Object?>?, | |
| ); | |
| } catch (e) { | |
| throw ArgumentError('Unexpected JSON format: $json'); | |
| } | |
| } | |
| static const _kName = 'name'; | |
| static const _kCapabilities = 'capabilities'; | |
| /// The name of the method. | |
| /// | |
| /// A client can register multiple methods for each service but can only use | |
| /// each name once (this is enforced by the implementation of | |
| /// `registerService`). | |
| final String name; | |
| /// Optional capabilities of this service method provided by the client. | |
| final Map<String, Object?>? capabilities; | |
| /// Serializes this [ClientServiceMethodInfo] object to JSON. | |
| Map<String, Object?> toJson() => { | |
| _kName: name, | |
| if (capabilities != null) _kCapabilities: capabilities, | |
| }; | |
| } | |
| /// Used for keeping track and managing clients that are connected to a given | |
| /// service. | |
| /// | |
| /// Call [addClient] when a client connects to your service, then call | |
| /// [removeClient] when it stops listening. | |
| abstract class ClientManager { | |
| /// Adds [client] to the list of connected clients. | |
| /// | |
| /// This should be called when a client connects to the service. | |
| @mustCallSuper | |
| void addClient(Client client) { | |
| setClientName( | |
| client, | |
| client.defaultClientName, | |
| ); | |
| clients.add(client); | |
| } | |
| /// Removes [client] from the list of connected clients. | |
| /// | |
| /// This should be called when the client disconnects from the service. | |
| @mustCallSuper | |
| void removeClient(Client client) { | |
| clients.remove(client); | |
| } | |
| /// Returns true if the client manager has and clients still connected. | |
| bool hasClients() => clients.isNotEmpty; | |
| /// Cleans up clients that are still connected by calling [Client.close] on | |
| /// all of them. | |
| Future<void> shutdown() async { | |
| // Close all incoming websocket connections. | |
| final futures = <Future>[]; | |
| // Copy `clients` to guard against modification while iterating. | |
| for (final client in clients.toList()) { | |
| futures.add( | |
| Future.sync(() => removeClient(client)) | |
| .whenComplete(() => client.close()), | |
| ); | |
| } | |
| await Future.wait(futures); | |
| } | |
| /// Returns the first client that contains a service+method from the list of | |
| /// connected clients. | |
| /// | |
| /// There should only ever be one client that owns a service+method but this | |
| /// method only assumes and does not verify that. | |
| @mustCallSuper | |
| Client? findClientThatHandlesServiceMethod( | |
| String serviceName, | |
| String methodName, | |
| ) { | |
| // TODO(dantup): Should we maintain a complete map of services to avoid | |
| // looping over clients? | |
| for (final client in clients) { | |
| final service = client.services[serviceName]; | |
| if (service?.methods.containsKey(methodName) ?? false) { | |
| return client; | |
| } | |
| } | |
| return null; | |
| } | |
| /// Associates a name with a given client. | |
| @mustCallSuper | |
| void setClientName( | |
| Client client, | |
| String name, | |
| ) { | |
| _setClientNameHelper(client, name); | |
| } | |
| /// Changes [client]'s name to [name] | |
| void _setClientNameHelper( | |
| Client client, | |
| String name, | |
| ) { | |
| clearClientName(client); | |
| client._setName(name.isEmpty ? client.defaultClientName : name); | |
| } | |
| static const _kServicePrologue = 's'; | |
| final NamedLookup<Client> clients = NamedLookup( | |
| prologue: _kServicePrologue, | |
| ); | |
| /// Unsets a client's name by setting it to null. | |
| @mustCallSuper | |
| String? clearClientName(Client client) { | |
| String? name = client.name; | |
| client._setName(null); | |
| return name; | |
| } | |
| } |