blob: 874511612e162bcd830be46c5884f1a624b370c5 [file] [log] [blame] [edit]
// 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;
}
}