| // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| library pub.load_transformers; |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:isolate'; |
| |
| import 'package:barback/barback.dart'; |
| import 'package:source_maps/source_maps.dart'; |
| |
| import '../barback.dart'; |
| import '../dart.dart' as dart; |
| import '../log.dart' as log; |
| import '../utils.dart'; |
| import 'server.dart'; |
| |
| /// A Dart script to run in an isolate. |
| /// |
| /// This script serializes one or more transformers defined in a Dart library |
| /// and marshals calls to and from them with the host isolate. |
| const _TRANSFORMER_ISOLATE = """ |
| import 'dart:async'; |
| import 'dart:isolate'; |
| import 'dart:convert'; |
| import 'dart:mirrors'; |
| |
| import 'http://<<HOST_AND_PORT>>/packages/source_maps/span.dart'; |
| import 'http://<<HOST_AND_PORT>>/packages/stack_trace/stack_trace.dart'; |
| import 'http://<<HOST_AND_PORT>>/packages/barback/barback.dart'; |
| |
| /// Sets up the initial communication with the host isolate. |
| void main(_, SendPort replyTo) { |
| var port = new ReceivePort(); |
| replyTo.send(port.sendPort); |
| port.first.then((wrappedMessage) { |
| _respond(wrappedMessage, (message) { |
| var library = Uri.parse(message['library']); |
| var configuration = JSON.decode(message['configuration']); |
| return initialize(library, configuration). |
| map(_serializeTransformerOrGroup).toList(); |
| }); |
| }); |
| } |
| |
| /// Loads all the transformers and groups defined in [uri]. |
| /// |
| /// Loads the library, finds any Transformer or TransformerGroup subclasses in |
| /// it, instantiates them (with [configuration] if it's non-null), and returns |
| /// them. |
| Iterable initialize(Uri uri, Map configuration) { |
| var mirrors = currentMirrorSystem(); |
| var transformerClass = reflectClass(Transformer); |
| var groupClass = reflectClass(TransformerGroup); |
| |
| // TODO(nweiz): if no valid transformers are found, throw an error message |
| // describing candidates and why they were rejected. |
| return mirrors.libraries[uri].classes.values.map((classMirror) { |
| if (classMirror.isPrivate) return null; |
| if (isAbstract(classMirror)) return null; |
| if (!classIsA(classMirror, transformerClass) && |
| !classIsA(classMirror, groupClass)) { |
| return null; |
| } |
| |
| var constructor = getConstructor(classMirror, 'asPlugin'); |
| if (constructor == null) return null; |
| if (constructor.parameters.isEmpty) { |
| if (configuration != null) return null; |
| return classMirror.newInstance(const Symbol('asPlugin'), []).reflectee; |
| } |
| if (constructor.parameters.length != 1) return null; |
| |
| // If the constructor expects configuration and none was passed, it defaults |
| // to an empty map. |
| if (configuration == null) configuration = {}; |
| |
| // TODO(nweiz): if the constructor accepts named parameters, automatically |
| // destructure the configuration map. |
| return classMirror.newInstance(const Symbol('asPlugin'), [configuration]) |
| .reflectee; |
| }).where((classMirror) => classMirror != null); |
| } |
| |
| /// A wrapper for a [Transform] that's in the host isolate. |
| /// |
| /// This retrieves inputs from and sends outputs and logs to the host isolate. |
| class ForeignTransform implements Transform { |
| /// The port with which we communicate with the host isolate. |
| /// |
| /// This port and all messages sent across it are specific to this transform. |
| final SendPort _port; |
| |
| final Asset primaryInput; |
| |
| TransformLogger get logger => _logger; |
| TransformLogger _logger; |
| |
| /// Creates a transform from a serializable map sent from the host isolate. |
| ForeignTransform(Map transform) |
| : _port = transform['port'], |
| primaryInput = _deserializeAsset(transform['primaryInput']) { |
| _logger = new TransformLogger((assetId, level, message, span) { |
| _call(_port, { |
| 'type': 'log', |
| 'level': level.name, |
| 'message': message, |
| 'assetId': assetId == null ? null : _serializeId(assetId), |
| 'span': span == null ? null : _serializeSpan(span) |
| }); |
| }); |
| } |
| |
| Future<Asset> getInput(AssetId id) { |
| return _call(_port, { |
| 'type': 'getInput', |
| 'id': _serializeId(id) |
| }).then(_deserializeAsset); |
| } |
| |
| Future<String> readInputAsString(AssetId id, {Encoding encoding}) { |
| if (encoding == null) encoding = UTF8; |
| return getInput(id).then((input) => input.readAsString(encoding: encoding)); |
| } |
| |
| Stream<List<int>> readInput(AssetId id) => |
| _futureStream(getInput(id).then((input) => input.read())); |
| |
| void addOutput(Asset output) { |
| _call(_port, { |
| 'type': 'addOutput', |
| 'output': _serializeAsset(output) |
| }); |
| } |
| } |
| |
| /// Returns the mirror for the root Object type. |
| ClassMirror get objectMirror { |
| if (_objectMirror == null) { |
| _objectMirror = currentMirrorSystem() |
| .libraries[Uri.parse('dart:core')] |
| .classes[const Symbol('Object')]; |
| } |
| return _objectMirror; |
| } |
| ClassMirror _objectMirror; |
| |
| // TODO(nweiz): clean this up when issue 13248 is fixed. |
| MethodMirror getConstructor(ClassMirror classMirror, String constructor) { |
| var name = new Symbol("\${MirrorSystem.getName(classMirror.simpleName)}" |
| ".\$constructor"); |
| return classMirror.constructors[name]; |
| } |
| |
| // TODO(nweiz): get rid of this when issue 12439 is fixed. |
| /// Returns whether or not [mirror] is a subtype of [superclass]. |
| /// |
| /// This includes [superclass] being mixed in to or implemented by [mirror]. |
| bool classIsA(ClassMirror mirror, ClassMirror superclass) { |
| if (mirror == superclass) return true; |
| if (mirror == objectMirror) return false; |
| return classIsA(mirror.superclass, superclass) || |
| mirror.superinterfaces.any((int) => classIsA(int, superclass)); |
| } |
| |
| // TODO(nweiz): get rid of this when issue 12826 is fixed. |
| /// Returns whether or not [mirror] is an abstract class. |
| bool isAbstract(ClassMirror mirror) => mirror.members.values |
| .any((member) => member is MethodMirror && member.isAbstract); |
| |
| /// Converts [transformerOrGroup] into a serializable map. |
| Map _serializeTransformerOrGroup(transformerOrGroup) { |
| if (transformerOrGroup is Transformer) { |
| return _serializeTransformer(transformerOrGroup); |
| } else { |
| assert(transformerOrGroup is TransformerGroup); |
| return _serializeTransformerGroup(transformerOrGroup); |
| } |
| } |
| |
| /// Converts [transformer] into a serializable map. |
| Map _serializeTransformer(Transformer transformer) { |
| var port = new ReceivePort(); |
| port.listen((wrappedMessage) { |
| _respond(wrappedMessage, (message) { |
| if (message['type'] == 'isPrimary') { |
| return transformer.isPrimary(_deserializeAsset(message['asset'])); |
| } else { |
| assert(message['type'] == 'apply'); |
| return transformer.apply( |
| new ForeignTransform(message['transform'])); |
| } |
| }); |
| }); |
| |
| return { |
| 'type': 'Transformer', |
| 'toString': transformer.toString(), |
| 'port': port.sendPort |
| }; |
| } |
| |
| // Converts [group] into a serializable map. |
| Map _serializeTransformerGroup(TransformerGroup group) { |
| return { |
| 'type': 'TransformerGroup', |
| 'toString': group.toString(), |
| 'phases': group.phases.map((phase) { |
| return phase.map(_serializeTransformerOrGroup).toList(); |
| }).toList() |
| }; |
| } |
| |
| /// Converts a serializable map into an [Asset]. |
| Asset _deserializeAsset(Map asset) { |
| return new Asset.fromStream( |
| _deserializeId(asset['id']), |
| _deserializeStream(asset['stream'])); |
| } |
| |
| /// The body of a [StreamTransformer] that deserializes the values in a stream |
| /// sent by [_serializeStream]. |
| StreamSubscription _deserializeTransformer(Stream input, bool cancelOnError) { |
| var subscription; |
| var transformed = input.transform(new StreamTransformer.fromHandlers( |
| handleData: (data, sink) { |
| if (data['type'] == 'data') { |
| sink.add(data['data']); |
| } else if (data['type'] == 'error') { |
| sink.addError(CrossIsolateException.deserialize(data['error'])); |
| } else { |
| assert(data['type'] == 'done'); |
| sink.close(); |
| subscription.cancel(); |
| } |
| })); |
| subscription = transformed.listen(null, cancelOnError: cancelOnError); |
| return subscription; |
| } |
| |
| /// Convert a [SendPort] whose opposite is waiting to send us a stream into a |
| /// [Stream]. |
| /// |
| /// No stream data will actually be sent across the isolate boundary until |
| /// someone subscribes to the returned stream. |
| Stream _deserializeStream(SendPort sendPort) { |
| return callbackStream(() { |
| var receivePort = new ReceivePort(); |
| sendPort.send(receivePort.sendPort); |
| return receivePort.transform( |
| const StreamTransformer(_deserializeTransformer)); |
| }); |
| } |
| |
| /// Converts a serializable map into an [AssetId]. |
| AssetId _deserializeId(Map id) => new AssetId(id['package'], id['path']); |
| |
| /// Converts [asset] into a serializable map. |
| Map _serializeAsset(Asset asset) { |
| return { |
| 'id': _serializeId(asset.id), |
| 'stream': _serializeStream(asset.read()) |
| }; |
| } |
| |
| /// Converts [stream] into a [SendPort] with which another isolate can request |
| /// the data from [stream]. |
| SendPort _serializeStream(Stream stream) { |
| var receivePort = new ReceivePort(); |
| receivePort.first.then((sendPort) { |
| stream.listen((data) => sendPort.send({'type': 'data', 'data': data}), |
| onDone: () => sendPort.send({'type': 'done'}), |
| onError: (error, stackTrace) { |
| sendPort.send({ |
| 'type': 'error', |
| 'error': CrossIsolateException.serialize(error, stackTrace) |
| }); |
| }); |
| }); |
| |
| return receivePort.sendPort; |
| } |
| |
| /// Converts [id] into a serializable map. |
| Map _serializeId(AssetId id) => {'package': id.package, 'path': id.path}; |
| |
| /// Converts [span] into a serializable map. |
| Map _serializeSpan(Span span) { |
| // TODO(nweiz): convert FileSpans to FileSpans. |
| return { |
| 'type': 'fixed', |
| 'sourceUrl': span.sourceUrl, |
| 'start': _serializeLocation(span.start), |
| 'text': span.text, |
| 'isIdentifier': span.isIdentifier |
| }; |
| } |
| |
| /// Converts [location] into a serializable map. |
| Map _serializeLocation(Location location) { |
| // TODO(nweiz): convert FileLocations to FileLocations. |
| return { |
| 'type': 'fixed', |
| 'sourceUrl': location.sourceUrl, |
| 'offset': location.offset, |
| 'line': location.line, |
| 'column': location.column |
| }; |
| } |
| |
| /// Responds to a message sent by [_call]. |
| /// |
| /// [wrappedMessage] is the raw message sent by [_call]. This unwraps it and |
| /// passes the contents of the message to [callback], then sends the return |
| /// value of [callback] back to [_call]. If [callback] returns a Future or |
| /// throws an error, that will also be sent. |
| void _respond(wrappedMessage, callback(message)) { |
| var replyTo = wrappedMessage['replyTo']; |
| new Future.sync(() => callback(wrappedMessage['message'])) |
| .then((result) => replyTo.send({'type': 'success', 'value': result})) |
| .catchError((error, stackTrace) { |
| // TODO(nweiz): at least MissingInputException should be preserved here. |
| replyTo.send({ |
| 'type': 'error', |
| 'error': CrossIsolateException.serialize(error, stackTrace) |
| }); |
| }); |
| } |
| |
| /// Wraps [message] and sends it across [port], then waits for a response which |
| /// should be sent using [_respond]. |
| /// |
| /// The returned Future will complete to the value or error returned by |
| /// [_respond]. |
| Future _call(SendPort port, message) { |
| var receivePort = new ReceivePort(); |
| port.send({ |
| 'message': message, |
| 'replyTo': receivePort.sendPort |
| }); |
| |
| return receivePort.first.then((response) { |
| if (response['type'] == 'success') return response['value']; |
| assert(response['type'] == 'error'); |
| return new Future.error( |
| new CrossIsolateException.deserialize(response['error'])); |
| }); |
| } |
| |
| /// An exception that was originally raised in another isolate. |
| /// |
| /// Exception objects can't cross isolate boundaries in general, so this class |
| /// wraps as much information as can be consistently serialized. |
| class CrossIsolateException implements Exception { |
| /// The name of the type of exception thrown. |
| /// |
| /// This is the return value of [error.runtimeType.toString()]. Keep in mind |
| /// that objects in different libraries may have the same type name. |
| final String type; |
| |
| /// The exception's message, or its [toString] if it didn't expose a `message` |
| /// property. |
| final String message; |
| |
| /// The exception's stack trace, or `null` if no stack trace was available. |
| final Trace stackTrace; |
| |
| /// Loads a [CrossIsolateException] from a map. |
| /// |
| /// [error] should be the result of [CrossIsolateException.serialize]. |
| CrossIsolateException.deserialize(Map error) |
| : type = error['type'], |
| message = error['message'], |
| stackTrace = error['stack'] == null ? null : |
| new Trace.parse(error['stack']); |
| |
| /// Serializes [error] to a map that can safely be passed across isolate |
| /// boundaries. |
| static Map serialize(error, [StackTrace stack]) { |
| if (stack == null) stack = getAttachedStackTrace(error); |
| return { |
| 'type': error.runtimeType.toString(), |
| 'message': getErrorMessage(error), |
| 'stack': stack == null ? null : stack.toString() |
| }; |
| } |
| |
| String toString() => "\$message\\n\$stackTrace"; |
| } |
| |
| // Get a string description of an exception. |
| // |
| // Most exception types have a "message" property. We prefer this since |
| // it skips the "Exception:", "HttpException:", etc. prefix that calling |
| // toString() adds. But, alas, "message" isn't actually defined in the |
| // base Exception type so there's no easy way to know if it's available |
| // short of a giant pile of type tests for each known exception type. |
| // |
| // So just try it. If it throws, default to toString(). |
| String getErrorMessage(error) { |
| try { |
| return error.message; |
| } on NoSuchMethodError catch (_) { |
| return error.toString(); |
| } |
| } |
| |
| /// Returns a buffered stream that will emit the same values as the stream |
| /// returned by [future] once [future] completes. If [future] completes to an |
| /// error, the return value will emit that error and then close. |
| Stream _futureStream(Future<Stream> future) { |
| var controller = new StreamController(sync: true); |
| future.then((stream) { |
| stream.listen( |
| controller.add, |
| onError: controller.addError, |
| onDone: controller.close); |
| }).catchError((e, stackTrace) { |
| controller.addError(e, stackTrace); |
| controller.close(); |
| }); |
| return controller.stream; |
| } |
| |
| Stream callbackStream(Stream callback()) { |
| var subscription; |
| var controller; |
| controller = new StreamController(onListen: () { |
| subscription = callback().listen(controller.add, |
| onError: controller.addError, |
| onDone: controller.close); |
| }, |
| onCancel: () => subscription.cancel(), |
| onPause: () => subscription.pause(), |
| onResume: () => subscription.resume(), |
| sync: true); |
| return controller.stream; |
| } |
| """; |
| |
| /// Load and return all transformers and groups from the library identified by |
| /// [id]. |
| /// |
| /// [server] is used to serve any Dart files needed to load the transformers. |
| Future<Set> loadTransformers(BarbackServer server, TransformerId id) { |
| return id.getAssetId(server.barback).then((assetId) { |
| var path = assetId.path.replaceFirst('lib/', ''); |
| // TODO(nweiz): load from a "package:" URI when issue 12474 is fixed. |
| var hostAndPort = '${server.address.address}:${server.port}'; |
| var uri = 'http://$hostAndPort/packages/${id.package}/$path'; |
| var code = 'import "$uri";' + |
| _TRANSFORMER_ISOLATE.replaceAll('<<HOST_AND_PORT>>', hostAndPort); |
| log.fine("Loading transformers from $assetId"); |
| |
| var port = new ReceivePort(); |
| return dart.runInIsolate(code, port.sendPort) |
| .then((_) => port.first) |
| .then((sendPort) { |
| return _call(sendPort, { |
| 'library': uri, |
| // TODO(nweiz): support non-JSON-encodable configuration maps. |
| 'configuration': JSON.encode(id.configuration) |
| }).then((transformers) { |
| transformers = transformers.map(_deserializeTransformerOrGroup).toSet(); |
| log.fine("Transformers from $assetId: $transformers"); |
| return transformers; |
| }); |
| }).catchError((error) { |
| if (error is! dart.CrossIsolateException) throw error; |
| if (error.type != 'IsolateSpawnException') throw error; |
| // TODO(nweiz): don't parse this as a string once issues 12617 and 12689 |
| // are fixed. |
| if (!error.message.split('\n')[1].startsWith('import "$uri";')) { |
| throw error; |
| } |
| |
| // If there was an IsolateSpawnException and the import that actually |
| // failed was the one we were loading transformers from, throw an |
| // application exception with a more user-friendly message. |
| fail('Transformer library "package:${id.package}/$path" not ' |
| 'found.'); |
| }); |
| }); |
| } |
| |
| /// A wrapper for a transformer that's in a different isolate. |
| class _ForeignTransformer extends Transformer { |
| /// The port with which we communicate with the child isolate. |
| /// |
| /// This port and all messages sent across it are specific to this |
| /// transformer. |
| final SendPort _port; |
| |
| /// The result of calling [toString] on the transformer in the isolate. |
| final String _toString; |
| |
| _ForeignTransformer(Map map) |
| : _port = map['port'], |
| _toString = map['toString']; |
| |
| Future<bool> isPrimary(Asset asset) { |
| return _call(_port, { |
| 'type': 'isPrimary', |
| 'asset': _serializeAsset(asset) |
| }); |
| } |
| |
| Future apply(Transform transform) { |
| return _call(_port, { |
| 'type': 'apply', |
| 'transform': _serializeTransform(transform) |
| }); |
| } |
| |
| String toString() => _toString; |
| } |
| |
| /// A wrapper for a transformer group that's in a different isolate. |
| class _ForeignGroup implements TransformerGroup { |
| final Iterable<Iterable> phases; |
| |
| /// The result of calling [toString] on the transformer group in the isolate. |
| final String _toString; |
| |
| _ForeignGroup(Map map) |
| : phases = map['phases'].map((phase) { |
| return phase.map(_deserializeTransformerOrGroup).toList(); |
| }).toList(), |
| _toString = map['toString']; |
| |
| String toString() => _toString; |
| } |
| |
| /// Converts a serializable map into a [Transformer] or a [TransformerGroup]. |
| _deserializeTransformerOrGroup(Map map) { |
| if (map['type'] == 'Transformer') return new _ForeignTransformer(map); |
| assert(map['type'] == 'TransformerGroup'); |
| return new _ForeignGroup(map); |
| } |
| |
| /// Converts [transform] into a serializable map. |
| Map _serializeTransform(Transform transform) { |
| var receivePort = new ReceivePort(); |
| receivePort.listen((wrappedMessage) { |
| _respond(wrappedMessage, (message) { |
| if (message['type'] == 'getInput') { |
| return transform.getInput(_deserializeId(message['id'])) |
| .then(_serializeAsset); |
| } |
| |
| if (message['type'] == 'addOutput') { |
| transform.addOutput(_deserializeAsset(message['output'])); |
| return; |
| } |
| |
| assert(message['type'] == 'log'); |
| var method; |
| if (message['level'] == 'Info') { |
| method = transform.logger.info; |
| } else if (message['level'] == 'Warning') { |
| method = transform.logger.warning; |
| } else { |
| assert(message['level'] == 'Error'); |
| method = transform.logger.error; |
| } |
| |
| var assetId = message['assetId'] == null ? null : |
| _deserializeId(message['assetId']); |
| var span = message['span'] == null ? null : |
| _deserializeSpan(message['span']); |
| method(message['message'], asset: assetId, span: span); |
| }); |
| }); |
| |
| return { |
| 'port': receivePort.sendPort, |
| 'primaryInput': _serializeAsset(transform.primaryInput) |
| }; |
| } |
| |
| /// Converts a serializable map into an [Asset]. |
| Asset _deserializeAsset(Map asset) { |
| return new Asset.fromStream( |
| _deserializeId(asset['id']), |
| _deserializeStream(asset['stream'])); |
| } |
| |
| /// A transformer that deserializes the values in a stream sent by |
| /// [_serializeStream]. |
| StreamSubscription _deserializeTransformer(Stream input, bool cancelOnError) { |
| var subscription; |
| var transformed = input.transform(new StreamTransformer.fromHandlers( |
| handleData: (data, sink) { |
| if (data['type'] == 'data') { |
| sink.add(data['data']); |
| } else if (data['type'] == 'error') { |
| sink.addError(CrossIsolateException.deserialize(data['error'])); |
| } else { |
| assert(data['type'] == 'done'); |
| sink.close(); |
| subscription.cancel(); |
| } |
| })); |
| subscription = transformed.listen(null, cancelOnError: cancelOnError); |
| return subscription; |
| } |
| |
| /// Convert a [SendPort] whose opposite is waiting to send us a stream into a |
| /// [Stream]. |
| /// |
| /// No stream data will actually be sent across the isolate boundary until |
| /// someone subscribes to the returned stream. |
| Stream _deserializeStream(SendPort sendPort) { |
| return callbackStream(() { |
| var receivePort = new ReceivePort(); |
| sendPort.send(receivePort.sendPort); |
| return receivePort.transform( |
| const StreamTransformer(_deserializeTransformer)); |
| }); |
| } |
| |
| /// Converts a serializable map into an [AssetId]. |
| AssetId _deserializeId(Map id) => new AssetId(id['package'], id['path']); |
| |
| /// Converts a serializable map into a [Span]. |
| Span _deserializeSpan(Map span) { |
| assert(span['type'] == 'fixed'); |
| var location = _deserializeLocation(span['start']); |
| return new FixedSpan(span['sourceUrl'], location.offset, location.line, |
| location.column, text: span['text'], isIdentifier: span['isIdentifier']); |
| } |
| |
| /// Converts a serializable map into a [Location]. |
| Location _deserializeLocation(Map location) { |
| assert(location['type'] == 'fixed'); |
| return new FixedLocation(location['offset'], location['sourceUrl'], |
| location['line'], location['column']); |
| } |
| |
| // TODO(nweiz): add custom serialization code for assets that can be more |
| // efficiently serialized. |
| /// Converts [asset] into a serializable map. |
| Map _serializeAsset(Asset asset) { |
| return { |
| 'id': _serializeId(asset.id), |
| 'stream': _serializeStream(asset.read()) |
| }; |
| } |
| |
| /// Converts [stream] into a [SendPort] with which another isolate can request |
| /// the data from [stream]. |
| SendPort _serializeStream(Stream stream) { |
| var receivePort = new ReceivePort(); |
| receivePort.first.then((sendPort) { |
| stream.listen((data) => sendPort.send({'type': 'data', 'data': data}), |
| onDone: () => sendPort.send({'type': 'done'}), |
| onError: (error, stackTrace) { |
| sendPort.send({ |
| 'type': 'error', |
| 'error': CrossIsolateException.serialize(error, stackTrace) |
| }); |
| }); |
| }); |
| |
| return receivePort.sendPort; |
| } |
| |
| /// Converts [id] into a serializable map. |
| Map _serializeId(AssetId id) => {'package': id.package, 'path': id.path}; |
| |
| /// Responds to a message sent by [_call]. |
| /// |
| /// [wrappedMessage] is the raw message sent by [_call]. This unwraps it and |
| /// passes the contents of the message to [callback], then sends the return |
| /// value of [callback] back to [_call]. If [callback] returns a Future or |
| /// throws an error, that will also be sent. |
| void _respond(wrappedMessage, callback(message)) { |
| var replyTo = wrappedMessage['replyTo']; |
| new Future.sync(() => callback(wrappedMessage['message'])) |
| .then((result) => replyTo.send({'type': 'success', 'value': result})) |
| .catchError((error, stackTrace) { |
| // TODO(nweiz): at least MissingInputException should be preserved here. |
| replyTo.send({ |
| 'type': 'error', |
| 'error': dart.CrossIsolateException.serialize(error, stackTrace) |
| }); |
| }); |
| } |
| |
| /// Wraps [message] and sends it across [port], then waits for a response which |
| /// should be sent using [_respond]. |
| /// |
| /// The returned Future will complete to the value or error returned by |
| /// [_respond]. |
| Future _call(SendPort port, message) { |
| var receivePort = new ReceivePort(); |
| port.send({ |
| 'message': message, |
| 'replyTo': receivePort.sendPort |
| }); |
| |
| return receivePort.first.then((response) { |
| if (response['type'] == 'success') return response['value']; |
| assert(response['type'] == 'error'); |
| return new Future.error( |
| new dart.CrossIsolateException.deserialize(response['error'])); |
| }); |
| } |