feat: Optimize note encryption and decryption processes with parallel execution

This commit is contained in:
2026-05-19 10:09:20 +02:00
parent 9769087fd8
commit 2a898111fa
3 changed files with 316 additions and 74 deletions
+270 -50
View File
@@ -1,3 +1,8 @@
import 'dart:async';
import 'dart:isolate';
import 'dart:math' as math;
import 'dart:io' show Platform;
import 'package:drift/drift.dart';
import 'package:notas/data/app_database.dart';
import 'package:notas/data/api_client.dart';
@@ -126,8 +131,7 @@ class NoteRepository {
final int totalNotesToEncrypt = unsyncedNotes.length;
// Build sync request (note: we send encrypted data, but locally we have plaintext)
// Encrypt all notes before sending
final List<SyncNotePayload> encryptedNotesPayload = [];
// Encrypt all notes before sending.
if (totalNotesToEncrypt == 0) {
onProgress?.call(
SyncStatus.encrypting,
@@ -136,32 +140,21 @@ class NoteRepository {
);
}
for (var index = 0; index < unsyncedNotes.length; index += 1) {
final DbNote dbNote = unsyncedNotes[index];
final note = _fromDbNote(dbNote);
final encryptedTitle = await NoteEncryption.encryptNote(
note.title,
_masterKey,
);
final encryptedBody = await NoteEncryption.encryptNote(
note.body,
_masterKey,
);
encryptedNotesPayload.add(
SyncNotePayload.fromNote(
note,
encryptedTitle: encryptedTitle,
encryptedBody: encryptedBody,
),
);
onProgress?.call(
SyncStatus.encrypting,
progress: (index + 1) / totalNotesToEncrypt,
message:
'Encriptando notas para subir: ${index + 1} de $totalNotesToEncrypt',
);
}
final List<SyncNotePayload>
encryptedNotesPayload = await _encryptNotesInParallel(
unsyncedNotes,
masterKey: _masterKey,
onProgress: (int encryptedCount) {
onProgress?.call(
SyncStatus.encrypting,
progress: totalNotesToEncrypt == 0
? 1.0
: encryptedCount / totalNotesToEncrypt,
message:
'Encriptando notas para subir: $encryptedCount de $totalNotesToEncrypt',
);
},
);
final List<SyncCategoryPayload> categoriesPayload = unsyncedCategories
.map((cat) => SyncCategoryPayload.fromCategory(_fromDbCategory(cat)))
@@ -241,35 +234,33 @@ class NoteRepository {
// Apply notes from server
final int totalNotesToDecrypt = response.changes.notes.length;
for (var index = 0; index < response.changes.notes.length; index += 1) {
final SyncNoteResponse noteResponse = response.changes.notes[index];
final List<Map<String, Object?>> decryptedNotes =
await _decryptResponseNotesInParallel(
response.changes.notes,
masterKey: _masterKey,
onProgress: onDecryptProgress == null
? null
: (processed) =>
onDecryptProgress(processed, totalNotesToDecrypt),
);
for (var index = 0; index < decryptedNotes.length; index += 1) {
final Map<String, Object?> decryptedNote = decryptedNotes[index];
final String noteId = decryptedNote['id']! as String;
final existingNote = await (_database.select(
_database.notes,
)..where((n) => n.uuid.equals(noteResponse.id))).getSingleOrNull();
)..where((n) => n.uuid.equals(noteId))).getSingleOrNull();
// Decrypt note content
String decryptedTitle = 'Encrypted';
String decryptedBody = 'Encrypted';
try {
decryptedTitle = await NoteEncryption.decryptNote(
noteResponse.encryptedTitle,
_masterKey,
);
decryptedBody = await NoteEncryption.decryptNote(
noteResponse.encryptedBody,
_masterKey,
);
} catch (e) {
// If decryption fails, keep default encrypted placeholders
print('Failed to decrypt note ${noteResponse.id}: $e');
}
final String decryptedTitle = decryptedNote['title']! as String;
final String decryptedBody = decryptedNote['body']! as String;
final SyncNoteResponse noteResponse = response.changes.notes[index];
if (existingNote != null) {
// Update existing note
await _database.updateNoteRow(
DbNote(
id: existingNote.id,
uuid: noteResponse.id,
uuid: noteId,
title: decryptedTitle,
body: decryptedBody,
createdAt: existingNote.createdAt,
@@ -298,8 +289,6 @@ class NoteRepository {
),
);
}
onDecryptProgress?.call(index + 1, totalNotesToDecrypt);
}
}
@@ -333,3 +322,234 @@ class NoteRepository {
);
}
}
List<Future<List<Map<String, Object?>>>> _encryptNoteBatches(
List<DbNote> notes, {
required String masterKey,
}) {
if (notes.isEmpty) {
return <Future<List<Map<String, Object?>>>>[];
}
final int workerCount = _parallelWorkerCount(notes.length);
final int batchSize = (notes.length / workerCount).ceil();
final List<Future<List<Map<String, Object?>>>> batchFutures = [];
for (var start = 0; start < notes.length; start += batchSize) {
final int end = math.min(start + batchSize, notes.length);
final List<Map<String, Object?>> batchNotes = [];
for (var index = start; index < end; index += 1) {
batchNotes.add(_dbNoteToEncryptionInput(notes[index], index));
}
batchFutures.add(
Isolate.run(
() => _encryptNoteBatch({'masterKey': masterKey, 'notes': batchNotes}),
),
);
}
return batchFutures;
}
Future<List<SyncNotePayload>> _encryptNotesInParallel(
List<DbNote> notes, {
required String masterKey,
void Function(int encryptedCount)? onProgress,
}) async {
final List<Future<List<Map<String, Object?>>>> batchFutures =
_encryptNoteBatches(notes, masterKey: masterKey);
final List<SyncNotePayload?> orderedPayloads = List<SyncNotePayload?>.filled(
notes.length,
null,
);
var encryptedCount = 0;
await for (final List<Map<String, Object?>> batchResult in Stream.fromFutures(
batchFutures,
)) {
for (final Map<String, Object?> row in batchResult) {
final int index = row['index']! as int;
orderedPayloads[index] = _syncNotePayloadFromEncryptionResult(row);
}
encryptedCount += batchResult.length;
onProgress?.call(encryptedCount);
}
return orderedPayloads.cast<SyncNotePayload>();
}
List<Future<List<Map<String, Object?>>>> _decryptNoteBatches(
List<SyncNoteResponse> notes, {
required String masterKey,
}) {
if (notes.isEmpty) {
return <Future<List<Map<String, Object?>>>>[];
}
final int workerCount = _parallelWorkerCount(notes.length);
final int batchSize = (notes.length / workerCount).ceil();
final List<Future<List<Map<String, Object?>>>> batchFutures = [];
for (var start = 0; start < notes.length; start += batchSize) {
final int end = math.min(start + batchSize, notes.length);
final List<Map<String, Object?>> batchNotes = [];
for (var index = start; index < end; index += 1) {
batchNotes.add(_syncNoteToDecryptionInput(notes[index], index));
}
batchFutures.add(
Isolate.run(
() => _decryptNoteBatch({'masterKey': masterKey, 'notes': batchNotes}),
),
);
}
return batchFutures;
}
Future<List<Map<String, Object?>>> _decryptResponseNotesInParallel(
List<SyncNoteResponse> notes, {
required String masterKey,
void Function(int processed)? onProgress,
}) async {
final List<Future<List<Map<String, Object?>>>> batchFutures =
_decryptNoteBatches(notes, masterKey: masterKey);
final List<Map<String, Object?>?> decryptedNotes =
List<Map<String, Object?>?>.filled(notes.length, null);
var processed = 0;
await for (final List<Map<String, Object?>> batchResult in Stream.fromFutures(
batchFutures,
)) {
for (final Map<String, Object?> row in batchResult) {
final int index = row['index']! as int;
decryptedNotes[index] = row;
}
processed += batchResult.length;
onProgress?.call(processed);
}
return decryptedNotes.cast<Map<String, Object?>>();
}
Map<String, Object?> _syncNoteToDecryptionInput(
SyncNoteResponse row,
int index,
) {
return <String, Object?>{
'index': index,
'id': row.id,
'encryptedTitle': row.encryptedTitle,
'encryptedBody': row.encryptedBody,
};
}
Map<String, Object?> _dbNoteToEncryptionInput(DbNote row, int index) {
return <String, Object?>{
'index': index,
'uuid': row.uuid,
'title': row.title,
'body': row.body,
'createdAt': row.createdAt.toIso8601String(),
'updatedAt': row.updatedAt.toIso8601String(),
'categoryId': row.categoryId,
'serverVersion': row.serverVersion,
'position': row.sortIndex,
'isDeleted': row.isDeleted,
};
}
SyncNotePayload _syncNotePayloadFromEncryptionResult(Map<String, Object?> row) {
return SyncNotePayload(
id: row['id']! as String,
categoryId: row['categoryId'] as String?,
encryptedTitle: row['encryptedTitle']! as String,
encryptedBody: row['encryptedBody']! as String,
serverVersion: row['serverVersion']! as int,
position: row['position']! as int,
isDeleted: row['isDeleted']! as bool,
updatedAt: DateTime.parse(row['updatedAt']! as String),
);
}
Future<List<Map<String, Object?>>> _encryptNoteBatch(
Map<String, Object?> request,
) async {
final String masterKey = request['masterKey']! as String;
final List<Map<String, Object?>> notes = (request['notes']! as List)
.cast<Map<String, Object?>>();
final List<Map<String, Object?>> encryptedNotes = [];
for (final Map<String, Object?> note in notes) {
final String title = note['title']! as String;
final String body = note['body']! as String;
final String encryptedTitle = await NoteEncryption.encryptNote(
title,
masterKey,
);
final String encryptedBody = await NoteEncryption.encryptNote(
body,
masterKey,
);
encryptedNotes.add(<String, Object?>{
'index': note['index'] as int,
'id': note['uuid'] as String,
'categoryId': note['categoryId'] as String?,
'encryptedTitle': encryptedTitle,
'encryptedBody': encryptedBody,
'serverVersion': note['serverVersion']! as int,
'position': note['position']! as int,
'isDeleted': note['isDeleted']! as bool,
'updatedAt': note['updatedAt']! as String,
});
}
return encryptedNotes;
}
Future<List<Map<String, Object?>>> _decryptNoteBatch(
Map<String, Object?> request,
) async {
final String masterKey = request['masterKey']! as String;
final List<Map<String, Object?>> notes = (request['notes']! as List)
.cast<Map<String, Object?>>();
final List<Map<String, Object?>> decryptedNotes = [];
for (final Map<String, Object?> note in notes) {
String decryptedTitle = 'Encrypted';
String decryptedBody = 'Encrypted';
try {
decryptedTitle = await NoteEncryption.decryptNote(
note['encryptedTitle']! as String,
masterKey,
);
decryptedBody = await NoteEncryption.decryptNote(
note['encryptedBody']! as String,
masterKey,
);
} catch (e) {
print('Failed to decrypt note ${note['id']}: $e');
}
decryptedNotes.add(<String, Object?>{
'index': note['index'] as int,
'id': note['id'] as String,
'title': decryptedTitle,
'body': decryptedBody,
});
}
return decryptedNotes;
}
int _parallelWorkerCount(int itemCount) {
final int cappedByCpu = math.max(
1,
(Platform.numberOfProcessors * 0.6).floor(),
);
return math.max(1, math.min(itemCount, cappedByCpu));
}