@@ -2,7 +2,7 @@ import 'dart:async';
2
2
import 'dart:convert' ;
3
3
import 'package:flutter/foundation.dart' ;
4
4
import 'package:web_socket_channel/web_socket_channel.dart' ;
5
- import 'package:web_socket_channel/status.dart' ;
5
+ import 'package:web_socket_channel/status.dart' as status ;
6
6
import 'exception.dart' ;
7
7
import 'realtime_subscription.dart' ;
8
8
import 'client.dart' ;
@@ -15,35 +15,46 @@ typedef GetFallbackCookie = String? Function();
15
15
16
16
mixin RealtimeMixin {
17
17
late Client client;
18
- final Map <String , List < StreamController < RealtimeMessage >> > _channels = {};
18
+ final Set <String > _channels = {};
19
19
WebSocketChannel ? _websok;
20
20
String ? _lastUrl;
21
21
late WebSocketFactory getWebSocket;
22
22
GetFallbackCookie ? getFallbackCookie;
23
23
int ? get closeCode => _websok? .closeCode;
24
+ int _subscriptionsCounter = 0 ;
25
+ Map <int , RealtimeSubscription > _subscriptions = {};
26
+ bool _notifyDone = true ;
27
+ StreamSubscription ? _websocketSubscription;
28
+ bool _creatingSocket = false ;
24
29
25
30
Future <dynamic > _closeConnection () async {
26
- await _websok? .sink.close (normalClosure);
31
+ await _websocketSubscription? .cancel ();
32
+ await _websok? .sink.close (status.normalClosure, 'Ending session' );
27
33
_lastUrl = null ;
28
34
}
29
35
30
36
_createSocket () async {
37
+ if (_creatingSocket || _channels.isEmpty) return ;
38
+ _creatingSocket = true ;
31
39
final uri = _prepareUri ();
32
40
if (_websok == null ) {
33
41
_websok = await getWebSocket (uri);
34
42
_lastUrl = uri.toString ();
35
43
} else {
36
44
if (_lastUrl == uri.toString () && _websok? .closeCode == null ) {
45
+ _creatingSocket = false ;
37
46
return ;
38
47
}
48
+ _notifyDone = false ;
39
49
await _closeConnection ();
40
50
_lastUrl = uri.toString ();
41
51
_websok = await getWebSocket (uri);
52
+ _notifyDone = true ;
42
53
}
43
54
debugPrint ('subscription: $_lastUrl ' );
44
55
45
56
try {
46
- _websok? .stream.listen ((response) {
57
+ _websocketSubscription = _websok? .stream.listen ((response) {
47
58
final data = RealtimeResponse .fromJson (response);
48
59
switch (data.type) {
49
60
case 'error' :
@@ -67,28 +78,25 @@ mixin RealtimeMixin {
67
78
break ;
68
79
case 'event' :
69
80
final message = RealtimeMessage .fromMap (data.data);
70
- for (var channel in message.channels ) {
71
- if (_channels[ channel] != null ) {
72
- for ( var stream in _channels[ channel] ! ) {
73
- stream.sink .add (message);
81
+ for (var subscription in _subscriptions.values ) {
82
+ for ( var channel in message.channels ) {
83
+ if (subscription.channels. contains ( channel) ) {
84
+ subscription.controller .add (message);
74
85
}
75
86
}
76
87
}
77
88
break ;
78
89
}
79
90
}, onDone: () {
80
- for (var list in _channels.values) {
81
- for (var stream in list) {
82
- stream.close ();
83
- }
91
+ if (! _notifyDone || _creatingSocket) return ;
92
+ for (var subscription in _subscriptions.values) {
93
+ subscription.close ();
84
94
}
85
95
_channels.clear ();
86
96
_closeConnection ();
87
97
}, onError: (err, stack) {
88
- for (var list in _channels.values) {
89
- for (var stream in list) {
90
- stream.sink.addError (err, stack);
91
- }
98
+ for (var subscription in _subscriptions.values) {
99
+ subscription.controller.addError (err, stack);
92
100
}
93
101
if (_websok? .closeCode != null && _websok? .closeCode != 1008 ) {
94
102
debugPrint ("Reconnecting in one second." );
@@ -103,6 +111,8 @@ mixin RealtimeMixin {
103
111
throw AppwriteException (e.message);
104
112
}
105
113
throw AppwriteException (e.toString ());
114
+ } finally {
115
+ _creatingSocket = false ;
106
116
}
107
117
}
108
118
@@ -118,40 +128,46 @@ mixin RealtimeMixin {
118
128
port: uri.port,
119
129
queryParameters: {
120
130
"project" : client.config['project' ],
121
- "channels[]" : _channels.keys. toList (),
131
+ "channels[]" : _channels.toList (),
122
132
},
123
133
path: uri.path + "/realtime" ,
124
134
);
125
135
}
126
136
127
137
RealtimeSubscription subscribeTo (List <String > channels) {
128
138
StreamController <RealtimeMessage > controller = StreamController .broadcast ();
129
- for (var channel in channels) {
130
- if (! _channels.containsKey (channel)) {
131
- _channels[channel] = [];
132
- }
133
- _channels[channel]! .add (controller);
134
- }
139
+ _channels.addAll (channels);
135
140
Future .delayed (Duration .zero, () => _createSocket ());
141
+ int id = DateTime .now ().microsecondsSinceEpoch;
136
142
RealtimeSubscription subscription = RealtimeSubscription (
137
- stream: controller.stream,
143
+ controller: controller,
144
+ channels: channels,
138
145
close: () async {
146
+ _subscriptions.remove (id);
147
+ _subscriptionsCounter-- ;
139
148
controller.close ();
140
- for (var channel in channels) {
141
- _channels[channel]! .remove (controller);
142
- if (_channels[channel]! .isEmpty) {
143
- _channels.remove (channel);
144
- }
145
- }
146
- if (_channels.isNotEmpty) {
149
+ _cleanup (channels);
150
+
151
+ if (_channels.isNotEmpty) {
147
152
await Future .delayed (Duration .zero, () => _createSocket ());
148
153
} else {
149
154
await _closeConnection ();
150
155
}
151
156
});
157
+ _subscriptions[id] = subscription;
152
158
return subscription;
153
159
}
154
160
161
+ void _cleanup (List <String > channels) {
162
+ for (var channel in channels) {
163
+ bool found = _subscriptions.values
164
+ .any ((subscription) => subscription.channels.contains (channel));
165
+ if (! found) {
166
+ _channels.remove (channel);
167
+ }
168
+ }
169
+ }
170
+
155
171
void handleError (RealtimeResponse response) {
156
172
if (response.data['code' ] == 1008 ) {
157
173
throw AppwriteException (response.data["message" ], response.data["code" ]);
0 commit comments