feat: Optimize note encryption and decryption processes with parallel execution
This commit is contained in:
+266
-46
@@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
final List<SyncNotePayload>
|
||||
encryptedNotesPayload = await _encryptNotesInParallel(
|
||||
unsyncedNotes,
|
||||
masterKey: _masterKey,
|
||||
onProgress: (int encryptedCount) {
|
||||
onProgress?.call(
|
||||
SyncStatus.encrypting,
|
||||
progress: (index + 1) / totalNotesToEncrypt,
|
||||
progress: totalNotesToEncrypt == 0
|
||||
? 1.0
|
||||
: encryptedCount / totalNotesToEncrypt,
|
||||
message:
|
||||
'Encriptando notas para subir: ${index + 1} de $totalNotesToEncrypt',
|
||||
'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));
|
||||
}
|
||||
|
||||
+44
-22
@@ -1,13 +1,12 @@
|
||||
import 'package:notas/models/note.dart';
|
||||
import 'package:notas/models/category.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
// DTOs para sincronización con el servidor
|
||||
|
||||
class SyncRequest {
|
||||
SyncRequest({
|
||||
DateTime? lastSyncAt,
|
||||
required this.changes,
|
||||
}) : lastSyncAt = lastSyncAt ?? DateTime.utc(1970, 1, 1);
|
||||
SyncRequest({DateTime? lastSyncAt, required this.changes})
|
||||
: lastSyncAt = lastSyncAt ?? DateTime.utc(1970, 1, 1);
|
||||
|
||||
final DateTime lastSyncAt;
|
||||
final SyncChanges changes;
|
||||
@@ -21,10 +20,7 @@ class SyncRequest {
|
||||
}
|
||||
|
||||
class SyncChanges {
|
||||
const SyncChanges({
|
||||
this.categories = const [],
|
||||
this.notes = const [],
|
||||
});
|
||||
const SyncChanges({this.categories = const [], this.notes = const []});
|
||||
|
||||
final List<SyncCategoryPayload> categories;
|
||||
final List<SyncNotePayload> notes;
|
||||
@@ -33,8 +29,7 @@ class SyncChanges {
|
||||
return {
|
||||
if (categories.isNotEmpty)
|
||||
'categories': categories.map((c) => c.toJson()).toList(),
|
||||
if (notes.isNotEmpty)
|
||||
'notes': notes.map((n) => n.toJson()).toList(),
|
||||
if (notes.isNotEmpty) 'notes': notes.map((n) => n.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -49,7 +44,8 @@ class SyncChangesResponse {
|
||||
final List<SyncNoteResponse> notes;
|
||||
|
||||
factory SyncChangesResponse.fromJson(Map<String, dynamic> json) {
|
||||
final List<dynamic> categoriesJson = json['categories'] as List<dynamic>? ?? [];
|
||||
final List<dynamic> categoriesJson =
|
||||
json['categories'] as List<dynamic>? ?? [];
|
||||
final List<dynamic> notesJson = json['notes'] as List<dynamic>? ?? [];
|
||||
|
||||
return SyncChangesResponse(
|
||||
@@ -62,6 +58,30 @@ class SyncChangesResponse {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _readStringValue(dynamic value) {
|
||||
if (value is String) {
|
||||
return value;
|
||||
}
|
||||
if (value == null) {
|
||||
throw FormatException('Expected String value but found null');
|
||||
}
|
||||
return jsonEncode(value);
|
||||
}
|
||||
|
||||
int _readIntValue(dynamic value) {
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
if (value is String) {
|
||||
final int? parsed = int.tryParse(value);
|
||||
if (parsed != null) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
throw FormatException('Expected int value but found $value');
|
||||
}
|
||||
|
||||
class SyncCategoryPayload {
|
||||
const SyncCategoryPayload({
|
||||
required this.id,
|
||||
@@ -163,11 +183,11 @@ class SyncResponse {
|
||||
|
||||
factory SyncResponse.fromJson(Map<String, dynamic> json) {
|
||||
return SyncResponse(
|
||||
serverTimestamp:
|
||||
DateTime.parse(json['serverTimestamp'] as String),
|
||||
serverTimestamp: DateTime.parse(json['serverTimestamp'] as String),
|
||||
synced: json['synced'] as bool? ?? false,
|
||||
changes: SyncChangesResponse.fromJson(
|
||||
json['changes'] as Map<String, dynamic>? ?? {}),
|
||||
json['changes'] as Map<String, dynamic>? ?? {},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -189,9 +209,9 @@ class SyncCategoryResponse {
|
||||
|
||||
factory SyncCategoryResponse.fromJson(Map<String, dynamic> json) {
|
||||
return SyncCategoryResponse(
|
||||
id: json['id'] as String,
|
||||
encryptedName: json['encrypted_name'] as String,
|
||||
serverVersion: json['serverVersion'] as int,
|
||||
id: _readStringValue(json['id']),
|
||||
encryptedName: _readStringValue(json['encrypted_name']),
|
||||
serverVersion: _readIntValue(json['serverVersion']),
|
||||
isDeleted: json['isDeleted'] as bool? ?? false,
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
);
|
||||
@@ -231,11 +251,13 @@ class SyncNoteResponse {
|
||||
|
||||
factory SyncNoteResponse.fromJson(Map<String, dynamic> json) {
|
||||
return SyncNoteResponse(
|
||||
id: json['id'] as String,
|
||||
categoryId: json['categoryId'] as String?,
|
||||
encryptedTitle: json['encrypted_title'] as String,
|
||||
encryptedBody: json['encrypted_body'] as String,
|
||||
serverVersion: json['serverVersion'] as int,
|
||||
id: _readStringValue(json['id']),
|
||||
categoryId: json['categoryId'] == null
|
||||
? null
|
||||
: _readStringValue(json['categoryId']),
|
||||
encryptedTitle: _readStringValue(json['encrypted_title']),
|
||||
encryptedBody: _readStringValue(json['encrypted_body']),
|
||||
serverVersion: _readIntValue(json['serverVersion']),
|
||||
position: json['position'] as int? ?? 0,
|
||||
isDeleted: json['isDeleted'] as bool? ?? false,
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
|
||||
@@ -150,7 +150,7 @@ class SyncStatusIndicator extends StatelessWidget {
|
||||
_buildStatusBadge(
|
||||
icon: Icons.cloud_download_outlined,
|
||||
color: const Color.fromARGB(255, 154, 194, 112),
|
||||
determinate: false,
|
||||
determinate: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user