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 : }
|