Skip to content

Commit 40ccd4c

Browse files
committed
Reconnect with other tab on close
1 parent 5d924c5 commit 40ccd4c

File tree

9 files changed

+149
-48
lines changed

9 files changed

+149
-48
lines changed

demos/django-todolist/README.md

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,6 @@ username: testuser
1919
password: testpassword
2020
```
2121

22-
## WEB NOTes
23-
24-
```
25-
flutter run -d web-server
26-
google-chrome --disable-web-security --user-data-dir='.dart_tool/.chrome'
27-
```
28-
2922
# Service Configuration
3023

3124
This demo can be used with cloud or local services.

demos/supabase-anonymous-auth/pubspec.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,9 @@ dev_dependencies:
2525

2626
flutter_lints: ^3.0.1
2727

28+
dependency_overrides:
29+
sqlite_async:
30+
path: /home/simon/src/sqlite_async.dart/packages/sqlite_async
31+
2832
flutter:
2933
uses-material-design: true

demos/supabase-edge-function-auth/pubspec.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,9 @@ dev_dependencies:
2525

2626
flutter_lints: ^3.0.1
2727

28+
dependency_overrides:
29+
sqlite_async:
30+
path: /home/simon/src/sqlite_async.dart/packages/sqlite_async
31+
2832
flutter:
2933
uses-material-design: true

demos/supabase-simple-chat/pubspec.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ dev_dependencies:
5454
# rules and activating additional ones.
5555
flutter_lints: ^3.0.1
5656

57+
dependency_overrides:
58+
sqlite_async:
59+
path: /home/simon/src/sqlite_async.dart/packages/sqlite_async
60+
5761
# For information on the generic Dart part of this file, see the
5862
# following page: https://dart.dev/tools/pub/pubspec
5963

demos/supabase-todolist/pubspec.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,9 @@ dev_dependencies:
2727

2828
flutter_lints: ^3.0.1
2929

30+
dependency_overrides:
31+
sqlite_async:
32+
path: /home/simon/src/sqlite_async.dart/packages/sqlite_async
33+
3034
flutter:
3135
uses-material-design: true

packages/powersync/lib/src/database/web/web_powersync_database.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ class PowerSyncDatabaseImpl
142142
final storage = BucketStorage(database);
143143
StreamingSync sync;
144144
// Try using a shared worker for the synchronization implementation to avoid
145-
// duplicate work across tabs.
145+
// duplicating work across tabs.
146146
try {
147147
sync = await SyncWorkerHandle.start(
148148
this,

packages/powersync/lib/src/web/sync_worker.dart

Lines changed: 123 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class _ConnectedClient {
5454
final _SyncWorker _worker;
5555

5656
_SyncRunner? _runner;
57+
StreamSubscription? _logSubscription;
5758

5859
_ConnectedClient(MessagePort port, this._worker) {
5960
channel = WorkerCommunicationChannel(
@@ -73,6 +74,30 @@ class _ConnectedClient {
7374
}
7475
},
7576
);
77+
78+
_logSubscription = _logger.onRecord.listen((record) {
79+
final msg = StringBuffer(
80+
'[${record.loggerName}] ${record.level.name}: ${record.time}: ${record.message}');
81+
82+
if (record.error != null) {
83+
msg
84+
..writeln()
85+
..write(record.error);
86+
}
87+
if (record.stackTrace != null) {
88+
msg
89+
..writeln()
90+
..write(record.stackTrace);
91+
}
92+
93+
channel.notify(SyncWorkerMessageType.logEvent, msg.toString().toJS);
94+
});
95+
}
96+
97+
void markClosed() {
98+
_logSubscription?.cancel();
99+
_runner?.unregisterClient(this);
100+
_runner = null;
76101
}
77102
}
78103

@@ -82,62 +107,39 @@ class _SyncRunner {
82107
final StreamGroup<_RunnerEvent> _group = StreamGroup();
83108
final StreamController<_RunnerEvent> _mainEvents = StreamController();
84109

110+
StreamingSync? sync;
111+
_ConnectedClient? databaseHost;
112+
final connections = <_ConnectedClient>[];
113+
85114
_SyncRunner(this.identifier) {
86115
_group.add(_mainEvents.stream);
87116

88117
Future(() async {
89-
final connections = <_ConnectedClient>[];
90-
StreamingSync? sync;
91-
92118
await for (final event in _group.stream) {
93119
try {
94120
switch (event) {
95121
case _AddConnection(:final client):
96122
connections.add(client);
97123
if (sync == null) {
98-
_logger.info('Sync setup: Requesting database');
99-
100-
// This is the first client, ask for a database connection
101-
final connection = await client.channel.requestDatabase();
102-
_logger.info('Sync setup: Connecting to endpoint');
103-
final database = await WebSqliteConnection.connectToEndpoint((
104-
connectPort: connection.databasePort,
105-
connectName: connection.databaseName,
106-
lockName: connection.lockName,
107-
));
108-
_logger.info('Sync setup: Has database, starting sync!');
109-
110-
// todo: Detect client going down (sqlite_web exposes this), fall
111-
// back to other connection in that case.
112-
113-
sync = StreamingSyncImplementation(
114-
adapter: BucketStorage(database),
115-
credentialsCallback: client.channel.credentialsCallback,
116-
invalidCredentialsCallback:
117-
client.channel.invalidCredentialsCallback,
118-
uploadCrud: client.channel.uploadCrud,
119-
updateStream: powerSyncUpdateNotifications(
120-
database.updates ?? const Stream.empty()),
121-
retryDelay: Duration(seconds: 3),
122-
client: FetchClient(mode: RequestMode.cors),
123-
identifier: identifier,
124-
);
125-
sync.statusStream.listen((event) {
126-
_logger.fine('Broadcasting sync event: $event');
127-
for (final client in connections) {
128-
client.channel.notify(
129-
SyncWorkerMessageType.notifySyncStatus,
130-
SerializedSyncStatus.from(event));
131-
}
132-
});
133-
sync.streamingSync();
124+
await _requestDatabase(client);
134125
}
135126
case _RemoveConnection(:final client):
136127
connections.remove(client);
137128
if (connections.isEmpty) {
138129
await sync?.abort();
139130
sync = null;
140131
}
132+
case _ActiveDatabaseClosed():
133+
_logger.info('Remote database closed, finding a new client');
134+
sync?.abort();
135+
sync = null;
136+
137+
final newHost = await _collectActiveClients();
138+
if (newHost == null) {
139+
_logger.info('No client remains');
140+
} else {
141+
await _requestDatabase(newHost);
142+
}
141143
}
142144
} catch (e, s) {
143145
_logger.warning('Error handling $event', e, s);
@@ -146,6 +148,84 @@ class _SyncRunner {
146148
});
147149
}
148150

151+
/// Pings all current [connections], removing those that don't answer in 5s
152+
/// (as they are likely closed tabs as well).
153+
///
154+
/// Returns the first client that responds (without waiting for others).
155+
Future<_ConnectedClient?> _collectActiveClients() async {
156+
final candidates = connections.toList();
157+
if (candidates.isEmpty) {
158+
return null;
159+
}
160+
161+
final firstResponder = Completer<_ConnectedClient?>();
162+
var pendingRequests = candidates.length;
163+
164+
for (final candidate in candidates) {
165+
candidate.channel.ping().then((_) {
166+
pendingRequests--;
167+
if (!firstResponder.isCompleted) {
168+
firstResponder.complete(candidate);
169+
}
170+
}).timeout(const Duration(seconds: 5), onTimeout: () {
171+
pendingRequests--;
172+
candidate.markClosed();
173+
if (pendingRequests == 0 && !firstResponder.isCompleted) {
174+
// All requests have timed out, no connection remains
175+
firstResponder.complete(null);
176+
}
177+
});
178+
}
179+
180+
return firstResponder.future;
181+
}
182+
183+
Future<void> _requestDatabase(_ConnectedClient client) async {
184+
_logger.info('Sync setup: Requesting database');
185+
186+
// This is the first client, ask for a database connection
187+
final connection = await client.channel.requestDatabase();
188+
_logger.info('Sync setup: Connecting to endpoint');
189+
final database = await WebSqliteConnection.connectToEndpoint((
190+
connectPort: connection.databasePort,
191+
connectName: connection.databaseName,
192+
lockName: connection.lockName,
193+
));
194+
_logger.info('Sync setup: Has database, starting sync!');
195+
databaseHost = client;
196+
197+
database.closedFuture.then((_) {
198+
_logger.fine('Detected closed client');
199+
client.markClosed();
200+
201+
if (client == databaseHost) {
202+
_logger
203+
.info('Tab providing sync database has gone down, reconnecting...');
204+
_mainEvents.add(const _ActiveDatabaseClosed());
205+
}
206+
});
207+
208+
sync = StreamingSyncImplementation(
209+
adapter: BucketStorage(database),
210+
credentialsCallback: client.channel.credentialsCallback,
211+
invalidCredentialsCallback: client.channel.invalidCredentialsCallback,
212+
uploadCrud: client.channel.uploadCrud,
213+
updateStream: powerSyncUpdateNotifications(
214+
database.updates ?? const Stream.empty()),
215+
retryDelay: Duration(seconds: 3),
216+
client: FetchClient(mode: RequestMode.cors),
217+
identifier: identifier,
218+
);
219+
sync!.statusStream.listen((event) {
220+
_logger.fine('Broadcasting sync event: $event');
221+
for (final client in connections) {
222+
client.channel.notify(SyncWorkerMessageType.notifySyncStatus,
223+
SerializedSyncStatus.from(event));
224+
}
225+
});
226+
sync!.streamingSync();
227+
}
228+
149229
void registerClient(_ConnectedClient client) {
150230
_mainEvents.add(_AddConnection(client));
151231
}
@@ -168,3 +248,7 @@ final class _RemoveConnection implements _RunnerEvent {
168248

169249
_RemoveConnection(this.client);
170250
}
251+
252+
final class _ActiveDatabaseClosed implements _RunnerEvent {
253+
const _ActiveDatabaseClosed();
254+
}

packages/powersync/lib/src/web/sync_worker_protocol.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ enum SyncWorkerMessageType {
4040
/// of [SerializedSyncStatus].
4141
notifySyncStatus,
4242

43+
/// Notifies clients about a log event emitted by the worker (typically only
44+
/// used when workers were compiled in debug mode).
45+
/// The payload is a [JSString].
46+
logEvent,
47+
4348
okResponse,
4449
errorResponse,
4550
}
@@ -254,6 +259,10 @@ final class WorkerCommunicationChannel {
254259
case SyncWorkerMessageType.notifySyncStatus:
255260
_events.add((type, message.payload));
256261
return;
262+
case SyncWorkerMessageType.logEvent:
263+
final msg = (message.payload as JSString).toDart;
264+
_logger.info('[Sync Worker]: $msg');
265+
return;
257266
}
258267

259268
try {

packages/powersync/pubspec.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ dev_dependencies:
4040
shelf_static: ^1.1.2
4141
stream_channel: ^2.1.2
4242
path: ^1.8.3
43-
js: ">=0.6.7 <0.8.0"
4443

4544
platforms:
4645
android:

0 commit comments

Comments
 (0)