message: Preparing socket connection auto reconnect and keep alive.

diff:
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
- 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; };
3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
- 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };
4A9CD12FDAA2BE6D4AEA7ACE /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; };
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
- 2D5378251FAA1A9400D5DBA9 /* flutter_assets */,
3B80C3931E831B6300D905FE /* App.framework */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEBA1CF902C7004384FC /* Flutter.framework */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
- 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;

diff:
import 'package:flutter/material.dart';
-
import 'package:graphql_flutter/graphql_flutter.dart';
import './mutations/addStar.dart' as mutations;
@@ -7,9 +6,22 @@ import './queries/readRepositories.dart' as queries;
const String YOUR_PERSONAL_ACCESS_TOKEN = '<YOUR_PERSONAL_ACCESS_TOKEN_HERE>';
-void main() => runApp(MyApp());
+void main() async {
+ final websocketLink = WebSocketLink(SocketClient(GraphQLSocket(
+ 'ws://192.168.88.79:8080/ws/graphql',
+ headers: <String, String>{
+ 'Authorization': 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN',
+ },
+ config: GraphQLSocketConfig(autoReconnect: true, inactivityTimeout: Duration(seconds: 15)),
+ )));
+
+ runApp(MyApp(websocketLink));
+}
class MyApp extends StatelessWidget {
+ final WebSocketLink wsLink;
+ MyApp(this.wsLink);
+
@override
Widget build(BuildContext context) {
final HttpLink link = HttpLink(
@@ -22,7 +34,7 @@ class MyApp extends StatelessWidget {
final ValueNotifier<GraphQLClient> client = ValueNotifier<GraphQLClient>(
GraphQLClient(
cache: InMemoryCache(),
- link: link,
+ link: wsLink,
),
);
@@ -87,8 +99,7 @@ class _MyHomePageState extends State<MyHomePage> {
variables: <String, dynamic>{
'nRepositories': nRepositories,
},
- pollInterval: 4,
- // you can optionally override some http options through the contexts
+ pollInterval: 4, // you can optionally override some http options through the contexts
context: <String, dynamic>{
'headers': <String, String>{
'Authorization': 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN',
@@ -107,15 +118,13 @@ class _MyHomePageState extends State<MyHomePage> {
}
// result.data can be either a [List<dynamic>] or a [Map<String, dynamic>]
- final List<dynamic> repositories =
- result.data['viewer']['repositories']['nodes'];
+ final List<dynamic> repositories = result.data['viewer']['repositories']['nodes'];
return Expanded(
child: ListView.builder(
itemCount: repositories.length,
itemBuilder: (BuildContext context, int index) {
- final Map<String, dynamic> repository =
- repositories[index];
+ final Map<String, dynamic> repository = repositories[index];
return Mutation(
options: MutationOptions(
@@ -125,11 +134,8 @@ class _MyHomePageState extends State<MyHomePage> {
RunMutation addStar,
QueryResult addStarResult,
) {
- if (addStarResult.data != null &&
- addStarResult.data.isNotEmpty) {
- repository['viewerHasStarred'] =
- addStarResult.data['addStar']['starrable']
- ['viewerHasStarred'];
+ if (addStarResult.data != null && addStarResult.data.isNotEmpty) {
+ repository['viewerHasStarred'] = addStarResult.data['addStar']['starrable']['viewerHasStarred'];
}
return ListTile(

diff:
<excludeFolder url="file://$MODULE_DIR$/example/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/example/.pub" />
<excludeFolder url="file://$MODULE_DIR$/example/build" />
+ <excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks/plugins/path_provider/.dart_tool" />
+ <excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks/plugins/path_provider/.pub" />
+ <excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks/plugins/path_provider/build" />
+ <excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks/plugins/path_provider/example/.dart_tool" />
+ <excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks/plugins/path_provider/example/.pub" />
+ <excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks/plugins/path_provider/example/build" />
+ <excludeFolder url="file://$MODULE_DIR$/example/ios/Flutter/App.framework/flutter_assets/packages" />
</content>
<orderEntry type="jdk" jdkName="Android API 25 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />

diff:
library graphql_flutter;
-export 'package:graphql_flutter/src/graphql_client.dart';
-export 'package:graphql_flutter/src/socket_client.dart';
-
+export 'package:graphql_flutter/src/cache/in_memory.dart';
+export 'package:graphql_flutter/src/cache/normalized_in_memory.dart';
+export 'package:graphql_flutter/src/core/graphql_error.dart';
export 'package:graphql_flutter/src/core/query_options.dart';
export 'package:graphql_flutter/src/core/query_result.dart';
-export 'package:graphql_flutter/src/core/graphql_error.dart';
-
-export 'package:graphql_flutter/src/link/link.dart';
+export 'package:graphql_flutter/src/graphql_client.dart';
export 'package:graphql_flutter/src/link/http/link_http.dart';
-
-export 'package:graphql_flutter/src/cache/in_memory.dart';
-export 'package:graphql_flutter/src/cache/normalized_in_memory.dart';
-
+export 'package:graphql_flutter/src/link/link.dart';
+export 'package:graphql_flutter/src/link/web_socket/link_web_socket.dart';
+export 'package:graphql_flutter/src/socket_client.dart';
export 'package:graphql_flutter/src/websocket/messages.dart';
export 'package:graphql_flutter/src/websocket/socket.dart';
-
-export 'package:graphql_flutter/src/widgets/graphql_provider.dart';
-export 'package:graphql_flutter/src/widgets/graphql_consumer.dart';
export 'package:graphql_flutter/src/widgets/cache_provider.dart';
-export 'package:graphql_flutter/src/widgets/query.dart';
+export 'package:graphql_flutter/src/widgets/graphql_consumer.dart';
+export 'package:graphql_flutter/src/widgets/graphql_provider.dart';
export 'package:graphql_flutter/src/widgets/mutation.dart';
+export 'package:graphql_flutter/src/widgets/query.dart';
export 'package:graphql_flutter/src/widgets/subscription.dart';

diff:
+import 'package:graphql_flutter/src/link/fetch_result.dart';
import 'package:graphql_flutter/src/link/link.dart';
+import 'package:graphql_flutter/src/link/operation.dart';
+import 'package:graphql_flutter/src/socket_client.dart';
+import 'package:graphql_flutter/src/websocket/messages.dart';
class WebSocketLink extends Link {
- RequestHandler subscriptionClient;
-
+ final SocketClient socketClient;
+ WebSocketLink(this.socketClient)
+ : super(
+ request: (
+ Operation operation, [
+ NextLink forward,
+ ]) {
+ return socketClient.subscribe(SubscriptionRequest(operation.operationName, operation.document, operation.variables)).map((result) {
+ return FetchResult(data: result.data, errors: result.errors, context: operation.getContext(), extensions: operation.extensions);
+ });
+ },
+ );
// TODO: implement https://github.com/apollographql/apollo-link/blob/master/packages/apollo-link-ws/src/webSocketLink.ts
}

diff:
import 'dart:async';
-import 'dart:io';
-
-import 'package:uuid/uuid.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
+import 'package:uuid/uuid.dart';
SocketClient socketClient;
+class SocketClientConfig {
+ /// The duration after which a query or mutation should timeout.
+ final Duration queryOrMutationTimeout;
+
+ const SocketClientConfig({this.queryOrMutationTimeout = const Duration(seconds: 30)});
+}
+
class SocketClient {
final Uuid _uuid = Uuid();
final GraphQLSocket _socket;
- static Map<String, String> _initPayload;
+ final SocketClientConfig config;
- SocketClient(this._socket) {
+ SocketClient(this._socket, {this.config = const SocketClientConfig(), Map<String, String> initPayload}) {
_socket.connectionAck.listen(print);
_socket.connectionError.listen(print);
_socket.unknownData.listen(print);
- _socket.write(InitOperation(_initPayload));
- }
- static Future<SocketClient> connect(
- final String endPoint, {
- final List<String> protocols = const <String>[
- 'graphql-ws',
- ],
- final Map<String, String> headers = const <String, String>{
- 'content-type': 'application/json',
- },
- final Map<String, String> initPayload,
- }) async {
- _initPayload = initPayload;
-
- return SocketClient(
- GraphQLSocket(
- await WebSocket.connect(
- endPoint,
- protocols: protocols,
- headers: headers,
- ),
- ),
- );
+ _socket.connectionState.listen((GraphQLSocketConnectionState state) {
+ print('WebSocket connection state changed to: $state');
+ if (state == GraphQLSocketConnectionState.CONNECTED) _socket.write(InitOperation(initPayload));
+ });
}
Stream<SubscriptionData> subscribe(final SubscriptionRequest payload) {
final String id = _uuid.v4();
- final StreamController<SubscriptionData> response =
- StreamController<SubscriptionData>();
+ final StreamController<SubscriptionData> response = StreamController<SubscriptionData>();
- final Stream<SubscriptionComplete> complete = _socket.subscriptionComplete
- .where((SubscriptionComplete message) => message.id == id)
- .take(1);
+ final Stream<SubscriptionComplete> complete = _socket.subscriptionComplete.where((SubscriptionComplete message) => message.id == id).take(1);
- final Stream<SubscriptionData> data = _socket.subscriptionData
- .where((SubscriptionData message) => message.id == id)
- .takeWhile((_) => !response.isClosed);
+ final Stream<SubscriptionData> data =
+ _socket.subscriptionData.where((SubscriptionData message) => message.id == id).takeWhile((_) => !response.isClosed);
- final Stream<SubscriptionError> error = _socket.subscriptionError
- .where((SubscriptionError message) => message.id == id)
- .takeWhile((_) => !response.isClosed);
+ final Stream<SubscriptionError> error =
+ _socket.subscriptionError.where((SubscriptionError message) => message.id == id).takeWhile((_) => !response.isClosed);
complete.listen((_) => response.close());
data.listen((SubscriptionData message) => response.add(message));
error.listen((SubscriptionError message) => response.addError(message));
- response.onListen = () => _socket.write(StartOperation(id, payload));
- response.onCancel = () => _socket.write(StopOperation(id));
+ _socket.connectionState
+ .where((GraphQLSocketConnectionState state) => state == GraphQLSocketConnectionState.CONNECTED)
+ .takeWhile((_) => !response.isClosed)
+ .listen((_) => _socket.write(StartOperation(id, payload)));
+
+ // response.onListen = () => );
+ response.onCancel = () {
+ _socket.write(StopOperation(id));
+ };
return response.stream;
}

diff:
@@ -15,6 +15,7 @@ class MessageTypes {
// server connections
static const String GQL_CONNECTION_ACK = 'connection_ack';
static const String GQL_CONNECTION_ERROR = 'connection_error';
+ static const String GQL_CONNECTION_KEEP_ALIVE = 'ka';
// client operations
static const String GQL_START = 'start';
@@ -141,6 +142,16 @@ class ConnectionError extends GraphQLSocketMessage {
};
}
+/// The server will send this connection keep alive message periodically.
+class ConnectionKeepAlive extends GraphQLSocketMessage {
+ ConnectionKeepAlive() : super(MessageTypes.GQL_CONNECTION_KEEP_ALIVE);
+
+ @override
+ dynamic toJson() => <String, dynamic>{
+ 'type': type,
+ };
+}
+
/// Data sent from the server to the client with subscription data or error
/// payload. The user should check the errors result before processing the
/// data value. These error are from the query resolvers.
@@ -149,8 +160,7 @@ class SubscriptionData extends GraphQLSocketMessage {
final dynamic data;
final dynamic errors;
- SubscriptionData(this.id, this.data, this.errors)
- : super(MessageTypes.GQL_DATA);
+ SubscriptionData(this.id, this.data, this.errors) : super(MessageTypes.GQL_DATA);
@override
dynamic toJson() => <String, dynamic>{

diff:
@@ -2,48 +2,134 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
-import '../../graphql_flutter.dart';
+import 'package:graphql_flutter/src/websocket/messages.dart';
+
+class GraphQLSocketConfig {
+ /// Whether to reconnect to the server after detecting connection loss.
+ final bool autoReconnect;
+
+ /// The duration after which the connection is considered unstable, because no keep alive message
+ /// was received from the server. The connection to the server will be closed. If [autoReconnect] is
+ /// set to true, we try to reconnect to the server.
+ ///
+ /// If null, the keep alive messages will be ignored.
+ final Duration inactivityTimeout;
+
+ /// The duration after a connection loss that needs to pass before trying to reconnect to the socket.
+ /// This field only takes effect when [autoReconnect] is set to true.
+ ///
+ /// If null, the reconnection will occur immediately, although not recommended.
+ final Duration delayBetweenReconnectionAttempts;
+
+ const GraphQLSocketConfig(
+ {this.autoReconnect = true,
+ this.inactivityTimeout = const Duration(seconds: 30),
+ this.delayBetweenReconnectionAttempts = const Duration(seconds: 5)});
+}
+
+enum GraphQLSocketConnectionState { NOT_CONNECTED, CONNECTING, CONNECTED }
/// Wraps a standard web socket instance to marshal and un-marshal the server /
/// client payloads into dart object representation.
+///
+/// This class also deals with reconnection, handles timeout and keep alive messages.
class GraphQLSocket {
- final StreamController<GraphQLSocketMessage> _subject =
- StreamController<GraphQLSocketMessage>.broadcast();
+ final String url;
+ final Iterable<String> protocols;
+ final Map<String, dynamic> headers;
+ final CompressionOptions compression;
+ final GraphQLSocketConfig config;
+ final _connectionStateController = StreamController<GraphQLSocketConnectionState>.broadcast();
+ final _messageController = StreamController<GraphQLSocketMessage>.broadcast();
+ WebSocket _socket;
- final WebSocket _socket;
+ StreamSubscription _keepAliveSubscription;
- GraphQLSocket(this._socket) {
- _socket
- .map<Map<String, dynamic>>((dynamic message) => json.decode(message))
- .listen(
- (Map<String, dynamic> message) {
- final String type = message['type'] ?? 'unknown';
- final dynamic payload = message['payload'] ?? <String, dynamic>{};
- final String id = message['id'] ?? 'none';
+ GraphQLSocket(this.url,
+ {this.protocols = const <String>[
+ 'graphql-ws',
+ ],
+ this.headers = const <String, String>{
+ 'content-type': 'application/json',
+ },
+ this.compression = CompressionOptions.compressionDefault,
+ this.config = const GraphQLSocketConfig()}) {
+ _connect();
+ }
+
+ Future<void> _connect({Duration delayUntilConnectionAttempt}) async {
+ if (delayUntilConnectionAttempt != null) {
+ print('Scheduling to connect in ${delayUntilConnectionAttempt.inSeconds} seconds...');
+ await Future<void>.delayed(delayUntilConnectionAttempt);
+ }
+
+ if (_socket != null) print('Reconnecting to socket...');
+ _connectionStateController.add(GraphQLSocketConnectionState.CONNECTING);
+
+ try {
+ _socket = await WebSocket.connect(url, protocols: protocols, headers: headers, compression: compression);
+ _connectionStateController.add(GraphQLSocketConnectionState.CONNECTED);
+
+ final messageStream = _socket.asBroadcastStream().map<GraphQLSocketMessage>(_parseSocketMessage);
+ _messageController.addStream(messageStream);
+
+ if (config.inactivityTimeout != null) {
+ _keepAliveSubscription = _connectionKeepAlive.timeout(config.inactivityTimeout, onTimeout: (event) {
+ event.close();
+ _socket.close(WebSocketStatus.goingAway);
+ }).listen(null);
+ }
+
+ messageStream.listen(
+ (dynamic data) {
+ print("data: $data");
+ },
+ onDone: () {
+ print('done');
+ onConnectionLost();
+ },
+ cancelOnError: true,
+ onError: (dynamic e) {
+ print("error: $e");
+ });
+ } catch (e) {
+ onConnectionLost();
+ }
+ }
+
+ void onConnectionLost() {
+ _keepAliveSubscription?.cancel();
+ _connectionStateController.add(GraphQLSocketConnectionState.NOT_CONNECTED);
+
+ if (config.autoReconnect) {
+ _connect(delayUntilConnectionAttempt: config.delayBetweenReconnectionAttempts);
+ }
+ }
+
+ GraphQLSocketMessage _parseSocketMessage(dynamic message) {
+ final Map<String, dynamic> map = json.decode(message);
+ final String type = map['type'] ?? 'unknown';
+ final dynamic payload = map['payload'] ?? <String, dynamic>{};
+ final String id = map['id'] ?? 'none';
switch (type) {
case MessageTypes.GQL_CONNECTION_ACK:
- _subject.add(ConnectionAck());
- break;
+ return ConnectionAck();
case MessageTypes.GQL_CONNECTION_ERROR:
- _subject.add(ConnectionError(payload));
- break;
+ return ConnectionError(payload);
+ case MessageTypes.GQL_CONNECTION_KEEP_ALIVE:
+ return ConnectionKeepAlive();
case MessageTypes.GQL_DATA:
final dynamic data = payload['data'];
final dynamic errors = payload['errors'];
- _subject.add(SubscriptionData(id, data, errors));
- break;
+ return SubscriptionData(id, data, errors);
case MessageTypes.GQL_ERROR:
- _subject.add(SubscriptionError(id, payload));
- break;
+ return SubscriptionError(id, payload);
case MessageTypes.GQL_COMPLETE:
- _subject.add(SubscriptionComplete(id));
- break;
+ return SubscriptionComplete(id);
default:
- _subject.add(UnknownData(message));
+ return UnknownData(map);
}
- },
- );
}
void write(final GraphQLSocketMessage message) {
@@ -55,27 +141,26 @@ class GraphQLSocket {
);
}
- Stream<ConnectionAck> get connectionAck => _subject.stream
- .where((GraphQLSocketMessage message) => message is ConnectionAck)
- .cast<ConnectionAck>();
+ Stream<GraphQLSocketConnectionState> get connectionState => _connectionStateController.stream.distinct();
+
+ Stream<ConnectionAck> get connectionAck =>
+ _messageController.stream.where((GraphQLSocketMessage message) => message is ConnectionAck).cast<ConnectionAck>();
+
+ Stream<ConnectionError> get connectionError =>
+ _messageController.stream.where((GraphQLSocketMessage message) => message is ConnectionError).cast<ConnectionError>();
- Stream<ConnectionError> get connectionError => _subject.stream
- .where((GraphQLSocketMessage message) => message is ConnectionError)
- .cast<ConnectionError>();
+ Stream<ConnectionKeepAlive> get _connectionKeepAlive =>
+ _messageController.stream.where((GraphQLSocketMessage message) => message is ConnectionKeepAlive).cast<ConnectionKeepAlive>();
- Stream<UnknownData> get unknownData => _subject.stream
- .where((GraphQLSocketMessage message) => message is UnknownData)
- .cast<UnknownData>();
+ Stream<UnknownData> get unknownData =>
+ _messageController.stream.where((GraphQLSocketMessage message) => message is UnknownData).cast<UnknownData>();
- Stream<SubscriptionData> get subscriptionData => _subject.stream
- .where((GraphQLSocketMessage message) => message is SubscriptionData)
- .cast<SubscriptionData>();
+ Stream<SubscriptionData> get subscriptionData =>
+ _messageController.stream.where((GraphQLSocketMessage message) => message is SubscriptionData).cast<SubscriptionData>();
- Stream<SubscriptionError> get subscriptionError => _subject.stream
- .where((GraphQLSocketMessage message) => message is SubscriptionError)
- .cast<SubscriptionError>();
+ Stream<SubscriptionError> get subscriptionError =>
+ _messageController.stream.where((GraphQLSocketMessage message) => message is SubscriptionError).cast<SubscriptionError>();
- Stream<SubscriptionComplete> get subscriptionComplete => _subject.stream
- .where((GraphQLSocketMessage message) => message is SubscriptionComplete)
- .cast<SubscriptionComplete>();
+ Stream<SubscriptionComplete> get subscriptionComplete =>
+ _messageController.stream.where((GraphQLSocketMessage message) => message is SubscriptionComplete).cast<SubscriptionComplete>();
}


message: Refactored SocketClient, and removed GraphQLSocket class.

change_type: MODIFY

diff:
@@ -7,13 +7,13 @@ import './queries/readRepositories.dart' as queries;
const String YOUR_PERSONAL_ACCESS_TOKEN = '<YOUR_PERSONAL_ACCESS_TOKEN_HERE>';
void main() async {
- final websocketLink = WebSocketLink(SocketClient(GraphQLSocket(
+ final websocketLink = WebSocketLink(SocketClient(
'ws://192.168.88.79:8080/ws/graphql',
headers: <String, String>{
'Authorization': 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN',
},
- config: GraphQLSocketConfig(autoReconnect: true, inactivityTimeout: Duration(seconds: 15)),
- )));
+ config: SocketClientConfig(autoReconnect: true, inactivityTimeout: Duration(seconds: 15)),
+ ));
runApp(MyApp(websocketLink));
}

diff:
import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
import 'package:graphql_flutter/graphql_flutter.dart';
+import 'package:graphql_flutter/src/websocket/messages.dart';
import 'package:uuid/uuid.dart';
SocketClient socketClient;
class SocketClientConfig {
- /// The duration after which a query or mutation should timeout.
- final Duration queryOrMutationTimeout;
-
- const SocketClientConfig({this.queryOrMutationTimeout = const Duration(seconds: 30)});
+ /// Whether to reconnect to the server after detecting connection loss.
+ final bool autoReconnect;
+
+ /// The duration after which the connection is considered unstable, because no keep alive message
+ /// was received from the server. The connection to the server will be closed. If [autoReconnect] is
+ /// set to true, we try to reconnect to the server.
+ ///
+ /// If null, the keep alive messages will be ignored.
+ final Duration inactivityTimeout;
+
+ /// The duration after a connection loss that needs to pass before trying to reconnect to the socket.
+ /// This field only takes effect when [autoReconnect] is set to true.
+ ///
+ /// If null, the reconnection will occur immediately, although not recommended.
+ final Duration delayBetweenReconnectionAttempts;
+
+ const SocketClientConfig(
+ {this.autoReconnect = true,
+ this.inactivityTimeout = const Duration(seconds: 30),
+ this.delayBetweenReconnectionAttempts = const Duration(seconds: 5)});
}
+enum SocketConnectionState { NOT_CONNECTED, CONNECTING, CONNECTED }
+
+/// Wraps a standard web socket instance to marshal and un-marshal the server /
+/// client payloads into dart object representation.
+///
+/// This class also deals with reconnection, handles timeout and keep alive messages.
class SocketClient {
final Uuid _uuid = Uuid();
- final GraphQLSocket _socket;
+ final String url;
final SocketClientConfig config;
+ final Iterable<String> protocols;
+ final Map<String, String> initPayload;
+ final Map<String, dynamic> headers;
+ final CompressionOptions compression;
+ final _connectionStateController = StreamController<SocketConnectionState>.broadcast();
+ final _messageController = StreamController<GraphQLSocketMessage>.broadcast();
+
+ WebSocket _socket;
+
+ StreamSubscription _keepAliveSubscription;
+ StreamSubscription _messageSubscription;
+ StreamSubscription _connectionStateSubscription;
+
+ SocketClient(this.url,
+ {this.protocols = const <String>[
+ 'graphql-ws',
+ ],
+ this.headers = const <String, String>{
+ 'content-type': 'application/json',
+ },
+ this.compression = CompressionOptions.compressionDefault,
+ this.config = const SocketClientConfig(),
+ this.initPayload}) {
+ _connect();
+
+ _connectionStateSubscription = connectionState.listen((SocketConnectionState state) {
+ print('WebSocket connection state changed to: $state');
+ if (state == SocketConnectionState.CONNECTED) _write(InitOperation(initPayload));
+ });
+ }
+
+ Future<void> _connect({Duration delayUntilConnectionAttempt}) async {
+ if (delayUntilConnectionAttempt != null) {
+ print('Scheduling to connect in ${delayUntilConnectionAttempt.inSeconds} seconds...');
+ await Future<void>.delayed(delayUntilConnectionAttempt);
+ }
- SocketClient(this._socket, {this.config = const SocketClientConfig(), Map<String, String> initPayload}) {
- _socket.connectionAck.listen(print);
- _socket.connectionError.listen(print);
- _socket.unknownData.listen(print);
+ if (_socket != null) print('Reconnecting to socket...');
+ _connectionStateController.add(SocketConnectionState.CONNECTING);
- _socket.connectionState.listen((GraphQLSocketConnectionState state) {
- print('WebSocket connection state changed to: $state');
- if (state == GraphQLSocketConnectionState.CONNECTED) _socket.write(InitOperation(initPayload));
+ try {
+ _socket = await WebSocket.connect(url, protocols: protocols, headers: headers, compression: compression);
+ _connectionStateController.add(SocketConnectionState.CONNECTED);
+
+ final messageStream = _socket.asBroadcastStream().map<GraphQLSocketMessage>(_parseSocketMessage);
+ _messageController.addStream(messageStream);
+
+ if (config.inactivityTimeout != null) {
+ _keepAliveSubscription = _connectionKeepAlive.timeout(config.inactivityTimeout, onTimeout: (event) {
+ event.close();
+ _socket.close(WebSocketStatus.goingAway);
+ }).listen(null);
+ }
+
+ _messageSubscription = messageStream.listen(
+ (dynamic data) {
+ print("data: $data");
+ },
+ onDone: () {
+ print('done');
+ onConnectionLost();
+ },
+ cancelOnError: true,
+ onError: (dynamic e) {
+ print("error: $e");
});
+ } catch (e) {
+ onConnectionLost();
+ }
+ }
+
+ void onConnectionLost() {
+ _keepAliveSubscription?.cancel();
+ _messageSubscription?.cancel();
+ _connectionStateController.add(SocketConnectionState.NOT_CONNECTED);
+
+ if (config.autoReconnect) {
+ _connect(delayUntilConnectionAttempt: config.delayBetweenReconnectionAttempts);
}
+ }
+
+ /// Closes the underlying socket if connected, and stops reconnection attempts.
+ /// After calling this method, this [GraphQLSocket] instance must be considered
+ /// obsolete, and should not be used. Instead, create a new instance of this class.
+ void dispose() {
+ _socket?.close();
+ _keepAliveSubscription?.cancel();
+ _messageSubscription?.cancel();
+ _connectionStateSubscription?.cancel();
+ }
+
+ static GraphQLSocketMessage _parseSocketMessage(dynamic message) {
+ final Map<String, dynamic> map = json.decode(message);
+ final String type = map['type'] ?? 'unknown';
+ final dynamic payload = map['payload'] ?? <String, dynamic>{};
+ final String id = map['id'] ?? 'none';
+
+ switch (type) {
+ case MessageTypes.GQL_CONNECTION_ACK:
+ return ConnectionAck();
+ case MessageTypes.GQL_CONNECTION_ERROR:
+ return ConnectionError(payload);
+ case MessageTypes.GQL_CONNECTION_KEEP_ALIVE:
+ return ConnectionKeepAlive();
+ case MessageTypes.GQL_DATA:
+ final dynamic data = payload['data'];
+ final dynamic errors = payload['errors'];
+ return SubscriptionData(id, data, errors);
+ case MessageTypes.GQL_ERROR:
+ return SubscriptionError(id, payload);
+ case MessageTypes.GQL_COMPLETE:
+ return SubscriptionComplete(id);
+ default:
+ return UnknownData(map);
+ }
+ }
+
+ void _write(final GraphQLSocketMessage message) {
+ _socket.add(
+ json.encode(
+ message,
+ toEncodable: (dynamic m) => m.toJson(),
+ ),
+ );
+ }
+
+ Stream<SocketConnectionState> get connectionState => _connectionStateController.stream.distinct();
+
+ Stream<ConnectionAck> get _connectionAck =>
+ _messageController.stream.where((GraphQLSocketMessage message) => message is ConnectionAck).cast<ConnectionAck>();
+
+ Stream<ConnectionError> get _connectionError =>
+ _messageController.stream.where((GraphQLSocketMessage message) => message is ConnectionError).cast<ConnectionError>();
+
+ Stream<ConnectionKeepAlive> get _connectionKeepAlive =>
+ _messageController.stream.where((GraphQLSocketMessage message) => message is ConnectionKeepAlive).cast<ConnectionKeepAlive>();
+
+ Stream<UnknownData> get _unknownData =>
+ _messageController.stream.where((GraphQLSocketMessage message) => message is UnknownData).cast<UnknownData>();
+
+ Stream<SubscriptionData> get _subscriptionData =>
+ _messageController.stream.where((GraphQLSocketMessage message) => message is SubscriptionData).cast<SubscriptionData>();
+
+ Stream<SubscriptionError> get _subscriptionError =>
+ _messageController.stream.where((GraphQLSocketMessage message) => message is SubscriptionError).cast<SubscriptionError>();
+
+ Stream<SubscriptionComplete> get _subscriptionComplete =>
+ _messageController.stream.where((GraphQLSocketMessage message) => message is SubscriptionComplete).cast<SubscriptionComplete>();
Stream<SubscriptionData> subscribe(final SubscriptionRequest payload) {
final String id = _uuid.v4();
final StreamController<SubscriptionData> response = StreamController<SubscriptionData>();
- final Stream<SubscriptionComplete> complete = _socket.subscriptionComplete.where((SubscriptionComplete message) => message.id == id).take(1);
+ final Stream<SubscriptionComplete> complete = _subscriptionComplete.where((SubscriptionComplete message) => message.id == id).take(1);
final Stream<SubscriptionData> data =
- _socket.subscriptionData.where((SubscriptionData message) => message.id == id).takeWhile((_) => !response.isClosed);
+ _subscriptionData.where((SubscriptionData message) => message.id == id).takeWhile((_) => !response.isClosed);
final Stream<SubscriptionError> error =
- _socket.subscriptionError.where((SubscriptionError message) => message.id == id).takeWhile((_) => !response.isClosed);
+ _subscriptionError.where((SubscriptionError message) => message.id == id).takeWhile((_) => !response.isClosed);
complete.listen((_) => response.close());
data.listen((SubscriptionData message) => response.add(message));
error.listen((SubscriptionError message) => response.addError(message));
- _socket.connectionState
- .where((GraphQLSocketConnectionState state) => state == GraphQLSocketConnectionState.CONNECTED)
+ connectionState
+ .where((SocketConnectionState state) => state == SocketConnectionState.CONNECTED)
.takeWhile((_) => !response.isClosed)
- .listen((_) => _socket.write(StartOperation(id, payload)));
+ .listen((_) => _write(StartOperation(id, payload)));
// response.onListen = () => );
response.onCancel = () {
- _socket.write(StopOperation(id));
+ _write(StopOperation(id));
};
return response.stream;

diff:
-import 'dart:async';
-import 'dart:convert';
-import 'dart:io';
-import 'package:graphql_flutter/src/websocket/messages.dart';
-
-class GraphQLSocketConfig {
- /// Whether to reconnect to the server after detecting connection loss.
- final bool autoReconnect;
-
- /// The duration after which the connection is considered unstable, because no keep alive message
- /// was received from the server. The connection to the server will be closed. If [autoReconnect] is
- /// set to true, we try to reconnect to the server.
- ///
- /// If null, the keep alive messages will be ignored.
- final Duration inactivityTimeout;
-
- /// The duration after a connection loss that needs to pass before trying to reconnect to the socket.
- /// This field only takes effect when [autoReconnect] is set to true.
- ///
- /// If null, the reconnection will occur immediately, although not recommended.
- final Duration delayBetweenReconnectionAttempts;
-
- const GraphQLSocketConfig(
- {this.autoReconnect = true,
- this.inactivityTimeout = const Duration(seconds: 30),
- this.delayBetweenReconnectionAttempts = const Duration(seconds: 5)});
-}
-
-enum GraphQLSocketConnectionState { NOT_CONNECTED, CONNECTING, CONNECTED }
-
-/// Wraps a standard web socket instance to marshal and un-marshal the server /
-/// client payloads into dart object representation.
-///
-/// This class also deals with reconnection, handles timeout and keep alive messages.
-class GraphQLSocket {
- final String url;
- final Iterable<String> protocols;
- final Map<String, dynamic> headers;
- final CompressionOptions compression;
- final GraphQLSocketConfig config;
- final _connectionStateController = StreamController<GraphQLSocketConnectionState>.broadcast();
- final _messageController = StreamController<GraphQLSocketMessage>.broadcast();
- WebSocket _socket;
-
- StreamSubscription _keepAliveSubscription;
-
- GraphQLSocket(this.url,
- {this.protocols = const <String>[
- 'graphql-ws',
- ],
- this.headers = const <String, String>{
- 'content-type': 'application/json',
- },
- this.compression = CompressionOptions.compressionDefault,
- this.config = const GraphQLSocketConfig()}) {
- _connect();
- }
-
- Future<void> _connect({Duration delayUntilConnectionAttempt}) async {
- if (delayUntilConnectionAttempt != null) {
- print('Scheduling to connect in ${delayUntilConnectionAttempt.inSeconds} seconds...');
- await Future<void>.delayed(delayUntilConnectionAttempt);
- }
-
- if (_socket != null) print('Reconnecting to socket...');
- _connectionStateController.add(GraphQLSocketConnectionState.CONNECTING);
-
- try {
- _socket = await WebSocket.connect(url, protocols: protocols, headers: headers, compression: compression);
- _connectionStateController.add(GraphQLSocketConnectionState.CONNECTED);
-
- final messageStream = _socket.asBroadcastStream().map<GraphQLSocketMessage>(_parseSocketMessage);
- _messageController.addStream(messageStream);
-
- if (config.inactivityTimeout != null) {
- _keepAliveSubscription = _connectionKeepAlive.timeout(config.inactivityTimeout, onTimeout: (event) {
- event.close();
- _socket.close(WebSocketStatus.goingAway);
- }).listen(null);
- }
-
- messageStream.listen(
- (dynamic data) {
- print("data: $data");
- },
- onDone: () {
- print('done');
- onConnectionLost();
- },
- cancelOnError: true,
- onError: (dynamic e) {
- print("error: $e");
- });
- } catch (e) {
- onConnectionLost();
- }
- }
-
- void onConnectionLost() {
- _keepAliveSubscription?.cancel();
- _connectionStateController.add(GraphQLSocketConnectionState.NOT_CONNECTED);
-
- if (config.autoReconnect) {
- _connect(delayUntilConnectionAttempt: config.delayBetweenReconnectionAttempts);
- }
- }
-
- GraphQLSocketMessage _parseSocketMessage(dynamic message) {
- final Map<String, dynamic> map = json.decode(message);
- final String type = map['type'] ?? 'unknown';
- final dynamic payload = map['payload'] ?? <String, dynamic>{};
- final String id = map['id'] ?? 'none';
-
- switch (type) {
- case MessageTypes.GQL_CONNECTION_ACK:
- return ConnectionAck();
- case MessageTypes.GQL_CONNECTION_ERROR:
- return ConnectionError(payload);
- case MessageTypes.GQL_CONNECTION_KEEP_ALIVE:
- return ConnectionKeepAlive();
- case MessageTypes.GQL_DATA:
- final dynamic data = payload['data'];
- final dynamic errors = payload['errors'];
- return SubscriptionData(id, data, errors);
- case MessageTypes.GQL_ERROR:
- return SubscriptionError(id, payload);
- case MessageTypes.GQL_COMPLETE:
- return SubscriptionComplete(id);
- default:
- return UnknownData(map);
- }
- }
-
- void write(final GraphQLSocketMessage message) {
- _socket.add(
- json.encode(
- message,
- toEncodable: (dynamic m) => m.toJson(),
- ),
- );
- }
-
- Stream<GraphQLSocketConnectionState> get connectionState => _connectionStateController.stream.distinct();
-
- Stream<ConnectionAck> get connectionAck =>
- _messageController.stream.where((GraphQLSocketMessage message) => message is ConnectionAck).cast<ConnectionAck>();
-
- Stream<ConnectionError> get connectionError =>
- _messageController.stream.where((GraphQLSocketMessage message) => message is ConnectionError).cast<ConnectionError>();
-
- Stream<ConnectionKeepAlive> get _connectionKeepAlive =>
- _messageController.stream.where((GraphQLSocketMessage message) => message is ConnectionKeepAlive).cast<ConnectionKeepAlive>();
-
- Stream<UnknownData> get unknownData =>
- _messageController.stream.where((GraphQLSocketMessage message) => message is UnknownData).cast<UnknownData>();
-
- Stream<SubscriptionData> get subscriptionData =>
- _messageController.stream.where((GraphQLSocketMessage message) => message is SubscriptionData).cast<SubscriptionData>();
-
- Stream<SubscriptionError> get subscriptionError =>
- _messageController.stream.where((GraphQLSocketMessage message) => message is SubscriptionError).cast<SubscriptionError>();
-
- Stream<SubscriptionComplete> get subscriptionComplete =>
- _messageController.stream.where((GraphQLSocketMessage message) => message is SubscriptionComplete).cast<SubscriptionComplete>();
-}

license: MIT License

repo: zino-hofmann/graphql-flutter


message: Keep track of disposed state in SocketClient.

diff:
@@ -8,7 +8,7 @@ const String YOUR_PERSONAL_ACCESS_TOKEN = '<YOUR_PERSONAL_ACCESS_TOKEN_HERE>';
void main() async {
final websocketLink = WebSocketLink(SocketClient(
- 'ws://192.168.88.79:8080/ws/graphql',
+ 'ws://localhost:8080/ws/graphql',
headers: <String, String>{
'Authorization': 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN',
},

diff:
@@ -17,5 +17,4 @@ class WebSocketLink extends Link {
});
},
);
-// TODO: implement https://github.com/apollographql/apollo-link/blob/master/packages/apollo-link-ws/src/webSocketLink.ts
}

diff:
@@ -13,14 +13,14 @@ class SocketClientConfig {
final bool autoReconnect;
/// The duration after which the connection is considered unstable, because no keep alive message
- /// was received from the server. The connection to the server will be closed. If [autoReconnect] is
- /// set to true, we try to reconnect to the server.
+ /// was received from the server in the given time-frame. The connection to the server will be closed.
+ /// If [autoReconnect] is set to true, we try to reconnect to the server.
///
/// If null, the keep alive messages will be ignored.
final Duration inactivityTimeout;
- /// The duration after a connection loss that needs to pass before trying to reconnect to the socket.
- /// This field only takes effect when [autoReconnect] is set to true.
+ /// The duration after a connection loss that needs to pass before trying to reconnect to the server.
+ /// This only takes effect when [autoReconnect] is set to true.
///
/// If null, the reconnection will occur immediately, although not recommended.
final Duration delayBetweenReconnectionAttempts;
@@ -37,6 +37,10 @@ enum SocketConnectionState { NOT_CONNECTED, CONNECTING, CONNECTED }
/// client payloads into dart object representation.
///
/// This class also deals with reconnection, handles timeout and keep alive messages.
+///
+/// It is meant to be instantiated once, and you can let this class handle all the heavy-
+/// lifting of socket state management. Once you're done with the socket connection, make sure
+/// you call the [dispose] method to release all allocated resources.
class SocketClient {
final Uuid _uuid = Uuid();
final String url;
@@ -48,11 +52,12 @@ class SocketClient {
final _connectionStateController = StreamController<SocketConnectionState>.broadcast();
final _messageController = StreamController<GraphQLSocketMessage>.broadcast();
+ bool disposed = false;
WebSocket _socket;
- StreamSubscription _keepAliveSubscription;
- StreamSubscription _messageSubscription;
- StreamSubscription _connectionStateSubscription;
+ StreamSubscription<ConnectionKeepAlive> _keepAliveSubscription;
+ StreamSubscription<GraphQLSocketMessage> _messageSubscription;
+ StreamSubscription<SocketConnectionState> _connectionStateSubscription;
SocketClient(this.url,
{this.protocols = const <String>[
@@ -64,15 +69,19 @@ class SocketClient {
this.compression = CompressionOptions.compressionDefault,
this.config = const SocketClientConfig(),
this.initPayload}) {
- _connect();
-
_connectionStateSubscription = connectionState.listen((SocketConnectionState state) {
print('WebSocket connection state changed to: $state');
- if (state == SocketConnectionState.CONNECTED) _write(InitOperation(initPayload));
});
+
+ _connect();
}
+ /// Connects to the server.
+ ///
+ /// If this instance is disposed, this method does nothing.
Future<void> _connect({Duration delayUntilConnectionAttempt}) async {
+ if (disposed) return;
+
if (delayUntilConnectionAttempt != null) {
print('Scheduling to connect in ${delayUntilConnectionAttempt.inSeconds} seconds...');
await Future<void>.delayed(delayUntilConnectionAttempt);
@@ -84,6 +93,7 @@ class SocketClient {
try {
_socket = await WebSocket.connect(url, protocols: protocols, headers: headers, compression: compression);
_connectionStateController.add(SocketConnectionState.CONNECTED);
+ _write(InitOperation(initPayload));
final messageStream = _socket.asBroadcastStream().map<GraphQLSocketMessage>(_parseSocketMessage);
_messageController.addStream(messageStream);
@@ -117,15 +127,19 @@ class SocketClient {
_messageSubscription?.cancel();
_connectionStateController.add(SocketConnectionState.NOT_CONNECTED);
- if (config.autoReconnect) {
+ if (config.autoReconnect && !disposed) {
_connect(delayUntilConnectionAttempt: config.delayBetweenReconnectionAttempts);
}
}
/// Closes the underlying socket if connected, and stops reconnection attempts.
- /// After calling this method, this [GraphQLSocket] instance must be considered
- /// obsolete, and should not be used. Instead, create a new instance of this class.
+ /// After calling this method, this [SocketClient] instance must be considered
+ /// unusable. Instead, create a new instance of this class.
+ ///
+ /// Use this method if you'd like to disconnect from the specified server permanently,
+ /// and you'd like to connect to another server instead of the current one.
void dispose() {
+ disposed = true;
_socket?.close();
_keepAliveSubscription?.cancel();
_messageSubscription?.cancel();


message: Set dispose to private variable.

diff:
@@ -52,7 +52,7 @@ class SocketClient {
final _connectionStateController = StreamController<SocketConnectionState>.broadcast();
final _messageController = StreamController<GraphQLSocketMessage>.broadcast();
- bool disposed = false;
+ bool _disposed = false;
WebSocket _socket;
StreamSubscription<ConnectionKeepAlive> _keepAliveSubscription;
@@ -80,7 +80,7 @@ class SocketClient {
///
/// If this instance is disposed, this method does nothing.
Future<void> _connect({Duration delayUntilConnectionAttempt}) async {
- if (disposed) return;
+ if (_disposed) return;
if (delayUntilConnectionAttempt != null) {
print('Scheduling to connect in ${delayUntilConnectionAttempt.inSeconds} seconds...');
@@ -127,7 +127,7 @@ class SocketClient {
_messageSubscription?.cancel();
_connectionStateController.add(SocketConnectionState.NOT_CONNECTED);
- if (config.autoReconnect && !disposed) {
+ if (config.autoReconnect && !_disposed) {
_connect(delayUntilConnectionAttempt: config.delayBetweenReconnectionAttempts);
}
}
@@ -139,7 +139,7 @@ class SocketClient {
/// Use this method if you'd like to disconnect from the specified server permanently,
/// and you'd like to connect to another server instead of the current one.
void dispose() {
- disposed = true;
+ _disposed = true;
_socket?.close();
_keepAliveSubscription?.cancel();
_messageSubscription?.cancel();

original_message: Set dispose to private variable.


message: Log keep alive inactivity.

diff:
@@ -14,12 +14,12 @@ class SocketClientConfig {
/// The duration after which the connection is considered unstable, because no keep alive message
/// was received from the server in the given time-frame. The connection to the server will be closed.
- /// If [autoReconnect] is set to true, we try to reconnect to the server.
+ /// If [autoReconnect] is set to true, we try to reconnect to the server after the specified [delayBetweenReconnectionAttempts].
///
/// If null, the keep alive messages will be ignored.
final Duration inactivityTimeout;
- /// The duration after a connection loss that needs to pass before trying to reconnect to the server.
+ /// The duration that needs to pass before trying to reconnect to the server after a connection loss.
/// This only takes effect when [autoReconnect] is set to true.
///
/// If null, the reconnection will occur immediately, although not recommended.
@@ -57,7 +57,6 @@ class SocketClient {
StreamSubscription<ConnectionKeepAlive> _keepAliveSubscription;
StreamSubscription<GraphQLSocketMessage> _messageSubscription;
- StreamSubscription<SocketConnectionState> _connectionStateSubscription;
SocketClient(this.url,
{this.protocols = const <String>[
@@ -69,10 +68,6 @@ class SocketClient {
this.compression = CompressionOptions.compressionDefault,
this.config = const SocketClientConfig(),
this.initPayload}) {
- _connectionStateSubscription = connectionState.listen((SocketConnectionState state) {
- print('WebSocket connection state changed to: $state');
- });
-
_connect();
}
@@ -100,6 +95,7 @@ class SocketClient {
if (config.inactivityTimeout != null) {
_keepAliveSubscription = _connectionKeepAlive.timeout(config.inactivityTimeout, onTimeout: (event) {
+ print("Haven't received keep alive message for ${config.inactivityTimeout.inSeconds} seconds. Disconnecting..");
event.close();
_socket.close(WebSocketStatus.goingAway);
}).listen(null);
@@ -143,7 +139,6 @@ class SocketClient {
_socket?.close();
_keepAliveSubscription?.cancel();
_messageSubscription?.cancel();
- _connectionStateSubscription?.cancel();
}
static GraphQLSocketMessage _parseSocketMessage(dynamic message) {

original_message: Log keep alive inactivity.


message: Log websocket connection states.

diff:
@@ -84,10 +84,12 @@ class SocketClient {
if (_socket != null) print('Reconnecting to socket...');
_connectionStateController.add(SocketConnectionState.CONNECTING);
+ print('Connecting to websocket: $url...');
try {
_socket = await WebSocket.connect(url, protocols: protocols, headers: headers, compression: compression);
_connectionStateController.add(SocketConnectionState.CONNECTED);
+ print('Connected to websocket.');
_write(InitOperation(initPayload));
final messageStream = _socket.asBroadcastStream().map<GraphQLSocketMessage>(_parseSocketMessage);
@@ -119,6 +121,7 @@ class SocketClient {
}
void onConnectionLost() {
+ print('Disconnected from websocket.');
_keepAliveSubscription?.cancel();
_messageSubscription?.cancel();
_connectionStateController.add(SocketConnectionState.NOT_CONNECTED);

original_message: Log websocket connection states.


message: Added queryOrMutationTimeout.

diff:
@@ -12,9 +12,8 @@ class WebSocketLink extends Link {
Operation operation, [
NextLink forward,
]) {
- return socketClient.subscribe(SubscriptionRequest(operation.operationName, operation.document, operation.variables)).map((result) {
- return FetchResult(data: result.data, errors: result.errors, context: operation.getContext(), extensions: operation.extensions);
- });
+ return socketClient.subscribe(SubscriptionRequest(operation.operationName, operation.document, operation.variables), false).map(
+ (result) => FetchResult(data: result.data, errors: result.errors, context: operation.getContext(), extensions: operation.extensions));
},
);
}

diff:
@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:graphql_flutter/src/websocket/messages.dart';
+import 'package:rxdart/subjects.dart';
import 'package:uuid/uuid.dart';
SocketClient socketClient;
@@ -25,8 +26,13 @@ class SocketClientConfig {
/// If null, the reconnection will occur immediately, although not recommended.
final Duration delayBetweenReconnectionAttempts;
+ // The duration after which a query or mutation should time out.
+ // If null, no timeout is applied, although not recommended.
+ final Duration queryAndMutationTimeout;
+
const SocketClientConfig(
{this.autoReconnect = true,
+ this.queryAndMutationTimeout = const Duration(seconds: 10),
this.inactivityTimeout = const Duration(seconds: 30),
this.delayBetweenReconnectionAttempts = const Duration(seconds: 5)});
}
@@ -49,11 +55,10 @@ class SocketClient {
final Map<String, String> initPayload;
final Map<String, dynamic> headers;
final CompressionOptions compression;
- final _connectionStateController = StreamController<SocketConnectionState>.broadcast();
- final _messageController = StreamController<GraphQLSocketMessage>.broadcast();
+ final _connectionStateController = BehaviorSubject<SocketConnectionState>();
- bool _disposed = false;
WebSocket _socket;
+ Stream<GraphQLSocketMessage> _messageStream;
StreamSubscription<ConnectionKeepAlive> _keepAliveSubscription;
StreamSubscription<GraphQLSocketMessage> _messageSubscription;
@@ -75,7 +80,7 @@ class SocketClient {
///
/// If this instance is disposed, this method does nothing.
Future<void> _connect({Duration delayUntilConnectionAttempt}) async {
- if (_disposed) return;
+ if (_connectionStateController.isClosed) return;
if (delayUntilConnectionAttempt != null) {
print('Scheduling to connect in ${delayUntilConnectionAttempt.inSeconds} seconds...');
@@ -83,17 +88,16 @@ class SocketClient {
}
if (_socket != null) print('Reconnecting to socket...');
- _connectionStateController.add(SocketConnectionState.CONNECTING);
+ _connectionStateController.value = SocketConnectionState.CONNECTING;
print('Connecting to websocket: $url...');
try {
_socket = await WebSocket.connect(url, protocols: protocols, headers: headers, compression: compression);
- _connectionStateController.add(SocketConnectionState.CONNECTED);
+ _connectionStateController.value = SocketConnectionState.CONNECTED;
print('Connected to websocket.');
_write(InitOperation(initPayload));
- final messageStream = _socket.asBroadcastStream().map<GraphQLSocketMessage>(_parseSocketMessage);
- _messageController.addStream(messageStream);
+ _messageStream = _socket.asBroadcastStream().map<GraphQLSocketMessage>(_parseSocketMessage);
if (config.inactivityTimeout != null) {
_keepAliveSubscription = _connectionKeepAlive.timeout(config.inactivityTimeout, onTimeout: (event) {
@@ -103,7 +107,7 @@ class SocketClient {
}).listen(null);
}
- _messageSubscription = messageStream.listen(
+ _messageSubscription = _messageStream.listen(
(dynamic data) {
print("data: $data");
},
@@ -124,9 +128,9 @@ class SocketClient {
print('Disconnected from websocket.');
_keepAliveSubscription?.cancel();
_messageSubscription?.cancel();
- _connectionStateController.add(SocketConnectionState.NOT_CONNECTED);
+ _connectionStateController.value = SocketConnectionState.NOT_CONNECTED;
- if (config.autoReconnect && !_disposed) {
+ if (config.autoReconnect && !_connectionStateController.isClosed) {
_connect(delayUntilConnectionAttempt: config.delayBetweenReconnectionAttempts);
}
}
@@ -138,10 +142,11 @@ class SocketClient {
/// Use this method if you'd like to disconnect from the specified server permanently,
/// and you'd like to connect to another server instead of the current one.
void dispose() {
- _disposed = true;
_socket?.close();
_keepAliveSubscription?.cancel();
_messageSubscription?.cancel();
+
+ _connectionStateController?.close();
}
static GraphQLSocketMessage _parseSocketMessage(dynamic message) {
@@ -179,56 +184,71 @@ class SocketClient {
);
}
- Stream<SocketConnectionState> get connectionState => _connectionStateController.stream.distinct();
-
- Stream<ConnectionAck> get _connectionAck =>
- _messageController.stream.where((GraphQLSocketMessage message) => message is ConnectionAck).cast<ConnectionAck>();
-
- Stream<ConnectionError> get _connectionError =>
- _messageController.stream.where((GraphQLSocketMessage message) => message is ConnectionError).cast<ConnectionError>();
-
- Stream<ConnectionKeepAlive> get _connectionKeepAlive =>
- _messageController.stream.where((GraphQLSocketMessage message) => message is ConnectionKeepAlive).cast<ConnectionKeepAlive>();
-
- Stream<UnknownData> get _unknownData =>
- _messageController.stream.where((GraphQLSocketMessage message) => message is UnknownData).cast<UnknownData>();
-
- Stream<SubscriptionData> get _subscriptionData =>
- _messageController.stream.where((GraphQLSocketMessage message) => message is SubscriptionData).cast<SubscriptionData>();
-
- Stream<SubscriptionError> get _subscriptionError =>
- _messageController.stream.where((GraphQLSocketMessage message) => message is SubscriptionError).cast<SubscriptionError>();
-
- Stream<SubscriptionComplete> get _subscriptionComplete =>
- _messageController.stream.where((GraphQLSocketMessage message) => message is SubscriptionComplete).cast<SubscriptionComplete>();
-
- Stream<SubscriptionData> subscribe(final SubscriptionRequest payload) {
+ /// Sends a query, mutation or subscription request to the server, and returns a stream of the response.
+ ///
+ /// If the request is a query or mutation, a timeout will be applied to the request as specified by
+ /// [SocketClientConfig]'s [queryAndMutationTimeout] field.
+ ///
+ /// If the request is a subscription, obviously no timeout is applied.
+ ///
+ /// In case of socket disconnection, the returned stream will be closed.
+ Stream<SubscriptionData> subscribe(final SubscriptionRequest payload, final bool isSubscription) {
final String id = _uuid.v4();
final StreamController<SubscriptionData> response = StreamController<SubscriptionData>();
+ response.onListen = () {
+ if (_connectionStateController.value == SocketConnectionState.CONNECTED && _socket != null) {
final Stream<SubscriptionComplete> complete = _subscriptionComplete.where((SubscriptionComplete message) => message.id == id).take(1);
-
- final Stream<SubscriptionData> data =
+ final Stream<SubscriptionData> dataWithoutTimeout =
_subscriptionData.where((SubscriptionData message) => message.id == id).takeWhile((_) => !response.isClosed);
-
- final Stream<SubscriptionError> error =
+ final Stream<SubscriptionError> errorWithoutTimeout =
_subscriptionError.where((SubscriptionError message) => message.id == id).takeWhile((_) => !response.isClosed);
+ final Stream<SubscriptionData> data = isSubscription || config.queryAndMutationTimeout == null
+ ? dataWithoutTimeout
+ : dataWithoutTimeout.timeout(config.queryAndMutationTimeout);
+ final Stream<SubscriptionError> error = isSubscription || config.queryAndMutationTimeout == null
+ ? errorWithoutTimeout
+ : errorWithoutTimeout.timeout(config.queryAndMutationTimeout);
+
complete.listen((_) => response.close());
data.listen((SubscriptionData message) => response.add(message));
error.listen((SubscriptionError message) => response.addError(message));
- connectionState
- .where((SocketConnectionState state) => state == SocketConnectionState.CONNECTED)
- .takeWhile((_) => !response.isClosed)
- .listen((_) => _write(StartOperation(id, payload)));
-
- // response.onListen = () => );
+ _write(StartOperation(id, payload));
+ } else {
+ response.addError(Exception('Not connected to the server.'));
+ }
+ };
response.onCancel = () {
- _write(StopOperation(id));
+ if (_connectionStateController.value == SocketConnectionState.CONNECTED && _socket != null) _write(StopOperation(id));
};
return response.stream;
}
+
+ /// These streams will emit done events when the current socket is done.
+
+ /// A stream that emits the last value of the connection state upon subscription.
+ Stream<SocketConnectionState> get connectionState => _connectionStateController.stream.distinct();
+
+ Stream<ConnectionAck> get _connectionAck => _messageStream.where((GraphQLSocketMessage message) => message is ConnectionAck).cast<ConnectionAck>();
+
+ Stream<ConnectionError> get _connectionError =>
+ _messageStream.where((GraphQLSocketMessage message) => message is ConnectionError).cast<ConnectionError>();
+
+ Stream<ConnectionKeepAlive> get _connectionKeepAlive =>
+ _messageStream.where((GraphQLSocketMessage message) => message is ConnectionKeepAlive).cast<ConnectionKeepAlive>();
+
+ Stream<UnknownData> get _unknownData => _messageStream.where((GraphQLSocketMessage message) => message is UnknownData).cast<UnknownData>();
+
+ Stream<SubscriptionData> get _subscriptionData =>
+ _messageStream.where((GraphQLSocketMessage message) => message is SubscriptionData).cast<SubscriptionData>();
+
+ Stream<SubscriptionError> get _subscriptionError =>
+ _messageStream.where((GraphQLSocketMessage message) => message is SubscriptionError).cast<SubscriptionError>();
+
+ Stream<SubscriptionComplete> get _subscriptionComplete =>
+ _messageStream.where((GraphQLSocketMessage message) => message is SubscriptionComplete).cast<SubscriptionComplete>();
}

diff:
@@ -104,7 +104,7 @@ class StartOperation extends GraphQLSocketMessage {
}
/// Tell the server to stop sending subscription data for a particular
-/// subscription instance. See StartOperation
+/// subscription instance. See [StartOperation].
class StopOperation extends GraphQLSocketMessage {
final String id;

diff:
@@ -46,9 +46,7 @@ class _SubscriptionState extends State<Subscription> {
void initState() {
super.initState();
- final Stream<SubscriptionData> stream = socketClient.subscribe(
- SubscriptionRequest(
- widget.operationName, widget.query, widget.variables));
+ final Stream<SubscriptionData> stream = socketClient.subscribe(SubscriptionRequest(widget.operationName, widget.query, widget.variables), true);
stream.takeWhile((SubscriptionData message) => _alive).listen(
_onData,

diff:
@@ -15,6 +15,7 @@ dependencies:
path_provider: ^0.4.1
uuid: ^1.0.3
graphql_parser: ^1.1.1
+ rxdart: ^0.20.0
dev_dependencies:
flutter_test:

original_message: Added queryOrMutationTimeout.


message: Fixed timeout bug.

diff:
@@ -104,6 +104,7 @@ class SocketClient {
print("Haven't received keep alive message for ${config.inactivityTimeout.inSeconds} seconds. Disconnecting..");
event.close();
_socket.close(WebSocketStatus.goingAway);
+ _connectionStateController.value = SocketConnectionState.NOT_CONNECTED;
}).listen(null);
}
@@ -128,6 +129,7 @@ class SocketClient {
print('Disconnected from websocket.');
_keepAliveSubscription?.cancel();
_messageSubscription?.cancel();
+ if (_connectionStateController.value != SocketConnectionState.NOT_CONNECTED)
_connectionStateController.value = SocketConnectionState.NOT_CONNECTED;
if (config.autoReconnect && !_connectionStateController.isClosed) {
@@ -176,6 +178,7 @@ class SocketClient {
}
void _write(final GraphQLSocketMessage message) {
+ if (_connectionStateController.value == SocketConnectionState.CONNECTED) {
_socket.add(
json.encode(
message,
@@ -183,6 +186,7 @@ class SocketClient {
),
);
}
+ }
/// Sends a query, mutation or subscription request to the server, and returns a stream of the response.
///
@@ -194,27 +198,40 @@ class SocketClient {
/// In case of socket disconnection, the returned stream will be closed.
Stream<SubscriptionData> subscribe(final SubscriptionRequest payload, final bool isSubscription) {
final String id = _uuid.v4();
-
final StreamController<SubscriptionData> response = StreamController<SubscriptionData>();
response.onListen = () {
if (_connectionStateController.value == SocketConnectionState.CONNECTED && _socket != null) {
- final Stream<SubscriptionComplete> complete = _subscriptionComplete.where((SubscriptionComplete message) => message.id == id).take(1);
- final Stream<SubscriptionData> dataWithoutTimeout =
- _subscriptionData.where((SubscriptionData message) => message.id == id).takeWhile((_) => !response.isClosed);
- final Stream<SubscriptionError> errorWithoutTimeout =
- _subscriptionError.where((SubscriptionError message) => message.id == id).takeWhile((_) => !response.isClosed);
-
- final Stream<SubscriptionData> data = isSubscription || config.queryAndMutationTimeout == null
- ? dataWithoutTimeout
- : dataWithoutTimeout.timeout(config.queryAndMutationTimeout);
- final Stream<SubscriptionError> error = isSubscription || config.queryAndMutationTimeout == null
- ? errorWithoutTimeout
- : errorWithoutTimeout.timeout(config.queryAndMutationTimeout);
-
- complete.listen((_) => response.close());
- data.listen((SubscriptionData message) => response.add(message));
- error.listen((SubscriptionError message) => response.addError(message));
+ final dataErrorComplete = _messageStream.where((GraphQLSocketMessage message) {
+ if (message is SubscriptionData) return message.id == id;
+ if (message is SubscriptionError) return message.id == id;
+ if (message is SubscriptionComplete) return message.id == id;
+ return false;
+ }).takeWhile((_) => !response.isClosed);
+
+ final addTimeout = !isSubscription && config.queryAndMutationTimeout != null;
+ final subscriptionComplete = addTimeout
+ ? dataErrorComplete
+ .where((GraphQLSocketMessage message) => message is SubscriptionComplete)
+ .take(1)
+ .timeout(config.queryAndMutationTimeout, onTimeout: (e) {
+ print('Request timed out.');
+ response.addError(TimeoutException('Request timed out.'));
+ e.close();
+ response.close();
+ })
+ : dataErrorComplete.where((GraphQLSocketMessage message) => message is SubscriptionComplete).take(1);
+
+ subscriptionComplete.listen((_) => response.close());
+
+ dataErrorComplete
+ .where((GraphQLSocketMessage message) => message is SubscriptionData)
+ .cast<SubscriptionData>()
+ .listen((SubscriptionData message) => response.add(message));
+
+ dataErrorComplete
+ .where((GraphQLSocketMessage message) => message is SubscriptionError)
+ .listen((GraphQLSocketMessage message) => response.addError(message));
_write(StartOperation(id, payload));
} else {
@@ -231,7 +248,7 @@ class SocketClient {
/// These streams will emit done events when the current socket is done.
/// A stream that emits the last value of the connection state upon subscription.
- Stream<SocketConnectionState> get connectionState => _connectionStateController.stream.distinct();
+ Stream<SocketConnectionState> get connectionState => _connectionStateController.stream;
Stream<ConnectionAck> get _connectionAck => _messageStream.where((GraphQLSocketMessage message) => message is ConnectionAck).cast<ConnectionAck>();
@@ -242,13 +259,4 @@ class SocketClient {
_messageStream.where((GraphQLSocketMessage message) => message is ConnectionKeepAlive).cast<ConnectionKeepAlive>();
Stream<UnknownData> get _unknownData => _messageStream.where((GraphQLSocketMessage message) => message is UnknownData).cast<UnknownData>();
-
- Stream<SubscriptionData> get _subscriptionData =>
- _messageStream.where((GraphQLSocketMessage message) => message is SubscriptionData).cast<SubscriptionData>();
-
- Stream<SubscriptionError> get _subscriptionError =>
- _messageStream.where((GraphQLSocketMessage message) => message is SubscriptionError).cast<SubscriptionError>();
-
- Stream<SubscriptionComplete> get _subscriptionComplete =>
- _messageStream.where((GraphQLSocketMessage message) => message is SubscriptionComplete).cast<SubscriptionComplete>();
}

original_message: Fixed timeout bug.


message: Updated example.

diff:
@@ -6,21 +6,12 @@ import './queries/readRepositories.dart' as queries;
const String YOUR_PERSONAL_ACCESS_TOKEN = '<YOUR_PERSONAL_ACCESS_TOKEN_HERE>';
-void main() async {
- final websocketLink = WebSocketLink(SocketClient(
- 'ws://localhost:8080/ws/graphql',
- headers: <String, String>{
- 'Authorization': 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN',
- },
- config: SocketClientConfig(autoReconnect: true, inactivityTimeout: Duration(seconds: 15)),
- ));
-
- runApp(MyApp(websocketLink));
+void main() {
+ runApp(MyApp());
}
class MyApp extends StatelessWidget {
- final WebSocketLink wsLink;
- MyApp(this.wsLink);
+ MyApp();
@override
Widget build(BuildContext context) {
@@ -31,10 +22,16 @@ class MyApp extends StatelessWidget {
},
);
+ final WebSocketLink websocketLink = WebSocketLink(SocketClient(
+ 'ws://api.github.com/graphql',
+ headers: <String, String>{
+ 'Authorization': 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN',
+ },
+ config: SocketClientConfig(autoReconnect: true, inactivityTimeout: Duration(seconds: 15)),
+ ));
+
final ValueNotifier<GraphQLClient> client = ValueNotifier<GraphQLClient>(
- GraphQLClient(
- cache: InMemoryCache(),
- link: wsLink,
+ GraphQLClient(cache: InMemoryCache(), link: link // or websocketLink,
),
);

original_message: Updated example.


message: Return Future from dispose method.

diff:
@@ -143,12 +143,11 @@ class SocketClient {
///
/// Use this method if you'd like to disconnect from the specified server permanently,
/// and you'd like to connect to another server instead of the current one.
- void dispose() {
- _socket?.close();
- _keepAliveSubscription?.cancel();
- _messageSubscription?.cancel();
-
- _connectionStateController?.close();
+ Future<void> dispose() async {
+ await _socket?.close();
+ await _keepAliveSubscription?.cancel();
+ await _messageSubscription?.cancel();
+ await _connectionStateController?.close();
}
static GraphQLSocketMessage _parseSocketMessage(dynamic message) {

original_message: Return Future from dispose method.


message: Fixed onConnectionLost() when socket is already closed

diff:
@@ -129,6 +129,9 @@ class SocketClient {
print('Disconnected from websocket.');
_keepAliveSubscription?.cancel();
_messageSubscription?.cancel();
+
+ if (_connectionStateController.isClosed) return;
+
if (_connectionStateController.value != SocketConnectionState.NOT_CONNECTED)
_connectionStateController.value = SocketConnectionState.NOT_CONNECTED;


message: Fixed dispose to cancel reconnectTimer

diff:
@@ -57,6 +57,7 @@ class SocketClient {
final CompressionOptions compression;
final _connectionStateController = BehaviorSubject<SocketConnectionState>();
+ Timer _reconnectTimer;
WebSocket _socket;
Stream<GraphQLSocketMessage> _messageStream;
@@ -79,14 +80,9 @@ class SocketClient {
/// Connects to the server.
///
/// If this instance is disposed, this method does nothing.
- Future<void> _connect({Duration delayUntilConnectionAttempt}) async {
+ Future<void> _connect() async {
if (_connectionStateController.isClosed) return;
- if (delayUntilConnectionAttempt != null) {
- print('Scheduling to connect in ${delayUntilConnectionAttempt.inSeconds} seconds...');
- await Future<void>.delayed(delayUntilConnectionAttempt);
- }
-
if (_socket != null) print('Reconnecting to socket...');
_connectionStateController.value = SocketConnectionState.CONNECTING;
print('Connecting to websocket: $url...');
@@ -136,7 +132,15 @@ class SocketClient {
_connectionStateController.value = SocketConnectionState.NOT_CONNECTED;
if (config.autoReconnect && !_connectionStateController.isClosed) {
- _connect(delayUntilConnectionAttempt: config.delayBetweenReconnectionAttempts);
+ if (config.delayBetweenReconnectionAttempts != null) {
+ print('Scheduling to connect in ${config.delayBetweenReconnectionAttempts.inSeconds} seconds...');
+
+ _reconnectTimer = Timer(config.delayBetweenReconnectionAttempts, () {
+ _connect();
+ });
+ } else {
+ Timer.run(() => _connect());
+ }
}
}
@@ -147,6 +151,7 @@ class SocketClient {
/// Use this method if you'd like to disconnect from the specified server permanently,
/// and you'd like to connect to another server instead of the current one.
Future<void> dispose() async {
+ _reconnectTimer?.cancel();
await _socket?.close();
await _keepAliveSubscription?.cancel();
await _messageSubscription?.cancel();

original_message: Fixed dispose to cancel reconnectTimer


message: Remove global socketClient variable, Subscription widget refactor to use WebsocketLink, update example app.

diff:
@@ -8,11 +8,6 @@ const String YOUR_PERSONAL_ACCESS_TOKEN = '<YOUR_PERSONAL_ACCESS_TOKEN>';
void main() => runApp(MyApp());
-class MyApp extends StatelessWidget {
- MyApp();
-
- @override
- Widget build(BuildContext context) {
final HttpLink httpLink = HttpLink(
uri: 'https://api.github.com/graphql',
);
@@ -21,15 +16,19 @@ class MyApp extends StatelessWidget {
getToken: () async => 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN',
);
- final Link link = authLink.concat(httpLink);
-
- final WebSocketLink websocketLink = WebSocketLink(SocketClient(
- 'ws://api.github.com/graphql',
- headers: <String, String>{
- 'Authorization': 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN',
- },
+// to handle subscriptions
+final WebSocketLink websocketLink = WebSocketLink(
+ url: 'ws://localhost:8080/ws/graphql',
config: SocketClientConfig(autoReconnect: true, inactivityTimeout: Duration(seconds: 15)),
- ));
+);
+
+// the order is important!
+final Link link = authLink.concat(httpLink).concat(websocketLink);
+
+class MyApp extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ // websocketLink.connectOrReconnect();
final ValueNotifier<GraphQLClient> client = ValueNotifier<GraphQLClient>(
GraphQLClient(
@@ -115,18 +114,31 @@ class _MyHomePageState extends State<MyHomePage> {
}
// result.data can be either a [List<dynamic>] or a [Map<String, dynamic>]
- final List<dynamic> repositories =
- result.data['viewer']['repositories']['nodes'];
+ final List<dynamic> repositories = result.data['viewer']['repositories']['nodes'];
return Expanded(
child: ListView.builder(
itemCount: repositories.length,
- itemBuilder: (BuildContext context, int index) =>
- StarrableRepository(repository: repositories[index]),
+ itemBuilder: (BuildContext context, int index) => StarrableRepository(repository: repositories[index]),
),
);
},
),
+ Subscription<Map<String, dynamic>>(
+ 'test',
+ queries.testSubscription,
+ builder: ({loading, payload, dynamic error}) => loading ? Text('Loading...') : Text(payload.toString()),
+ ),
+ Subscription<Map<String, dynamic>>(
+ 'test',
+ queries.testSubscription,
+ builder: ({loading, payload, dynamic error}) => loading ? Text('Loading2...') : Text(payload.toString()),
+ ),
+ Subscription<Map<String, dynamic>>(
+ 'test',
+ queries.testSubscription,
+ builder: ({loading, payload, dynamic error}) => loading ? Text('Loading3...') : Text(payload.toString()),
+ )
],
),
),
@@ -198,9 +210,7 @@ class StarrableRepositoryState extends State<StarrableRepository> {
if (result.hasErrors) {
print(result.errors);
} else {
- final Map<String, Object> updated =
- Map<String, Object>.from(widget.repository)
- ..addAll(extractRepositoryData(result.data));
+ final Map<String, Object> updated = Map<String, Object>.from(widget.repository)..addAll(extractRepositoryData(result.data));
cache.write(typenameDataIdFromObject(updated), updated);
}
@@ -211,9 +221,7 @@ class StarrableRepositoryState extends State<StarrableRepository> {
builder: (BuildContext context) {
return AlertDialog(
title: Text(
- extractRepositoryData(result.data)['viewerHasStarred']
- ? 'Thanks for your star!'
- : 'Sorry you changed your mind!',
+ extractRepositoryData(result.data)['viewerHasStarred'] ? 'Thanks for your star!' : 'Sorry you changed your mind!',
),
actions: <Widget>[
SimpleDialogOption(

diff:
@@ -12,3 +12,12 @@ const String readRepositories = r'''
}
}
''';
+
+const String testSubscription = r'''
+ subscription test {
+ deviceChanged(id: 2) {
+ id
+ name
+ }
+ }
+''';

diff:
+import 'package:graphql_flutter/src/utilities/helpers.dart';
import 'package:meta/meta.dart';
/// [FetchPolicy] determines where the client may return a result from. The options are:
@@ -69,7 +70,7 @@ class QueryOptions extends BaseOptions {
);
/// The time interval (in milliseconds) on which this query should be
- /// refetched from the server.
+ /// re-fetched from the server.
int pollInterval;
}
@@ -143,33 +144,6 @@ class WatchQueryOptions extends QueryOptions {
}
// compare variables last, because maps take more time
- return _areDifferentVariables(a.variables, b.variables);
- }
-
- bool _areDifferentVariables(
- Map<String, dynamic> a,
- Map<String, dynamic> b,
- ) {
- if (a == null && b == null) {
- return false;
- }
-
- if (a == null || b == null) {
- return true;
- }
-
- if (a.length != b.length) {
- return true;
- }
-
- bool areDifferent = false;
-
- a.forEach((String key, dynamic value) {
- if ((!b.containsKey(key)) || b[key] != value) {
- areDifferent = true;
- }
- });
-
- return areDifferent;
+ return areDifferentVariables(a.variables, b.variables);
}
}

diff:
import 'dart:async';
-import 'package:meta/meta.dart';
-
-import 'package:graphql_flutter/src/core/query_manager.dart';
-import 'package:graphql_flutter/src/core/query_result.dart';
+import 'package:graphql_flutter/src/cache/cache.dart';
import 'package:graphql_flutter/src/core/observable_query.dart';
+import 'package:graphql_flutter/src/core/query_manager.dart';
import 'package:graphql_flutter/src/core/query_options.dart';
-
+import 'package:graphql_flutter/src/core/query_result.dart';
+import 'package:graphql_flutter/src/link/fetch_result.dart';
import 'package:graphql_flutter/src/link/link.dart';
-import 'package:graphql_flutter/src/cache/cache.dart';
+import 'package:graphql_flutter/src/link/operation.dart';
+import 'package:meta/meta.dart';
/// The link is a [Link] over which GraphQL documents will be resolved into a [FetchResult].
/// The cache is the initial [Cache] to use in the data store.
@@ -50,10 +50,9 @@ class GraphQLClient {
return queryManager.mutate(options);
}
- /// This subscribes to a GraphQL subscription according to the options specified and returns an
+ /// This subscribes to a GraphQL subscription according to the options specified and returns a
/// [Stream] which either emits received data or an error.
- Stream<dynamic> subscribe(dynamic options) {
- // TODO: merge the subscription client with the new client
- return const Stream<dynamic>.empty();
+ Stream<FetchResult> subscribe(Operation operation) {
+ return execute(link: link, operation: operation);
}
}

diff:
import 'dart:async';
import 'dart:convert';
-import 'package:meta/meta.dart';
-import 'package:http/http.dart';
-import 'package:http_parser/http_parser.dart';
-
-import 'package:graphql_flutter/src/link/link.dart';
-import 'package:graphql_flutter/src/link/operation.dart';
import 'package:graphql_flutter/src/link/fetch_result.dart';
-import 'package:graphql_flutter/src/link/http/http_config.dart';
import 'package:graphql_flutter/src/link/http/fallback_http_config.dart';
+import 'package:graphql_flutter/src/link/http/http_config.dart';
+import 'package:graphql_flutter/src/link/link.dart';
+import 'package:graphql_flutter/src/link/operation.dart';
+import 'package:http/http.dart';
+import 'package:http_parser/http_parser.dart';
+import 'package:meta/meta.dart';
+import 'package:rxdart/rxdart.dart';
class HttpLink extends Link {
HttpLink({
@@ -24,6 +24,11 @@ class HttpLink extends Link {
Operation operation, [
NextLink forward,
]) {
+ if (operation.isSubscription) {
+ if (forward == null) return Observable.error(Exception('This link does not support subscriptions.'));
+ return forward(operation);
+ }
+
final Client fetcher = fetch ?? Client();
final HttpConfig linkConfig = HttpConfig(
@@ -50,8 +55,7 @@ class HttpLink extends Link {
);
}
- final HttpOptionsAndBody httpOptionsAndBody =
- _selectHttpOptionsAndBody(
+ final HttpOptionsAndBody httpOptionsAndBody = _selectHttpOptionsAndBody(
operation,
fallbackHttpConfig,
linkConfig,
@@ -221,8 +225,7 @@ FetchResult _parseResponse(Response response) {
/// The default fallback encoding is set to UTF-8 according to the IETF RFC4627 standard
/// which specifies the application/json media type:
/// "JSON text SHALL be encoded in Unicode. The default encoding is UTF-8."
-Encoding _determineEncodingFromResponse(Response response,
- [Encoding fallback = utf8]) {
+Encoding _determineEncodingFromResponse(Response response, [Encoding fallback = utf8]) {
final String contentType = response.headers['content-type'];
if (contentType == null) {

diff:
import 'dart:async';
-import 'package:graphql_flutter/src/link/operation.dart';
import 'package:graphql_flutter/src/link/fetch_result.dart';
+import 'package:graphql_flutter/src/link/operation.dart';
typedef NextLink = Stream<FetchResult> Function(
Operation operation,
@@ -27,20 +27,11 @@ Link _concat(
}
class Link {
- Link({
- this.request,
- });
+ Link({this.request});
- final RequestHandler request;
+ RequestHandler request;
- Link concat(Link next) {
- return _concat(this, next);
- }
+ Link concat(Link next) => _concat(this, next);
}
-Stream<FetchResult> execute({
- Link link,
- Operation operation,
-}) {
- return link.request(operation);
-}
+Stream<FetchResult> execute({Link link, Operation operation}) => link.request(operation);

diff:
@@ -15,7 +15,7 @@ class Operation {
final Map<String, dynamic> _context = <String, dynamic>{};
- /// Sets the context of an opration by merging the new context with the old one.
+ /// Sets the context of an operation by merging the new context with the old one.
void setContext(Map<String, dynamic> next) {
_context.addAll(next);
}
@@ -27,6 +27,8 @@ class Operation {
return result;
}
+ bool get isSubscription => document.contains(RegExp(r'.*?subscription ' + operationName));
+
String toKey() {
/// XXX we're assuming here that variables will be serialized in the same order.
/// that might not always be true

diff:
@@ -2,18 +2,63 @@ import 'package:graphql_flutter/src/link/fetch_result.dart';
import 'package:graphql_flutter/src/link/link.dart';
import 'package:graphql_flutter/src/link/operation.dart';
import 'package:graphql_flutter/src/socket_client.dart';
+import 'package:graphql_flutter/src/utilities/helpers.dart';
import 'package:graphql_flutter/src/websocket/messages.dart';
+import 'package:meta/meta.dart';
+/// A websocket [Link] implementation to support the websocket transport.
+/// It supports subscriptions, query and mutation operations as well.
+///
+/// This link is aware of [AuthLink], so the headers specified there are automatically
+/// applied when connecting to the socket server, unless explicitly overridden by the [headers]
+/// parameter.
+///
+/// There's an option called [reconnectOnHeaderChange] that makes it possible to reconnect to the server when the
+/// headers have changed. For example, if the user logs in with another user account and the `Authorization` header changes.
+/// This could be desired because of the nature of websocket connections: headers can only be specified upon connecting.
+///
+/// NOTE: the actual socket connection will only get established after an [Operation] is handled by this [WebSocketLink].
+/// If you'd like to connect to the socket server instantly, call the [connectOrReconnect] method after creating this [WebSocketLink] instance.
class WebSocketLink extends Link {
- final SocketClient socketClient;
- WebSocketLink(this.socketClient)
- : super(
- request: (
- Operation operation, [
- NextLink forward,
- ]) {
- return socketClient.subscribe(SubscriptionRequest(operation.operationName, operation.document, operation.variables), false).map(
- (result) => FetchResult(data: result.data, errors: result.errors, context: operation.getContext(), extensions: operation.extensions));
- },
- );
+ /// Creates a new [WebSocketLink] instance with the specified config.
+ WebSocketLink({@required this.url, this.headers, this.reconnectOnHeaderChange = true, this.config = const SocketClientConfig()}) : super() {
+ request = _doOperation;
+ }
+
+ final String url;
+ final Map<String, dynamic> headers;
+ final bool reconnectOnHeaderChange;
+ final SocketClientConfig config;
+
+ // cannot be final because we're changing the instance upon a header change.
+ SocketClient _socketClient;
+
+ Stream<FetchResult> _doOperation(Operation operation, [NextLink forward]) {
+ final concatHeaders = <String, dynamic>{};
+ final Map<String, dynamic> context = operation.getContext();
+ if (context != null && context.containsKey('headers')) concatHeaders.addAll(context['headers']);
+ if (headers != null) concatHeaders.addAll(headers);
+
+ if (_socketClient == null || (reconnectOnHeaderChange && areDifferentVariables(_socketClient.headers, concatHeaders))) {
+ if (_socketClient != null) print('Creating a new socket client, because the headers have changed..');
+ connectOrReconnect(headers: concatHeaders);
+ }
+
+ return _socketClient
+ .subscribe(SubscriptionRequest(operation), true)
+ .map((result) => FetchResult(data: result.data, errors: result.errors, context: operation.getContext(), extensions: operation.extensions));
+ }
+
+ /// Connects or reconnects to the server with the specified headers.
+ void connectOrReconnect({Map<String, dynamic> headers}) {
+ _socketClient?.dispose();
+ _socketClient = SocketClient(url, headers: headers ?? this.headers, config: config);
+ }
+
+ /// Disposes the underlying socket client explicitly. Only use this, if you want to disconnect from
+ /// the current server in favour of another one. If that's the case, create a new [WebSocketLink] instance.
+ Future<void> dispose() async {
+ await _socketClient?.dispose();
+ _socketClient = null;
+ }
}

diff:
@@ -7,9 +7,15 @@ import 'package:graphql_flutter/src/websocket/messages.dart';
import 'package:rxdart/subjects.dart';
import 'package:uuid/uuid.dart';
-SocketClient socketClient;
-
class SocketClientConfig {
+ const SocketClientConfig(
+ {this.autoReconnect = true,
+ this.queryAndMutationTimeout = const Duration(seconds: 10),
+ this.inactivityTimeout = const Duration(seconds: 30),
+ this.delayBetweenReconnectionAttempts = const Duration(seconds: 5),
+ this.compression = CompressionOptions.compressionDefault,
+ this.initPayload});
+
/// Whether to reconnect to the server after detecting connection loss.
final bool autoReconnect;
@@ -30,11 +36,10 @@ class SocketClientConfig {
// If null, no timeout is applied, although not recommended.
final Duration queryAndMutationTimeout;
- const SocketClientConfig(
- {this.autoReconnect = true,
- this.queryAndMutationTimeout = const Duration(seconds: 10),
- this.inactivityTimeout = const Duration(seconds: 30),
- this.delayBetweenReconnectionAttempts = const Duration(seconds: 5)});
+ final CompressionOptions compression;
+
+ /// The initial payload that will be sent to the server upon connection. Can be null.
+ final Map<String, String> initPayload;
}
enum SocketConnectionState { NOT_CONNECTED, CONNECTING, CONNECTED }
@@ -48,13 +53,24 @@ enum SocketConnectionState { NOT_CONNECTED, CONNECTING, CONNECTED }
/// lifting of socket state management. Once you're done with the socket connection, make sure
/// you call the [dispose] method to release all allocated resources.
class SocketClient {
+ SocketClient(
+ this.url, {
+ this.protocols = const <String>[
+ 'graphql-ws',
+ ],
+ this.headers = const <String, String>{
+ 'content-type': 'application/json',
+ },
+ this.config = const SocketClientConfig(),
+ }) {
+ _connect();
+ }
+
final Uuid _uuid = Uuid();
final String url;
final SocketClientConfig config;
final Iterable<String> protocols;
- final Map<String, String> initPayload;
final Map<String, dynamic> headers;
- final CompressionOptions compression;
final _connectionStateController = BehaviorSubject<SocketConnectionState>();
Timer _reconnectTimer;
@@ -64,34 +80,20 @@ class SocketClient {
StreamSubscription<ConnectionKeepAlive> _keepAliveSubscription;
StreamSubscription<GraphQLSocketMessage> _messageSubscription;
- SocketClient(this.url,
- {this.protocols = const <String>[
- 'graphql-ws',
- ],
- this.headers = const <String, String>{
- 'content-type': 'application/json',
- },
- this.compression = CompressionOptions.compressionDefault,
- this.config = const SocketClientConfig(),
- this.initPayload}) {
- _connect();
- }
-
/// Connects to the server.
///
/// If this instance is disposed, this method does nothing.
Future<void> _connect() async {
if (_connectionStateController.isClosed) return;
- if (_socket != null) print('Reconnecting to socket...');
_connectionStateController.value = SocketConnectionState.CONNECTING;
print('Connecting to websocket: $url...');
try {
- _socket = await WebSocket.connect(url, protocols: protocols, headers: headers, compression: compression);
+ _socket = await WebSocket.connect(url, protocols: protocols, headers: headers, compression: config.compression);
_connectionStateController.value = SocketConnectionState.CONNECTED;
print('Connected to websocket.');
- _write(InitOperation(initPayload));
+ _write(InitOperation(config.initPayload));
_messageStream = _socket.asBroadcastStream().map<GraphQLSocketMessage>(_parseSocketMessage);
@@ -106,7 +108,7 @@ class SocketClient {
_messageSubscription = _messageStream.listen(
(dynamic data) {
- print("data: $data");
+ print('data: $data');
},
onDone: () {
print('done');
@@ -114,7 +116,7 @@ class SocketClient {
},
cancelOnError: true,
onError: (dynamic e) {
- print("error: $e");
+ print('error: $e');
});
} catch (e) {
onConnectionLost();
@@ -152,6 +154,7 @@ class SocketClient {
/// Use this method if you'd like to disconnect from the specified server permanently,
/// and you'd like to connect to another server instead of the current one.
Future<void> dispose() async {
+ print('Disposing socket client..');
_reconnectTimer?.cancel();
await _socket?.close();
await _keepAliveSubscription?.cancel();
@@ -204,12 +207,28 @@ class SocketClient {
/// If the request is a subscription, obviously no timeout is applied.
///
/// In case of socket disconnection, the returned stream will be closed.
- Stream<SubscriptionData> subscribe(final SubscriptionRequest payload, final bool isSubscription) {
+ Stream<SubscriptionData> subscribe(final SubscriptionRequest payload, final bool waitForConnection) {
final String id = _uuid.v4();
final StreamController<SubscriptionData> response = StreamController<SubscriptionData>();
+ StreamSubscription<SocketConnectionState> sub;
+ final addTimeout = !payload.operation.isSubscription && config.queryAndMutationTimeout != null;
response.onListen = () {
- if (_connectionStateController.value == SocketConnectionState.CONNECTED && _socket != null) {
+ final waitForConnectedStateWithoutTimeout = _connectionStateController
+ .startWith(waitForConnection ? null : SocketConnectionState.CONNECTED)
+ .where((state) => state == SocketConnectionState.CONNECTED)
+ .take(1);
+
+ final waitForConnectedState = addTimeout
+ ? waitForConnectedStateWithoutTimeout.timeout(config.queryAndMutationTimeout, onTimeout: (e) {
+ print('Connection timed out.');
+ response.addError(TimeoutException('Connection timed out.'));
+ e.close();
+ response.close();
+ })
+ : waitForConnectedStateWithoutTimeout;
+
+ sub = waitForConnectedState.listen((_) {
final dataErrorComplete = _messageStream.where((GraphQLSocketMessage message) {
if (message is SubscriptionData) return message.id == id;
if (message is SubscriptionError) return message.id == id;
@@ -217,7 +236,6 @@ class SocketClient {
return false;
}).takeWhile((_) => !response.isClosed);
- final addTimeout = !isSubscription && config.queryAndMutationTimeout != null;
final subscriptionComplete = addTimeout
? dataErrorComplete
.where((GraphQLSocketMessage message) => message is SubscriptionComplete)
@@ -242,11 +260,11 @@ class SocketClient {
.listen((GraphQLSocketMessage message) => response.addError(message));
_write(StartOperation(id, payload));
- } else {
- response.addError(Exception('Not connected to the server.'));
- }
+ });
};
+
response.onCancel = () {
+ sub?.cancel();
if (_connectionStateController.value == SocketConnectionState.CONNECTED && _socket != null) _write(StopOperation(id));
};

diff:
bool notNull(Object any) {
return any != null;
}
+
+bool areDifferentVariables(
+ Map<String, dynamic> a,
+ Map<String, dynamic> b,
+) {
+ if (a == null && b == null) {
+ return false;
+ }
+
+ if (a == null || b == null) {
+ return true;
+ }
+
+ if (a.length != b.length) {
+ return true;
+ }
+
+ bool areDifferent = false;
+
+ a.forEach((String key, dynamic value) {
+ if ((!b.containsKey(key)) || b[key] != value) {
+ areDifferent = true;
+ }
+ });
+
+ return areDifferent;
+}

diff:
import 'dart:convert';
+import 'package:graphql_flutter/src/link/operation.dart';
+
/// These messages represent the structures used for Client-server communication
/// in a GraphQL web-socket subscription. Each message is represented in a JSON
/// format where the data type is denoted by the `type` field.
@@ -70,17 +72,14 @@ class InitOperation extends GraphQLSocketMessage {
/// defined in the query provided. Additional variables can be provided
/// and sent to the server for processing.
class SubscriptionRequest extends JsonSerializable {
- SubscriptionRequest(this.operationName, this.query, this.variables);
-
- final String operationName;
- final String query;
- final dynamic variables;
+ SubscriptionRequest(this.operation);
+ final Operation operation;
@override
dynamic toJson() => <String, dynamic>{
- 'operationName': operationName,
- 'query': query,
- 'variables': variables,
+ 'operationName': operation.operationName,
+ 'query': operation.document,
+ 'variables': operation.variables,
};
}
@@ -156,8 +155,7 @@ class ConnectionKeepAlive extends GraphQLSocketMessage {
/// payload. The user should check the errors result before processing the
/// data value. These error are from the query resolvers.
class SubscriptionData extends GraphQLSocketMessage {
- SubscriptionData(this.id, this.data, this.errors)
- : super(MessageTypes.GQL_DATA);
+ SubscriptionData(this.id, this.data, this.errors) : super(MessageTypes.GQL_DATA);
final String id;
final dynamic data;

diff:
import 'dart:async';
import 'package:flutter/widgets.dart';
+import 'package:graphql_flutter/src/graphql_client.dart';
+import 'package:graphql_flutter/src/link/fetch_result.dart';
+import 'package:graphql_flutter/src/link/operation.dart';
+import 'package:graphql_flutter/src/utilities/helpers.dart';
+import 'package:graphql_flutter/src/widgets/graphql_provider.dart';
-import '../socket_client.dart';
import '../websocket/messages.dart';
typedef OnSubscriptionCompleted = void Function();
-typedef SubscriptionBuilder = Widget Function({
+typedef SubscriptionBuilder<T> = Widget Function({
final bool loading,
- final dynamic payload,
+ final T payload,
final dynamic error,
});
-class Subscription extends StatefulWidget {
+class Subscription<T> extends StatefulWidget {
const Subscription(
this.operationName,
this.query, {
@@ -27,33 +31,29 @@ class Subscription extends StatefulWidget {
final String operationName;
final String query;
final dynamic variables;
- final SubscriptionBuilder builder;
+ final SubscriptionBuilder<T> builder;
final OnSubscriptionCompleted onCompleted;
- final dynamic initial;
+ final T initial;
@override
- _SubscriptionState createState() => _SubscriptionState();
+ _SubscriptionState<T> createState() => _SubscriptionState<T>();
}
-class _SubscriptionState extends State<Subscription> {
+class _SubscriptionState<T> extends State<Subscription<T>> {
bool _loading = true;
- dynamic _data;
+ T _data;
dynamic _error;
+ StreamSubscription<FetchResult> _subscription;
- bool _alive = true;
+ void _initSubscription() {
+ final GraphQLClient client = GraphQLProvider.of(context).value;
+ assert(client != null);
+ final Operation operation = Operation(document: widget.query, variables: widget.variables, operationName: widget.operationName);
- @override
- void initState() {
- super.initState();
-
- final Stream<SubscriptionData> stream = socketClient.subscribe(SubscriptionRequest(widget.operationName, widget.query, widget.variables), true);
-
- stream.takeWhile((SubscriptionData message) => _alive).listen(
- _onData,
- onError: _onError,
- onDone: _onDone,
- );
+ final Stream<FetchResult> stream = client.subscribe(operation);
+ if (_subscription == null) {
+ // Set the initial value for the first time.
if (widget.initial != null) {
setState(() {
_loading = true;
@@ -63,13 +63,38 @@ class _SubscriptionState extends State<Subscription> {
}
}
+ _subscription?.cancel();
+ _subscription = stream.listen(
+ _onData,
+ onError: _onError,
+ onDone: _onDone,
+ );
+ }
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ _initSubscription();
+ }
+
+ @override
+ void didUpdateWidget(Subscription<T> oldWidget) {
+ super.didUpdateWidget(oldWidget);
+
+ if (widget.query != oldWidget.query ||
+ widget.operationName != oldWidget.operationName ||
+ areDifferentVariables(widget.variables, oldWidget.variables)) {
+ _initSubscription();
+ }
+ }
+
@override
void dispose() {
- _alive = false;
+ _subscription?.cancel();
super.dispose();
}
- void _onData(final SubscriptionData message) {
+ void _onData(final FetchResult message) {
setState(() {
_loading = false;
_data = message.data;


message: add recursive add all to fix cases where object does not have typename

diff:
@@ -2,11 +2,25 @@ import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
-
import 'package:path_provider/path_provider.dart';
-
import 'package:graphql_flutter/src/cache/cache.dart';
+void _recursiveAddAll(
+ Map<String, dynamic> oldData,
+ Map<String, dynamic> newData,
+) {
+ newData.forEach((String key, dynamic value) {
+ if (oldData.containsKey(key) &&
+ oldData[key] is Map &&
+ value != null &&
+ value is Map) {
+ _recursiveAddAll(oldData[key], value);
+ } else {
+ oldData[key] = value;
+ }
+ });
+}
+
class InMemoryCache implements Cache {
InMemoryCache({
this.customStorageDirectory,
@@ -38,7 +52,7 @@ class InMemoryCache implements Cache {
value != null &&
value is Map) {
// Avoid overriding a superset with a subset of a field (#155)
- _inMemoryCache[key].addAll(value);
+ _recursiveAddAll(_inMemoryCache[key], value);
} else {
_inMemoryCache[key] = value;
}


message: Update normalized_in_memory_test.dart

diff:
@@ -31,6 +31,10 @@ final Map<String, Object> rawOperationData = <String, Object>{
},
'bField': <String, Object>{'field': true}
},
+ 'd': <String, Object>{
+ 'id': 9,
+ 'dField': <String, Object>{'field': true}
+ },
},
'aField': <String, Object>{'field': false}
};
@@ -62,6 +66,10 @@ final Map<String, Object> updatedCOperationData = <String, Object>{
'c': updatedCValue,
'bField': <String, Object>{'field': true}
},
+ 'd': <String, Object>{
+ 'id': 9,
+ 'dField': <String, Object>{'field': true}
+ },
},
'aField': <String, Object>{'field': false}
};
@@ -80,6 +88,9 @@ final Map<String, Object> subsetAValue = <String, Object>{
'value': 8,
}
],
+ 'd': <String, Object>{
+ 'id': 10,
+ },
},
};
@@ -103,6 +114,10 @@ final Map<String, Object> updatedSubsetOperationData = <String, Object>{
'c': updatedCValue,
'bField': <String, Object>{'field': true}
},
+ 'd': <String, Object>{
+ 'id': 10,
+ 'dField': <String, Object>{'field': true}
+ },
},
'aField': <String, Object>{'field': false}
};

original_message: Update normalized_in_memory_test.dart