Use synchronous file writing in BlobStreamReference (#24)

Doing so allows us to avoid dealing in futures all the way back
to `encode()`. This becomes important when implementing replay
since replay uses `noSuchMethod`, which can't await futures.

This also extracts out a few constants in preparation for their
use in replay.

Part of #11
diff --git a/lib/src/backends/record_replay/common.dart b/lib/src/backends/record_replay/common.dart
index 9ba9d1c..afd95e0 100644
--- a/lib/src/backends/record_replay/common.dart
+++ b/lib/src/backends/record_replay/common.dart
@@ -2,12 +2,64 @@
 // 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.
 
+import 'events.dart';
+
 /// Encoded value of the file system in a recording.
 const String kFileSystemEncodedValue = '__fs__';
 
 /// The name of the recording manifest file.
 const String kManifestName = 'MANIFEST.txt';
 
+/// The key in a serialized [InvocationEvent] map that is used to store the
+/// type of invocation.
+///
+/// See also:
+///   - [kGetType]
+///   - [kSetType]
+///   - [kInvokeType]
+const String kManifestTypeKey = 'type';
+
+/// The key in a serialized [InvocationEvent] map that is used to store the
+/// target of the invocation.
+const String kManifestObjectKey = 'object';
+
+/// The key in a serialized [InvocationEvent] map that is used to store the
+/// result (return value) of the invocation.
+const String kManifestResultKey = 'result';
+
+/// The key in a serialized [InvocationEvent] map that is used to store the
+/// timestamp of the invocation.
+const String kManifestTimestampKey = 'timestamp';
+
+/// The key in a serialized [PropertyGetEvent] or [PropertySetEvent] map that
+/// is used to store the property that was accessed or mutated.
+const String kManifestPropertyKey = 'property';
+
+/// The key in a serialized [PropertySetEvent] map that is used to store the
+/// value to which the property was set.
+const String kManifestValueKey = 'value';
+
+/// The key in a serialized [MethodEvent] map that is used to store the name of
+/// the method that was invoked.
+const String kManifestMethodKey = 'method';
+
+/// The key in a serialized [MethodEvent] map that is used to store the
+/// positional arguments that were passed to the method.
+const String kManifestPositionalArgumentsKey = 'positionalArguments';
+
+/// The key in a serialized [MethodEvent] map that is used to store the
+/// named arguments that were passed to the method.
+const String kManifestNamedArgumentsKey = 'namedArguments';
+
+/// The serialized [kManifestTypeKey] for property retrievals.
+const String kGetType = 'get';
+
+/// The serialized [kManifestTypeKey] for property mutations.
+const String kSetType = 'set';
+
+/// The serialized [kManifestTypeKey] for method invocations.
+const String kInvokeType = 'invoke';
+
 /// Gets an id guaranteed to be unique on this isolate for objects within this
 /// library.
 int newUid() => _nextUid++;
diff --git a/lib/src/backends/record_replay/encoding.dart b/lib/src/backends/record_replay/encoding.dart
index ac78f8a..086e959 100644
--- a/lib/src/backends/record_replay/encoding.dart
+++ b/lib/src/backends/record_replay/encoding.dart
@@ -2,7 +2,6 @@
 // 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.
 
-import 'dart:async';
 import 'dart:convert';
 
 import 'package:file/file.dart';
@@ -20,7 +19,7 @@
 
 /// Encodes an object into a JSON-ready representation.
 ///
-/// It is legal for an encoder to return a future value.
+/// Must return one of {number, boolean, string, null, list, or map}.
 typedef dynamic _Encoder(dynamic object);
 
 /// Known encoders. Types not covered here will be encoded using
@@ -35,8 +34,8 @@
   const TypeMatcher<bool>(): _encodeRaw,
   const TypeMatcher<String>(): _encodeRaw,
   const TypeMatcher<Null>(): _encodeRaw,
-  const TypeMatcher<Iterable<dynamic>>(): encodeIterable,
-  const TypeMatcher<Map<dynamic, dynamic>>(): encodeMap,
+  const TypeMatcher<Iterable<dynamic>>(): _encodeIterable,
+  const TypeMatcher<Map<dynamic, dynamic>>(): _encodeMap,
   const TypeMatcher<Symbol>(): getSymbolName,
   const TypeMatcher<DateTime>(): _encodeDateTime,
   const TypeMatcher<Uri>(): _encodeUri,
@@ -59,9 +58,9 @@
 /// Encodes an arbitrary [object] into a JSON-ready representation (a number,
 /// boolean, string, null, list, or map).
 ///
-/// Returns a future that completes with a value suitable for conversion into
-/// JSON using [JsonEncoder] without the need for a `toEncodable` argument.
-Future<dynamic> encode(dynamic object) async {
+/// Returns a value suitable for conversion into JSON using [JsonEncoder]
+/// without the need for a `toEncodable` argument.
+dynamic encode(dynamic object) {
   _Encoder encoder = _encodeDefault;
   for (TypeMatcher<dynamic> matcher in _kEncoders.keys) {
     if (matcher.matches(object)) {
@@ -69,37 +68,31 @@
       break;
     }
   }
-  return await encoder(object);
+  return encoder(object);
 }
 
 /// Default encoder (used for types not covered in [_kEncoders]).
 String _encodeDefault(dynamic object) => object.runtimeType.toString();
 
-/// Pass-through encoder.
+/// Pass-through encoder (used on `num`, `bool`, `String`, and `Null`).
 dynamic _encodeRaw(dynamic object) => object;
 
 /// Encodes the specified [iterable] into a JSON-ready list of encoded items.
-///
-/// Returns a future that completes with a list suitable for conversion into
-/// JSON using [JsonEncoder] without the need for a `toEncodable` argument.
-Future<List<dynamic>> encodeIterable(Iterable<dynamic> iterable) async {
+List<dynamic> _encodeIterable(Iterable<dynamic> iterable) {
   List<dynamic> encoded = <dynamic>[];
   for (dynamic element in iterable) {
-    encoded.add(await encode(element));
+    encoded.add(encode(element));
   }
   return encoded;
 }
 
 /// Encodes the specified [map] into a JSON-ready map of encoded key/value
 /// pairs.
-///
-/// Returns a future that completes with a map suitable for conversion into
-/// JSON using [JsonEncoder] without the need for a `toEncodable` argument.
-Future<Map<String, dynamic>> encodeMap(Map<dynamic, dynamic> map) async {
+Map<String, dynamic> _encodeMap(Map<dynamic, dynamic> map) {
   Map<String, dynamic> encoded = <String, dynamic>{};
   for (dynamic key in map.keys) {
-    String encodedKey = await encode(key);
-    encoded[encodedKey] = await encode(map[key]);
+    String encodedKey = encode(key);
+    encoded[encodedKey] = encode(map[key]);
   }
   return encoded;
 }
@@ -115,10 +108,10 @@
   };
 }
 
-Future<dynamic> _encodeResultReference(ResultReference<dynamic> reference) =>
+dynamic _encodeResultReference(ResultReference<dynamic> reference) =>
     reference.serializedValue;
 
-Future<Map<String, dynamic>> _encodeEvent(LiveInvocationEvent<dynamic> event) =>
+Map<String, dynamic> _encodeEvent(LiveInvocationEvent<dynamic> event) =>
     event.serialize();
 
 String _encodeFileSystem(FileSystem fs) => kFileSystemEncodedValue;
diff --git a/lib/src/backends/record_replay/events.dart b/lib/src/backends/record_replay/events.dart
index 821f4e4..75ffd15 100644
--- a/lib/src/backends/record_replay/events.dart
+++ b/lib/src/backends/record_replay/events.dart
@@ -103,11 +103,11 @@
   }
 
   /// Returns this event as a JSON-serializable object.
-  Future<Map<String, dynamic>> serialize() async {
+  Map<String, dynamic> serialize() {
     return <String, dynamic>{
-      'object': await encode(object),
-      'result': await encode(_result),
-      'timestamp': timestamp,
+      kManifestObjectKey: encode(object),
+      kManifestResultKey: encode(_result),
+      kManifestTimestampKey: timestamp,
     };
   }
 
@@ -126,11 +126,11 @@
   final Symbol property;
 
   @override
-  Future<Map<String, dynamic>> serialize() async {
+  Map<String, dynamic> serialize() {
     return <String, dynamic>{
-      'type': 'get',
-      'property': getSymbolName(property),
-    }..addAll(await super.serialize());
+      kManifestTypeKey: kGetType,
+      kManifestPropertyKey: getSymbolName(property),
+    }..addAll(super.serialize());
   }
 }
 
@@ -148,12 +148,12 @@
   final T value;
 
   @override
-  Future<Map<String, dynamic>> serialize() async {
+  Map<String, dynamic> serialize() {
     return <String, dynamic>{
-      'type': 'set',
-      'property': getSymbolName(property),
-      'value': await encode(value),
-    }..addAll(await super.serialize());
+      kManifestTypeKey: kSetType,
+      kManifestPropertyKey: getSymbolName(property),
+      kManifestValueKey: encode(value),
+    }..addAll(super.serialize());
   }
 }
 
@@ -185,12 +185,12 @@
   final Map<Symbol, dynamic> namedArguments;
 
   @override
-  Future<Map<String, dynamic>> serialize() async {
+  Map<String, dynamic> serialize() {
     return <String, dynamic>{
-      'type': 'invoke',
-      'method': getSymbolName(method),
-      'positionalArguments': await encodeIterable(positionalArguments),
-      'namedArguments': await encodeMap(namedArguments),
-    }..addAll(await super.serialize());
+      kManifestTypeKey: kInvokeType,
+      kManifestMethodKey: getSymbolName(method),
+      kManifestPositionalArgumentsKey: encode(positionalArguments),
+      kManifestNamedArgumentsKey: encode(namedArguments),
+    }..addAll(super.serialize());
   }
 }
diff --git a/lib/src/backends/record_replay/mutable_recording.dart b/lib/src/backends/record_replay/mutable_recording.dart
index d6bac00..60ce88e 100644
--- a/lib/src/backends/record_replay/mutable_recording.dart
+++ b/lib/src/backends/record_replay/mutable_recording.dart
@@ -46,8 +46,7 @@
             .timeout(awaitPendingResults, onTimeout: () {});
       }
       Directory dir = destination;
-      List<dynamic> encodedEvents = await encode(_events);
-      String json = new JsonEncoder.withIndent('  ').convert(encodedEvents);
+      String json = new JsonEncoder.withIndent('  ').convert(encode(_events));
       String filename = dir.fileSystem.path.join(dir.path, kManifestName);
       await dir.fileSystem.file(filename).writeAsString(json, flush: true);
     } finally {
diff --git a/lib/src/backends/record_replay/recording_file.dart b/lib/src/backends/record_replay/recording_file.dart
index ddfe697..1453b7e 100644
--- a/lib/src/backends/record_replay/recording_file.dart
+++ b/lib/src/backends/record_replay/recording_file.dart
@@ -20,6 +20,7 @@
 ///
 /// See also:
 ///   - [_BlobReference]
+///   - [_BlobStreamReference]
 typedef void _BlobDataSyncWriter<T>(File file, T data);
 
 /// Callback responsible for asynchronously writing result [data] to the
@@ -29,13 +30,6 @@
 ///   - [_BlobFutureReference]
 typedef Future<Null> _BlobDataAsyncWriter<T>(File file, T data);
 
-/// Callback responsible writing streaming result [data] to the specified
-/// [sink].
-///
-/// See also:
-///   - [_BlobStreamReference]
-typedef void _BlobDataStreamWriter<T>(IOSink sink, T data);
-
 /// [File] implementation that records all invocation activity to its file
 /// system's recording.
 class RecordingFile extends RecordingFileSystemEntity<File> implements File {
@@ -93,8 +87,8 @@
     return new _BlobStreamReference<List<int>>(
       file: _newRecordingFile(),
       stream: delegate.openRead(start, end),
-      writer: (IOSink sink, List<int> bytes) {
-        sink.add(bytes);
+      writer: (File file, List<int> bytes) {
+        file.writeAsBytesSync(bytes, mode: FileMode.APPEND, flush: true);
       },
     );
   }
@@ -209,7 +203,7 @@
   T get recordedValue => _value;
 
   @override
-  Future<String> get serializedValue async => '!${_file.basename}';
+  String get serializedValue => '!${_file.basename}';
 }
 
 /// A [FutureReference] that serializes its value data to a separate file.
@@ -235,64 +229,28 @@
   }
 
   @override
-  Future<String> get serializedValue async => '!${_file.basename}';
+  String get serializedValue => '!${_file.basename}';
 }
 
 /// A [StreamReference] that serializes its value data to a separate file.
 class _BlobStreamReference<T> extends StreamReference<T> {
   final File _file;
-  final _BlobDataStreamWriter<T> _writer;
-  IOSink _sink;
-  Future<dynamic> _pendingFlush;
+  final _BlobDataSyncWriter<T> _writer;
 
   _BlobStreamReference({
     @required File file,
     @required Stream<T> stream,
-    @required _BlobDataStreamWriter<T> writer,
+    @required _BlobDataSyncWriter<T> writer,
   })
       : _file = file,
         _writer = writer,
-        _sink = file.openWrite(),
         super(stream);
 
   @override
   void onData(T event) {
-    if (_pendingFlush == null) {
-      _writer(_sink, event);
-    } else {
-      // It's illegal to write to an IOSink while a flush is pending.
-      // https://github.com/dart-lang/sdk/issues/28635
-      _pendingFlush.whenComplete(() {
-        _writer(_sink, event);
-      });
-    }
+    _writer(_file, event);
   }
 
   @override
-  void onDone() {
-    if (_sink != null) {
-      _sink.close();
-    }
-  }
-
-  @override
-  Future<String> get serializedValue async {
-    if (_pendingFlush != null) {
-      await _pendingFlush;
-    } else {
-      _pendingFlush = _sink.flush();
-      try {
-        await _pendingFlush;
-      } finally {
-        _pendingFlush = null;
-      }
-    }
-
-    return '!${_file.basename}';
-  }
-
-  // TODO(tvolkert): remove `.then()` once Dart 1.22 is in stable
-  @override
-  Future<Null> get complete =>
-      Future.wait(<Future<dynamic>>[super.complete, _sink.done]).then((_) {});
+  String get serializedValue => '!${_file.basename}';
 }
diff --git a/lib/src/backends/record_replay/result_reference.dart b/lib/src/backends/record_replay/result_reference.dart
index 1fcdf3b..211d006 100644
--- a/lib/src/backends/record_replay/result_reference.dart
+++ b/lib/src/backends/record_replay/result_reference.dart
@@ -49,7 +49,7 @@
   /// actually a byte array that was read from a file). In this case, the
   /// method can return a `ResultReference` to the list, and it will have a
   /// hook into the serialization process.
-  Future<dynamic> get serializedValue => encode(recordedValue);
+  dynamic get serializedValue => encode(recordedValue);
 
   /// A [Future] that completes when [value] has completed.
   ///
@@ -139,7 +139,6 @@
         _controller.addError(error, stackTrace);
       },
       onDone: () {
-        onDone();
         _completer.complete();
         _controller.close();
       },
@@ -153,13 +152,6 @@
   @protected
   void onData(T event) {}
 
-  /// Called when the underlying delegate stream fires a "done" event.
-  ///
-  /// Subclasses may override this method to be notified when the underlying
-  /// stream is done.
-  @protected
-  void onDone() {}
-
   @override
   Stream<T> get value => _controller.stream;