From ac20f66f86e10b324bca9587fd73e2247b35597a Mon Sep 17 00:00:00 2001 From: Aashish Patil Date: Fri, 27 Mar 2026 09:51:22 -0700 Subject: [PATCH 1/6] Initial Commit --- .../firebase_data_connect/.metadata | 30 ++ .../analysis_options.yaml | 1 + .../example/test_stream.dart | 68 +++ .../lib/src/cache/result_tree_processor.dart | 2 +- .../lib/src/common/common_library.dart | 9 + .../lib/src/core/ref.dart | 78 +++- .../lib/src/firebase_data_connect.dart | 108 ++++- .../lib/src/network/grpc_transport.dart | 14 +- .../lib/src/network/rest_library.dart | 1 + .../lib/src/network/rest_transport.dart | 12 + .../lib/src/network/stream_protocol.dart | 174 ++++++++ .../lib/src/network/test_stream.dart | 45 ++ .../lib/src/network/transport_library.dart | 10 + .../lib/src/network/transport_stub.dart | 13 + .../lib/src/network/websocket_transport.dart | 391 ++++++++++++++++++ .../firebase_data_connect/pubspec.yaml | 1 + .../firebase_data_connect/test.dart | 12 + .../src/cache/cache_manager_test.mocks.dart | 27 +- .../test/src/common/common_library_test.dart | 11 + .../src/firebase_data_connect_test.mocks.dart | 27 +- .../network/rest_transport_test.mocks.dart | 19 +- 21 files changed, 970 insertions(+), 83 deletions(-) create mode 100644 packages/firebase_data_connect/firebase_data_connect/.metadata create mode 100644 packages/firebase_data_connect/firebase_data_connect/analysis_options.yaml create mode 100644 packages/firebase_data_connect/firebase_data_connect/example/test_stream.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/network/stream_protocol.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/network/test_stream.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/network/websocket_transport.dart create mode 100644 packages/firebase_data_connect/firebase_data_connect/test.dart diff --git a/packages/firebase_data_connect/firebase_data_connect/.metadata b/packages/firebase_data_connect/firebase_data_connect/.metadata new file mode 100644 index 000000000000..fca9f99c599e --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + - platform: macos + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/firebase_data_connect/firebase_data_connect/analysis_options.yaml b/packages/firebase_data_connect/firebase_data_connect/analysis_options.yaml new file mode 100644 index 000000000000..f9b303465f19 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/packages/firebase_data_connect/firebase_data_connect/example/test_stream.dart b/packages/firebase_data_connect/firebase_data_connect/example/test_stream.dart new file mode 100644 index 000000000000..1b0a3bbae112 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/test_stream.dart @@ -0,0 +1,68 @@ +import 'dart:async'; +import 'package:firebase_data_connect/firebase_data_connect.dart'; + +// Since the user didn't specify project info, we use dummy data that hits the emulator. +void main() async { + print('Initializing test script for WebSocket emulator...'); + + // Create connector config + final connectorConfig = ConnectorConfig( + 'us-central1', + 'default', + 'default', + ); + + // We don't have a real Firebase app here, so we will manually instantiate + // the transport and connect. + final options = DataConnectOptions( + 'demo-project', + connectorConfig.location, + connectorConfig.connector, + connectorConfig.serviceId, + ); + + final transportOptions = TransportOptions('127.0.0.1', 9399, false); + + // We can use the core SDK type + final transport = getTransport( + transportOptions, + options, + 'test-app-id', + CallerSDKType.core, + null // no app check + ); + + final queryName = 'ListBlogs'; + + // Execute via REST for comparison + print('Trying to perform a standard unary executeQuery...'); + try { + final response = await transport.invokeQuery( + queryName, + (String s) => s, // dummy deserializer + (dynamic v) => '{}', // dummy serializer + null, + null + ); + print('Unary Response: \${response.data}'); + } catch (e) { + print('Unary Request Failed: \$e'); + } + + print('\\nInitiating Stream Subscribe for \$queryName...'); + final subscription = transport.invokeStreamQuery( + queryName, + (String s) => s, + (dynamic v) => '{}', + null, + null + ); + + subscription.listen((response) { + print('Received Pushed Data: \${response.data}'); + }, onError: (err) { + print('Stream Error: \$err'); + }, onDone: () { + print('Stream Closed.'); + }); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/result_tree_processor.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/result_tree_processor.dart index ce4bf1bad24a..1b2652a24d63 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/result_tree_processor.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/result_tree_processor.dart @@ -29,7 +29,7 @@ class DehydrationResult { class ResultTreeProcessor { /// Takes a server response, traverses the data, creates or updates `EntityDataObject`s, /// and builds a dehydrated `EntityNode` tree. - Future dehydrateResults( + Future dehydrateResults( String queryId, Map serverResponse, CacheProvider cacheProvider, diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/common/common_library.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/common/common_library.dart index 9247287f5adf..5f523644870c 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/common/common_library.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/common/common_library.dart @@ -117,4 +117,13 @@ abstract class DataConnectTransport { Variables? vars, String? token, ); + + /// Invokes corresponding stream query endpoint. + Stream invokeStreamQuery( + String queryName, + Deserializer deserializer, + Serializer serializer, + Variables? vars, + String? token, + ); } diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart index 5b359f6a15b3..01b155790461 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart @@ -60,8 +60,16 @@ abstract class OperationRef { FirebaseDataConnect dataConnect; - Future> execute( - {QueryFetchPolicy fetchPolicy = QueryFetchPolicy.preferCache}); + static String createOperationId(String operationName, + Variables? vars, Serializer? serializer) { + if (vars != null && serializer != null) { + return '$operationName::${serializer(vars)}'; + } else { + return operationName; + } + } + + Future> execute(); Future _shouldRetry() async { String? newToken; @@ -184,14 +192,7 @@ class QueryManager { return streamController; } - static String createQueryId(String queryName, - QueryVariables? vars, Serializer varSerializer) { - if (vars != null) { - return '$queryName::${varSerializer(vars)}'; - } else { - return queryName; - } - } + void dispose() { _impactedQueriesSubscription?.cancel(); @@ -240,7 +241,7 @@ class QueryRef extends OperationRef { } String get _queryId => - QueryManager.createQueryId(operationName, variables, serializer); + OperationRef.createOperationId(operationName, variables, serializer); Future> _executeFromCache( QueryFetchPolicy fetchPolicy) async { @@ -310,10 +311,56 @@ class QueryRef extends OperationRef { Stream> subscribe() { _streamController ??= _queryManager.addQuery(this); + + final stream = _streamController!.stream.cast>(); + + // Return the stream to the caller, then execute fetches + Future.microtask(() { + if (dataConnect.cacheManager != null) { + _executeFromCache(QueryFetchPolicy.cacheOnly).then((_) {}).catchError((err) { + log("Error fetching from cache during subscribe $err"); + // Ignore cache misses here, server stream will provide latest data + }); + } + + // Initiate Web Socket stream + _streamFromServer(); + }); + + return stream; + } + + void _streamFromServer() async { + bool shouldRetry = await _shouldRetry(); + try { + final stream = _transport.invokeStreamQuery( + operationName, + deserializer, + serializer, + variables, + _lastToken, + ); - execute(); + await for (final serverResponse in stream) { + if (dataConnect.cacheManager != null) { + await dataConnect.cacheManager!.update(_queryId, serverResponse); + } + Data typedData = _convertBodyJsonToData(serverResponse.data); - return _streamController!.stream.cast>(); + QueryResult res = + QueryResult(dataConnect, typedData, DataSource.server, this); + publishResultToStream(res); + } + } on DataConnectError catch (e) { + if (shouldRetry && + e.code == DataConnectErrorCode.unauthorized.toString()) { + _streamFromServer(); + } else { + publishErrorToStream(e); + } + } catch (e) { + publishErrorToStream(e as Error); + } } void publishResultToStream(QueryResult result) { @@ -322,7 +369,7 @@ class QueryRef extends OperationRef { } } - void publishErrorToStream(Error err) { + void publishErrorToStream(Object err) { if (_streamController != null) { _streamController?.addError(err); } @@ -347,8 +394,7 @@ class MutationRef extends OperationRef { ); @override - Future> execute( - {QueryFetchPolicy fetchPolicy = QueryFetchPolicy.serverOnly}) async { + Future> execute() async { bool shouldRetry = await _shouldRetry(); try { // Logic below is duplicated due to the fact that `executeOperation` returns diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/firebase_data_connect.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/firebase_data_connect.dart index d5813f94dc89..ea1a7738a352 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/firebase_data_connect.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/firebase_data_connect.dart @@ -20,10 +20,8 @@ import 'package:firebase_data_connect/src/common/common_library.dart'; import 'package:firebase_data_connect/src/core/ref.dart'; import 'package:flutter/foundation.dart'; -import './network/transport_library.dart' - if (dart.library.io) './network/grpc_library.dart' - if (dart.library.js_interop) './network/rest_library.dart' - if (dart.library.html) './network/rest_library.dart'; +import './network/rest_library.dart'; +import './network/transport_library.dart'; import 'cache/cache_data_types.dart'; import 'cache/cache.dart'; @@ -93,13 +91,22 @@ class FirebaseDataConnect extends FirebasePluginPlatform { void checkTransport() { transportOptions ??= TransportOptions('firebasedataconnect.googleapis.com', null, true); - transport = getTransport( + final rest = RestTransport( transportOptions!, options, app.options.appId, _sdkType, appCheck, ); + final ws = WebSocketTransport( + transportOptions!, + options, + app.options.appId, + _sdkType, + appCheck, + auth, + ); + transport = _RoutingTransport(rest, ws); } @visibleForTesting @@ -120,7 +127,7 @@ class FirebaseDataConnect extends FirebasePluginPlatform { checkTransport(); checkAndInitializeCache(); String queryId = - QueryManager.createQueryId(operationName, vars, varsSerializer); + OperationRef.createOperationId(operationName, vars, varsSerializer); QueryRef? ref = _queryManager.trackedQueries[queryId] as QueryRef?; @@ -218,3 +225,92 @@ class FirebaseDataConnect extends FirebasePluginPlatform { return newInstance; } } + +class _RoutingTransport implements DataConnectTransport { + _RoutingTransport(this.rest, this.websocket); + final RestTransport rest; + final WebSocketTransport websocket; + + @override + FirebaseAppCheck? get appCheck => rest.appCheck; + @override + set appCheck(FirebaseAppCheck? val) { + rest.appCheck = val; + websocket.appCheck = val; + } + + @override + CallerSDKType get sdkType => rest.sdkType; + @override + set sdkType(CallerSDKType val) { + rest.sdkType = val; + websocket.sdkType = val; + } + + @override + TransportOptions get transportOptions => rest.transportOptions; + @override + set transportOptions(TransportOptions val) { + rest.transportOptions = val; + websocket.transportOptions = val; + } + + @override + DataConnectOptions get options => rest.options; + @override + set options(DataConnectOptions val) { + rest.options = val; + websocket.options = val; + } + + @override + String get appId => rest.appId; + @override + set appId(String val) { + rest.appId = val; + websocket.appId = val; + } + + @override + Future invokeMutation( + String queryName, + Deserializer deserializer, + Serializer? serializer, + Variables? vars, + String? token, + ) { + if (websocket.isConnected) { + return websocket.invokeMutation( + queryName, deserializer, serializer, vars, token); + } + return rest.invokeMutation( + queryName, deserializer, serializer, vars, token); + } + + @override + Future invokeQuery( + String queryName, + Deserializer deserializer, + Serializer? serialize, + Variables? vars, + String? token, + ) { + if (websocket.isConnected) { + return websocket.invokeQuery( + queryName, deserializer, serialize, vars, token); + } + return rest.invokeQuery(queryName, deserializer, serialize, vars, token); + } + + @override + Stream invokeStreamQuery( + String queryName, + Deserializer deserializer, + Serializer? serializer, + Variables? vars, + String? token, + ) { + return websocket.invokeStreamQuery( + queryName, deserializer, serializer, vars, token); + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/grpc_transport.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/grpc_transport.dart index 180bb209168b..e0188085f29e 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/grpc_transport.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/grpc_transport.dart @@ -14,7 +14,7 @@ part of 'grpc_library.dart'; -/// Transport used for Android/iOS. Uses a GRPC transport instead of REST. +@Deprecated('Use RestTransport and WebSocketTransport instead. The Data Connect SDK has moved away from gRPC.') class GRPCTransport implements DataConnectTransport { /// GRPCTransport creates a new channel GRPCTransport( @@ -167,6 +167,18 @@ class GRPCTransport implements DataConnectTransport { rethrow; } } + + /// Invokes stream query using WebSockets (even for GRPC clients we fall back to WebSockets for streaming right now). + @override + Stream invokeStreamQuery( + String queryName, + Deserializer deserializer, + Serializer? serializer, + Variables? vars, + String? token, + ) { + throw UnsupportedError('Streaming should be routed through WebSocketTransport'); + } } ServerResponse handleResponse(CommonResponse commonResponse) { diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_library.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_library.dart index e269fa840de8..5aa255986ddd 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_library.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_library.dart @@ -21,5 +21,6 @@ import 'package:http/http.dart' as http; import '../common/common_library.dart'; import '../dataconnect_version.dart'; +import 'transport_library.dart'; part 'rest_transport.dart'; diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart index 2680f692e350..d78a7cb71815 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart @@ -184,6 +184,18 @@ class RestTransport implements DataConnectTransport { token, ); } + + /// WebSockets are now handled by WebSocketTransport in FirebaseDataConnect. + @override + Stream invokeStreamQuery( + String queryName, + Deserializer deserializer, + Serializer? serializer, + Variables? vars, + String? token, + ) { + throw UnsupportedError('Streaming should be routed through WebSocketTransport'); + } } /// Initializes Rest transport for Data Connect. diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/stream_protocol.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/stream_protocol.dart new file mode 100644 index 000000000000..67b264d0f439 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/stream_protocol.dart @@ -0,0 +1,174 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// The kind of streaming request. +enum RequestKind { + subscribe, + execute, + resume, + cancel, +} + +/// Request to execute or subscribe to a Data Connect query or mutation. +class ExecuteRequest { + ExecuteRequest(this.operationName, this.variables); + + final String operationName; + final Map? variables; + + Map toJson() { + final Map data = {}; + data['operationName'] = operationName; + if (variables != null) { + data['variables'] = variables; + } + return data; + } +} + +/// Request to resume a query. +class ResumeRequest { + ResumeRequest(); + + Map toJson() { + return {}; + } +} + +/// StreamRequest defines the request of Data Connect's bi-directional streaming API. +class StreamRequest { + StreamRequest({ + this.name, + this.headers, + this.authToken, + this.appCheckToken, + this.requestId, + this.requestKind, + this.subscribe, + this.execute, + this.resume, + this.cancel, + this.dataEtag, + }); + + /// The resource name of the connector. + final String? name; + + /// Optional headers. + final Map? headers; + + /// Optional Auth token. + final String? authToken; + + /// Optional App Check token. + final String? appCheckToken; + + /// The request id used to identify a request within the stream. + final String? requestId; + + /// Kind of the request. + final RequestKind? requestKind; + + /// Subscribe to a Data Connect query. + final ExecuteRequest? subscribe; + + /// Execute a Data Connect query or mutation. + final ExecuteRequest? execute; + + /// Resume a query. + final ResumeRequest? resume; + + /// Signal that the client is no longer interested. + final bool? cancel; + + /// Etag for caching. + final String? dataEtag; + + Map toJson() { + final Map data = {}; + if (name != null) { + data['name'] = name; + } + if (headers != null) { + data['headers'] = headers; + } + if (authToken != null) { + data['authToken'] = authToken; + } + if (appCheckToken != null) { + data['appCheckToken'] = appCheckToken; + } + if (requestId != null) { + data['requestId'] = requestId; + } + if (dataEtag != null) { + data['dataEtag'] = dataEtag; + } + + if (subscribe != null) { + data['subscribe'] = subscribe!.toJson(); + } else if (execute != null) { + data['execute'] = execute!.toJson(); + } else if (resume != null) { + data['resume'] = resume!.toJson(); + } else if (cancel == true) { + data['cancel'] = {}; + } + + return data; + } +} + +/// StreamResponse defines the response of Data Connect's bi-directional streaming API. +class StreamResponse { + StreamResponse({ + this.requestId, + this.data, + this.dataEtag, + this.errors, + this.cancelled, + this.extensions, + }); + + factory StreamResponse.fromJson(Map json) { + if (json.containsKey('result')) { + json = json['result'] as Map; + } else if (json.containsKey('error')) { + final errObj = json['error'] as Map; + json = { + 'errors': [ + {'message': errObj['message']} + ] + }; + } + + List? errorsList = json['errors'] as List?; + + return StreamResponse( + requestId: json['requestId'] as String?, + data: json['data'] as Map?, + dataEtag: json['dataEtag'] as String?, + errors: errorsList, + cancelled: json['cancelled'] as bool?, + extensions: json['extensions'] as Map?, + ); + } + + final String? requestId; + final Map? data; + final String? dataEtag; + final List? errors; + final bool? cancelled; + final Map? extensions; +} diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/test_stream.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/test_stream.dart new file mode 100644 index 000000000000..699ebd19f958 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/test_stream.dart @@ -0,0 +1,45 @@ +import 'dart:async'; +import 'package:firebase_data_connect/src/common/common_library.dart'; +import 'package:firebase_data_connect/src/network/rest_library.dart'; + +void main() async { + print('Initializing test script for WebSocket emulator...'); + + final options = DataConnectOptions( + 'demo-project', + 'us-central1', + 'default', + 'default', + ); + + final transportOptions = TransportOptions('127.0.0.1', 9399, false); + + final transport = getTransport( + transportOptions, + options, + 'test-app-id', + CallerSDKType.core, + null + ); + + final queryName = 'ListBlogs'; + + print('\nInitiating Stream Subscribe for $queryName...'); + final subscription = transport.invokeStreamQuery( + queryName, + (String s) => s, + (dynamic v) => '{}', + null, + null + ); + + subscription.listen((response) { + print('Received Pushed Data: ${response.data}'); + }, onError: (err) { + print('Stream Error: $err'); + }, onDone: () { + print('Stream Closed.'); + }); + + await Future.delayed(Duration(seconds: 10)); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/transport_library.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/transport_library.dart index 7da28d92b812..3d69eb55be67 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/transport_library.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/transport_library.dart @@ -12,8 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer' as developer; +import 'dart:math'; import 'package:firebase_app_check/firebase_app_check.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; import '../common/common_library.dart'; +import '../core/ref.dart'; +import '../dataconnect_version.dart'; +import 'stream_protocol.dart'; part 'transport_stub.dart'; +part 'websocket_transport.dart'; diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/transport_stub.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/transport_stub.dart index e00c53b5bac1..c20f4ff64fb4 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/transport_stub.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/transport_stub.dart @@ -60,6 +60,19 @@ class TransportStub implements DataConnectTransport { throw UnimplementedError(); } + /// Stub for subscribing to a query. + @override + Stream invokeStreamQuery( + String queryName, + Deserializer deserializer, + Serializer? serializer, + Variables? vars, + String? token, + ) { + // TODO: implement invokeStreamQuery + throw UnimplementedError(); + } + /// Stub for invoking a query. @override Future invokeQuery( diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/websocket_transport.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/websocket_transport.dart new file mode 100644 index 000000000000..927fb85defeb --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/websocket_transport.dart @@ -0,0 +1,391 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +part of 'transport_library.dart'; + +/// WebSocketTransport makes requests out to the streaming endpoints of the configured backend, +/// multiplexing multiple subscriptions and unary operations over a single WebSocket connection. +class WebSocketTransport implements DataConnectTransport { + /// Initializes necessary protocol and port. + WebSocketTransport( + this.transportOptions, + this.options, + this.appId, + this.sdkType, + this.appCheck, [ + this.auth, + ]) { + String protocol = 'ws'; + if (transportOptions.isSecure ?? true) { + protocol += 's'; + } + String host = transportOptions.host; + int port = transportOptions.port ?? 443; + String location = options.location; + _url = '$protocol://$host:$port/v1/Connect/locations/$location'; + + _currentUid = auth?.currentUser?.uid; + _authSubscription = auth?.idTokenChanges().listen((user) async { + final newUid = user?.uid; + // Don't disconnect if auth state changes from not logged in to logged in. + // Only disconnect if logged in user changes. + if (_currentUid != null && _currentUid != newUid) { + _disconnect(); + } else if (newUid != null && isConnected) { + try { + final token = await user?.getIdToken(); + final request = StreamRequest( + requestId: _generateRequestId('auth'), + authToken: token, + ); + _channel!.sink.add(jsonEncode(request.toJson())); + } catch (e) { + // Ignored + } + } + _currentUid = newUid; + }); + } + + FirebaseAuth? auth; + String? _currentUid; + StreamSubscription? _authSubscription; + + + @override + FirebaseAppCheck? appCheck; + + @override + CallerSDKType sdkType; + + late String _url; + + @override + TransportOptions transportOptions; + + @override + DataConnectOptions options; + + @override + String appId; + + WebSocketChannel? _channel; + StreamSubscription? _channelSubscription; + + // Active listeners for stream subscriptions mapped by requestId. + final Map>> _streamListeners = {}; + + // Active completers for unary operations mapped by requestId. + final Map>> _unaryListeners = {}; + + // Active subscriptions mapped by operationId => requestId. + final Map _activeSubscriptions = {}; + + final Random _random = Random(); + static const String _chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; + + String _generateRequestId(String operationName) { + final randStr = String.fromCharCodes(Iterable.generate( + 15, (_) => _chars.codeUnitAt(_random.nextInt(_chars.length)))); + return '${operationName}_$randStr'; + } + + bool get isConnected => _channel != null; + + Map _buildHeaders(String? authToken, String? appCheckToken) { + Map headers = { + 'x-goog-api-client': getGoogApiVal(sdkType, packageVersion), + 'x-firebase-client': getFirebaseClientVal(packageVersion) + }; + if (authToken != null) { + headers['X-Firebase-Auth-Token'] = authToken; + } + if (appCheckToken != null) { + headers['X-Firebase-AppCheck'] = appCheckToken; + } + headers['x-firebase-gmpid'] = appId; + return headers; + } + + Future _ensureConnected(String? authToken) async { + if (_channel != null) return; + + String? appCheckToken; + try { + appCheckToken = await appCheck?.getToken(); + } catch (e) { + // Ignored + } + + final headers = _buildHeaders(authToken, appCheckToken); + + _channel = WebSocketChannel.connect(Uri.parse(_url)); + _channelSubscription = _channel!.stream.listen( + _onMessage, + onError: _onError, + onDone: _onDone, + ); + + final initRequest = StreamRequest( + name: 'projects/${options.projectId}/locations/${options.location}/services/${options.serviceId}/connectors/${options.connector}', + headers: headers, + ); + _channel!.sink.add(jsonEncode(initRequest.toJson())); + } + + void _onMessage(dynamic message) { + try { + developer.log("Received stream response \n $message"); + final bodyJson = jsonDecode(message as String) as Map; + final response = StreamResponse.fromJson(bodyJson); + + final requestId = response.requestId; + if (requestId == null) return; + + final serverResponse = ServerResponse( + response.data ?? {}, + extensions: response.extensions, + ); + + // Append errors if any exist on the stream payload + if (response.errors != null && response.errors!.isNotEmpty) { + // We simulate a DataConnectOperationError payload structure + // so that ref.dart can parse it correctly + serverResponse.data['errors'] = response.errors; + } + + if (_unaryListeners.containsKey(requestId)) { + final completers = _unaryListeners.remove(requestId)!; + for (var completer in completers) { + completer.complete(serverResponse); + } + } + + if (_streamListeners.containsKey(requestId)) { + final controllers = _streamListeners[requestId]!; + if (response.cancelled == true) { + for (var controller in controllers) { + controller.close(); + } + _streamListeners.remove(requestId); + _activeSubscriptions.removeWhere((key, value) => value == requestId); + } else { + for (var controller in controllers) { + controller.add(serverResponse); + } + } + } + } catch (e) { + // JSON decoding error or unknown format + developer.log('error decoding server response $e'); + } + } + + void _onError(dynamic error) { + final e = DataConnectError(DataConnectErrorCode.other, 'WebSocket error: $error'); + for (final completers in _unaryListeners.values) { + for (var completer in completers) completer.completeError(e); + } + for (final controllers in _streamListeners.values) { + for (var controller in controllers) controller.addError(e); + } + _unaryListeners.clear(); + _streamListeners.clear(); + _activeSubscriptions.clear(); + _channel = null; + } + + void _disconnect() { + _channel?.sink.close(); + } + + void _onDone() { + _channel = null; + for (final controllers in _streamListeners.values) { + for (var controller in controllers) controller.close(); + } + _unaryListeners.clear(); + _streamListeners.clear(); + _activeSubscriptions.clear(); + } + + @override + Future invokeQuery( + String queryName, + Deserializer deserializer, + Serializer? serializer, + Variables? vars, + String? authToken, + ) async { + return _invokeUnary(queryName, deserializer, serializer, vars, authToken, RequestKind.execute); + } + + @override + Future invokeMutation( + String queryName, + Deserializer deserializer, + Serializer? serializer, + Variables? vars, + String? authToken, + ) async { + return _invokeUnary(queryName, deserializer, serializer, vars, authToken, RequestKind.execute); + } + + Future _invokeUnary( + String operationName, + Deserializer deserializer, + Serializer? serializer, + Variables? vars, + String? authToken, + RequestKind requestKind, + ) async { + await _ensureConnected(authToken); + + final operationId = OperationRef.createOperationId(operationName, vars, serializer); + final completer = Completer(); + + if (_activeSubscriptions.containsKey(operationId)) { + final existingRequestId = _activeSubscriptions[operationId]!; + _unaryListeners.putIfAbsent(existingRequestId, () => []).add(completer); + + String? appCheckToken; + try { + appCheckToken = await appCheck?.getToken(); + } catch (e) { + // Ignored + } + + final headers = _buildHeaders(authToken, appCheckToken); + + final request = StreamRequest( + authToken: authToken, + appCheckToken: appCheckToken, + requestId: existingRequestId, + requestKind: RequestKind.resume, + resume: ResumeRequest(), + headers: headers, + ); + _channel!.sink.add(jsonEncode(request.toJson())); + + return completer.future; + } + + final requestId = _generateRequestId(operationId); + _unaryListeners.putIfAbsent(requestId, () => []).add(completer); + + Map? variables; + if (vars != null && serializer != null) { + variables = json.decode(serializer(vars)); + } + + String? appCheckToken; + try { + appCheckToken = await appCheck?.getToken(); + } catch (e) { + // Ignored + } + + final headers = _buildHeaders(authToken, appCheckToken); + + final request = StreamRequest( + authToken: authToken, + appCheckToken: appCheckToken, + requestId: requestId, + requestKind: requestKind, + execute: ExecuteRequest(operationName, variables), + headers: headers, + ); + + _channel!.sink.add(jsonEncode(request.toJson())); + + return completer.future; + } + + @override + Stream invokeStreamQuery( + String queryName, + Deserializer deserializer, + Serializer? serializer, + Variables? vars, + String? authToken, + ) { + late StreamController controller; + final operationId = OperationRef.createOperationId(queryName, vars, serializer); + + controller = StreamController( + onListen: () async { + await _ensureConnected(authToken); + + if (_activeSubscriptions.containsKey(operationId)) { + final existingRequestId = _activeSubscriptions[operationId]!; + _streamListeners.putIfAbsent(existingRequestId, () => []).add(controller); + return; + } + + final requestId = _generateRequestId(operationId); + _activeSubscriptions[operationId] = requestId; + _streamListeners.putIfAbsent(requestId, () => []).add(controller); + + Map? variables; + if (vars != null && serializer != null) { + variables = json.decode(serializer(vars)); + } + + String? appCheckToken; + try { + appCheckToken = await appCheck?.getToken(); + } catch (e) { + // Ignored + } + + final headers = _buildHeaders(authToken, appCheckToken); + + final request = StreamRequest( + authToken: authToken, + appCheckToken: appCheckToken, + requestId: requestId, + requestKind: RequestKind.subscribe, + subscribe: ExecuteRequest(queryName, variables), + headers: headers, + ); + + _channel!.sink.add(jsonEncode(request.toJson())); + }, + onCancel: () { + if (!_activeSubscriptions.containsKey(operationId)) return; + final requestId = _activeSubscriptions[operationId]!; + + final listeners = _streamListeners[requestId]; + if (listeners != null) { + listeners.remove(controller); + if (listeners.isEmpty) { + _streamListeners.remove(requestId); + _activeSubscriptions.remove(operationId); + + if (_channel != null) { + final cancelReq = StreamRequest( + requestId: requestId, + requestKind: RequestKind.cancel, + cancel: true, + ); + _channel!.sink.add(jsonEncode(cancelReq.toJson())); + } + } + } + }, + ); + + return controller.stream; + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/pubspec.yaml b/packages/firebase_data_connect/firebase_data_connect/pubspec.yaml index d0145d0290ac..3bd28b86b42c 100644 --- a/packages/firebase_data_connect/firebase_data_connect/pubspec.yaml +++ b/packages/firebase_data_connect/firebase_data_connect/pubspec.yaml @@ -27,6 +27,7 @@ dependencies: protobuf: ^3.1.0 sqlite3: ^2.9.0 sqlite3_flutter_libs: ^0.5.40 + web_socket_channel: ^3.0.1 dev_dependencies: build_runner: ^2.4.12 diff --git a/packages/firebase_data_connect/firebase_data_connect/test.dart b/packages/firebase_data_connect/firebase_data_connect/test.dart new file mode 100644 index 000000000000..6d5f7bb5cc72 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test.dart @@ -0,0 +1,12 @@ +abstract class Base { + void execute(); +} +class Derived extends Base { + @override + void execute({int arg = 0}) { + print(arg); + } +} +void main() { + Derived().execute(arg: 1); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/cache/cache_manager_test.mocks.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/cache/cache_manager_test.mocks.dart index 09f67581ad75..a9aadaeea02a 100644 --- a/packages/firebase_data_connect/firebase_data_connect/test/src/cache/cache_manager_test.mocks.dart +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/cache/cache_manager_test.mocks.dart @@ -1,17 +1,3 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - // Mocks generated by Mockito 5.4.6 from annotations // in firebase_data_connect/test/src/cache/cache_manager_test.dart. // Do not manually edit this file. @@ -39,7 +25,6 @@ import 'package:mockito/src/dummies.dart' as _i4; // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -// ignore_for_file: invalid_use_of_internal_member class _FakeFirebaseOptions_0 extends _i1.SmartFake implements _i2.FirebaseOptions { @@ -166,28 +151,28 @@ class MockConnectorConfig extends _i1.Mock implements _i6.ConnectorConfig { ) as String); @override - set location(String? value) => super.noSuchMethod( + set location(String? _location) => super.noSuchMethod( Invocation.setter( #location, - value, + _location, ), returnValueForMissingStub: null, ); @override - set connector(String? value) => super.noSuchMethod( + set connector(String? _connector) => super.noSuchMethod( Invocation.setter( #connector, - value, + _connector, ), returnValueForMissingStub: null, ); @override - set serviceId(String? value) => super.noSuchMethod( + set serviceId(String? _serviceId) => super.noSuchMethod( Invocation.setter( #serviceId, - value, + _serviceId, ), returnValueForMissingStub: null, ); diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/common/common_library_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/common/common_library_test.dart index 353f34aeec75..76fe916147c3 100644 --- a/packages/firebase_data_connect/firebase_data_connect/test/src/common/common_library_test.dart +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/common/common_library_test.dart @@ -166,4 +166,15 @@ class TestDataConnectTransport extends DataConnectTransport { // Simulate mutation invocation logic here return ServerResponse({}); } + + @override + Stream invokeStreamQuery( + String queryName, + Deserializer deserializer, + Serializer? serializer, + Variables? vars, + String? authToken, + ) { + return Stream.value(ServerResponse({})); + } } diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.mocks.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.mocks.dart index 06634cc3f1ec..fcb4c067acd1 100644 --- a/packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.mocks.dart +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.mocks.dart @@ -1,17 +1,3 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - // Mocks generated by Mockito 5.4.6 from annotations // in firebase_data_connect/test/src/firebase_data_connect_test.dart. // Do not manually edit this file. @@ -39,7 +25,6 @@ import 'package:mockito/src/dummies.dart' as _i4; // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -// ignore_for_file: invalid_use_of_internal_member class _FakeFirebaseOptions_0 extends _i1.SmartFake implements _i2.FirebaseOptions { @@ -166,28 +151,28 @@ class MockConnectorConfig extends _i1.Mock implements _i6.ConnectorConfig { ) as String); @override - set location(String? value) => super.noSuchMethod( + set location(String? _location) => super.noSuchMethod( Invocation.setter( #location, - value, + _location, ), returnValueForMissingStub: null, ); @override - set connector(String? value) => super.noSuchMethod( + set connector(String? _connector) => super.noSuchMethod( Invocation.setter( #connector, - value, + _connector, ), returnValueForMissingStub: null, ); @override - set serviceId(String? value) => super.noSuchMethod( + set serviceId(String? _serviceId) => super.noSuchMethod( Invocation.setter( #serviceId, - value, + _serviceId, ), returnValueForMissingStub: null, ); diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.mocks.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.mocks.dart index 625e092f5989..00ac6fc278e2 100644 --- a/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.mocks.dart +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.mocks.dart @@ -1,17 +1,3 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - // Mocks generated by Mockito 5.4.6 from annotations // in firebase_data_connect/test/src/network/rest_transport_test.dart. // Do not manually edit this file. @@ -45,7 +31,6 @@ import 'package:mockito/src/dummies.dart' as _i8; // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -// ignore_for_file: invalid_use_of_internal_member class _FakeResponse_0 extends _i1.SmartFake implements _i2.Response { _FakeResponse_0( @@ -754,10 +739,10 @@ class MockFirebaseAppCheck extends _i1.Mock implements _i10.FirebaseAppCheck { ) as _i6.Stream); @override - set app(_i5.FirebaseApp? value) => super.noSuchMethod( + set app(_i5.FirebaseApp? _app) => super.noSuchMethod( Invocation.setter( #app, - value, + _app, ), returnValueForMissingStub: null, ); From 70e43e68769409db61d66c0eceda5ad9b7010c54 Mon Sep 17 00:00:00 2001 From: Aashish Patil Date: Fri, 27 Mar 2026 11:30:23 -0700 Subject: [PATCH 2/6] Fix formatting and analyzer warnings --- .../example/test_stream.dart | 68 ---------------- .../lib/src/cache/cache_data_types.dart | 12 +-- .../lib/src/cache/result_tree_processor.dart | 2 +- .../lib/src/common/dataconnect_error.dart | 4 +- .../lib/src/core/ref.dart | 36 ++++----- .../lib/src/network/grpc_transport.dart | 6 +- .../lib/src/network/rest_library.dart | 1 - .../lib/src/network/rest_transport.dart | 3 +- .../lib/src/network/stream_protocol.dart | 8 +- .../lib/src/network/test_stream.dart | 45 ----------- .../lib/src/network/websocket_transport.dart | 78 +++++++++++-------- .../firebase_data_connect/test.dart | 12 --- .../test/src/cache/cache_manager_test.dart | 2 +- .../test/src/common/common_library_test.dart | 14 ++-- .../src/common/dataconnect_error_test.dart | 6 +- .../test/src/core/ref_test.dart | 20 ++--- .../test/src/network/rest_transport_test.dart | 16 ++-- 17 files changed, 108 insertions(+), 225 deletions(-) delete mode 100644 packages/firebase_data_connect/firebase_data_connect/example/test_stream.dart delete mode 100644 packages/firebase_data_connect/firebase_data_connect/lib/src/network/test_stream.dart delete mode 100644 packages/firebase_data_connect/firebase_data_connect/test.dart diff --git a/packages/firebase_data_connect/firebase_data_connect/example/test_stream.dart b/packages/firebase_data_connect/firebase_data_connect/example/test_stream.dart deleted file mode 100644 index 1b0a3bbae112..000000000000 --- a/packages/firebase_data_connect/firebase_data_connect/example/test_stream.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'dart:async'; -import 'package:firebase_data_connect/firebase_data_connect.dart'; - -// Since the user didn't specify project info, we use dummy data that hits the emulator. -void main() async { - print('Initializing test script for WebSocket emulator...'); - - // Create connector config - final connectorConfig = ConnectorConfig( - 'us-central1', - 'default', - 'default', - ); - - // We don't have a real Firebase app here, so we will manually instantiate - // the transport and connect. - final options = DataConnectOptions( - 'demo-project', - connectorConfig.location, - connectorConfig.connector, - connectorConfig.serviceId, - ); - - final transportOptions = TransportOptions('127.0.0.1', 9399, false); - - // We can use the core SDK type - final transport = getTransport( - transportOptions, - options, - 'test-app-id', - CallerSDKType.core, - null // no app check - ); - - final queryName = 'ListBlogs'; - - // Execute via REST for comparison - print('Trying to perform a standard unary executeQuery...'); - try { - final response = await transport.invokeQuery( - queryName, - (String s) => s, // dummy deserializer - (dynamic v) => '{}', // dummy serializer - null, - null - ); - print('Unary Response: \${response.data}'); - } catch (e) { - print('Unary Request Failed: \$e'); - } - - print('\\nInitiating Stream Subscribe for \$queryName...'); - final subscription = transport.invokeStreamQuery( - queryName, - (String s) => s, - (dynamic v) => '{}', - null, - null - ); - - subscription.listen((response) { - print('Received Pushed Data: \${response.data}'); - }, onError: (err) { - print('Stream Error: \$err'); - }, onDone: () { - print('Stream Closed.'); - }); -} diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/cache_data_types.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/cache_data_types.dart index 0768da15232c..9b991a946a79 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/cache_data_types.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/cache_data_types.dart @@ -332,11 +332,11 @@ class EntityNode { srcListMap.forEach((key, value) { List enodeList = []; List jsonList = value as List; - jsonList.forEach((jsonObj) { + for (var jsonObj in jsonList) { Map jmap = jsonObj as Map; EntityNode en = EntityNode.fromJson(jmap, cacheProvider); enodeList.add(en); - }); + } objLists?[key] = enodeList; }); } @@ -367,9 +367,9 @@ class EntityNode { if (nestedObjectLists != null) { nestedObjectLists!.forEach((key, edoList) { List> jsonList = []; - edoList.forEach((edo) { + for (var edo in edoList) { jsonList.add(edo.toJson(mode: mode)); - }); + } jsonData[key] = jsonList; }); } @@ -396,9 +396,9 @@ class EntityNode { Map nestedObjectListsJson = {}; nestedObjectLists!.forEach((key, edoList) { List> jsonList = []; - edoList.forEach((edo) { + for (var edo in edoList) { jsonList.add(edo.toJson(mode: mode)); - }); + } nestedObjectListsJson[key] = jsonList; }); jsonData[listsKey] = nestedObjectListsJson; diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/result_tree_processor.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/result_tree_processor.dart index 1b2652a24d63..ce4bf1bad24a 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/result_tree_processor.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/cache/result_tree_processor.dart @@ -29,7 +29,7 @@ class DehydrationResult { class ResultTreeProcessor { /// Takes a server response, traverses the data, creates or updates `EntityDataObject`s, /// and builds a dehydrated `EntityNode` tree. - Future dehydrateResults( + Future dehydrateResults( String queryId, Map serverResponse, CacheProvider cacheProvider, diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_error.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_error.dart index 43b7fd964418..309b0974d94b 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_error.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_error.dart @@ -36,9 +36,7 @@ class DataConnectError extends FirebaseException { /// Error thrown when an operation is partially successful. class DataConnectOperationError extends DataConnectError { - DataConnectOperationError( - DataConnectErrorCode code, String message, this.response) - : super(code, message); + DataConnectOperationError(super.code, String super.message, this.response); final DataConnectOperationFailureResponse response; } diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart index 01b155790461..84648df8e2c9 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart @@ -53,7 +53,7 @@ abstract class OperationRef { ); Variables? variables; String operationName; - DataConnectTransport _transport; + final DataConnectTransport _transport; Deserializer deserializer; Serializer serializer; String? _lastToken; @@ -192,8 +192,6 @@ class QueryManager { return streamController; } - - void dispose() { _impactedQueriesSubscription?.cancel(); } @@ -217,7 +215,7 @@ class QueryRef extends OperationRef { variables, ); - QueryManager _queryManager; + final QueryManager _queryManager; @override Future> execute( @@ -311,13 +309,16 @@ class QueryRef extends OperationRef { Stream> subscribe() { _streamController ??= _queryManager.addQuery(this); - - final stream = _streamController!.stream.cast>(); + + final stream = + _streamController!.stream.cast>(); // Return the stream to the caller, then execute fetches Future.microtask(() { if (dataConnect.cacheManager != null) { - _executeFromCache(QueryFetchPolicy.cacheOnly).then((_) {}).catchError((err) { + _executeFromCache(QueryFetchPolicy.cacheOnly) + .then((_) {}) + .catchError((err) { log("Error fetching from cache during subscribe $err"); // Ignore cache misses here, server stream will provide latest data }); @@ -378,20 +379,13 @@ class QueryRef extends OperationRef { class MutationRef extends OperationRef { MutationRef( - FirebaseDataConnect dataConnect, - String operationName, - DataConnectTransport transport, - Deserializer deserializer, - Serializer serializer, - Variables? variables, - ) : super( - dataConnect, - operationName, - transport, - deserializer, - serializer, - variables, - ); + super.dataConnect, + super.operationName, + super.transport, + super.deserializer, + super.serializer, + super.variables, + ); @override Future> execute() async { diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/grpc_transport.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/grpc_transport.dart index e0188085f29e..3ce72fe65e88 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/grpc_transport.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/grpc_transport.dart @@ -14,7 +14,8 @@ part of 'grpc_library.dart'; -@Deprecated('Use RestTransport and WebSocketTransport instead. The Data Connect SDK has moved away from gRPC.') +@Deprecated( + 'Use RestTransport and WebSocketTransport instead. The Data Connect SDK has moved away from gRPC.') class GRPCTransport implements DataConnectTransport { /// GRPCTransport creates a new channel GRPCTransport( @@ -177,7 +178,8 @@ class GRPCTransport implements DataConnectTransport { Variables? vars, String? token, ) { - throw UnsupportedError('Streaming should be routed through WebSocketTransport'); + throw UnsupportedError( + 'Streaming should be routed through WebSocketTransport'); } } diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_library.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_library.dart index 5aa255986ddd..e269fa840de8 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_library.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_library.dart @@ -21,6 +21,5 @@ import 'package:http/http.dart' as http; import '../common/common_library.dart'; import '../dataconnect_version.dart'; -import 'transport_library.dart'; part 'rest_transport.dart'; diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart index d78a7cb71815..ec8b87103aea 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart @@ -194,7 +194,8 @@ class RestTransport implements DataConnectTransport { Variables? vars, String? token, ) { - throw UnsupportedError('Streaming should be routed through WebSocketTransport'); + throw UnsupportedError( + 'Streaming should be routed through WebSocketTransport'); } } diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/stream_protocol.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/stream_protocol.dart index 67b264d0f439..f9c84d2869ac 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/stream_protocol.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/stream_protocol.dart @@ -40,7 +40,7 @@ class ExecuteRequest { /// Request to resume a query. class ResumeRequest { ResumeRequest(); - + Map toJson() { return {}; } @@ -85,7 +85,7 @@ class StreamRequest { /// Execute a Data Connect query or mutation. final ExecuteRequest? execute; - + /// Resume a query. final ResumeRequest? resume; @@ -152,9 +152,9 @@ class StreamResponse { ] }; } - + List? errorsList = json['errors'] as List?; - + return StreamResponse( requestId: json['requestId'] as String?, data: json['data'] as Map?, diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/test_stream.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/test_stream.dart deleted file mode 100644 index 699ebd19f958..000000000000 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/test_stream.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'dart:async'; -import 'package:firebase_data_connect/src/common/common_library.dart'; -import 'package:firebase_data_connect/src/network/rest_library.dart'; - -void main() async { - print('Initializing test script for WebSocket emulator...'); - - final options = DataConnectOptions( - 'demo-project', - 'us-central1', - 'default', - 'default', - ); - - final transportOptions = TransportOptions('127.0.0.1', 9399, false); - - final transport = getTransport( - transportOptions, - options, - 'test-app-id', - CallerSDKType.core, - null - ); - - final queryName = 'ListBlogs'; - - print('\nInitiating Stream Subscribe for $queryName...'); - final subscription = transport.invokeStreamQuery( - queryName, - (String s) => s, - (dynamic v) => '{}', - null, - null - ); - - subscription.listen((response) { - print('Received Pushed Data: ${response.data}'); - }, onError: (err) { - print('Stream Error: $err'); - }, onDone: () { - print('Stream Closed.'); - }); - - await Future.delayed(Duration(seconds: 10)); -} diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/websocket_transport.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/websocket_transport.dart index 927fb85defeb..ea078e939349 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/websocket_transport.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/websocket_transport.dart @@ -34,7 +34,7 @@ class WebSocketTransport implements DataConnectTransport { int port = transportOptions.port ?? 443; String location = options.location; _url = '$protocol://$host:$port/v1/Connect/locations/$location'; - + _currentUid = auth?.currentUser?.uid; _authSubscription = auth?.idTokenChanges().listen((user) async { final newUid = user?.uid; @@ -50,7 +50,7 @@ class WebSocketTransport implements DataConnectTransport { authToken: token, ); _channel!.sink.add(jsonEncode(request.toJson())); - } catch (e) { + } catch (_) { // Ignored } } @@ -60,8 +60,8 @@ class WebSocketTransport implements DataConnectTransport { FirebaseAuth? auth; String? _currentUid; - StreamSubscription? _authSubscription; - + // ignore: unused_field + StreamSubscription? _authSubscription; //required to hold reference @override FirebaseAppCheck? appCheck; @@ -81,10 +81,12 @@ class WebSocketTransport implements DataConnectTransport { String appId; WebSocketChannel? _channel; + // ignore: unused_field StreamSubscription? _channelSubscription; // Active listeners for stream subscriptions mapped by requestId. - final Map>> _streamListeners = {}; + final Map>> _streamListeners = + {}; // Active completers for unary operations mapped by requestId. final Map>> _unaryListeners = {}; @@ -120,11 +122,11 @@ class WebSocketTransport implements DataConnectTransport { Future _ensureConnected(String? authToken) async { if (_channel != null) return; - + String? appCheckToken; try { appCheckToken = await appCheck?.getToken(); - } catch (e) { + } catch (_) { // Ignored } @@ -138,7 +140,8 @@ class WebSocketTransport implements DataConnectTransport { ); final initRequest = StreamRequest( - name: 'projects/${options.projectId}/locations/${options.location}/services/${options.serviceId}/connectors/${options.connector}', + name: + 'projects/${options.projectId}/locations/${options.location}/services/${options.serviceId}/connectors/${options.connector}', headers: headers, ); _channel!.sink.add(jsonEncode(initRequest.toJson())); @@ -167,21 +170,21 @@ class WebSocketTransport implements DataConnectTransport { if (_unaryListeners.containsKey(requestId)) { final completers = _unaryListeners.remove(requestId)!; - for (var completer in completers) { + for (final completer in completers) { completer.complete(serverResponse); } - } - + } + if (_streamListeners.containsKey(requestId)) { final controllers = _streamListeners[requestId]!; if (response.cancelled == true) { - for (var controller in controllers) { + for (final controller in controllers) { controller.close(); } _streamListeners.remove(requestId); _activeSubscriptions.removeWhere((key, value) => value == requestId); } else { - for (var controller in controllers) { + for (final controller in controllers) { controller.add(serverResponse); } } @@ -193,12 +196,17 @@ class WebSocketTransport implements DataConnectTransport { } void _onError(dynamic error) { - final e = DataConnectError(DataConnectErrorCode.other, 'WebSocket error: $error'); + final e = + DataConnectError(DataConnectErrorCode.other, 'WebSocket error: $error'); for (final completers in _unaryListeners.values) { - for (var completer in completers) completer.completeError(e); + for (final completer in completers) { + completer.completeError(e); + } } for (final controllers in _streamListeners.values) { - for (var controller in controllers) controller.addError(e); + for (final controller in controllers) { + controller.addError(e); + } } _unaryListeners.clear(); _streamListeners.clear(); @@ -213,7 +221,9 @@ class WebSocketTransport implements DataConnectTransport { void _onDone() { _channel = null; for (final controllers in _streamListeners.values) { - for (var controller in controllers) controller.close(); + for (final controller in controllers) { + controller.close(); + } } _unaryListeners.clear(); _streamListeners.clear(); @@ -228,7 +238,8 @@ class WebSocketTransport implements DataConnectTransport { Variables? vars, String? authToken, ) async { - return _invokeUnary(queryName, deserializer, serializer, vars, authToken, RequestKind.execute); + return _invokeUnary(queryName, deserializer, serializer, vars, authToken, + RequestKind.execute); } @override @@ -239,7 +250,8 @@ class WebSocketTransport implements DataConnectTransport { Variables? vars, String? authToken, ) async { - return _invokeUnary(queryName, deserializer, serializer, vars, authToken, RequestKind.execute); + return _invokeUnary(queryName, deserializer, serializer, vars, authToken, + RequestKind.execute); } Future _invokeUnary( @@ -252,20 +264,21 @@ class WebSocketTransport implements DataConnectTransport { ) async { await _ensureConnected(authToken); - final operationId = OperationRef.createOperationId(operationName, vars, serializer); + final operationId = + OperationRef.createOperationId(operationName, vars, serializer); final completer = Completer(); if (_activeSubscriptions.containsKey(operationId)) { final existingRequestId = _activeSubscriptions[operationId]!; _unaryListeners.putIfAbsent(existingRequestId, () => []).add(completer); - + String? appCheckToken; try { appCheckToken = await appCheck?.getToken(); - } catch (e) { + } catch (_) { // Ignored } - + final headers = _buildHeaders(authToken, appCheckToken); final request = StreamRequest( @@ -277,7 +290,7 @@ class WebSocketTransport implements DataConnectTransport { headers: headers, ); _channel!.sink.add(jsonEncode(request.toJson())); - + return completer.future; } @@ -292,7 +305,7 @@ class WebSocketTransport implements DataConnectTransport { String? appCheckToken; try { appCheckToken = await appCheck?.getToken(); - } catch (e) { + } catch (_) { // Ignored } @@ -321,15 +334,18 @@ class WebSocketTransport implements DataConnectTransport { String? authToken, ) { late StreamController controller; - final operationId = OperationRef.createOperationId(queryName, vars, serializer); + final operationId = + OperationRef.createOperationId(queryName, vars, serializer); controller = StreamController( onListen: () async { await _ensureConnected(authToken); - + if (_activeSubscriptions.containsKey(operationId)) { final existingRequestId = _activeSubscriptions[operationId]!; - _streamListeners.putIfAbsent(existingRequestId, () => []).add(controller); + _streamListeners + .putIfAbsent(existingRequestId, () => []) + .add(controller); return; } @@ -345,7 +361,7 @@ class WebSocketTransport implements DataConnectTransport { String? appCheckToken; try { appCheckToken = await appCheck?.getToken(); - } catch (e) { + } catch (_) { // Ignored } @@ -365,14 +381,14 @@ class WebSocketTransport implements DataConnectTransport { onCancel: () { if (!_activeSubscriptions.containsKey(operationId)) return; final requestId = _activeSubscriptions[operationId]!; - + final listeners = _streamListeners[requestId]; if (listeners != null) { listeners.remove(controller); if (listeners.isEmpty) { _streamListeners.remove(requestId); _activeSubscriptions.remove(operationId); - + if (_channel != null) { final cancelReq = StreamRequest( requestId: requestId, diff --git a/packages/firebase_data_connect/firebase_data_connect/test.dart b/packages/firebase_data_connect/firebase_data_connect/test.dart deleted file mode 100644 index 6d5f7bb5cc72..000000000000 --- a/packages/firebase_data_connect/firebase_data_connect/test.dart +++ /dev/null @@ -1,12 +0,0 @@ -abstract class Base { - void execute(); -} -class Derived extends Base { - @override - void execute({int arg = 0}) { - print(arg); - } -} -void main() { - Derived().execute(arg: 1); -} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/cache/cache_manager_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/cache/cache_manager_test.dart index dec53744b7e3..6848824996c4 100644 --- a/packages/firebase_data_connect/firebase_data_connect/test/src/cache/cache_manager_test.dart +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/cache/cache_manager_test.dart @@ -233,7 +233,7 @@ void main() { }); test('maxAge conformance', () async { - final deserializer = (String data) => 'Deserialized Data'; + String deserializer(String data) => 'Deserialized Data'; final mockResponseSuccess = http.Response('{"success": true}', 200); if (dataConnect.cacheManager == null) { diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/common/common_library_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/common/common_library_test.dart index 76fe916147c3..2d526f0ef8c0 100644 --- a/packages/firebase_data_connect/firebase_data_connect/test/src/common/common_library_test.dart +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/common/common_library_test.dart @@ -103,7 +103,7 @@ void main() { test('should handle invokeQuery with proper deserializer', () async { const queryName = 'testQuery'; - final deserializer = (json) => json; + deserializer(json) => json; final result = await transport.invokeQuery( queryName, deserializer, @@ -117,7 +117,7 @@ void main() { test('should handle invokeMutation with proper deserializer', () async { const queryName = 'testMutation'; - final deserializer = (json) => json; + deserializer(json) => json; final result = await transport.invokeMutation( queryName, deserializer, @@ -134,12 +134,12 @@ void main() { // Test class extending DataConnectTransport for testing purposes class TestDataConnectTransport extends DataConnectTransport { TestDataConnectTransport( - TransportOptions transportOptions, - DataConnectOptions options, - String appId, - CallerSDKType sdkType, { + super.transportOptions, + super.options, + super.appId, + super.sdkType, { FirebaseAppCheck? appCheck, - }) : super(transportOptions, options, appId, sdkType) { + }) { this.appCheck = appCheck; } diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/common/dataconnect_error_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/common/dataconnect_error_test.dart index 3cb6c9ce26c8..5735f324f3b4 100644 --- a/packages/firebase_data_connect/firebase_data_connect/test/src/common/dataconnect_error_test.dart +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/common/dataconnect_error_test.dart @@ -79,8 +79,7 @@ void main() { group('Serializer and Deserializer', () { test('should serialize variables into string format', () { - Serializer> serializer = - (Map vars) => vars.toString(); + String serializer(Map vars) => vars.toString(); final inputVars = {'key1': 'value1', 'key2': 123}; final serializedString = serializer(inputVars); @@ -89,8 +88,7 @@ void main() { }); test('should deserialize string data into expected format', () { - Deserializer> deserializer = - (String data) => {'data': data}; + deserializer(String data) => {'data': data}; const inputData = '{"message": "Hello World"}'; final deserializedData = deserializer(inputData); diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/core/ref_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/core/ref_test.dart index 636cae0cde52..4e615d7ab8f1 100644 --- a/packages/firebase_data_connect/firebase_data_connect/test/src/core/ref_test.dart +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/core/ref_test.dart @@ -82,7 +82,7 @@ void main() { test( 'addQuery should create a new StreamController if query does not exist', () { - final deserializer = (String data) => 'Deserialized Data'; + String deserializer(String data) => 'Deserialized Data'; String varSerializer(Object? _) { return 'varsAsStr'; } @@ -149,7 +149,7 @@ void main() { mockDataConnect.transport = transport; }); test('executeQuery should gracefully handle getIdToken failures', () async { - final deserializer = (String data) => 'Deserialized Data'; + String deserializer(String data) => 'Deserialized Data'; final mockResponseSuccess = http.Response('{"success": true}', 200); when(mockUser.getIdToken()).thenThrow(Exception('Auth error')); QueryRef ref = QueryRef( @@ -175,7 +175,7 @@ void main() { () async { final mockResponse = http.Response('{"error": "Unauthorized"}', 401); final mockResponseSuccess = http.Response('{"success": true}', 200); - final deserializer = (String data) => 'Deserialized Data'; + String deserializer(String data) => 'Deserialized Data'; int count = 0; int idTokenCount = 0; QueryRef ref = QueryRef( @@ -219,7 +219,7 @@ void main() { }); test('throw Error if server throws one', () { - final deserializer = (String data) => 'Deserialized Data'; + String deserializer(String data) => 'Deserialized Data'; final mockResponse = http.Response( ''' { @@ -285,10 +285,10 @@ void main() { ), ).thenAnswer((_) async => mockResponse); - final deserializer = (String data) { + AbcHolder deserializer(String data) { Map decoded = jsonDecode(data) as Map; return AbcHolder(decoded['abc']!); - }; + } QueryRef ref = QueryRef( mockDataConnect, @@ -339,10 +339,10 @@ void main() { ), ).thenAnswer((_) async => mockResponse); - final deserializer = (String data) { + AbcHolder deserializer(String data) { Map decoded = jsonDecode(data) as Map; return AbcHolder(decoded['abc']!); - }; + } QueryRef ref = QueryRef( mockDataConnect, @@ -390,10 +390,10 @@ void main() { ), ).thenAnswer((_) async => mockResponse); - final deserializer = (String data) { + AbcHolder deserializer(String data) { Map decoded = jsonDecode(data) as Map; return AbcHolder(decoded['abc']!); - }; + } QueryRef ref = QueryRef( mockDataConnect, diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.dart index bad97d364d32..d5325e89faab 100644 --- a/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.dart +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.dart @@ -98,7 +98,7 @@ void main() { ), ).thenAnswer((_) async => mockResponse); - final deserializer = (String data) => 'Deserialized Data'; + String deserializer(String data) => 'Deserialized Data'; expect( () => transport.invokeOperation( @@ -124,7 +124,7 @@ void main() { ), ).thenAnswer((_) async => mockResponse); - final deserializer = (String data) => 'Deserialized Data'; + String deserializer(String data) => 'Deserialized Data'; expect( () => transport.invokeOperation( @@ -150,7 +150,7 @@ void main() { ), ).thenAnswer((_) async => mockResponse); - final deserializer = (String data) => 'Deserialized Data'; + String deserializer(String data) => 'Deserialized Data'; await transport.invokeQuery('testQuery', deserializer, null, null, null); @@ -178,7 +178,7 @@ void main() { ), ).thenAnswer((_) async => mockResponse); - final deserializer = (String data) => 'Deserialized Mutation Data'; + String deserializer(String data) => 'Deserialized Mutation Data'; await transport.invokeMutation( 'testMutation', @@ -215,7 +215,7 @@ void main() { when(mockUser.getIdToken()).thenAnswer((_) async => 'authToken123'); when(mockAppCheck.getToken()).thenAnswer((_) async => 'appCheckToken123'); - final deserializer = (String data) => 'Deserialized Data'; + String deserializer(String data) => 'Deserialized Data'; await transport.invokeOperation( 'testQuery', @@ -250,7 +250,7 @@ void main() { when(mockUser.getIdToken()).thenAnswer((_) async => 'authToken123'); when(mockAppCheck.getToken()).thenAnswer((_) async => 'appCheckToken123'); - final deserializer = (String data) => 'Deserialized Data'; + String deserializer(String data) => 'Deserialized Data'; await transport.invokeOperation( 'testQuery', @@ -297,7 +297,7 @@ void main() { ), ).thenAnswer((_) async => mockResponse); - final deserializer = (String data) => 'Deserialized Data'; + String deserializer(String data) => 'Deserialized Data'; final result = await transport.invokeOperation( 'testQuery', @@ -327,7 +327,7 @@ void main() { when(mockUser.getIdToken()).thenThrow(Exception('Auth error')); when(mockAppCheck.getToken()).thenThrow(Exception('AppCheck error')); - final deserializer = (String data) => 'Deserialized Data'; + String deserializer(String data) => 'Deserialized Data'; await transport.invokeOperation( 'testQuery', From 83a43c0d5fcac2bb2d925620190abf0facf068ad Mon Sep 17 00:00:00 2001 From: Aashish Patil Date: Fri, 27 Mar 2026 12:29:55 -0700 Subject: [PATCH 3/6] Fix tests and licenses --- .../firebase_data_connect/lib/src/core/ref.dart | 2 +- .../lib/src/network/stream_protocol.dart | 2 +- .../lib/src/network/transport_library.dart | 2 +- .../lib/src/network/transport_stub.dart | 2 +- .../lib/src/network/websocket_transport.dart | 2 +- .../test/src/cache/cache_manager_test.mocks.dart | 16 +++++++++++++--- .../test/src/firebase_data_connect_test.dart | 11 ++++++++++- .../src/firebase_data_connect_test.mocks.dart | 16 +++++++++++++--- .../src/network/rest_transport_test.mocks.dart | 16 +++++++++++++--- 9 files changed, 54 insertions(+), 15 deletions(-) diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart index 84648df8e2c9..f128743c63ea 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart @@ -1,4 +1,4 @@ -// Copyright 2024 Google LLC +// Copyright 2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/stream_protocol.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/stream_protocol.dart index f9c84d2869ac..f64ee0bad44b 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/stream_protocol.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/stream_protocol.dart @@ -1,4 +1,4 @@ -// Copyright 2024 Google LLC +// Copyright 2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/transport_library.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/transport_library.dart index 3d69eb55be67..3b851c62989b 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/transport_library.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/transport_library.dart @@ -1,4 +1,4 @@ -// Copyright 2024 Google LLC +// Copyright 2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/transport_stub.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/transport_stub.dart index c20f4ff64fb4..a0083e0253d6 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/transport_stub.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/transport_stub.dart @@ -1,4 +1,4 @@ -// Copyright 2024 Google LLC +// Copyright 2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/websocket_transport.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/websocket_transport.dart index ea078e939349..ed33cac83e53 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/websocket_transport.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/websocket_transport.dart @@ -1,4 +1,4 @@ -// Copyright 2024 Google LLC +// Copyright 2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/cache/cache_manager_test.mocks.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/cache/cache_manager_test.mocks.dart index a9aadaeea02a..a99e554cb2d7 100644 --- a/packages/firebase_data_connect/firebase_data_connect/test/src/cache/cache_manager_test.mocks.dart +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/cache/cache_manager_test.mocks.dart @@ -1,6 +1,16 @@ -// Mocks generated by Mockito 5.4.6 from annotations -// in firebase_data_connect/test/src/cache/cache_manager_test.dart. -// Do not manually edit this file. +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.dart index 2deb28f22749..b4edc25b2a81 100644 --- a/packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.dart +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.dart @@ -25,7 +25,16 @@ import 'package:mockito/mockito.dart'; @GenerateNiceMocks([MockSpec(), MockSpec()]) import 'firebase_data_connect_test.mocks.dart'; -class MockFirebaseAuth extends Mock implements FirebaseAuth {} +class MockFirebaseAuth extends Mock implements FirebaseAuth { + @override + Stream idTokenChanges() { + return super.noSuchMethod( + Invocation.method(#idTokenChanges, []), + returnValue: const Stream.empty(), + returnValueForMissingStub: const Stream.empty(), + ) as Stream; + } +} class MockFirebaseAppCheck extends Mock implements FirebaseAppCheck {} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.mocks.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.mocks.dart index fcb4c067acd1..a99e554cb2d7 100644 --- a/packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.mocks.dart +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.mocks.dart @@ -1,6 +1,16 @@ -// Mocks generated by Mockito 5.4.6 from annotations -// in firebase_data_connect/test/src/firebase_data_connect_test.dart. -// Do not manually edit this file. +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.mocks.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.mocks.dart index 00ac6fc278e2..68cd5cbf2f92 100644 --- a/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.mocks.dart +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.mocks.dart @@ -1,6 +1,16 @@ -// Mocks generated by Mockito 5.4.6 from annotations -// in firebase_data_connect/test/src/network/rest_transport_test.dart. -// Do not manually edit this file. +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i6; From 50a192122e51b42d98fb57cba6a21f835657e1da Mon Sep 17 00:00:00 2001 From: Aashish Patil Date: Fri, 27 Mar 2026 12:44:06 -0700 Subject: [PATCH 4/6] Denver feedback: var initialization best practice --- .../lib/src/network/websocket_transport.dart | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/websocket_transport.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/websocket_transport.dart index ed33cac83e53..03daed7602c0 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/websocket_transport.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/websocket_transport.dart @@ -26,14 +26,17 @@ class WebSocketTransport implements DataConnectTransport { this.appCheck, [ this.auth, ]) { - String protocol = 'ws'; - if (transportOptions.isSecure ?? true) { - protocol += 's'; - } - String host = transportOptions.host; - int port = transportOptions.port ?? 443; - String location = options.location; - _url = '$protocol://$host:$port/v1/Connect/locations/$location'; + final protocol = (transportOptions.isSecure ?? true) ? 'wss' : 'ws'; + final host = transportOptions.host; + final port = transportOptions.port ?? 443; + final location = options.location; + + _url = Uri( + scheme: protocol, + host: host, + port: port, + path: '/v1/Connect/locations/$location', + ).toString(); _currentUid = auth?.currentUser?.uid; _authSubscription = auth?.idTokenChanges().listen((user) async { From 567fb8cc745c569bf588c764e64e36a6c841f2b9 Mon Sep 17 00:00:00 2001 From: Aashish Patil Date: Fri, 27 Mar 2026 13:13:53 -0700 Subject: [PATCH 5/6] sorted keys for id generation --- .../lib/src/core/ref.dart | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart index f128743c63ea..7a5fd8c2e198 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart @@ -60,10 +60,30 @@ abstract class OperationRef { FirebaseDataConnect dataConnect; + static dynamic _sortKeys(dynamic value) { + if (value is Map) { + final sortedMap = {}; + final sortedKeys = value.keys.toList()..sort(); + for (final key in sortedKeys) { + sortedMap[key.toString()] = _sortKeys(value[key]); + } + return sortedMap; + } else if (value is List) { + return value.map(_sortKeys).toList(); + } + return value; + } + static String createOperationId(String operationName, Variables? vars, Serializer? serializer) { if (vars != null && serializer != null) { - return '$operationName::${serializer(vars)}'; + try { + final decoded = jsonDecode(serializer(vars)); + final sortedStr = jsonEncode(_sortKeys(decoded)); + return '$operationName::$sortedStr'; + } catch (_) { + return '$operationName::${serializer(vars)}'; + } } else { return operationName; } From 80039473918cf891f5237cab7d41fe127c9ea095 Mon Sep 17 00:00:00 2001 From: Aashish Patil Date: Fri, 27 Mar 2026 14:52:31 -0700 Subject: [PATCH 6/6] Fix analyze info messages --- .../lib/src/generated/google/protobuf/duration.pb.dart | 1 + .../lib/src/generated/google/protobuf/struct.pb.dart | 1 + .../firebase_data_connect/lib/src/network/grpc_transport.dart | 1 + 3 files changed, 3 insertions(+) diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/duration.pb.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/duration.pb.dart index 4bcbcd32a4c2..6c32fb50221c 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/duration.pb.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/duration.pb.dart @@ -1,3 +1,4 @@ +// ignore_for_file: implementation_imports // // Generated code. Do not modify. // source: google/protobuf/duration.proto diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pb.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pb.dart index 42d55e426602..42164fbc928f 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pb.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/generated/google/protobuf/struct.pb.dart @@ -1,3 +1,4 @@ +// ignore_for_file: implementation_imports // // Generated code. Do not modify. // source: google/protobuf/struct.proto diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/grpc_transport.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/grpc_transport.dart index 3ce72fe65e88..298fb25cfb4e 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/grpc_transport.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/grpc_transport.dart @@ -1,3 +1,4 @@ +// ignore_for_file: deprecated_member_use_from_same_package // Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License");