Skip to content
This repository was archived by the owner on Dec 17, 2024. It is now read-only.

Commit ddaadac

Browse files
committed
Fix: Retry when inserting records offline
1 parent 7696340 commit ddaadac

File tree

13 files changed

+416
-250
lines changed

13 files changed

+416
-250
lines changed

android/app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ android {
4949
applicationId "co.powersync.demotodolist"
5050
// You can update the following values to match your application needs.
5151
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
52-
minSdkVersion 19
52+
minSdkVersion 23
5353
targetSdkVersion flutter.targetSdkVersion
5454
versionCode flutterVersionCode.toInteger()
5555
versionName flutterVersionName

android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
buildscript {
2-
ext.kotlin_version = '1.7.10'
2+
ext.kotlin_version = '2.0.0'
33
repositories {
44
google()
55
mavenCentral()

android/gradle/wrapper/gradle-wrapper.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
33
zipStoreBase=GRADLE_USER_HOME
44
zipStorePath=wrapper/dists
5-
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
5+
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip

lib/powersync.dart

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,9 @@ class BackendConnector extends PowerSyncBackendConnector {
3737
/// Get a token to authenticate against the PowerSync instance.
3838
@override
3939
Future<PowerSyncCredentials?> fetchCredentials() async {
40-
4140
final user = FirebaseAuth.instance.currentUser;
42-
if(user == null) {
43-
// Not logged in
41+
if (user == null) {
42+
// Not logged in
4443
return null;
4544
}
4645
final idToken = await user.getIdToken();
@@ -64,14 +63,16 @@ class BackendConnector extends PowerSyncBackendConnector {
6463
// userId and expiresAt are for debugging purposes only
6564
final expiresAt = parsedBody['expiresAt'] == null
6665
? null
67-
: DateTime.fromMillisecondsSinceEpoch(parsedBody['expiresAt']! * 1000);
66+
: DateTime.fromMillisecondsSinceEpoch(
67+
parsedBody['expiresAt']! * 1000);
6868
return PowerSyncCredentials(
6969
endpoint: parsedBody['powerSyncUrl'],
7070
token: parsedBody['token'],
7171
userId: parsedBody['userId'],
7272
expiresAt: expiresAt);
7373
} else {
7474
print('Request failed with status: ${response.statusCode}');
75+
return null;
7576
}
7677
}
7778

@@ -109,33 +110,44 @@ class BackendConnector extends PowerSyncBackendConnector {
109110

110111
var row = Map<String, dynamic>.of(op.opData!);
111112
row['id'] = op.id;
112-
Map<String, dynamic> data = {
113-
"table": op.table,
114-
"data": row
115-
};
116-
113+
Map<String, dynamic> data = {"table": op.table, "data": row};
117114
if (op.op == UpdateType.put) {
118115
await upsert(data);
119116
} else if (op.op == UpdateType.patch) {
120117
await update(data);
121118
} else if (op.op == UpdateType.delete) {
119+
data = {
120+
"table": op.table,
121+
"data": {"id": op.id}
122+
};
122123
await delete(data);
123124
}
124125
}
125126

126127
// All operations successful.
127128
await transaction.complete();
129+
} on http.ClientException catch (e) {
130+
// Error may be retryable - e.g. network error or temporary server error.
131+
// Throwing an error here causes this call to be retried after a delay.
132+
log.warning('Client exception', e);
133+
rethrow;
128134
} catch (e) {
129-
log.severe('Failed to update object $e');
130-
transaction.complete();
135+
/// Instead of blocking the queue with these errors,
136+
/// discard the (rest of the) transaction.
137+
///
138+
/// Note that these errors typically indicate a bug in the application.
139+
/// If protecting against data loss is important, save the failing records
140+
/// elsewhere instead of discarding, and/or notify the user.
141+
log.severe('Data upload error - discarding $lastOp', e);
142+
await transaction.complete();
131143
}
132144
}
133145
}
134146

135147
/// Global reference to the database
136148
late final PowerSyncDatabase db;
137149

138-
upsert (data) async {
150+
upsert(data) async {
139151
var url = Uri.parse("${AppConfig.backendUrl}/api/data");
140152

141153
try {
@@ -154,10 +166,11 @@ upsert (data) async {
154166
}
155167
} catch (e) {
156168
log.severe('Exception occurred: $e');
169+
rethrow;
157170
}
158171
}
159172

160-
update (data) async {
173+
update(data) async {
161174
var url = Uri.parse("${AppConfig.backendUrl}/api/data");
162175

163176
try {
@@ -176,10 +189,11 @@ update (data) async {
176189
}
177190
} catch (e) {
178191
log.severe('Exception occurred: $e');
192+
rethrow;
179193
}
180194
}
181195

182-
delete (data) async {
196+
delete(data) async {
183197
var url = Uri.parse("${AppConfig.backendUrl}/api/data");
184198

185199
try {
@@ -198,6 +212,7 @@ delete (data) async {
198212
}
199213
} catch (e) {
200214
log.severe('Exception occurred: $e');
215+
rethrow;
201216
}
202217
}
203218

@@ -219,7 +234,11 @@ Future<String> getDatabasePath() async {
219234

220235
Future<void> openDatabase() async {
221236
// Open the local database
222-
db = PowerSyncDatabase(schema: schema, path: await getDatabasePath());
237+
db = PowerSyncDatabase(
238+
schema: schema,
239+
path: await getDatabasePath(),
240+
logger: attachedLogger,
241+
);
223242
await db.initialize();
224243
BackendConnector? currentConnector;
225244

@@ -235,9 +254,7 @@ Future<void> openDatabase() async {
235254
log.info('User not logged in, setting connection');
236255
}
237256

238-
FirebaseAuth.instance
239-
.authStateChanges()
240-
.listen((User? user) async {
257+
FirebaseAuth.instance.authStateChanges().listen((User? user) async {
241258
if (user != null) {
242259
// Connect to PowerSync when the user is signed in
243260
currentConnector = BackendConnector(db);
@@ -252,5 +269,5 @@ Future<void> openDatabase() async {
252269
/// Explicit sign out - clear database and log out.
253270
Future<void> logout() async {
254271
await FirebaseAuth.instance.signOut();
255-
await db.disconnectedAndClear();
272+
await db.disconnectAndClear();
256273
}

lib/widgets/list_item.dart

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,6 @@ class ListItemWidget extends StatelessWidget {
1717

1818
@override
1919
Widget build(BuildContext context) {
20-
viewList() {
21-
var navigator = Navigator.of(context);
22-
23-
navigator.push(
24-
MaterialPageRoute(builder: (context) => TodoListPage(list: list)));
25-
}
26-
2720
final subtext =
2821
'${list.pendingCount} pending, ${list.completedCount} completed';
2922

@@ -32,7 +25,10 @@ class ListItemWidget extends StatelessWidget {
3225
mainAxisSize: MainAxisSize.min,
3326
children: <Widget>[
3427
ListTile(
35-
onTap: viewList,
28+
onTap: () {
29+
Navigator.of(context).push(MaterialPageRoute(
30+
builder: (context) => TodoListPage(list: list)));
31+
},
3632
leading: const Icon(Icons.list),
3733
title: Text(list.name),
3834
subtitle: Text(subtext)),

lib/widgets/status_app_bar.dart

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'dart:async';
22

3+
import 'package:flutter/foundation.dart';
34
import 'package:flutter/material.dart';
45
import 'package:powersync/powersync.dart';
56
import '../powersync.dart';
@@ -39,22 +40,56 @@ class _StatusAppBarState extends State<StatusAppBar> {
3940

4041
@override
4142
Widget build(BuildContext context) {
42-
const connectedIcon = IconButton(
43-
icon: Icon(Icons.wifi),
44-
tooltip: 'Connected',
45-
onPressed: null,
46-
);
47-
const disconnectedIcon = IconButton(
48-
icon: Icon(Icons.wifi_off),
49-
tooltip: 'Not connected',
50-
onPressed: null,
51-
);
43+
final statusIcon = _getStatusIcon(_connectionState, context);
5244

5345
return AppBar(
5446
title: Text(widget.title),
5547
actions: <Widget>[
56-
_connectionState.connected ? connectedIcon : disconnectedIcon
48+
statusIcon,
49+
// Make some space for the "Debug" banner, so that the status
50+
// icon isn't hidden
51+
if (kDebugMode) _makeIcon('Debug mode', Icons.developer_mode, context),
5752
],
5853
);
5954
}
6055
}
56+
57+
Widget _makeIcon(String text, IconData icon, BuildContext context) {
58+
return Tooltip(
59+
message: text,
60+
child: InkWell(
61+
onTap: () {
62+
ScaffoldMessenger.of(context)
63+
.showSnackBar(SnackBar(content: Text('Status: $text')));
64+
},
65+
child:
66+
SizedBox(width: 40, height: null, child: Icon(icon, size: 24))));
67+
}
68+
69+
Widget _getStatusIcon(SyncStatus status, BuildContext context) {
70+
if (status.anyError != null) {
71+
// The error message is verbose, could be replaced with something
72+
// more user-friendly
73+
if (!status.connected) {
74+
return _makeIcon(status.anyError!.toString(), Icons.cloud_off, context);
75+
} else {
76+
return _makeIcon(
77+
status.anyError!.toString(), Icons.sync_problem, context);
78+
}
79+
} else if (status.connecting) {
80+
return _makeIcon('Connecting', Icons.cloud_sync_outlined, context);
81+
} else if (!status.connected) {
82+
return _makeIcon('Not connected', Icons.cloud_off, context);
83+
} else if (status.uploading && status.downloading) {
84+
// The status changes often between downloading, uploading and both,
85+
// so we use the same icon for all three
86+
return _makeIcon(
87+
'Uploading and downloading', Icons.cloud_sync_outlined, context);
88+
} else if (status.uploading) {
89+
return _makeIcon('Uploading', Icons.cloud_sync_outlined, context);
90+
} else if (status.downloading) {
91+
return _makeIcon('Downloading', Icons.cloud_sync_outlined, context);
92+
} else {
93+
return _makeIcon('Connected', Icons.cloud_queue, context);
94+
}
95+
}

linux/flutter/generated_plugin_registrant.cc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,18 @@
66

77
#include "generated_plugin_registrant.h"
88

9+
#include <gtk/gtk_plugin.h>
10+
#include <powersync_flutter_libs/powersync_flutter_libs_plugin.h>
911
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
1012
#include <url_launcher_linux/url_launcher_plugin.h>
1113

1214
void fl_register_plugins(FlPluginRegistry* registry) {
15+
g_autoptr(FlPluginRegistrar) gtk_registrar =
16+
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
17+
gtk_plugin_register_with_registrar(gtk_registrar);
18+
g_autoptr(FlPluginRegistrar) powersync_flutter_libs_registrar =
19+
fl_plugin_registry_get_registrar_for_plugin(registry, "PowersyncFlutterLibsPlugin");
20+
powersync_flutter_libs_plugin_register_with_registrar(powersync_flutter_libs_registrar);
1321
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
1422
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
1523
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);

linux/flutter/generated_plugins.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
#
44

55
list(APPEND FLUTTER_PLUGIN_LIST
6+
gtk
7+
powersync_flutter_libs
68
sqlite3_flutter_libs
79
url_launcher_linux
810
)

macos/Flutter/GeneratedPluginRegistrant.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import app_links
99
import firebase_auth
1010
import firebase_core
1111
import path_provider_foundation
12+
import powersync_flutter_libs
1213
import shared_preferences_foundation
13-
import sign_in_with_apple
1414
import sqlite3_flutter_libs
1515
import url_launcher_macos
1616

@@ -19,8 +19,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
1919
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
2020
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
2121
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
22+
PowersyncFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "PowersyncFlutterLibsPlugin"))
2223
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
23-
SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin"))
2424
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
2525
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
2626
}

0 commit comments

Comments
 (0)