Add Worker service with isolated task management and integrate into app

This commit is contained in:
2025-09-22 19:30:18 +02:00
parent cfd38211a2
commit d374ff6bf9
5 changed files with 370 additions and 58 deletions

View File

@@ -0,0 +1,158 @@
// test/system/worker/worker_impl_test.dart
import 'dart:async';
import 'dart:isolate';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fluttery/fluttery.dart';
import 'package:fluttery/preferences.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:fluttery/src/system/worker/worker_impl.dart';
import 'package:fluttery/worker.dart';
Future<void> pumpMicro([int times = 10]) => pumpEventQueue(times: times);
Future<void> waitFor(
bool Function() predicate, {
Duration timeout = const Duration(seconds: 2),
Duration step = const Duration(milliseconds: 20),
}) async {
final deadline = DateTime.now().add(timeout);
while (DateTime.now().isBefore(deadline)) {
if (predicate()) return;
await Future<void>.delayed(step);
}
fail('Condition not met within $timeout');
}
void main() {
setUpAll(() async {
TestWidgetsFlutterBinding.ensureInitialized();
SharedPreferences.setMockInitialValues({});
expect(ServicesBinding.rootIsolateToken, isNotNull);
});
group('worker', () {
test(
'spawn returns value; preTask runs; active->history tracking',
() async {
final worker = WorkerImpl();
App.service<Preferences>().setBool("test", false);
var preCalled = false;
final fut = worker.spawn<int>(
'ok',
() async {
await Future.delayed(const Duration(milliseconds: 20));
return 7;
},
// Ensure the worker isolate has the prefs mock (even if not used).
preTask: () {
SharedPreferences.setMockInitialValues({});
preCalled = true;
},
);
// Shortly after spawn there should be one active job.
await Future<void>.delayed(const Duration(milliseconds: 10));
expect(worker.getActiveWorkers().length, 1);
final res = await fut;
expect(res, 7);
expect(preCalled, isTrue);
await waitFor(() => worker.getActiveWorkers().isEmpty);
await waitFor(() => worker.getAllWorkers().isNotEmpty);
final all = worker.getAllWorkers();
expect(all.first.status, WorkerStatus.completed);
expect(all.first.name, 'ok');
},
// If you still see VM callback warnings here, consider making
// Preferences lazy in your app code to avoid plugin calls on registration.
// skip: true,
);
test('timeout marks job as timedOut and throws TimeoutException', () async {
final worker = WorkerImpl(
defaultTimeout: const Duration(milliseconds: 50),
);
await expectLater(
worker.spawn<void>(
'timeout',
// Long task so the wrapper .timeout triggers
() async => Future.delayed(const Duration(milliseconds: 220)),
// Make sure prefs mock is available in the worker isolate even if
// App.registerDefaultServices touches SharedPreferences.
preTask: () =>
SharedPreferences.setMockInitialValues({}),
),
throwsA(isA<TimeoutException>()),
);
// Wait until the worker updates history in its catchError path
await waitFor(() => worker.getAllWorkers().isNotEmpty);
final all = worker.getAllWorkers();
expect(all.first.status, WorkerStatus.timedOut);
expect(all.first.name, 'timeout');
});
test('failure marks job as failed and surfaces RemoteError', () async {
final worker = WorkerImpl();
await expectLater(
worker.spawn<void>(
'fail',
() async {
await Future<void>.delayed(const Duration(milliseconds: 10));
throw StateError('boom');
},
// Ensure plugin mocks exist if defaults touch plugins
preTask: () =>
SharedPreferences.setMockInitialValues({}),
),
// Isolate.run returns a RemoteError to the caller isolate
throwsA(isA<RemoteError>()),
);
await waitFor(() => worker.getAllWorkers().isNotEmpty);
final all = worker.getAllWorkers();
expect(all.first.status, WorkerStatus.failed);
expect(all.first.name, 'fail');
});
test(
'getWorker while running, then after completion; purge removes old',
() async {
final worker = WorkerImpl();
final fut = worker.spawn<void>(
'long',
() async => Future.delayed(const Duration(milliseconds: 160)),
preTask: () =>
SharedPreferences.setMockInitialValues({}),
);
await Future<void>.delayed(const Duration(milliseconds: 25));
final active = worker.getActiveWorkers();
expect(active.length, 1);
final id = active.first.id;
expect(worker.getWorker(id)?.status, WorkerStatus.running);
await fut;
await waitFor(
() => worker.getWorker(id)?.status == WorkerStatus.completed,
);
expect(worker.getAllWorkers().length, 1);
worker.purge(maxAge: Duration.zero);
await pumpMicro();
expect(worker.getAllWorkers(), isEmpty);
},
// skip: true,
);
});
}