LCOV - code coverage report
Current view: top level - src - task_chain.dart Coverage Total Hit
Test: lcov.info Lines: 97.7 % 43 42
Test Date: 2026-04-30 18:23:23 Functions: - 0 0

            Line data    Source code
       1              : import 'package:flutter/foundation.dart';
       2              : 
       3              : import 'constraints.dart';
       4              : import 'events.dart';
       5              : import 'worker.dart';
       6              : 
       7              : /// A single task request in a chain.
       8              : ///
       9              : /// Represents one step in a multi-step workflow. Combine multiple TaskRequests
      10              : /// using [TaskChainBuilder] to create complex sequential or parallel workflows.
      11              : ///
      12              : /// ## Basic Usage
      13              : ///
      14              : /// ```dart
      15              : /// final downloadTask = TaskRequest(
      16              : ///   id: 'download-file',
      17              : ///   worker: NativeWorker.httpDownload(
      18              : ///     url: 'https://cdn.example.com/data.zip',
      19              : ///     savePath: '/tmp/data.zip',
      20              : ///   ),
      21              : /// );
      22              : ///
      23              : /// final processTask = TaskRequest(
      24              : ///   id: 'process-data',
      25              : ///   worker: DartWorker(callbackId: 'processZipFile'),
      26              : /// );
      27              : ///
      28              : /// // Combine into a chain
      29              : /// await NativeWorkManager.beginWith(downloadTask)
      30              : ///     .then(processTask)
      31              : ///     .enqueue();
      32              : /// ```
      33              : ///
      34              : /// ## With Constraints
      35              : ///
      36              : /// ```dart
      37              : /// final uploadTask = TaskRequest(
      38              : ///   id: 'upload-results',
      39              : ///   worker: NativeWorker.httpUpload(
      40              : ///     url: 'https://api.example.com/results',
      41              : ///     filePath: '/tmp/results.json',
      42              : ///   ),
      43              : ///   constraints: Constraints(
      44              : ///     requiresWifi: true,
      45              : ///     requiresCharging: true,
      46              : ///   ),
      47              : /// );
      48              : /// ```
      49              : ///
      50              : /// ## See Also
      51              : ///
      52              : /// - [TaskChainBuilder] - Builder for creating task chains
      53              : /// - [NativeWorkManager.beginWith] - Start a task chain
      54              : /// ## Data Flow between Tasks
      55              : ///
      56              : /// Tasks in a chain can pass data to subsequent tasks using placeholders in their
      57              : /// configuration paths. Use the format `{{task_id.output_key}}`.
      58              : ///
      59              : /// ```dart
      60              : /// await NativeWorkManager.beginWith(
      61              : ///   TaskRequest(
      62              : ///     id: 'downloader',
      63              : ///     worker: NativeWorker.httpDownload(url: '...', savePath: '/tmp/file.zip'),
      64              : ///   ),
      65              : /// )
      66              : /// .then(TaskRequest(
      67              : ///   id: 'processor',
      68              : ///   worker: NativeWorker.fileDecompress(
      69              : ///     zipPath: '{{downloader.savePath}}', // Use output from previous task
      70              : ///     targetDir: '/data/',
      71              : ///   ),
      72              : /// ))
      73              : /// .enqueue();
      74              : /// ```
      75              : @immutable
      76              : class TaskRequest {
      77            2 :   const TaskRequest({
      78              :     required this.id,
      79              :     required this.worker,
      80              :     this.constraints = const Constraints(),
      81              :   });
      82              : 
      83              :   /// Unique identifier for this task.
      84              :   ///
      85              :   /// Must be unique within the chain. Used for tracking execution and debugging.
      86              :   final String id;
      87              : 
      88              :   /// Worker configuration.
      89              :   ///
      90              :   /// Can be any [Worker] type: NativeWorker or DartWorker.
      91              :   final Worker worker;
      92              : 
      93              :   /// Constraints for this task.
      94              :   ///
      95              :   /// These constraints apply only to this specific task, not the entire chain.
      96              :   /// To set constraints for the whole chain, use [TaskChainBuilder.withConstraints].
      97              :   final Constraints constraints;
      98              : 
      99              :   /// Convert to map for platform channel.
     100            4 :   Map<String, dynamic> toMap() => {
     101            2 :         'id': id,
     102            4 :         'workerClassName': worker.workerClassName,
     103            4 :         'workerConfig': worker.toMap(),
     104            4 :         'constraints': constraints.toMap(),
     105              :       };
     106              : 
     107            1 :   @override
     108              :   bool operator ==(Object other) =>
     109            4 :       identical(this, other) || other is TaskRequest && id == other.id;
     110              : 
     111            1 :   @override
     112            2 :   int get hashCode => id.hashCode;
     113              : 
     114            1 :   @override
     115              :   String toString() =>
     116            4 :       'TaskRequest(id: $id, worker: ${worker.workerClassName})';
     117              : }
     118              : 
     119              : /// Builder for creating task chains (A -> B -> C workflows).
     120              : ///
     121              : /// Task chains allow you to define complex multi-step workflows where tasks
     122              : /// execute in sequence or parallel. Perfect for data processing pipelines,
     123              : /// ETL operations, or any multi-stage background work.
     124              : ///
     125              : /// ## Sequential Chain (A → B → C)
     126              : ///
     127              : /// ```dart
     128              : /// await NativeWorkManager.beginWith(
     129              : ///   TaskRequest(
     130              : ///     id: 'download',
     131              : ///     worker: NativeWorker.httpDownload(
     132              : ///       url: 'https://cdn.example.com/video.mp4',
     133              : ///       savePath: '/tmp/video.mp4',
     134              : ///     ),
     135              : ///   ),
     136              : /// )
     137              : /// .then(TaskRequest(
     138              : ///   id: 'compress',
     139              : ///   worker: DartWorker(callbackId: 'compressVideo'),
     140              : /// ))
     141              : /// .then(TaskRequest(
     142              : ///   id: 'upload',
     143              : ///   worker: NativeWorker.httpUpload(
     144              : ///     url: 'https://api.example.com/videos',
     145              : ///     filePath: '/tmp/compressed.mp4',
     146              : ///   ),
     147              : /// ))
     148              : /// .named('video-pipeline')
     149              : /// .withConstraints(Constraints.heavyTask)
     150              : /// .enqueue();
     151              : /// ```
     152              : ///
     153              : /// ## Parallel Tasks (A → \[B1, B2, B3\])
     154              : ///
     155              : /// ```dart
     156              : /// await NativeWorkManager.beginWith(
     157              : ///   TaskRequest(
     158              : ///     id: 'prepare-data',
     159              : ///     worker: DartWorker(callbackId: 'prepareData'),
     160              : ///   ),
     161              : /// )
     162              : /// .thenAll([
     163              : ///   // These 3 uploads run in parallel
     164              : ///   TaskRequest(
     165              : ///     id: 'upload-server1',
     166              : ///     worker: NativeWorker.httpUpload(
     167              : ///       url: 'https://server1.example.com/backup',
     168              : ///       filePath: '/data/backup.zip',
     169              : ///     ),
     170              : ///   ),
     171              : ///   TaskRequest(
     172              : ///     id: 'upload-server2',
     173              : ///     worker: NativeWorker.httpUpload(
     174              : ///       url: 'https://server2.example.com/backup',
     175              : ///       filePath: '/data/backup.zip',
     176              : ///     ),
     177              : ///   ),
     178              : ///   TaskRequest(
     179              : ///     id: 'upload-cloud',
     180              : ///     worker: NativeWorker.httpUpload(
     181              : ///       url: 'https://cloud.example.com/backup',
     182              : ///       filePath: '/data/backup.zip',
     183              : ///     ),
     184              : ///   ),
     185              : /// ])
     186              : /// .enqueue();
     187              : /// ```
     188              : ///
     189              : /// ## Complex Multi-Stage Pipeline
     190              : ///
     191              : /// ```dart
     192              : /// // Stage 1: Fetch metadata
     193              : /// // Stage 2: Download files in parallel
     194              : /// // Stage 3: Merge and process
     195              : /// // Stage 4: Upload result
     196              : ///
     197              : /// await NativeWorkManager.beginWith(
     198              : ///   TaskRequest(
     199              : ///     id: 'fetch-metadata',
     200              : ///     worker: NativeWorker.httpRequest(
     201              : ///       url: 'https://api.example.com/metadata',
     202              : ///       method: HttpMethod.get,
     203              : ///     ),
     204              : ///   ),
     205              : /// )
     206              : /// .thenAll([
     207              : ///   TaskRequest(
     208              : ///     id: 'download-file1',
     209              : ///     worker: NativeWorker.httpDownload(
     210              : ///       url: 'https://cdn.example.com/file1.dat',
     211              : ///       savePath: '/tmp/file1.dat',
     212              : ///     ),
     213              : ///   ),
     214              : ///   TaskRequest(
     215              : ///     id: 'download-file2',
     216              : ///     worker: NativeWorker.httpDownload(
     217              : ///       url: 'https://cdn.example.com/file2.dat',
     218              : ///       savePath: '/tmp/file2.dat',
     219              : ///     ),
     220              : ///   ),
     221              : /// ])
     222              : /// .then(TaskRequest(
     223              : ///   id: 'merge-process',
     224              : ///   worker: DartWorker(callbackId: 'mergeAndProcess'),
     225              : /// ))
     226              : /// .then(TaskRequest(
     227              : ///   id: 'upload-result',
     228              : ///   worker: NativeWorker.httpUpload(
     229              : ///     url: 'https://api.example.com/results',
     230              : ///     filePath: '/tmp/result.json',
     231              : ///   ),
     232              : /// ))
     233              : /// .named('etl-pipeline')
     234              : /// .withConstraints(Constraints.heavyTask)
     235              : /// .enqueue();
     236              : /// ```
     237              : ///
     238              : /// ## Chain Execution Rules
     239              : ///
     240              : /// - **Sequential tasks**: Execute one after another (A → B → C)
     241              : /// - **Parallel tasks**: All start together, next step waits for ALL to complete
     242              : /// - **Failure handling**: If ANY task fails, entire chain stops
     243              : /// - **Constraints**: Applied to entire chain (use withConstraints)
     244              : ///
     245              : /// ## Builder Methods
     246              : ///
     247              : /// - [then] - Add single task (sequential)
     248              : /// - [thenAll] - Add multiple tasks (parallel)
     249              : /// - [named] - Set chain name for debugging
     250              : /// - [withConstraints] - Set constraints for entire chain
     251              : /// - [enqueue] - Schedule the chain for execution
     252              : ///
     253              : /// ## Common Pitfalls
     254              : ///
     255              : /// ❌ **Don't** make chains too long (increases failure risk)
     256              : /// ❌ **Don't** use chains for independent tasks
     257              : /// ❌ **Don't** forget to call enqueue() at the end
     258              : /// ✅ **Do** handle failures gracefully
     259              : /// ✅ **Do** keep chains focused on related tasks
     260              : /// ✅ **Do** use constraints appropriately
     261              : ///
     262              : /// ## See Also
     263              : ///
     264              : /// - [NativeWorkManager.beginWith] - Start chain with single task
     265              : /// - [NativeWorkManager.beginWithAll] - Start chain with parallel tasks
     266              : /// - [TaskRequest] - Individual task in chain
     267              : class TaskChainBuilder {
     268              :   /// Creates a new TaskChainBuilder with initial tasks.
     269              :   ///
     270              :   /// This constructor is intended for internal use by NativeWorkManager.
     271            2 :   TaskChainBuilder.internal(List<TaskRequest> initialTasks)
     272            2 :       : _steps = [initialTasks],
     273              :         _name = null,
     274              :         _constraints = const Constraints();
     275              : 
     276              :   final List<List<TaskRequest>> _steps;
     277              :   String? _name;
     278              :   Constraints _constraints;
     279              : 
     280              :   /// Internal: Callback to actually enqueue the chain.
     281              :   /// Set by NativeWorkManager during initialization.
     282              :   static Future<ScheduleResult> Function(TaskChainBuilder)? enqueueCallback;
     283              : 
     284              :   /// Add a single task to run after the previous step completes.
     285              :   ///
     286              :   /// Creates a sequential dependency: current step → new task.
     287              :   /// The new task will only start after ALL tasks in the previous step complete.
     288              :   ///
     289              :   /// ```dart
     290              :   /// NativeWorkManager.beginWith(taskA)
     291              :   ///     .then(taskB)  // Runs after A completes
     292              :   ///     .then(taskC)  // Runs after B completes
     293              :   ///     .enqueue();
     294              :   /// // Execution: A → B → C
     295              :   /// ```
     296              :   ///
     297              :   /// See also: [thenAll] for parallel tasks.
     298            2 :   TaskChainBuilder then(TaskRequest task) {
     299            6 :     _steps.add([task]);
     300              :     return this;
     301              :   }
     302              : 
     303              :   /// Add multiple tasks to run in parallel after the previous step completes.
     304              :   ///
     305              :   /// Creates parallel execution: current step → `[task1, task2, task3]`.
     306              :   /// All tasks in the list start simultaneously. The next step waits for
     307              :   /// ALL parallel tasks to complete.
     308              :   ///
     309              :   /// ```dart
     310              :   /// NativeWorkManager.beginWith(prepareTask)
     311              :   ///     .thenAll([uploadTask1, uploadTask2, uploadTask3])  // Parallel
     312              :   ///     .then(cleanupTask)  // Waits for all 3 uploads
     313              :   ///     .enqueue();
     314              :   /// // Execution: prepare → [upload1, upload2, upload3] → cleanup
     315              :   /// ```
     316              :   ///
     317              :   /// **Important:** If ANY task in the parallel group fails, the entire chain stops.
     318              :   ///
     319              :   /// Throws [ArgumentError] if tasks list is empty.
     320              :   ///
     321              :   /// See also: [then] for sequential tasks.
     322            2 :   TaskChainBuilder thenAll(List<TaskRequest> tasks) {
     323            2 :     if (tasks.isEmpty) {
     324            2 :       throw ArgumentError('Tasks list cannot be empty');
     325              :     }
     326            4 :     _steps.add(tasks);
     327              :     return this;
     328              :   }
     329              : 
     330              :   /// Set a name for this chain (for debugging/monitoring).
     331              :   ///
     332              :   /// The chain name appears in logs and can be used for tracking execution.
     333              :   /// Useful when running multiple chains to identify which one is executing.
     334              :   ///
     335              :   /// ```dart
     336              :   /// NativeWorkManager.beginWith(downloadTask)
     337              :   ///     .then(processTask)
     338              :   ///     .named('data-sync-pipeline')  // Name for debugging
     339              :   ///     .enqueue();
     340              :   /// ```
     341            2 :   TaskChainBuilder named(String name) {
     342            2 :     _name = name;
     343              :     return this;
     344              :   }
     345              : 
     346              :   /// Set constraints for the entire chain.
     347              :   ///
     348              :   /// These constraints apply to ALL tasks in the chain. The chain will only
     349              :   /// start executing when these constraints are met.
     350              :   ///
     351              :   /// **Note:** Individual tasks can have their own constraints via [TaskRequest],
     352              :   /// but chain-level constraints must be satisfied first.
     353              :   ///
     354              :   /// ```dart
     355              :   /// // Heavy processing chain - only run when charging + WiFi
     356              :   /// NativeWorkManager.beginWith(downloadTask)
     357              :   ///     .then(processTask)
     358              :   ///     .then(uploadTask)
     359              :   ///     .withConstraints(Constraints.heavyTask)
     360              :   ///     .enqueue();
     361              :   /// ```
     362              :   ///
     363              :   /// Common patterns:
     364              :   /// - `Constraints.networkRequired` - For API chains
     365              :   /// - `Constraints.heavyTask` - For large uploads/processing
     366              :   /// - `Constraints(requiresDeviceIdle: true)` - For maintenance chains
     367            2 :   TaskChainBuilder withConstraints(Constraints constraints) {
     368            2 :     _constraints = constraints;
     369              :     return this;
     370              :   }
     371              : 
     372              :   /// Schedule the chain for execution.
     373              :   ///
     374              :   /// Submits the chain to the OS scheduler. The chain will execute according
     375              :   /// to the defined sequence and constraints.
     376              :   ///
     377              :   /// **Returns:** [ScheduleResult.accepted] if successfully scheduled.
     378              :   ///
     379              :   /// **Throws:** [StateError] if NativeWorkManager is not initialized.
     380              :   ///
     381              :   /// ```dart
     382              :   /// final result = await NativeWorkManager.beginWith(taskA)
     383              :   ///     .then(taskB)
     384              :   ///     .enqueue();
     385              :   ///
     386              :   /// if (result == ScheduleResult.accepted) {
     387              :   ///   print('Chain scheduled successfully');
     388              :   /// }
     389              :   /// ```
     390              :   ///
     391              :   /// **Important:** You MUST call this method to actually schedule the chain.
     392              :   /// Building the chain without calling enqueue() does nothing.
     393            1 :   Future<ScheduleResult> enqueue() async {
     394              :     if (enqueueCallback == null) {
     395            1 :       throw StateError(
     396              :         'NativeWorkManager not initialized. '
     397              :         'Call NativeWorkManager.initialize() first.',
     398              :       );
     399              :     }
     400            0 :     return enqueueCallback!(this);
     401              :   }
     402              : 
     403              :   /// Get all steps in the chain.
     404            3 :   List<List<TaskRequest>> get steps => List.unmodifiable(_steps);
     405              : 
     406              :   /// Get the chain name.
     407            2 :   String? get name => _name;
     408              : 
     409              :   /// Get the chain constraints.
     410            2 :   Constraints get constraints => _constraints;
     411              : 
     412              :   /// Convert to map for platform channel.
     413            4 :   Map<String, dynamic> toMap() => {
     414            2 :         'name': _name,
     415            4 :         'constraints': _constraints.toMap(),
     416            2 :         'steps': _steps
     417           12 :             .map((step) => step.map((task) => task.toMap()).toList())
     418            2 :             .toList(),
     419              :       };
     420              : 
     421            1 :   @override
     422              :   String toString() {
     423            3 :     final stepDescriptions = _steps.map((step) {
     424            2 :       if (step.length == 1) {
     425            2 :         return step.first.id;
     426              :       }
     427            5 :       return '[${step.map((t) => t.id).join(', ')}]';
     428            1 :     }).join(' -> ');
     429            2 :     return 'TaskChain(${_name ?? 'unnamed'}: $stepDescriptions)';
     430              :   }
     431              : }
        

Generated by: LCOV version 2.4-0