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
Browse files Browse the repository at this point in the history
  • Loading branch information
mugikhan committed Nov 18, 2024
1 parent 7696340 commit ddaadac
Show file tree
Hide file tree
Showing 13 changed files with 416 additions and 250 deletions.
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
55 changes: 36 additions & 19 deletions lib/powersync.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,9 @@ class BackendConnector extends PowerSyncBackendConnector {
/// 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
57 changes: 46 additions & 11 deletions lib/widgets/status_app_bar.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:async';

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

@override
Widget build(BuildContext context) {
const connectedIcon = IconButton(
icon: Icon(Icons.wifi),
tooltip: 'Connected',
onPressed: null,
);
const disconnectedIcon = IconButton(
icon: Icon(Icons.wifi_off),
tooltip: 'Not connected',
onPressed: null,
);
final statusIcon = _getStatusIcon(_connectionState, context);

return AppBar(
title: Text(widget.title),
actions: <Widget>[
_connectionState.connected ? connectedIcon : disconnectedIcon
statusIcon,
// Make some space for the "Debug" banner, so that the status
// icon isn't hidden
if (kDebugMode) _makeIcon('Debug mode', Icons.developer_mode, context),
],
);
}
}

Widget _makeIcon(String text, IconData icon, BuildContext context) {
return Tooltip(
message: text,
child: InkWell(
onTap: () {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Status: $text')));
},
child:
SizedBox(width: 40, height: null, child: Icon(icon, size: 24))));
}

Widget _getStatusIcon(SyncStatus status, BuildContext context) {
if (status.anyError != null) {
// The error message is verbose, could be replaced with something
// more user-friendly
if (!status.connected) {
return _makeIcon(status.anyError!.toString(), Icons.cloud_off, context);
} else {
return _makeIcon(
status.anyError!.toString(), Icons.sync_problem, context);
}
} else if (status.connecting) {
return _makeIcon('Connecting', Icons.cloud_sync_outlined, context);
} else if (!status.connected) {
return _makeIcon('Not connected', Icons.cloud_off, context);
} else if (status.uploading && status.downloading) {
// The status changes often between downloading, uploading and both,
// so we use the same icon for all three
return _makeIcon(
'Uploading and downloading', Icons.cloud_sync_outlined, context);
} else if (status.uploading) {
return _makeIcon('Uploading', Icons.cloud_sync_outlined, context);
} else if (status.downloading) {
return _makeIcon('Downloading', Icons.cloud_sync_outlined, context);
} else {
return _makeIcon('Connected', Icons.cloud_queue, context);
}
}
8 changes: 8 additions & 0 deletions linux/flutter/generated_plugin_registrant.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@

#include "generated_plugin_registrant.h"

#include <gtk/gtk_plugin.h>
#include <powersync_flutter_libs/powersync_flutter_libs_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>

void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) gtk_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
gtk_plugin_register_with_registrar(gtk_registrar);
g_autoptr(FlPluginRegistrar) powersync_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "PowersyncFlutterLibsPlugin");
powersync_flutter_libs_plugin_register_with_registrar(powersync_flutter_libs_registrar);
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
Expand Down
2 changes: 2 additions & 0 deletions linux/flutter/generated_plugins.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#

list(APPEND FLUTTER_PLUGIN_LIST
gtk
powersync_flutter_libs
sqlite3_flutter_libs
url_launcher_linux
)
Expand Down
4 changes: 2 additions & 2 deletions macos/Flutter/GeneratedPluginRegistrant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import app_links
import firebase_auth
import firebase_core
import path_provider_foundation
import powersync_flutter_libs
import shared_preferences_foundation
import sign_in_with_apple
import sqlite3_flutter_libs
import url_launcher_macos

Expand All @@ -19,8 +19,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
PowersyncFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "PowersyncFlutterLibsPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin"))
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}
Loading

0 comments on commit ddaadac

Please sign in to comment.