From f481eee687dee464b63b809b938ebbd4d60d48b1 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Tue, 26 May 2026 13:34:26 +0200 Subject: [PATCH] feat(client): GrpcWebClientChannel on web targets via conditional import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splits channel construction into two files selected at compile-time via `if (dart.library.html)`: channel_factory_io.dart — native ClientChannel (HTTP/2) channel_factory_web.dart — GrpcWebClientChannel.xhr (HTTP/1.1) HubClient._channel is now `ClientChannelBase` so both implementations satisfy the same field type. dart:html-only imports (`package:grpc/grpc_web.dart`) stay isolated from the native compile path. Pairs with the hub-side `fai_grpc_web` adapter crate (fai/platform 0d07892) — `tonic-web` on the same port as native gRPC, so one `fai serve` listener handles both surfaces. Signed-off-by: flemming-it --- lib/src/channel_factory_io.dart | 21 +++++++++++++++++++++ lib/src/channel_factory_web.dart | 17 +++++++++++++++++ lib/src/hub_client.dart | 24 +++++++++++------------- 3 files changed, 49 insertions(+), 13 deletions(-) create mode 100644 lib/src/channel_factory_io.dart create mode 100644 lib/src/channel_factory_web.dart diff --git a/lib/src/channel_factory_io.dart b/lib/src/channel_factory_io.dart new file mode 100644 index 0000000..4b593da --- /dev/null +++ b/lib/src/channel_factory_io.dart @@ -0,0 +1,21 @@ +// Native channel factory — plain gRPC over HTTP/2. +// Selected on every non-web target (macOS, Linux, Windows, iOS, +// Android) via the conditional import in hub_client.dart. + +import 'package:grpc/grpc.dart'; +import 'package:grpc/grpc_connection_interface.dart'; + +import 'hub_client.dart' show HubEndpoint; + +ClientChannelBase createChannel(HubEndpoint endpoint) { + return ClientChannel( + endpoint.host, + port: endpoint.port, + options: ChannelOptions( + credentials: endpoint.secure + ? const ChannelCredentials.secure() + : const ChannelCredentials.insecure(), + idleTimeout: const Duration(minutes: 5), + ), + ); +} diff --git a/lib/src/channel_factory_web.dart b/lib/src/channel_factory_web.dart new file mode 100644 index 0000000..565f4be --- /dev/null +++ b/lib/src/channel_factory_web.dart @@ -0,0 +1,17 @@ +// Web channel factory — gRPC-Web over HTTP/1.1. +// Selected on dart:html targets (Flutter web, Dart-for-web) via +// the conditional import in hub_client.dart. The hub must be +// running with `tonic-web` enabled — see fai_platform docs +// architecture/protocol-surfaces.md. + +import 'package:grpc/grpc_connection_interface.dart'; +import 'package:grpc/grpc_web.dart'; + +import 'hub_client.dart' show HubEndpoint; + +ClientChannelBase createChannel(HubEndpoint endpoint) { + final scheme = endpoint.secure ? 'https' : 'http'; + return GrpcWebClientChannel.xhr( + Uri.parse('$scheme://${endpoint.host}:${endpoint.port}'), + ); +} diff --git a/lib/src/hub_client.dart b/lib/src/hub_client.dart index 5765b8e..7b4fb02 100644 --- a/lib/src/hub_client.dart +++ b/lib/src/hub_client.dart @@ -6,8 +6,10 @@ import 'dart:typed_data'; -import 'package:grpc/grpc.dart'; +import 'package:grpc/grpc_connection_interface.dart'; +import 'channel_factory_io.dart' + if (dart.library.html) 'channel_factory_web.dart' as channel_factory; import 'generated/fai/v1/common.pb.dart' as pb_common; import 'generated/fai/v1/hub.pb.dart' as pb; import 'generated/fai/v1/hub.pbgrpc.dart' as grpc_hub; @@ -78,9 +80,15 @@ typedef SubmitResponse = pb.SubmitResponse; /// Typed client for the F∆I Hub gRPC surface. Construct once, /// reuse for the process lifetime, call [close] on shutdown. +/// +/// The channel type is picked at compile time: +/// * native targets → plain HTTP/2 gRPC via [ClientChannel]. +/// * web targets → HTTP/1.1 gRPC-Web via [GrpcWebClientChannel]. +/// Requires the hub to be running with the +/// `tonic-web` layer enabled. class HubClient { final HubEndpoint endpoint; - final ClientChannel _channel; + final ClientChannelBase _channel; final grpc_hub.HubClient _hub; final grpc_hub.HubAdminClient _admin; @@ -89,17 +97,7 @@ class HubClient { _admin = grpc_hub.HubAdminClient(_channel); factory HubClient({HubEndpoint endpoint = const HubEndpoint()}) { - final channel = ClientChannel( - endpoint.host, - port: endpoint.port, - options: ChannelOptions( - credentials: endpoint.secure - ? const ChannelCredentials.secure() - : const ChannelCredentials.insecure(), - idleTimeout: const Duration(minutes: 5), - ), - ); - return HubClient._(endpoint, channel); + return HubClient._(endpoint, channel_factory.createChannel(endpoint)); } /// Liveness probe. Returns true when the hub responds with