Skip to content

Commit 56f8f5b

Browse files
feat(fdc): Happy Path Implementation (#18151)
* Initial Commit * Fix formatting and analyzer warnings * Fix tests and licenses * Denver feedback: var initialization best practice * sorted keys for id generation * Fix analyze info messages
1 parent 81f3032 commit 56f8f5b

25 files changed

Lines changed: 947 additions & 112 deletions
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# This file tracks properties of this Flutter project.
2+
# Used by Flutter tool to assess capabilities and perform upgrades etc.
3+
#
4+
# This file should be version controlled and should not be manually edited.
5+
6+
version:
7+
revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2"
8+
channel: "stable"
9+
10+
project_type: app
11+
12+
# Tracks metadata for the flutter migrate command
13+
migration:
14+
platforms:
15+
- platform: root
16+
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
17+
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
18+
- platform: macos
19+
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
20+
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
21+
22+
# User provided section
23+
24+
# List of Local paths (relative to this file) that should be
25+
# ignored by the migrate tool.
26+
#
27+
# Files that are not part of the templates will be ignored by default.
28+
unmanaged_files:
29+
- 'lib/main.dart'
30+
- 'ios/Runner.xcodeproj/project.pbxproj'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include: package:flutter_lints/flutter.yaml

packages/firebase_data_connect/firebase_data_connect/lib/src/cache/cache_data_types.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -332,11 +332,11 @@ class EntityNode {
332332
srcListMap.forEach((key, value) {
333333
List<EntityNode> enodeList = [];
334334
List<dynamic> jsonList = value as List<dynamic>;
335-
jsonList.forEach((jsonObj) {
335+
for (var jsonObj in jsonList) {
336336
Map<String, dynamic> jmap = jsonObj as Map<String, dynamic>;
337337
EntityNode en = EntityNode.fromJson(jmap, cacheProvider);
338338
enodeList.add(en);
339-
});
339+
}
340340
objLists?[key] = enodeList;
341341
});
342342
}
@@ -367,9 +367,9 @@ class EntityNode {
367367
if (nestedObjectLists != null) {
368368
nestedObjectLists!.forEach((key, edoList) {
369369
List<Map<String, dynamic>> jsonList = [];
370-
edoList.forEach((edo) {
370+
for (var edo in edoList) {
371371
jsonList.add(edo.toJson(mode: mode));
372-
});
372+
}
373373
jsonData[key] = jsonList;
374374
});
375375
}
@@ -396,9 +396,9 @@ class EntityNode {
396396
Map<String, dynamic> nestedObjectListsJson = {};
397397
nestedObjectLists!.forEach((key, edoList) {
398398
List<Map<String, dynamic>> jsonList = [];
399-
edoList.forEach((edo) {
399+
for (var edo in edoList) {
400400
jsonList.add(edo.toJson(mode: mode));
401-
});
401+
}
402402
nestedObjectListsJson[key] = jsonList;
403403
});
404404
jsonData[listsKey] = nestedObjectListsJson;

packages/firebase_data_connect/firebase_data_connect/lib/src/common/common_library.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,13 @@ abstract class DataConnectTransport {
117117
Variables? vars,
118118
String? token,
119119
);
120+
121+
/// Invokes corresponding stream query endpoint.
122+
Stream<ServerResponse> invokeStreamQuery<Data, Variables>(
123+
String queryName,
124+
Deserializer<Data> deserializer,
125+
Serializer<Variables> serializer,
126+
Variables? vars,
127+
String? token,
128+
);
120129
}

packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_error.dart

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,7 @@ class DataConnectError extends FirebaseException {
3636

3737
/// Error thrown when an operation is partially successful.
3838
class DataConnectOperationError<T> extends DataConnectError {
39-
DataConnectOperationError(
40-
DataConnectErrorCode code, String message, this.response)
41-
: super(code, message);
39+
DataConnectOperationError(super.code, String super.message, this.response);
4240
final DataConnectOperationFailureResponse<T> response;
4341
}
4442

packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart

Lines changed: 94 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 Google LLC
1+
// Copyright 2026 Google LLC
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -53,15 +53,43 @@ abstract class OperationRef<Data, Variables> {
5353
);
5454
Variables? variables;
5555
String operationName;
56-
DataConnectTransport _transport;
56+
final DataConnectTransport _transport;
5757
Deserializer<Data> deserializer;
5858
Serializer<Variables> serializer;
5959
String? _lastToken;
6060

6161
FirebaseDataConnect dataConnect;
6262

63-
Future<OperationResult<Data, Variables>> execute(
64-
{QueryFetchPolicy fetchPolicy = QueryFetchPolicy.preferCache});
63+
static dynamic _sortKeys(dynamic value) {
64+
if (value is Map) {
65+
final sortedMap = <String, dynamic>{};
66+
final sortedKeys = value.keys.toList()..sort();
67+
for (final key in sortedKeys) {
68+
sortedMap[key.toString()] = _sortKeys(value[key]);
69+
}
70+
return sortedMap;
71+
} else if (value is List) {
72+
return value.map(_sortKeys).toList();
73+
}
74+
return value;
75+
}
76+
77+
static String createOperationId<Variables>(String operationName,
78+
Variables? vars, Serializer<Variables>? serializer) {
79+
if (vars != null && serializer != null) {
80+
try {
81+
final decoded = jsonDecode(serializer(vars));
82+
final sortedStr = jsonEncode(_sortKeys(decoded));
83+
return '$operationName::$sortedStr';
84+
} catch (_) {
85+
return '$operationName::${serializer(vars)}';
86+
}
87+
} else {
88+
return operationName;
89+
}
90+
}
91+
92+
Future<OperationResult<Data, Variables>> execute();
6593

6694
Future<bool> _shouldRetry() async {
6795
String? newToken;
@@ -184,15 +212,6 @@ class QueryManager {
184212
return streamController;
185213
}
186214

187-
static String createQueryId<QueryVariables>(String queryName,
188-
QueryVariables? vars, Serializer<QueryVariables> varSerializer) {
189-
if (vars != null) {
190-
return '$queryName::${varSerializer(vars)}';
191-
} else {
192-
return queryName;
193-
}
194-
}
195-
196215
void dispose() {
197216
_impactedQueriesSubscription?.cancel();
198217
}
@@ -216,7 +235,7 @@ class QueryRef<Data, Variables> extends OperationRef<Data, Variables> {
216235
variables,
217236
);
218237

219-
QueryManager _queryManager;
238+
final QueryManager _queryManager;
220239

221240
@override
222241
Future<QueryResult<Data, Variables>> execute(
@@ -240,7 +259,7 @@ class QueryRef<Data, Variables> extends OperationRef<Data, Variables> {
240259
}
241260

242261
String get _queryId =>
243-
QueryManager.createQueryId(operationName, variables, serializer);
262+
OperationRef.createOperationId(operationName, variables, serializer);
244263

245264
Future<QueryResult<Data, Variables>> _executeFromCache(
246265
QueryFetchPolicy fetchPolicy) async {
@@ -311,9 +330,58 @@ class QueryRef<Data, Variables> extends OperationRef<Data, Variables> {
311330
Stream<QueryResult<Data, Variables>> subscribe() {
312331
_streamController ??= _queryManager.addQuery(this);
313332

314-
execute();
333+
final stream =
334+
_streamController!.stream.cast<QueryResult<Data, Variables>>();
335+
336+
// Return the stream to the caller, then execute fetches
337+
Future.microtask(() {
338+
if (dataConnect.cacheManager != null) {
339+
_executeFromCache(QueryFetchPolicy.cacheOnly)
340+
.then((_) {})
341+
.catchError((err) {
342+
log("Error fetching from cache during subscribe $err");
343+
// Ignore cache misses here, server stream will provide latest data
344+
});
345+
}
346+
347+
// Initiate Web Socket stream
348+
_streamFromServer();
349+
});
350+
351+
return stream;
352+
}
353+
354+
void _streamFromServer() async {
355+
bool shouldRetry = await _shouldRetry();
356+
try {
357+
final stream = _transport.invokeStreamQuery<Data, Variables>(
358+
operationName,
359+
deserializer,
360+
serializer,
361+
variables,
362+
_lastToken,
363+
);
364+
365+
await for (final serverResponse in stream) {
366+
if (dataConnect.cacheManager != null) {
367+
await dataConnect.cacheManager!.update(_queryId, serverResponse);
368+
}
369+
Data typedData = _convertBodyJsonToData(serverResponse.data);
315370

316-
return _streamController!.stream.cast<QueryResult<Data, Variables>>();
371+
QueryResult<Data, Variables> res =
372+
QueryResult(dataConnect, typedData, DataSource.server, this);
373+
publishResultToStream(res);
374+
}
375+
} on DataConnectError catch (e) {
376+
if (shouldRetry &&
377+
e.code == DataConnectErrorCode.unauthorized.toString()) {
378+
_streamFromServer();
379+
} else {
380+
publishErrorToStream(e);
381+
}
382+
} catch (e) {
383+
publishErrorToStream(e as Error);
384+
}
317385
}
318386

319387
void publishResultToStream(QueryResult<Data, Variables> result) {
@@ -322,7 +390,7 @@ class QueryRef<Data, Variables> extends OperationRef<Data, Variables> {
322390
}
323391
}
324392

325-
void publishErrorToStream(Error err) {
393+
void publishErrorToStream(Object err) {
326394
if (_streamController != null) {
327395
_streamController?.addError(err);
328396
}
@@ -331,24 +399,16 @@ class QueryRef<Data, Variables> extends OperationRef<Data, Variables> {
331399

332400
class MutationRef<Data, Variables> extends OperationRef<Data, Variables> {
333401
MutationRef(
334-
FirebaseDataConnect dataConnect,
335-
String operationName,
336-
DataConnectTransport transport,
337-
Deserializer<Data> deserializer,
338-
Serializer<Variables> serializer,
339-
Variables? variables,
340-
) : super(
341-
dataConnect,
342-
operationName,
343-
transport,
344-
deserializer,
345-
serializer,
346-
variables,
347-
);
402+
super.dataConnect,
403+
super.operationName,
404+
super.transport,
405+
super.deserializer,
406+
super.serializer,
407+
super.variables,
408+
);
348409

349410
@override
350-
Future<OperationResult<Data, Variables>> execute(
351-
{QueryFetchPolicy fetchPolicy = QueryFetchPolicy.serverOnly}) async {
411+
Future<OperationResult<Data, Variables>> execute() async {
352412
bool shouldRetry = await _shouldRetry();
353413
try {
354414
// Logic below is duplicated due to the fact that `executeOperation` returns

0 commit comments

Comments
 (0)