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

Commit

Permalink
Fix: Retry when inserting records offline (#2)
Browse files Browse the repository at this point in the history
* Fix: Retry when inserting records offline

* Update workflow dart version

* Fix dart formatting

* Fix dart analysis
  • Loading branch information
mugikhan authored Nov 20, 2024
1 parent 7696340 commit 7464c6a
Show file tree
Hide file tree
Showing 18 changed files with 469 additions and 301 deletions.
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

0 comments on commit 7464c6a

Please sign in to comment.