Skip to content

Commit f17ea6f

Browse files
authored
[Feature] Add warning if crud transactions are not completed (#172)
* Show warning if crud transactions are not completed * Test on native for upload tests * Fix crudMutex and uploading status * Add back retry delay for crudMutex * chore(release): publish packages - [email protected] - [email protected]
1 parent 70b487e commit f17ea6f

File tree

18 files changed

+200
-49
lines changed

18 files changed

+200
-49
lines changed

CHANGELOG.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,32 @@
33
All notable changes to this project will be documented in this file.
44
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
55

6+
## 2024-10-01
7+
8+
### Changes
9+
10+
---
11+
12+
Packages with breaking changes:
13+
14+
- There are no breaking changes in this release.
15+
16+
Packages with other changes:
17+
18+
- [`powersync` - `v1.8.4`](#powersync---v184)
19+
- [`powersync_attachments_helper` - `v0.6.8`](#powersync_attachments_helper---v068)
20+
21+
---
22+
23+
#### `powersync` - `v1.8.4`
24+
25+
- **FEAT**: Added a warning if connector `uploadData` functions don't process CRUD items completely.
26+
27+
#### `powersync_attachments_helper` - `v0.6.8`
28+
29+
- Update a dependency to the latest release.
30+
31+
632
## 2024-09-30
733

834
### Changes
@@ -22,7 +48,7 @@ Packages with other changes:
2248

2349
#### `powersync` - `v1.8.3`
2450

25-
- **FIX**: Pass maxReaders parameter to `PowerSyncDatabase.withFactory()`
51+
- **FIX**: Pass maxReaders parameter to `PowerSyncDatabase.withFactory()`.
2652

2753
#### `powersync_attachments_helper` - `v0.6.7`
2854

demos/django-todolist/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ environment:
1010
dependencies:
1111
flutter:
1212
sdk: flutter
13-
powersync: ^1.8.3
13+
powersync: ^1.8.4
1414
path_provider: ^2.1.1
1515
path: ^1.8.3
1616
logging: ^1.2.0

demos/supabase-anonymous-auth/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ dependencies:
1111
flutter:
1212
sdk: flutter
1313

14-
powersync: ^1.8.3
14+
powersync: ^1.8.4
1515
path_provider: ^2.1.1
1616
supabase_flutter: ^2.0.2
1717
path: ^1.8.3

demos/supabase-edge-function-auth/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ dependencies:
1111
flutter:
1212
sdk: flutter
1313

14-
powersync: ^1.8.3
14+
powersync: ^1.8.4
1515
path_provider: ^2.1.1
1616
supabase_flutter: ^2.0.2
1717
path: ^1.8.3

demos/supabase-simple-chat/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ dependencies:
3737

3838
supabase_flutter: ^2.0.2
3939
timeago: ^3.6.0
40-
powersync: ^1.8.3
40+
powersync: ^1.8.4
4141
path_provider: ^2.1.1
4242
path: ^1.8.3
4343
logging: ^1.2.0

demos/supabase-todolist-drift/pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ environment:
99
dependencies:
1010
flutter:
1111
sdk: flutter
12-
powersync_attachments_helper: ^0.6.7
13-
powersync: ^1.8.3
12+
powersync_attachments_helper: ^0.6.8
13+
powersync: ^1.8.4
1414
path_provider: ^2.1.1
1515
supabase_flutter: ^2.0.1
1616
path: ^1.8.3

demos/supabase-todolist-optional-sync/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ environment:
1010
dependencies:
1111
flutter:
1212
sdk: flutter
13-
powersync: ^1.8.3
13+
powersync: ^1.8.4
1414
path_provider: ^2.1.1
1515
supabase_flutter: ^2.0.1
1616
path: ^1.8.3

demos/supabase-todolist/pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ environment:
1010
dependencies:
1111
flutter:
1212
sdk: flutter
13-
powersync_attachments_helper: ^0.6.7
14-
powersync: ^1.8.3
13+
powersync_attachments_helper: ^0.6.8
14+
powersync: ^1.8.4
1515
path_provider: ^2.1.1
1616
supabase_flutter: ^2.0.1
1717
path: ^1.8.3

packages/powersync/CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
## 1.8.4
2+
3+
- **FEAT**: Added a warning if connector `uploadData` functions don't process CRUD items completely.
4+
15
## 1.8.3
26

3-
- **FIX**: Pass maxReaders parameter to `PowerSyncDatabase.withFactory()`
7+
- **FIX**: Pass maxReaders parameter to `PowerSyncDatabase.withFactory()`.
48

59
## 1.8.2
610

packages/powersync/lib/src/bucket_storage.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,12 @@ class BucketStorage {
270270
});
271271
}
272272

273+
Future<CrudEntry?> nextCrudItem() async {
274+
var next = await _internalDb
275+
.getOptional('SELECT * FROM ps_crud ORDER BY id ASC LIMIT 1');
276+
return next == null ? null : CrudEntry.fromRow(next);
277+
}
278+
273279
Future<bool> hasCrud() async {
274280
final anyData = await select('SELECT 1 FROM ps_crud LIMIT 1');
275281
return anyData.isNotEmpty;

packages/powersync/lib/src/streaming_sync.dart

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:sqlite_async/mutex.dart';
1010

1111
import 'bucket_storage.dart';
1212
import 'connector.dart';
13+
import 'crud.dart';
1314
import 'stream_utils.dart';
1415
import 'sync_status.dart';
1516
import 'sync_types.dart';
@@ -102,6 +103,10 @@ class StreamingSyncImplementation {
102103
return _abort?.aborted ?? false;
103104
}
104105

106+
bool get isConnected {
107+
return lastStatus.connected;
108+
}
109+
105110
Future<void> streamingSync() async {
106111
try {
107112
_abort = AbortController();
@@ -159,38 +164,50 @@ class StreamingSyncImplementation {
159164
}
160165

161166
Future<void> uploadAllCrud() async {
162-
while (true) {
163-
try {
164-
bool done = await uploadCrudBatch();
165-
_updateStatus(uploadError: _noError);
166-
if (done) {
167-
break;
168-
}
169-
} catch (e, stacktrace) {
170-
isolateLogger.warning('Data upload error', e, stacktrace);
171-
_updateStatus(uploading: false, uploadError: e);
172-
await Future.delayed(retryDelay);
173-
}
174-
}
175-
_updateStatus(uploading: false);
176-
}
177-
178-
Future<bool> uploadCrudBatch() async {
179167
return crudMutex.lock(() async {
180-
if ((await adapter.hasCrud())) {
168+
// Keep track of the first item in the CRUD queue for the last `uploadCrud` iteration.
169+
CrudEntry? checkedCrudItem;
170+
171+
while (true) {
181172
_updateStatus(uploading: true);
182-
await uploadCrud();
183-
return false;
184-
} else {
185-
// This isolate is the only one triggering
186-
final updated = await adapter.updateLocalTarget(() async {
187-
return getWriteCheckpoint();
188-
});
189-
if (updated) {
190-
_localPingController.add(null);
173+
try {
174+
// This is the first item in the FIFO CRUD queue.
175+
CrudEntry? nextCrudItem = await adapter.nextCrudItem();
176+
if (nextCrudItem != null) {
177+
if (nextCrudItem.clientId == checkedCrudItem?.clientId) {
178+
// This will force a higher log level than exceptions which are caught here.
179+
isolateLogger.warning(
180+
"""Potentially previously uploaded CRUD entries are still present in the upload queue.
181+
Make sure to handle uploads and complete CRUD transactions or batches by calling and awaiting their [.complete()] method.
182+
The next upload iteration will be delayed.""");
183+
throw Exception(
184+
'Delaying due to previously encountered CRUD item.');
185+
}
186+
187+
checkedCrudItem = nextCrudItem;
188+
await uploadCrud();
189+
_updateStatus(uploadError: _noError);
190+
} else {
191+
// Uploading is completed
192+
await adapter.updateLocalTarget(() => getWriteCheckpoint());
193+
break;
194+
}
195+
} catch (e, stacktrace) {
196+
checkedCrudItem = null;
197+
isolateLogger.warning('Data upload error', e, stacktrace);
198+
_updateStatus(uploading: false, uploadError: e);
199+
await Future.delayed(retryDelay);
200+
if (!isConnected) {
201+
// Exit the upload loop if the sync stream is no longer connected
202+
break;
203+
}
204+
isolateLogger.warning(
205+
"Caught exception when uploading. Upload will retry after a delay",
206+
e,
207+
stacktrace);
208+
} finally {
209+
_updateStatus(uploading: false);
191210
}
192-
193-
return true;
194211
}
195212
}, timeout: retryDelay);
196213
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
const String libraryVersion = '1.8.3';
1+
const String libraryVersion = '1.8.4';

packages/powersync/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: powersync
2-
version: 1.8.3
2+
version: 1.8.4
33
homepage: https://powersync.com
44
repository: https://github.com/powersync-ja/powersync.dart
55
description: PowerSync Flutter SDK - sync engine for building local-first apps.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
@TestOn('!browser')
2+
3+
import 'package:powersync/powersync.dart';
4+
import 'package:test/test.dart';
5+
6+
import 'test_server.dart';
7+
import 'utils/abstract_test_utils.dart';
8+
import 'utils/test_utils_impl.dart';
9+
10+
final testUtils = TestUtils();
11+
const testId = "2290de4f-0488-4e50-abed-f8e8eb1d0b42";
12+
const testId2 = "2290de4f-0488-4e50-abed-f8e8eb1d0b43";
13+
const partialWarning =
14+
'Potentially previously uploaded CRUD entries are still present';
15+
16+
class TestConnector extends PowerSyncBackendConnector {
17+
final Function _fetchCredentials;
18+
final Future<void> Function(PowerSyncDatabase database) _uploadData;
19+
20+
TestConnector(this._fetchCredentials, this._uploadData);
21+
22+
@override
23+
Future<PowerSyncCredentials?> fetchCredentials() {
24+
return _fetchCredentials();
25+
}
26+
27+
@override
28+
Future<void> uploadData(PowerSyncDatabase database) async {
29+
return _uploadData(database);
30+
}
31+
}
32+
33+
void main() {
34+
group('CRUD Tests', () {
35+
late PowerSyncDatabase powersync;
36+
late String path;
37+
38+
setUp(() async {
39+
path = testUtils.dbPath();
40+
await testUtils.cleanDb(path: path);
41+
});
42+
43+
tearDown(() async {
44+
// await powersync.disconnectAndClear();
45+
await powersync.close();
46+
});
47+
48+
test('should warn for missing upload operations in uploadData', () async {
49+
var server = await createServer();
50+
51+
credentialsCallback() async {
52+
return PowerSyncCredentials(
53+
endpoint: server.endpoint,
54+
token: 'token',
55+
userId: 'userId',
56+
);
57+
}
58+
59+
uploadData(PowerSyncDatabase db) async {
60+
// Do nothing
61+
}
62+
63+
final records = <String>[];
64+
final sub =
65+
testWarningLogger.onRecord.listen((log) => records.add(log.message));
66+
67+
powersync =
68+
await testUtils.setupPowerSync(path: path, logger: testWarningLogger);
69+
powersync.retryDelay = Duration(milliseconds: 0);
70+
var connector = TestConnector(credentialsCallback, uploadData);
71+
powersync.connect(connector: connector);
72+
73+
// Create something with CRUD in it.
74+
await powersync.execute(
75+
'INSERT INTO assets(id, description) VALUES(?, ?)', [testId, 'test']);
76+
77+
// Wait for the uploadData to be called.
78+
await Future.delayed(Duration(milliseconds: 100));
79+
80+
// Create something else with CRUD in it.
81+
await powersync.execute(
82+
'INSERT INTO assets(id, description) VALUES(?, ?)',
83+
[testId2, 'test2']);
84+
85+
sub.cancel();
86+
87+
expect(records, hasLength(2));
88+
expect(records, anyElement(contains(partialWarning)));
89+
});
90+
});
91+
}

packages/powersync/test/utils/abstract_test_utils.dart

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@ const defaultSchema = schema;
2424

2525
final testLogger = _makeTestLogger();
2626

27-
Logger _makeTestLogger() {
27+
final testWarningLogger = _makeTestLogger(level: Level.WARNING);
28+
29+
Logger _makeTestLogger({Level level = Level.ALL}) {
2830
final logger = Logger.detached('PowerSync Tests');
29-
logger.level = Level.ALL;
31+
logger.level = level;
3032
logger.onRecord.listen((record) {
3133
print(
3234
'[${record.loggerName}] ${record.level.name}: ${record.time}: ${record.message}');
@@ -70,9 +72,9 @@ abstract class AbstractTestUtils {
7072

7173
/// Creates a SqliteDatabaseConnection
7274
Future<PowerSyncDatabase> setupPowerSync(
73-
{String? path, Schema? schema}) async {
75+
{String? path, Schema? schema, Logger? logger}) async {
7476
final db = PowerSyncDatabase.withFactory(await testFactory(path: path),
75-
schema: schema ?? defaultSchema, logger: testLogger);
77+
schema: schema ?? defaultSchema, logger: logger ?? testLogger);
7678
await db.initialize();
7779
return db;
7880
}

packages/powersync/test/utils/web_test_utils.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:async';
22
import 'dart:html';
33

44
import 'package:js/js.dart';
5+
import 'package:logging/logging.dart';
56
import 'package:powersync/powersync.dart';
67
import 'package:sqlite_async/sqlite3_common.dart';
78
import 'package:sqlite_async/sqlite_async.dart';
@@ -51,7 +52,7 @@ class TestUtils extends AbstractTestUtils {
5152

5253
@override
5354
Future<PowerSyncDatabase> setupPowerSync(
54-
{String? path, Schema? schema}) async {
55+
{String? path, Schema? schema, Logger? logger}) async {
5556
await _isInitialized;
5657
return super.setupPowerSync(path: path, schema: schema);
5758
}

packages/powersync_attachments_helper/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.6.8
2+
3+
- Update a dependency to the latest release.
4+
15
## 0.6.7
26

37
- Update a dependency to the latest release.

0 commit comments

Comments
 (0)