blob: b8c6a846b028418d50f7b002d1fd2463fa5073fd [file] [log] [blame] [edit]
// Copyright (c) 2026, 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:json_rpc_2/json_rpc_2.dart' as json_rpc;
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:stream_channel/stream_channel.dart';
import 'dart_runtime_service_rpcs.dart';
import 'rpc_exceptions.dart';
import 'utils.dart';
typedef ServiceName = String;
typedef ServiceAlias = String;
/// Represents a client that is connected to a service.
base class Client {
Client({
required this.clientManager,
required StreamChannel<String> connection,
}) {
_clientPeer = json_rpc.Peer(connection, strictProtocolChecks: false);
registerRpcHandlers();
done = _listen();
}
final ClientManager clientManager;
late json_rpc.Peer _clientPeer;
late final _internalRpcs = DartRuntimeServiceRpcs(
clientManager: clientManager,
client: this,
);
/// The logger to be used when handling requests from this client.
Logger get logger => Logger('Client ($name)');
/// A [Future] that completes when [close] is invoked.
late final Future<void> done;
/// Start receiving JSON RPC requests from the client.
///
/// Returned future completes when the peer is closed.
Future<void> _listen() => _clientPeer.listen();
/// Called if the connection to the client should be closed.
@mustCallSuper
Future<void> close() async {
await _clientPeer.close();
}
@mustCallSuper
void registerRpcHandlers() {
_internalRpcs.registerRpcsWithPeer(_clientPeer);
}
/// Attempts to register a [service] to be provided by this client.
///
/// [alias] is a human-readable description of the provided service.
///
/// If [service] is already registered with this client, an error is
/// returned.
bool registerService({
required ServiceName service,
required ServiceAlias alias,
}) {
if (hasService(service)) {
logger.info("Service '$service' is already registered by this client.");
return false;
}
logger.info(
"Successfully registered service '$service' as "
"'${clientManager.clients.keyOf(this)}.$service'.",
);
_services[service] = alias;
// TODO(bkonyi): send notification of newly registered service.
return true;
}
/// Returns true if [service] has already been registered by this client.
bool hasService(String service) => _services.containsKey(service);
/// Invokes a JSON-RPC [method] provided by this client.
Future<RpcResponse> sendRequest({
required String method,
Map<String, Object?>? parameters,
}) async {
if (_clientPeer.isClosed) {
RpcException.serviceDisappeared.throwException();
}
try {
return await _clientPeer.sendRequest(method, parameters) as RpcResponse;
// ignore: avoid_catching_errors
} on StateError {
RpcException.serviceDisappeared.throwException();
}
}
final _services = <ServiceName, ServiceAlias>{};
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;
/// Sets the name associated with this client.
///
/// If [n] is null, the client name is reset to [defaultClientName].
void setName(String? n) => _name = n ?? defaultClientName;
late String _name = defaultClientName;
}
/// 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.
base class ClientManager {
/// Creates a [Client] from [connection] and adds it to the list of connected
/// clients.
///
/// This should be called when a client connects to the service.
@mustCallSuper
Client addClient(StreamChannel<String> connection) {
final client = Client(clientManager: this, connection: connection);
clients.add(client);
return 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);
}
/// 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<void>>[];
// 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);
}
static const _kServicePrologue = 's';
/// The set of [Client]s currently connected to the service.
///
/// Each client is assigned a unique identifier, prefixed with
/// [_kServicePrologue] (e.g., 's1'). This identifier is used when invoking
/// service extensions registered by the client to indicate which client
/// is responsible for handling the service extension invocation.
final clients = NamedLookup<Client>(prefix: _kServicePrologue);
}