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

Fix: Retry when inserting records offline #2

Merged
merged 4 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ jobs:
- name: Install Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.10.6'
channel: 'stable'
flutter-version: "3.24.0"
channel: "stable"

- name: Install dependencies
run: flutter pub get
Expand Down
7 changes: 5 additions & 2 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ linter:
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule

# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

analyzer:
exclude:
- lib/firebase.dart # Exclude this as it imports a file that is not checked in
2 changes: 1 addition & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ android {
applicationId "co.powersync.demotodolist"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion 19
minSdkVersion 23
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
Expand Down
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.7.10'
ext.kotlin_version = '2.0.0'
repositories {
google()
mavenCentral()
Expand Down
2 changes: 1 addition & 1 deletion android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
3 changes: 2 additions & 1 deletion lib/app_config.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// Update these values
class AppConfig {
static const String backendUrl = 'https://4be6-71-211-245-221.ngrok-free.app';
static const String powersyncUrl = 'https://65663910ce6b81ac131b8c62.powersync.journeyapps.com';
static const String powersyncUrl =
'https://65663910ce6b81ac131b8c62.powersync.journeyapps.com';
}
57 changes: 37 additions & 20 deletions lib/powersync.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,17 @@ final List<RegExp> fatalResponseCodes = [
/// Use Custom Node.js backend for authentication and data upload.
class BackendConnector extends PowerSyncBackendConnector {
PowerSyncDatabase db;

//ignore: unused_field
Future<void>? _refreshFuture;

BackendConnector(this.db);

/// Get a token to authenticate against the PowerSync instance.
@override
Future<PowerSyncCredentials?> fetchCredentials() async {

final user = FirebaseAuth.instance.currentUser;
if(user == null) {
// Not logged in
if (user == null) {
// Not logged in
return null;
}
final idToken = await user.getIdToken();
Expand All @@ -64,14 +63,16 @@ class BackendConnector extends PowerSyncBackendConnector {
// userId and expiresAt are for debugging purposes only
final expiresAt = parsedBody['expiresAt'] == null
? null
: DateTime.fromMillisecondsSinceEpoch(parsedBody['expiresAt']! * 1000);
: DateTime.fromMillisecondsSinceEpoch(
parsedBody['expiresAt']! * 1000);
return PowerSyncCredentials(
endpoint: parsedBody['powerSyncUrl'],
token: parsedBody['token'],
userId: parsedBody['userId'],
expiresAt: expiresAt);
} else {
print('Request failed with status: ${response.statusCode}');
return null;
}
}

Expand Down Expand Up @@ -109,33 +110,44 @@ class BackendConnector extends PowerSyncBackendConnector {

var row = Map<String, dynamic>.of(op.opData!);
row['id'] = op.id;
Map<String, dynamic> data = {
"table": op.table,
"data": row
};

Map<String, dynamic> data = {"table": op.table, "data": row};
if (op.op == UpdateType.put) {
await upsert(data);
} else if (op.op == UpdateType.patch) {
await update(data);
} else if (op.op == UpdateType.delete) {
data = {
"table": op.table,
"data": {"id": op.id}
};
await delete(data);
}
}

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

/// Global reference to the database
late final PowerSyncDatabase db;

upsert (data) async {
upsert(data) async {
var url = Uri.parse("${AppConfig.backendUrl}/api/data");

try {
Expand All @@ -154,10 +166,11 @@ upsert (data) async {
}
} catch (e) {
log.severe('Exception occurred: $e');
rethrow;
}
}

update (data) async {
update(data) async {
var url = Uri.parse("${AppConfig.backendUrl}/api/data");

try {
Expand All @@ -176,10 +189,11 @@ update (data) async {
}
} catch (e) {
log.severe('Exception occurred: $e');
rethrow;
}
}

delete (data) async {
delete(data) async {
var url = Uri.parse("${AppConfig.backendUrl}/api/data");

try {
Expand All @@ -198,6 +212,7 @@ delete (data) async {
}
} catch (e) {
log.severe('Exception occurred: $e');
rethrow;
}
}

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

Future<void> openDatabase() async {
// Open the local database
db = PowerSyncDatabase(schema: schema, path: await getDatabasePath());
db = PowerSyncDatabase(
schema: schema,
path: await getDatabasePath(),
logger: attachedLogger,
);
await db.initialize();
BackendConnector? currentConnector;

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

FirebaseAuth.instance
.authStateChanges()
.listen((User? user) async {
FirebaseAuth.instance.authStateChanges().listen((User? user) async {
if (user != null) {
// Connect to PowerSync when the user is signed in
currentConnector = BackendConnector(db);
Expand All @@ -252,5 +269,5 @@ Future<void> openDatabase() async {
/// Explicit sign out - clear database and log out.
Future<void> logout() async {
await FirebaseAuth.instance.signOut();
await db.disconnectedAndClear();
await db.disconnectAndClear();
}
12 changes: 4 additions & 8 deletions lib/widgets/list_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,6 @@ class ListItemWidget extends StatelessWidget {

@override
Widget build(BuildContext context) {
viewList() {
var navigator = Navigator.of(context);

navigator.push(
MaterialPageRoute(builder: (context) => TodoListPage(list: list)));
}

final subtext =
'${list.pendingCount} pending, ${list.completedCount} completed';

Expand All @@ -32,7 +25,10 @@ class ListItemWidget extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
onTap: viewList,
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => TodoListPage(list: list)));
},
leading: const Icon(Icons.list),
title: Text(list.name),
subtitle: Text(subtext)),
Expand Down
40 changes: 19 additions & 21 deletions lib/widgets/login_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,35 +29,33 @@ class _LoginPageState extends State<LoginPage> {
_busy = true;
_error = null;
});
try {
final credential = await FirebaseAuth.instance.signInWithEmailAndPassword(
email: _usernameController.text,
password: _passwordController.text
);
if(mounted) {
try {
await FirebaseAuth.instance.signInWithEmailAndPassword(
email: _usernameController.text, password: _passwordController.text);
if (context.mounted) {
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (context) => listsPage,
));
}
} on FirebaseAuthException catch (e) {
if (e.code == 'user-not-found') {
setState(() {
_error = 'No user found for that email.';
});
} else if (e.code == 'wrong-password') {
}
} on FirebaseAuthException catch (e) {
if (e.code == 'user-not-found') {
setState(() {
_error = 'Wrong password provided for that user.';
_error = 'No user found for that email.';
});
}
} catch (e) {
} else if (e.code == 'wrong-password') {
setState(() {
_error = 'Wrong password provided for that user.';
});
}
} catch (e) {
setState(() {
_error = e.toString();
});
} finally {
setState(() {
_busy = false;
});
}
} finally {
setState(() {
_busy = false;
});
}
}

@override
Expand Down
48 changes: 24 additions & 24 deletions lib/widgets/signup_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,35 +29,35 @@ class _SignupPageState extends State<SignupPage> {
_busy = true;
_error = null;
});
try {
final credential = await FirebaseAuth.instance.createUserWithEmailAndPassword(
email: _usernameController.text,
password: _passwordController.text,
);
if(mounted) {
try {
await FirebaseAuth.instance.createUserWithEmailAndPassword(
email: _usernameController.text,
password: _passwordController.text,
);
if (context.mounted) {
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (context) => homePage,
builder: (context) => homePage,
));
}
} on FirebaseAuthException catch (e) {
if (e.code == 'weak-password') {
setState(() {
_error = 'The password provided is too weak.';
});
} else if (e.code == 'email-already-in-use') {
}
} on FirebaseAuthException catch (e) {
if (e.code == 'weak-password') {
setState(() {
_error = 'The account already exists for that email.';
_error = 'The password provided is too weak.';
});
}
} catch (e) {
setState(() {
_error = e.toString();
});
} finally {
setState(() {
} else if (e.code == 'email-already-in-use') {
setState(() {
_error = 'The account already exists for that email.';
});
}
} catch (e) {
setState(() {
_error = e.toString();
});
} finally {
setState(() {
_busy = false;
});
}
});
}
}

@override
Expand Down
Loading