| 1 | | | import 'dart:async'; |
| 2 | | | |
| 3 | | | import 'package:logger/web.dart'; |
| 4 | | | import 'package:meta/meta.dart'; |
| 5 | | | |
| 6 | | | import '../_impl/xplat/_time_stamp.dart'; |
| 7 | | | import '../channel.dart'; |
| 8 | | | import '../exceptions/squadron_error.dart'; |
| 9 | | | import '../exceptions/squadron_exception.dart'; |
| 10 | | | import 'worker_message.dart'; |
| 11 | | | |
| 12 | | | /// [WorkerResponse]s are used to communicate from [Worker]s to clients and |
| 13 | | | /// carry a single piece of data. [Future]-based services simply return a |
| 14 | | | /// single [WorkerResponse] with the result. [Stream]ing services will return |
| 15 | | | /// one [WorkerResponse]s for each stream item and mmust send a |
| 16 | | | /// [WorkerResponse.closeStream] message to indicate completion. |
| 17 | | | /// [WorkerResponse]s can also send error messages and log events. |
| 18 | | | extension type WorkerResponse._(List data) implements WorkerMessage { |
| 19 | | | /// [WorkerResponse] with a valid [result]. If [result] is an [Iterable] but |
| 20 | | | /// not a [List], it will be converted to a [List] by [wrapInPlace]. |
| 21 | | 0 | static WorkerResponse ready([bool status = true]) => WorkerResponse._([ |
| 22 | | 0 | microsecTimeStamp(), // 0 - travel time |
| 23 | | | status, // 1 - ready |
| 24 | | | null, // 2 - error |
| 25 | | | null, // 3 - end of stream |
| 26 | | | null, // 4 - log message |
| 27 | | | ]); |
| 28 | | | |
| 29 | | | /// [WorkerResponse] with a valid [result]. If [result] is an [Iterable] but |
| 30 | | | /// not a [List], it will be converted to a [List] by [wrapInPlace]. |
| 31 | | 30 | static WorkerResponse withResult(dynamic result) => WorkerResponse._([ |
| 32 | | 10 | microsecTimeStamp(), // 0 - travel time |
| 33 | | | result, // 1 - result |
| 34 | | | null, // 2 - error |
| 35 | | | null, // 3 - end of stream |
| 36 | | | null, // 4 - log message |
| 37 | | | ]); |
| 38 | | | |
| 39 | | | /// [WorkerResponse] with an error message and an optional (string) [StackTrace]. |
| 40 | | 5 | static WorkerResponse withError(SquadronException exception, |
| 41 | | | [StackTrace? stackTrace]) => |
| 42 | | 10 | WorkerResponse._([ |
| 43 | | 5 | microsecTimeStamp(), // 0 - travel time |
| 44 | | | null, // 1 - result |
| 45 | | | exception, // 2 - error |
| 46 | | | null, // 3 - end of stream |
| 47 | | | null, // 4 - log message |
| 48 | | | ]); |
| 49 | | | |
| 50 | | | /// [WorkerResponse] with log event information. |
| 51 | | 30 | static WorkerResponse log(LogEvent message) => WorkerResponse._([ |
| 52 | | 10 | microsecTimeStamp(), // 0 - travel time |
| 53 | | | null, // 1 - result |
| 54 | | | null, // 2 - error |
| 55 | | | null, // 3 - end of stream |
| 56 | | 10 | message.serialize(), // 4 - log message |
| 57 | | | ]); |
| 58 | | | |
| 59 | | | /// Special [WorkerResponse] message to indicate the end of a stream. |
| 60 | | 18 | static WorkerResponse closeStream() => WorkerResponse._([ |
| 61 | | 6 | microsecTimeStamp(), // 0 - travel time |
| 62 | | | null, // 1 - result |
| 63 | | | null, // 2 - error |
| 64 | | | true, // 3 - end of stream |
| 65 | | | null, // 4 - log message |
| 66 | | | ]); |
| 67 | | | |
| 68 | | | /// Flag indicating the end of the [Stream]ing operation. |
| 69 | | 20 | bool get endOfStream => data[_$endOfStream]; |
| 70 | | | |
| 71 | | | /// The [WorkerResponse] exception, if any. |
| 72 | | 20 | SquadronException? get error => data[_$error]; |
| 73 | | | |
| 74 | | | /// Retrieves the result associated to this [WorkerResponse]. If the |
| 75 | | | /// [WorkerResponse] contains an error, an the [error] exception is thrown. |
| 76 | | 10 | dynamic get result { |
| 77 | | 10 | final err = error; |
| 78 | | | if (err != null) { |
| 79 | | | throw err; |
| 80 | | | } else { |
| 81 | | 10 | return data[_$result]; |
| 82 | | | } |
| 83 | | | } |
| 84 | | | } |
| 85 | | | |
| 86 | | | // 0 is reserved for travel time |
| 87 | | | const _$result = 1; |
| 88 | | | const _$error = 2; |
| 89 | | | const _$endOfStream = 3; |
| 90 | | | const _$log = 4; |
| 91 | | | |
| 92 | | | @internal |
| 93 | | | extension WorkerResponseImpl on WorkerResponse { |
| 94 | | | /// In-place deserialization of a [WorkerResponse] sent by the worker. |
| 95 | | | /// Returns `false` if the message requires no further processing (currently |
| 96 | | | /// used for log messages only). |
| 97 | | 10 | bool unwrapInPlace(Channel channel) { |
| 98 | | 10 | unwrapTravelTime(); |
| 99 | | 20 | final log = _LogEventSerializationExt.deserialize(data[_$log]); |
| 100 | | | if (log != null) { |
| 101 | | 19 | channel.logger?.log(log.level, log.message, |
| 102 | | 9 | time: log.time, error: log.error, stackTrace: log.stackTrace); |
| 103 | | | return false; |
| 104 | | | } else { |
| 105 | | 40 | data[_$error] = channel.exceptionManager.deserialize(data[_$error]); |
| 106 | | 10 | data[_$endOfStream] ??= false; |
| 107 | | | return true; |
| 108 | | | } |
| 109 | | | } |
| 110 | | | |
| 111 | | | /// In-place serialization of a [WorkerResponse]. |
| 112 | | 10 | List wrapInPlace() { |
| 113 | | 10 | final result = data[_$result]; |
| 114 | | 13 | if (result is Iterable && result is! List) { |
| 115 | | 2 | data[_$result] = result.toList(); |
| 116 | | | } |
| 117 | | 25 | data[_$error] = (data[_$error] as SquadronException?)?.serialize(); |
| 118 | | | return data; |
| 119 | | | } |
| 120 | | | |
| 121 | | 10 | static WorkerResponse from(List data) { |
| 122 | | 20 | if (data.length != 5) { |
| 123 | | 0 | throw SquadronErrorImpl.create('Invalid worker response'); |
| 124 | | | } |
| 125 | | 10 | return WorkerResponse._(data); |
| 126 | | | } |
| 127 | | | } |
| 128 | | | |
| 129 | | | extension _LogEventSerializationExt on LogEvent { |
| 130 | | 20 | List serialize() => [ |
| 131 | | 20 | level.value, |
| 132 | | 20 | _stringify(message), |
| 133 | | 20 | microsecTimeStamp(time), |
| 134 | | 10 | error?.toString(), |
| 135 | | 10 | stackTrace?.toString(), |
| 136 | | | ]; |
| 137 | | | |
| 138 | | 10 | static LogEvent? deserialize(List? props) => (props == null) |
| 139 | | | ? null |
| 140 | | 10 | : LogEvent( |
| 141 | | 30 | _getLevel((props[0] as num?)?.toInt()), |
| 142 | | 10 | props[1], |
| 143 | | 30 | time: fromMicrosecTimeStamp((props[2] as num?)?.toInt()), |
| 144 | | 10 | error: props[3], |
| 145 | | 20 | stackTrace: SquadronException.loadStackTrace(props[4]), |
| 146 | | | ); |
| 147 | | | |
| 148 | | 10 | static Level _getLevel(int? value) { |
| 149 | | | if (value == null) return Level.debug; |
| 150 | | 50 | return Level.values.where((l) => l.value == value).first; |
| 151 | | | } |
| 152 | | | |
| 153 | | 10 | static String? _stringify(dynamic message) { |
| 154 | | 10 | if (message is Function) { |
| 155 | | | try { |
| 156 | | 4 | return message().toString(); |
| 157 | | | } catch (ex) { |
| 158 | | 0 | return 'Deferred message failed with error: $ex'; |
| 159 | | | } |
| 160 | | | } else { |
| 161 | | 10 | return message.toString(); |
| 162 | | | } |
| 163 | | | } |
| 164 | | | } |