Moved long running task to an isolate
This commit is contained in:
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../models/office.dart';
|
||||
import '../models/ticket.dart';
|
||||
@@ -127,58 +128,114 @@ final ticketsProvider = StreamProvider<List<Ticket>>((ref) {
|
||||
profile.role == 'dispatcher' ||
|
||||
profile.role == 'it_staff';
|
||||
|
||||
// Use stream for realtime updates, then apply pagination & search filters
|
||||
// client-side because `.range(...)` is not supported on the stream builder.
|
||||
final baseStream = client
|
||||
.from('tickets')
|
||||
.stream(primaryKey: ['id'])
|
||||
.map((rows) => rows.map(Ticket.fromMap).toList());
|
||||
// Use stream for realtime updates. Offload expensive client-side
|
||||
// filtering/sorting/pagination to a background isolate via `compute`
|
||||
// so UI navigation and builds remain smooth.
|
||||
final baseStream = client.from('tickets').stream(primaryKey: ['id']);
|
||||
|
||||
return baseStream.map((allTickets) {
|
||||
debugPrint('[ticketsProvider] stream event: ${allTickets.length} rows');
|
||||
var list = allTickets;
|
||||
return baseStream.asyncMap((rows) async {
|
||||
// rows is List<dynamic> of maps coming from Supabase
|
||||
final rowsList = (rows as List<dynamic>).cast<Map<String, dynamic>>();
|
||||
|
||||
if (!isGlobal) {
|
||||
final officeIds =
|
||||
assignmentsAsync.valueOrNull
|
||||
?.where((assignment) => assignment.userId == profile.id)
|
||||
.map((assignment) => assignment.officeId)
|
||||
.toSet()
|
||||
.toList() ??
|
||||
<String>[];
|
||||
if (officeIds.isEmpty) return <Ticket>[];
|
||||
final allowedOffices = officeIds.toSet();
|
||||
list = list.where((t) => allowedOffices.contains(t.officeId)).toList();
|
||||
}
|
||||
// Prepare lightweight serializable args for background processing
|
||||
final allowedOfficeIds =
|
||||
assignmentsAsync.valueOrNull
|
||||
?.where((assignment) => assignment.userId == profile.id)
|
||||
.map((assignment) => assignment.officeId)
|
||||
.toList() ??
|
||||
<String>[];
|
||||
|
||||
if (query.officeId != null) {
|
||||
list = list.where((t) => t.officeId == query.officeId).toList();
|
||||
}
|
||||
if (query.status != null) {
|
||||
list = list.where((t) => t.status == query.status).toList();
|
||||
}
|
||||
if (query.searchQuery.isNotEmpty) {
|
||||
final q = query.searchQuery.toLowerCase();
|
||||
list = list
|
||||
.where(
|
||||
(t) =>
|
||||
t.subject.toLowerCase().contains(q) ||
|
||||
t.description.toLowerCase().contains(q),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
final payload = <String, dynamic>{
|
||||
'rows': rowsList,
|
||||
'isGlobal': isGlobal,
|
||||
'allowedOfficeIds': allowedOfficeIds,
|
||||
'offset': query.offset,
|
||||
'limit': query.limit,
|
||||
'searchQuery': query.searchQuery,
|
||||
'officeId': query.officeId,
|
||||
'status': query.status,
|
||||
'dateStart': query.dateRange?.start.millisecondsSinceEpoch,
|
||||
'dateEnd': query.dateRange?.end.millisecondsSinceEpoch,
|
||||
};
|
||||
|
||||
// Sort: newest first
|
||||
list.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
final processed = await compute(_processTicketsInIsolate, payload);
|
||||
|
||||
// Pagination
|
||||
final start = query.offset;
|
||||
final end = (start + query.limit).clamp(0, list.length);
|
||||
if (start >= list.length) return <Ticket>[];
|
||||
return list.sublist(start, end);
|
||||
// `processed` is List<Map<String,dynamic>> — convert to Ticket objects
|
||||
final tickets = (processed as List<dynamic>)
|
||||
.cast<Map<String, dynamic>>()
|
||||
.map(Ticket.fromMap)
|
||||
.toList();
|
||||
|
||||
debugPrint('[ticketsProvider] processed ${tickets.length} tickets');
|
||||
return tickets;
|
||||
});
|
||||
});
|
||||
|
||||
// Runs inside a background isolate. Accepts a serializable payload and
|
||||
// returns a list of ticket maps after filtering/sorting/pagination.
|
||||
List<Map<String, dynamic>> _processTicketsInIsolate(
|
||||
Map<String, dynamic> payload,
|
||||
) {
|
||||
final rows = (payload['rows'] as List).cast<Map<String, dynamic>>();
|
||||
var list = List<Map<String, dynamic>>.from(rows);
|
||||
|
||||
final isGlobal = payload['isGlobal'] as bool? ?? false;
|
||||
final allowedOfficeIds =
|
||||
(payload['allowedOfficeIds'] as List?)?.cast<String>().toSet() ??
|
||||
<String>{};
|
||||
|
||||
if (!isGlobal) {
|
||||
if (allowedOfficeIds.isEmpty) return <Map<String, dynamic>>[];
|
||||
list = list
|
||||
.where((t) => allowedOfficeIds.contains(t['office_id']))
|
||||
.toList();
|
||||
}
|
||||
|
||||
final officeId = payload['officeId'] as String?;
|
||||
if (officeId != null) {
|
||||
list = list.where((t) => t['office_id'] == officeId).toList();
|
||||
}
|
||||
final status = payload['status'] as String?;
|
||||
if (status != null) {
|
||||
list = list.where((t) => t['status'] == status).toList();
|
||||
}
|
||||
|
||||
final searchQuery = (payload['searchQuery'] as String?) ?? '';
|
||||
if (searchQuery.isNotEmpty) {
|
||||
final q = searchQuery.toLowerCase();
|
||||
list = list.where((t) {
|
||||
final subj = (t['subject'] as String?)?.toLowerCase() ?? '';
|
||||
final desc = (t['description'] as String?)?.toLowerCase() ?? '';
|
||||
return subj.contains(q) || desc.contains(q);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Sort newest first. `created_at` may be ISO strings or timestamps;
|
||||
// handle strings and numeric values.
|
||||
int _parseCreatedAt(Map<String, dynamic> m) {
|
||||
final v = m['created_at'];
|
||||
if (v == null) return 0;
|
||||
if (v is int) return v;
|
||||
if (v is double) return v.toInt();
|
||||
if (v is String) {
|
||||
try {
|
||||
return DateTime.parse(v).millisecondsSinceEpoch;
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
list.sort((a, b) => _parseCreatedAt(b).compareTo(_parseCreatedAt(a)));
|
||||
|
||||
final start = (payload['offset'] as int?) ?? 0;
|
||||
final limit = (payload['limit'] as int?) ?? 50;
|
||||
final end = (start + limit).clamp(0, list.length);
|
||||
if (start >= list.length) return <Map<String, dynamic>>[];
|
||||
return list.sublist(start, end);
|
||||
}
|
||||
|
||||
/// Provider for ticket query parameters.
|
||||
final ticketsQueryProvider = StateProvider<TicketQuery>(
|
||||
(ref) => const TicketQuery(),
|
||||
|
||||
Reference in New Issue
Block a user