feat(api): Optimize conversation parsing with worker-based decoding

This commit is contained in:
cogwheel0
2025-11-01 14:54:08 +05:30
parent a374c744ef
commit a005c14a67
7 changed files with 497 additions and 301 deletions

View File

@@ -911,6 +911,26 @@ class ApiService {
return [];
}
Future<List<Conversation>> _parseConversationSummaryList(
List<dynamic> regular, {
required String debugLabel,
}) async {
final payload = <String, dynamic>{
'regular': List<dynamic>.from(regular),
'pinned': const <dynamic>[],
'archived': const <dynamic>[],
};
final parsed = await _workerManager
.schedule<Map<String, dynamic>, List<Map<String, dynamic>>>(
parseConversationSummariesWorker,
payload,
debugLabel: debugLabel,
);
return parsed
.map((json) => Conversation.fromJson(json))
.toList(growable: false);
}
// Tools - Check available tools on server
Future<List<Map<String, dynamic>>> getAvailableTools() async {
_traceApi('Fetching available tools');
@@ -1005,10 +1025,10 @@ class ApiService {
final response = await _dio.get('/api/v1/chats/folder/$folderId');
final data = response.data;
if (data is List) {
return data.whereType<Map>().map((chatData) {
final map = Map<String, dynamic>.from(chatData);
return Conversation.fromJson(parseConversationSummary(map));
}).toList();
return _parseConversationSummaryList(
data,
debugLabel: 'parse_folder_$folderId',
);
}
return [];
}
@@ -1052,10 +1072,7 @@ class ApiService {
final response = await _dio.get('/api/v1/chats/tags/$tag');
final data = response.data;
if (data is List) {
return data.whereType<Map>().map((chatData) {
final map = Map<String, dynamic>.from(chatData);
return Conversation.fromJson(parseConversationSummary(map));
}).toList();
return _parseConversationSummaryList(data, debugLabel: 'parse_tag_$tag');
}
return [];
}
@@ -2738,8 +2755,11 @@ class ApiService {
'/api/v1/chats/search',
queryParameters: {'q': query},
);
final results = response.data as List;
return results.map((c) => Conversation.fromJson(c)).toList();
final results = response.data;
if (results is List) {
return _parseConversationSummaryList(results, debugLabel: 'parse_search');
}
return [];
}
// Debug method to test API endpoints

View File

@@ -9,6 +9,7 @@ import '../persistence/hive_boxes.dart';
import '../persistence/persistence_keys.dart';
import '../utils/debug_logger.dart';
import 'secure_credential_storage.dart';
import 'worker_manager.dart';
/// Optimized storage service backed by Hive for non-sensitive data and
/// FlutterSecureStorage for credentials.
@@ -16,19 +17,22 @@ class OptimizedStorageService {
OptimizedStorageService({
required FlutterSecureStorage secureStorage,
required HiveBoxes boxes,
required WorkerManager workerManager,
}) : _preferencesBox = boxes.preferences,
_cachesBox = boxes.caches,
_attachmentQueueBox = boxes.attachmentQueue,
_metadataBox = boxes.metadata,
_secureCredentialStorage = SecureCredentialStorage(
instance: secureStorage,
);
),
_workerManager = workerManager;
final Box<dynamic> _preferencesBox;
final Box<dynamic> _cachesBox;
final Box<dynamic> _attachmentQueueBox;
final Box<dynamic> _metadataBox;
final SecureCredentialStorage _secureCredentialStorage;
final WorkerManager _workerManager;
static const String _authTokenKey = 'auth_token_v3';
static const String _activeServerIdKey = PreferenceKeys.activeServerId;
@@ -298,19 +302,13 @@ class OptimizedStorageService {
if (stored == null) {
return const [];
}
if (stored is String) {
final decoded = jsonDecode(stored) as List<dynamic>;
return decoded.map((item) => Conversation.fromJson(item)).toList();
}
if (stored is List) {
return stored
.map(
(item) =>
Conversation.fromJson(Map<String, dynamic>.from(item as Map)),
)
.toList();
}
return const [];
final parsed = await _workerManager
.schedule<Map<String, dynamic>, List<Map<String, dynamic>>>(
_decodeStoredConversationsWorker,
{'stored': stored},
debugLabel: 'decode_local_conversations',
);
return parsed.map(Conversation.fromJson).toList(growable: false);
} catch (error, stack) {
DebugLogger.error(
'Failed to retrieve local conversations',
@@ -455,3 +453,28 @@ class OptimizedStorageService {
};
}
}
List<Map<String, dynamic>> _decodeStoredConversationsWorker(
Map<String, dynamic> payload,
) {
final stored = payload['stored'];
if (stored is String) {
final decoded = jsonDecode(stored);
if (decoded is List) {
return decoded
.whereType<Map>()
.map((item) => Map<String, dynamic>.from(item))
.toList();
}
return <Map<String, dynamic>>[];
}
if (stored is List) {
return stored
.whereType<Map>()
.map((item) => Map<String, dynamic>.from(item))
.toList();
}
return <Map<String, dynamic>>[];
}

View File

@@ -170,12 +170,14 @@ class WorkerManager {
/// Keep a single [WorkerManager] alive across the app.
@Riverpod(keepAlive: true)
// ignore: functional_ref
WorkerManager workerManager(Ref ref) {
final concurrency = kIsWeb ? 1 : WorkerManager._defaultMaxConcurrentTasks;
final manager = WorkerManager(maxConcurrentTasks: concurrency);
ref.onDispose(manager.dispose);
return manager;
class WorkerManagerNotifier extends _$WorkerManagerNotifier {
@override
WorkerManager build() {
final concurrency = kIsWeb ? 1 : WorkerManager._defaultMaxConcurrentTasks;
final manager = WorkerManager(maxConcurrentTasks: concurrency);
ref.onDispose(manager.dispose);
return manager;
}
}
class _EnqueuedJob {