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

Commit 7464c6a

Browse files
authored
Fix: Retry when inserting records offline (#2)
* Fix: Retry when inserting records offline * Update workflow dart version * Fix dart formatting * Fix dart analysis
1 parent 7696340 commit 7464c6a

18 files changed

+469
-301
lines changed

.github/workflows/test.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ jobs:
1515
- name: Install Flutter
1616
uses: subosito/flutter-action@v2
1717
with:
18-
flutter-version: '3.10.6'
19-
channel: 'stable'
18+
flutter-version: "3.24.0"
19+
channel: "stable"
2020

2121
- name: Install dependencies
2222
run: flutter pub get

analysis_options.yaml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@ linter:
2222
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
2323
# producing the lint.
2424
rules:
25-
# avoid_print: false # Uncomment to disable the `avoid_print` rule
25+
avoid_print: false # Uncomment to disable the `avoid_print` rule
2626
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
27-
2827
# Additional information about this file can be found at
2928
# https://dart.dev/guides/language/analysis-options
29+
30+
analyzer:
31+
exclude:
32+
- lib/firebase.dart # Exclude this as it imports a file that is not checked in

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/app_config.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/// Update these values
22
class AppConfig {
33
static const String backendUrl = 'https://4be6-71-211-245-221.ngrok-free.app';
4-
static const String powersyncUrl = 'https://65663910ce6b81ac131b8c62.powersync.journeyapps.com';
4+
static const String powersyncUrl =
5+
'https://65663910ce6b81ac131b8c62.powersync.journeyapps.com';
56
}

lib/powersync.dart

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,17 @@ final List<RegExp> fatalResponseCodes = [
2929
/// Use Custom Node.js backend for authentication and data upload.
3030
class BackendConnector extends PowerSyncBackendConnector {
3131
PowerSyncDatabase db;
32-
32+
//ignore: unused_field
3333
Future<void>? _refreshFuture;
3434

3535
BackendConnector(this.db);
3636

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/login_page.dart

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,35 +29,33 @@ class _LoginPageState extends State<LoginPage> {
2929
_busy = true;
3030
_error = null;
3131
});
32-
try {
33-
final credential = await FirebaseAuth.instance.signInWithEmailAndPassword(
34-
email: _usernameController.text,
35-
password: _passwordController.text
36-
);
37-
if(mounted) {
32+
try {
33+
await FirebaseAuth.instance.signInWithEmailAndPassword(
34+
email: _usernameController.text, password: _passwordController.text);
35+
if (context.mounted) {
3836
Navigator.of(context).pushReplacement(MaterialPageRoute(
3937
builder: (context) => listsPage,
4038
));
41-
}
42-
} on FirebaseAuthException catch (e) {
43-
if (e.code == 'user-not-found') {
44-
setState(() {
45-
_error = 'No user found for that email.';
46-
});
47-
} else if (e.code == 'wrong-password') {
39+
}
40+
} on FirebaseAuthException catch (e) {
41+
if (e.code == 'user-not-found') {
4842
setState(() {
49-
_error = 'Wrong password provided for that user.';
43+
_error = 'No user found for that email.';
5044
});
51-
}
52-
} catch (e) {
45+
} else if (e.code == 'wrong-password') {
46+
setState(() {
47+
_error = 'Wrong password provided for that user.';
48+
});
49+
}
50+
} catch (e) {
5351
setState(() {
5452
_error = e.toString();
5553
});
56-
} finally {
57-
setState(() {
58-
_busy = false;
59-
});
60-
}
54+
} finally {
55+
setState(() {
56+
_busy = false;
57+
});
58+
}
6159
}
6260

6361
@override

lib/widgets/signup_page.dart

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,35 +29,35 @@ class _SignupPageState extends State<SignupPage> {
2929
_busy = true;
3030
_error = null;
3131
});
32-
try {
33-
final credential = await FirebaseAuth.instance.createUserWithEmailAndPassword(
34-
email: _usernameController.text,
35-
password: _passwordController.text,
36-
);
37-
if(mounted) {
32+
try {
33+
await FirebaseAuth.instance.createUserWithEmailAndPassword(
34+
email: _usernameController.text,
35+
password: _passwordController.text,
36+
);
37+
if (context.mounted) {
3838
Navigator.of(context).pushReplacement(MaterialPageRoute(
39-
builder: (context) => homePage,
39+
builder: (context) => homePage,
4040
));
41-
}
42-
} on FirebaseAuthException catch (e) {
43-
if (e.code == 'weak-password') {
44-
setState(() {
45-
_error = 'The password provided is too weak.';
46-
});
47-
} else if (e.code == 'email-already-in-use') {
41+
}
42+
} on FirebaseAuthException catch (e) {
43+
if (e.code == 'weak-password') {
4844
setState(() {
49-
_error = 'The account already exists for that email.';
45+
_error = 'The password provided is too weak.';
5046
});
51-
}
52-
} catch (e) {
53-
setState(() {
54-
_error = e.toString();
55-
});
56-
} finally {
57-
setState(() {
47+
} else if (e.code == 'email-already-in-use') {
48+
setState(() {
49+
_error = 'The account already exists for that email.';
50+
});
51+
}
52+
} catch (e) {
53+
setState(() {
54+
_error = e.toString();
55+
});
56+
} finally {
57+
setState(() {
5858
_busy = false;
59-
});
60-
}
59+
});
60+
}
6161
}
6262

6363
@override

0 commit comments

Comments
 (0)