|
7 | 7 | // `web/background.dart.js` and executed both by the in-page Web Worker and by |
8 | 8 | // the Service Worker, neither of which can run the Flutter engine. |
9 | 9 |
|
| 10 | +import 'dart:async'; |
| 11 | + |
10 | 12 | import 'package:workmanager_web/execution.dart'; |
11 | 13 |
|
| 14 | +import 'web_glue.dart'; |
| 15 | + |
12 | 16 | /// Dispatcher used on the web: wired into the compiled worker bundle |
13 | 17 | /// (`web/background.dart`) and passed to `WorkmanagerWeb().initialize(...)` on |
14 | 18 | /// the page (fallback path). |
15 | 19 | @pragma('vm:entry-point') |
16 | 20 | void webCallbackDispatcher() { |
17 | 21 | WorkmanagerExecution.instance.executeTask(handleWebBackgroundTask); |
| 22 | + WorkmanagerExecution.instance.messageHandler = handleWorkerMessage; |
| 23 | +} |
| 24 | + |
| 25 | +// --------------------------------------------------------------------------- |
| 26 | +// Use case: a simulated "weather watch". |
| 27 | +// |
| 28 | +// The demo simulates a temperature feed so it stays self-contained (no |
| 29 | +// network, no API key). The same shape applies to any real background work: |
| 30 | +// |
| 31 | +// * the page sends a message to the worker -> messageHandler runs in |
| 32 | +// the Web Worker (off the main thread), |
| 33 | +// * the worker pushes updates back -> sendToPage surfaces them |
| 34 | +// on `WorkmanagerWeb.workerMessages`, |
| 35 | +// * background tasks (also while the page is closed, via the Service Worker) |
| 36 | +// run the same handler and their results are replayed into the event log. |
| 37 | +// --------------------------------------------------------------------------- |
| 38 | + |
| 39 | +/// Simulated baseline temperature per city, in °C. |
| 40 | +const Map<String, double> _baseTemps = <String, double>{ |
| 41 | + 'cardiff': 11.0, |
| 42 | + 'taipei': 26.0, |
| 43 | +}; |
| 44 | + |
| 45 | +Timer? _watchTimer; |
| 46 | + |
| 47 | +/// Handler for free-form messages sent by the page with |
| 48 | +/// `WorkmanagerWeb().sendMessageToWorker(...)`. |
| 49 | +/// |
| 50 | +/// Messages: |
| 51 | +/// * `{'op': 'watch', 'city': 'cardiff', 'threshold': 5.0}` — start pushing |
| 52 | +/// simulated temperatures every few seconds; stops itself when the |
| 53 | +/// temperature drops below [threshold]. |
| 54 | +/// * `{'op': 'stop'}` — stop the current watch. |
| 55 | +/// * `{'op': 'check', 'city': ..., 'threshold': ...}` — run a one-off |
| 56 | +/// background check (same logic as the task path). |
| 57 | +/// * `{'op': 'text', 'text': ...}` — echo arbitrary text back. |
| 58 | +void handleWorkerMessage(Object? payload) { |
| 59 | + if (payload is! Map) { |
| 60 | + return; |
| 61 | + } |
| 62 | + final op = payload['op']; |
| 63 | + switch (op) { |
| 64 | + case 'watch': |
| 65 | + final city = (payload['city'] as String?)?.toLowerCase() ?? 'cardiff'; |
| 66 | + final threshold = (payload['threshold'] as num?)?.toDouble(); |
| 67 | + _watchTimer?.cancel(); |
| 68 | + _post(<String, Object?>{ |
| 69 | + 'kind': 'watching', |
| 70 | + 'city': city, |
| 71 | + 'threshold': threshold, |
| 72 | + }); |
| 73 | + _postTick(city, threshold); |
| 74 | + _watchTimer = Timer.periodic( |
| 75 | + const Duration(seconds: 3), |
| 76 | + (_) => _postTick(city, threshold), |
| 77 | + ); |
| 78 | + case 'stop': |
| 79 | + _watchTimer?.cancel(); |
| 80 | + _watchTimer = null; |
| 81 | + _post(<String, Object?>{'kind': 'stopped'}); |
| 82 | + case 'check': |
| 83 | + // One-off background check on demand (same logic as the task path). |
| 84 | + final city = (payload['city'] as String?)?.toLowerCase() ?? 'cardiff'; |
| 85 | + final threshold = (payload['threshold'] as num?)?.toDouble(); |
| 86 | + _post(<String, Object?>{'kind': 'task-start', 'city': city}); |
| 87 | + final tempC = _simulatedTemp(city); |
| 88 | + final below = threshold != null && tempC < threshold; |
| 89 | + _post(<String, Object?>{ |
| 90 | + 'kind': 'task-done', |
| 91 | + 'city': city, |
| 92 | + 'tempC': tempC, |
| 93 | + 'below': below, |
| 94 | + }); |
| 95 | + case 'text': |
| 96 | + _post(<String, Object?>{'kind': 'echo', 'text': payload['text']}); |
| 97 | + } |
| 98 | +} |
| 99 | + |
| 100 | +void _postTick(String city, double? threshold) { |
| 101 | + final tempC = _simulatedTemp(city); |
| 102 | + final below = threshold != null && tempC < threshold; |
| 103 | + _post(<String, Object?>{ |
| 104 | + 'kind': below ? 'alert' : 'tick', |
| 105 | + 'city': city, |
| 106 | + 'tempC': tempC, |
| 107 | + 'threshold': threshold, |
| 108 | + }); |
| 109 | + if (below) { |
| 110 | + _watchTimer?.cancel(); |
| 111 | + _watchTimer = null; |
| 112 | + } |
| 113 | +} |
| 114 | + |
| 115 | +/// Sends a free-form message back to the page (if a page is reachable). |
| 116 | +void _post(Object? payload) { |
| 117 | + WorkmanagerExecution.instance.sendToPage?.call(payload); |
| 118 | +} |
| 119 | + |
| 120 | +/// Deterministic, time-varying simulated temperature: stable within a 30s |
| 121 | +/// bucket so consecutive ticks change, but the demo never needs the network. |
| 122 | +double _simulatedTemp(String city) { |
| 123 | + final base = _baseTemps[city] ?? 15.0; |
| 124 | + final bucket = DateTime.now().millisecondsSinceEpoch ~/ 30000; |
| 125 | + final hash = _hash('$city:$bucket'); |
| 126 | + final wiggle = (hash % 1000) / 1000 * 0.10 - 0.05; // ±5% |
| 127 | + return base * (1 + wiggle); |
| 128 | +} |
| 129 | + |
| 130 | +int _hash(String input) { |
| 131 | + var hash = 0; |
| 132 | + for (final codeUnit in input.codeUnits) { |
| 133 | + hash = (hash * 31 + codeUnit) & 0x7fffffff; |
| 134 | + } |
| 135 | + return hash; |
18 | 136 | } |
19 | 137 |
|
20 | 138 | /// Pure-Dart background task handler. |
21 | 139 | /// |
22 | | -/// The result is recorded by the runtime: when the page is open it appears in |
23 | | -/// the status panel immediately; when the Service Worker ran the task while |
24 | | -/// the page was closed, it is replayed on the next page load. |
| 140 | +/// With `inputData['city']` it behaves like a background "temperature |
| 141 | +/// check": it pushes progress messages to the page while running and returns |
| 142 | +/// the temperature + alert state as the task result. The result is recorded |
| 143 | +/// by the runtime: when the page is open it appears in the event log |
| 144 | +/// immediately; when the Service Worker ran the task while the page was |
| 145 | +/// closed, it is replayed on the next page load. |
25 | 146 | Future<bool> handleWebBackgroundTask( |
26 | 147 | String taskName, |
27 | 148 | Map<String, dynamic>? inputData, |
28 | 149 | ) async { |
| 150 | + final input = inputData; |
| 151 | + if (input != null && input['fail'] == true) { |
| 152 | + return false; |
| 153 | + } |
| 154 | + final city = (input?['city'] as String?)?.toLowerCase() ?? 'cardiff'; |
| 155 | + final threshold = (input?['threshold'] as num?)?.toDouble(); |
| 156 | + |
29 | 157 | // A small CPU loop so the Web Worker's parallel execution is observable: |
30 | 158 | // the UI stays responsive while this runs off the main thread. |
31 | 159 | // ignore: unused_local_variable |
32 | 160 | var checksum = 0; |
33 | 161 | for (var i = 0; i < 2000000; i++) { |
34 | 162 | checksum += i; |
35 | 163 | } |
36 | | - final input = inputData; |
37 | | - return input != null && input['fail'] == true ? false : true; |
| 164 | + |
| 165 | + _post(<String, Object?>{ |
| 166 | + 'kind': 'task-start', |
| 167 | + 'city': city, |
| 168 | + 'threshold': threshold, |
| 169 | + }); |
| 170 | + final tempC = _simulatedTemp(city); |
| 171 | + final below = threshold != null && tempC < threshold; |
| 172 | + _post(<String, Object?>{ |
| 173 | + 'kind': 'task-done', |
| 174 | + 'city': city, |
| 175 | + 'tempC': tempC, |
| 176 | + 'below': below, |
| 177 | + }); |
| 178 | + _notify( |
| 179 | + below |
| 180 | + ? '❄️ $city below the alert threshold' |
| 181 | + : 'Temperature check: ${_capitalize(city)}', |
| 182 | + '${tempC.toStringAsFixed(1)}°C — ' |
| 183 | + '${below ? 'below the alert threshold' : 'all good'}', |
| 184 | + ); |
| 185 | + // The task result itself stays a plain success/failure bool; the |
| 186 | + // temperature detail is delivered via the chat messages above. |
| 187 | + return true; |
| 188 | +} |
| 189 | + |
| 190 | +/// Shows a browser notification when a background task finishes inside the |
| 191 | +/// Service Worker (i.e. while the page is closed). The dedicated Web Worker |
| 192 | +/// has no `registration`, so there the page shows the notification instead. |
| 193 | +void _notify(String title, String body) { |
| 194 | + showServiceWorkerNotification(title, body); |
| 195 | +} |
| 196 | + |
| 197 | +String _capitalize(String input) { |
| 198 | + if (input.isEmpty) { |
| 199 | + return input; |
| 200 | + } |
| 201 | + return input[0].toUpperCase() + input.substring(1); |
38 | 202 | } |
0 commit comments