Backport fixes from master to 0.9.7+x.
diff --git a/benchmark/path_set.dart b/benchmark/path_set.dart
new file mode 100644
index 0000000..aba3ed7
--- /dev/null
+++ b/benchmark/path_set.dart
@@ -0,0 +1,147 @@
+// Copyright (c) 2015, 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.
+
+/// Benchmarks for the PathSet class.
+library watcher.benchmark.path_set;
+
+import 'dart:io';
+import 'dart:math' as math;
+
+import 'package:benchmark_harness/benchmark_harness.dart';
+import 'package:path/path.dart' as p;
+
+import 'package:watcher/src/path_set.dart';
+
+final String root = Platform.isWindows ? r"C:\root" : "/root";
+
+/// Base class for benchmarks on [PathSet].
+abstract class PathSetBenchmark extends BenchmarkBase {
+  PathSetBenchmark(String method) : super("PathSet.$method");
+
+  final PathSet pathSet = new PathSet(root);
+
+  /// Use a fixed [Random] with a constant seed to ensure the tests are
+  /// deterministic.
+  final math.Random random = new math.Random(1234);
+
+  /// Walks over a virtual directory [depth] levels deep invoking [callback]
+  /// for each "file".
+  ///
+  /// Each virtual directory contains ten entries: either subdirectories or
+  /// files.
+  void walkTree(int depth, callback(String path)) {
+    recurse(path, remainingDepth) {
+      for (var i = 0; i < 10; i++) {
+        var padded = i.toString().padLeft(2, '0');
+        if (remainingDepth == 0) {
+          callback(p.join(path, "file_$padded.txt"));
+        } else {
+          var subdir = p.join(path, "subdirectory_$padded");
+          recurse(subdir, remainingDepth - 1);
+        }
+      }
+    }
+
+    recurse(root, depth);
+  }
+}
+
+class AddBenchmark extends PathSetBenchmark {
+  AddBenchmark() : super("add()");
+
+  final List<String> paths = [];
+
+  void setup() {
+    // Make a bunch of paths in about the same order we expect to get them from
+    // Directory.list().
+    walkTree(3, paths.add);
+  }
+
+  void run() {
+    for (var path in paths) pathSet.add(path);
+  }
+}
+
+class ContainsBenchmark extends PathSetBenchmark {
+  ContainsBenchmark() : super("contains()");
+
+  final List<String> paths = [];
+
+  void setup() {
+    // Add a bunch of paths to the set.
+    walkTree(3, (path) {
+      pathSet.add(path);
+      paths.add(path);
+    });
+
+    // Add some non-existent paths to test the false case.
+    for (var i = 0; i < 100; i++) {
+      paths.addAll([
+        "/nope",
+        "/root/nope",
+        "/root/subdirectory_04/nope",
+        "/root/subdirectory_04/subdirectory_04/nope",
+        "/root/subdirectory_04/subdirectory_04/subdirectory_04/nope",
+        "/root/subdirectory_04/subdirectory_04/subdirectory_04/nope/file_04.txt",
+      ]);
+    }
+  }
+
+  void run() {
+    var contained = 0;
+    for (var path in paths) {
+      if (pathSet.contains(path)) contained++;
+    }
+
+    if (contained != 10000) throw "Wrong result: $contained";
+  }
+}
+
+class PathsBenchmark extends PathSetBenchmark {
+  PathsBenchmark() : super("toSet()");
+
+  void setup() {
+    walkTree(3, pathSet.add);
+  }
+
+  void run() {
+    var count = 0;
+    for (var _ in pathSet.paths) {
+      count++;
+    }
+
+    if (count != 10000) throw "Wrong result: $count";
+  }
+}
+
+class RemoveBenchmark extends PathSetBenchmark {
+  RemoveBenchmark() : super("remove()");
+
+  final List<String> paths = [];
+
+  void setup() {
+    // Make a bunch of paths. Do this here so that we don't spend benchmarked
+    // time synthesizing paths.
+    walkTree(3, (path) {
+      pathSet.add(path);
+      paths.add(path);
+    });
+
+    // Shuffle the paths so that we delete them in a random order that
+    // hopefully mimics real-world file system usage. Do the shuffling here so
+    // that we don't spend benchmarked time shuffling.
+    paths.shuffle(random);
+  }
+
+  void run() {
+    for (var path in paths) pathSet.remove(path);
+  }
+}
+
+main() {
+  new AddBenchmark().report();
+  new ContainsBenchmark().report();
+  new PathsBenchmark().report();
+  new RemoveBenchmark().report();
+}
diff --git a/lib/src/async_queue.dart b/lib/src/async_queue.dart
index b83493d..adf6671 100644
--- a/lib/src/async_queue.dart
+++ b/lib/src/async_queue.dart
@@ -2,8 +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.
 
-library watcher.async_queue;
-
 import 'dart:async';
 import 'dart:collection';
 
diff --git a/lib/src/constructable_file_system_event.dart b/lib/src/constructable_file_system_event.dart
index d00a1dc..63b51c1 100644
--- a/lib/src/constructable_file_system_event.dart
+++ b/lib/src/constructable_file_system_event.dart
@@ -2,8 +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.
 
-library watcher.constructable_file_system_event;
-
 import 'dart:io';
 
 abstract class _ConstructableFileSystemEvent implements FileSystemEvent {
diff --git a/lib/src/directory_watcher.dart b/lib/src/directory_watcher.dart
index 8283785..6beebd0 100644
--- a/lib/src/directory_watcher.dart
+++ b/lib/src/directory_watcher.dart
@@ -2,11 +2,8 @@
 // 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 watcher.directory_watcher;
-
 import 'dart:io';
 
-import 'watch_event.dart';
 import '../watcher.dart';
 import 'directory_watcher/linux.dart';
 import 'directory_watcher/mac_os.dart';
diff --git a/lib/src/directory_watcher/linux.dart b/lib/src/directory_watcher/linux.dart
index a747839..f327bb0 100644
--- a/lib/src/directory_watcher/linux.dart
+++ b/lib/src/directory_watcher/linux.dart
@@ -2,12 +2,13 @@
 // 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 watcher.directory_watcher.linux;
-
 import 'dart:async';
 import 'dart:io';
 
+import 'package:async/async.dart';
+
 import '../directory_watcher.dart';
+import '../path_set.dart';
 import '../resubscribable.dart';
 import '../utils.dart';
 import '../watch_event.dart';
@@ -32,8 +33,8 @@
 
 class _LinuxDirectoryWatcher
     implements DirectoryWatcher, ManuallyClosedWatcher {
-  String get directory => path;
-  final String path;
+  String get directory => _files.root;
+  String get path => _files.root;
 
   Stream<WatchEvent> get events => _eventsController.stream;
   final _eventsController = new StreamController<WatchEvent>.broadcast();
@@ -43,15 +44,17 @@
   Future get ready => _readyCompleter.future;
   final _readyCompleter = new Completer();
 
-  /// The last known state for each entry in this directory.
-  ///
-  /// The keys in this map are the paths to the directory entries; the values
-  /// are [_EntryState]s indicating whether the entries are files or
-  /// directories.
-  final _entries = new Map<String, _EntryState>();
+  /// A stream group for the [Directory.watch] events of [path] and all its
+  /// subdirectories.
+  var _nativeEvents = new StreamGroup<FileSystemEvent>();
 
-  /// The watchers for subdirectories of [directory].
-  final _subWatchers = new Map<String, _LinuxDirectoryWatcher>();
+  /// All known files recursively within [path].
+  final PathSet _files;
+
+  /// [Directory.watch] streams for [path]'s subdirectories, indexed by name.
+  ///
+  /// A stream is in this map if and only if it's also in [_nativeEvents].
+  final _subdirStreams = <String, Stream<FileSystemEvent>>{};
 
   /// A set of all subscriptions that this watcher subscribes to.
   ///
@@ -59,93 +62,51 @@
   /// watcher is closed.
   final _subscriptions = new Set<StreamSubscription>();
 
-  _LinuxDirectoryWatcher(this.path) {
+  _LinuxDirectoryWatcher(String path)
+      : _files = new PathSet(path) {
+    _nativeEvents.add(new Directory(path).watch().transform(
+        new StreamTransformer.fromHandlers(handleDone: (sink) {
+      // Once the root directory is deleted, no more new subdirectories will be
+      // watched.
+      _nativeEvents.close();
+      sink.close();
+    })));
+
     // Batch the inotify changes together so that we can dedup events.
-    var innerStream = new Directory(path).watch()
+    var innerStream = _nativeEvents.stream
         .transform(new BatchedStreamTransformer<FileSystemEvent>());
     _listen(innerStream, _onBatch,
         onError: _eventsController.addError,
         onDone: _onDone);
 
-    _listen(new Directory(path).list(), (entity) {
-      _entries[entity.path] = new _EntryState(entity is Directory);
-      if (entity is! Directory) return;
-      _watchSubdir(entity.path);
+    _listen(new Directory(path).list(recursive: true), (entity) {
+      if (entity is Directory) {
+        _watchSubdir(entity.path);
+      } else {
+        _files.add(entity.path);
+      }
     }, onError: (error, stackTrace) {
       _eventsController.addError(error, stackTrace);
       close();
     }, onDone: () {
-      _waitUntilReady().then((_) => _readyCompleter.complete());
+      _readyCompleter.complete();
     }, cancelOnError: true);
   }
 
-  /// Returns a [Future] that completes once all the subdirectory watchers are
-  /// fully initialized.
-  Future _waitUntilReady() {
-    return Future.wait(_subWatchers.values.map((watcher) => watcher.ready))
-        .then((_) {
-      if (_subWatchers.values.every((watcher) => watcher.isReady)) return null;
-      return _waitUntilReady();
-    });
-  }
-
   void close() {
     for (var subscription in _subscriptions) {
       subscription.cancel();
     }
-    for (var watcher in _subWatchers.values) {
-      watcher.close();
-    }
 
-    _subWatchers.clear();
     _subscriptions.clear();
+    _subdirStreams.clear();
+    _files.clear();
+    _nativeEvents.close();
     _eventsController.close();
   }
 
-  /// Returns all files (not directories) that this watcher knows of are
-  /// recursively in the watched directory.
-  Set<String> get _allFiles {
-    var files = new Set<String>();
-    _getAllFiles(files);
-    return files;
-  }
-
-  /// Helper function for [_allFiles].
-  ///
-  /// Adds all files that this watcher knows of to [files].
-  void _getAllFiles(Set<String> files) {
-    files.addAll(_entries.keys
-        .where((path) => _entries[path] == _EntryState.FILE).toSet());
-    for (var watcher in _subWatchers.values) {
-      watcher._getAllFiles(files);
-    }
-  }
-
   /// Watch a subdirectory of [directory] for changes.
-  ///
-  /// If the subdirectory was added after [this] began emitting events, its
-  /// contents will be emitted as ADD events.
   void _watchSubdir(String path) {
-    if (_subWatchers.containsKey(path)) return;
-    var watcher = new _LinuxDirectoryWatcher(path);
-    _subWatchers[path] = watcher;
-
-    // TODO(nweiz): Catch any errors here that indicate that the directory in
-    // question doesn't exist and silently stop watching it instead of
-    // propagating the errors.
-    _listen(watcher.events, (event) {
-      if (isReady) _eventsController.add(event);
-    }, onError: (error, stackTrace) {
-      _eventsController.addError(error, stackTrace);
-      close();
-    }, onDone: () {
-      if (_subWatchers[path] == watcher) _subWatchers.remove(path);
-
-      // It's possible that a directory was removed and recreated very quickly.
-      // If so, make sure we're still watching it.
-      if (new Directory(path).existsSync()) _watchSubdir(path);
-    });
-
     // TODO(nweiz): Right now it's possible for the watcher to emit an event for
     // a file before the directory list is complete. This could lead to the user
     // seeing a MODIFY or REMOVE event for a file before they see an ADD event,
@@ -157,96 +118,110 @@
     // top-level clients such as barback as well, and could be implemented with
     // a wrapper similar to how listening/canceling works now.
 
-    // If a directory is added after we're finished with the initial scan, emit
-    // an event for each entry in it. This gives the user consistently gets an
-    // event for every new file.
-    watcher.ready.then((_) {
-      if (!isReady || _eventsController.isClosed) return;
-      _listen(new Directory(path).list(recursive: true), (entry) {
-        if (entry is Directory) return;
-        _eventsController.add(new WatchEvent(ChangeType.ADD, entry.path));
-      }, onError: (error, stackTrace) {
-        // Ignore an exception caused by the dir not existing. It's fine if it
-        // was added and then quickly removed.
-        if (error is FileSystemException) return;
-
-        _eventsController.addError(error, stackTrace);
-        close();
-      }, cancelOnError: true);
-    });
+    // TODO(nweiz): Catch any errors here that indicate that the directory in
+    // question doesn't exist and silently stop watching it instead of
+    // propagating the errors.
+    var stream = new Directory(path).watch();
+    _subdirStreams[path] = stream;
+    _nativeEvents.add(stream);
   }
 
   /// The callback that's run when a batch of changes comes in.
   void _onBatch(List<FileSystemEvent> batch) {
-    var changedEntries = new Set<String>();
-    var oldEntries = new Map.from(_entries);
+    var files = new Set();
+    var dirs = new Set();
+    var changed = new Set();
 
     // inotify event batches are ordered by occurrence, so we treat them as a
-    // log of what happened to a file.
+    // log of what happened to a file. We only emit events based on the
+    // difference between the state before the batch and the state after it, not
+    // the intermediate state.
     for (var event in batch) {
       // If the watched directory is deleted or moved, we'll get a deletion
       // event for it. Ignore it; we handle closing [this] when the underlying
       // stream is closed.
       if (event.path == path) continue;
 
-      changedEntries.add(event.path);
+      changed.add(event.path);
 
       if (event is FileSystemMoveEvent) {
-        changedEntries.add(event.destination);
-        _changeEntryState(event.path, ChangeType.REMOVE, event.isDirectory);
-        _changeEntryState(event.destination, ChangeType.ADD, event.isDirectory);
-      } else {
-        _changeEntryState(event.path, _changeTypeFor(event), event.isDirectory);
-      }
-    }
+        files.remove(event.path);
+        dirs.remove(event.path);
 
-    for (var path in changedEntries) {
-      emitEvent(ChangeType type) {
-        if (isReady) _eventsController.add(new WatchEvent(type, path));
-      }
-
-      var oldState = oldEntries[path];
-      var newState = _entries[path];
-
-      if (oldState != _EntryState.FILE && newState == _EntryState.FILE) {
-        emitEvent(ChangeType.ADD);
-      } else if (oldState == _EntryState.FILE && newState == _EntryState.FILE) {
-        emitEvent(ChangeType.MODIFY);
-      } else if (oldState == _EntryState.FILE && newState != _EntryState.FILE) {
-        emitEvent(ChangeType.REMOVE);
-      }
-
-      if (oldState == _EntryState.DIRECTORY) {
-        var watcher = _subWatchers.remove(path);
-        if (watcher == null) continue;
-        for (var path in watcher._allFiles) {
-          _eventsController.add(new WatchEvent(ChangeType.REMOVE, path));
+        changed.add(event.destination);
+        if (event.isDirectory) {
+          files.remove(event.destination);
+          dirs.add(event.destination);
+        } else {
+          files.add(event.destination);
+          dirs.remove(event.destination);
         }
-        watcher.close();
+      } else if (event is FileSystemDeleteEvent) {
+        files.remove(event.path);
+        dirs.remove(event.path);
+      } else if (event.isDirectory) {
+        files.remove(event.path);
+        dirs.add(event.path);
+      } else {
+        files.add(event.path);
+        dirs.remove(event.path);
       }
+    }
 
-      if (newState == _EntryState.DIRECTORY) _watchSubdir(path);
+    _applyChanges(files, dirs, changed);
+  }
+
+  /// Applies the net changes computed for a batch.
+  ///
+  /// The [files] and [dirs] sets contain the files and directories that now
+  /// exist, respectively. The [changed] set contains all files and directories
+  /// that have changed (including being removed), and so is a superset of
+  /// [files] and [dirs].
+  void _applyChanges(Set<String> files, Set<String> dirs, Set<String> changed) {
+    for (var path in changed) {
+      var stream = _subdirStreams.remove(path);
+      if (stream != null) _nativeEvents.add(stream);
+
+      // Unless [path] was a file and still is, emit REMOVE events for it or its
+      // contents,
+      if (files.contains(path) && _files.contains(path)) continue;
+      for (var file in _files.remove(path)) {
+        _emit(ChangeType.REMOVE, file);
+      }
+    }
+
+    for (var file in files) {
+      if (_files.contains(file)) {
+        _emit(ChangeType.MODIFY, file);
+      } else {
+        _emit(ChangeType.ADD, file);
+        _files.add(file);
+      }
+    }
+
+    for (var dir in dirs) {
+      _watchSubdir(dir);
+      _addSubdir(dir);
     }
   }
 
-  /// Changes the known state of the entry at [path] based on [change] and
-  /// [isDir].
-  void _changeEntryState(String path, ChangeType change, bool isDir) {
-    if (change == ChangeType.ADD || change == ChangeType.MODIFY) {
-      _entries[path] = new _EntryState(isDir);
-    } else {
-      assert(change == ChangeType.REMOVE);
-      _entries.remove(path);
-    }
-  }
+  /// Emits [ChangeType.ADD] events for the recursive contents of [path].
+  void _addSubdir(String path) {
+    _listen(new Directory(path).list(recursive: true), (entity) {
+      if (entity is Directory) {
+        _watchSubdir(entity.path);
+      } else {
+        _files.add(entity.path);
+        _emit(ChangeType.ADD, entity.path);
+      }
+    }, onError: (error, stackTrace) {
+      // Ignore an exception caused by the dir not existing. It's fine if it
+      // was added and then quickly removed.
+      if (error is FileSystemException) return;
 
-  /// Determines the [ChangeType] associated with [event].
-  ChangeType _changeTypeFor(FileSystemEvent event) {
-    if (event is FileSystemDeleteEvent) return ChangeType.REMOVE;
-    if (event is FileSystemCreateEvent) return ChangeType.ADD;
-
-    assert(event is FileSystemModifyEvent);
-    return ChangeType.MODIFY;
+      _eventsController.addError(error, stackTrace);
+      close();
+    }, cancelOnError: true);
   }
 
   /// Handles the underlying event stream closing, indicating that the directory
@@ -254,28 +229,23 @@
   void _onDone() {
     // Most of the time when a directory is removed, its contents will get
     // individual REMOVE events before the watch stream is closed -- in that
-    // case, [_entries] will be empty here. However, if the directory's removal
-    // is caused by a MOVE, we need to manually emit events.
+    // case, [_files] will be empty here. However, if the directory's removal is
+    // caused by a MOVE, we need to manually emit events.
     if (isReady) {
-      _entries.forEach((path, state) {
-        if (state == _EntryState.DIRECTORY) return;
-        _eventsController.add(new WatchEvent(ChangeType.REMOVE, path));
-      });
+      for (var file in _files.paths) {
+        _emit(ChangeType.REMOVE, file);
+      }
     }
 
-    // The parent directory often gets a close event before the subdirectories
-    // are done emitting events. We wait for them to finish before we close
-    // [events] so that we can be sure to emit a remove event for every file
-    // that used to exist.
-    Future.wait(_subWatchers.values.map((watcher) {
-      try {
-        return watcher.events.toList();
-      } on StateError catch (_) {
-        // It's possible that [watcher.events] is closed but the onDone event
-        // hasn't reached us yet. It's fine if so.
-        return new Future.value();
-      }
-    })).then((_) => close());
+    close();
+  }
+
+  /// Emits a [WatchEvent] with [type] and [path] if this watcher is in a state
+  /// to emit events.
+  void _emit(ChangeType type, String path) {
+    if (!isReady) return;
+    if (_eventsController.isClosed) return;
+    _eventsController.add(new WatchEvent(type, path));
   }
 
   /// Like [Stream.listen], but automatically adds the subscription to
@@ -290,22 +260,3 @@
     _subscriptions.add(subscription);
   }
 }
-
-/// An enum for the possible states of entries in a watched directory.
-class _EntryState {
-  final String _name;
-
-  /// The entry is a file.
-  static const FILE = const _EntryState._("file");
-
-  /// The entry is a directory.
-  static const DIRECTORY = const _EntryState._("directory");
-
-  const _EntryState._(this._name);
-
-  /// Returns [DIRECTORY] if [isDir] is true, and [FILE] otherwise.
-  factory _EntryState(bool isDir) =>
-      isDir ? _EntryState.DIRECTORY : _EntryState.FILE;
-
-  String toString() => _name;
-}
diff --git a/lib/src/directory_watcher/mac_os.dart b/lib/src/directory_watcher/mac_os.dart
index 487225e..8a17e2e 100644
--- a/lib/src/directory_watcher/mac_os.dart
+++ b/lib/src/directory_watcher/mac_os.dart
@@ -2,8 +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.
 
-library watcher.directory_watcher.mac_os;
-
 import 'dart:async';
 import 'dart:io';
 
@@ -58,7 +56,7 @@
   ///
   /// This is separate from [_subscriptions] because this stream occasionally
   /// needs to be resubscribed in order to work around issue 14849.
-  StreamSubscription<FileSystemEvent> _watchSubscription;
+  StreamSubscription<List<FileSystemEvent>> _watchSubscription;
 
   /// The subscription to the [Directory.list] call for the initial listing of
   /// the directory to determine its initial state.
@@ -116,9 +114,9 @@
       return;
     }
 
-    _sortEvents(batch).forEach((path, events) {
-      var canonicalEvent = _canonicalEvent(events);
-      events = canonicalEvent == null ?
+    _sortEvents(batch).forEach((path, eventSet) {
+      var canonicalEvent = _canonicalEvent(eventSet);
+      var events = canonicalEvent == null ?
           _eventsBasedOnFileSystem(path) : [canonicalEvent];
 
       for (var event in events) {
@@ -139,7 +137,7 @@
 
           if (_files.containsDir(path)) continue;
 
-          var subscription;
+          StreamSubscription<FileSystemEntity> subscription;
           subscription = new Directory(path).list(recursive: true)
               .listen((entity) {
             if (entity is Directory) return;
@@ -175,7 +173,7 @@
   /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it
   /// contain any events relating to [path].
   Map<String, Set<FileSystemEvent>> _sortEvents(List<FileSystemEvent> batch) {
-    var eventsForPaths = {};
+    var eventsForPaths = <String, Set>{};
 
     // FSEvents can report past events, including events on the root directory
     // such as it being created. We want to ignore these. If the directory is
@@ -187,8 +185,10 @@
     // events. Emitting them could cause useless or out-of-order events.
     var directories = unionAll(batch.map((event) {
       if (!event.isDirectory) return new Set();
-      if (event is! FileSystemMoveEvent) return new Set.from([event.path]);
-      return new Set.from([event.path, event.destination]);
+      if (event is FileSystemMoveEvent) {
+        return new Set.from([event.path, event.destination]);
+      }
+      return new Set.from([event.path]);
     }));
 
     isInModifiedDirectory(path) =>
@@ -294,7 +294,7 @@
     var fileExists = new File(path).existsSync();
     var dirExists = new Directory(path).existsSync();
 
-    var events = [];
+    var events = <FileSystemEvent>[];
     if (fileExisted) {
       if (fileExists) {
         events.add(new ConstructableFileSystemModifyEvent(path, false, false));
@@ -337,7 +337,7 @@
     // FSEvents can fail to report the contents of the directory being removed
     // when the directory itself is removed, so we need to manually mark the
     // files as removed.
-    for (var file in _files.toSet()) {
+    for (var file in _files.paths) {
       _emitEvent(ChangeType.REMOVE, file);
     }
     _files.clear();
diff --git a/lib/src/directory_watcher/polling.dart b/lib/src/directory_watcher/polling.dart
index 7f417d6..ebc1709 100644
--- a/lib/src/directory_watcher/polling.dart
+++ b/lib/src/directory_watcher/polling.dart
@@ -2,8 +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.
 
-library watcher.directory_watcher.polling;
-
 import 'dart:async';
 import 'dart:io';
 
diff --git a/lib/src/directory_watcher/windows.dart b/lib/src/directory_watcher/windows.dart
index 0899519..67a2741 100644
--- a/lib/src/directory_watcher/windows.dart
+++ b/lib/src/directory_watcher/windows.dart
@@ -3,8 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.

 // TODO(rnystrom): Merge with mac_os version.

 

-library watcher.directory_watcher.windows;

-

 import 'dart:async';

 import 'dart:collection';

 import 'dart:io';

@@ -136,7 +134,7 @@
           event is FileSystemDeleteEvent ||

           (FileSystemEntity.typeSync(path) ==

            FileSystemEntityType.NOT_FOUND)) {

-        for (var path in _files.toSet()) {

+        for (var path in _files.paths) {

           _emitEvent(ChangeType.REMOVE, path);

         }

         _files.clear();

@@ -163,10 +161,10 @@
 

   /// The callback that's run when [Directory.watch] emits a batch of events.

   void _onBatch(List<FileSystemEvent> batch) {

-    _sortEvents(batch).forEach((path, events) {

+    _sortEvents(batch).forEach((path, eventSet) {

 

-      var canonicalEvent = _canonicalEvent(events);

-      events = canonicalEvent == null ?

+      var canonicalEvent = _canonicalEvent(eventSet);

+      var events = canonicalEvent == null ?

           _eventsBasedOnFileSystem(path) : [canonicalEvent];

 

       for (var event in events) {

@@ -182,20 +180,20 @@
           if (_files.containsDir(path)) continue;

 

           var stream = new Directory(path).list(recursive: true);

-          var sub;

-          sub = stream.listen((entity) {

+          StreamSubscription<FileSystemEntity> subscription;

+          subscription = stream.listen((entity) {

             if (entity is Directory) return;

             if (_files.contains(path)) return;

 

             _emitEvent(ChangeType.ADD, entity.path);

             _files.add(entity.path);

           }, onDone: () {

-            _listSubscriptions.remove(sub);

+            _listSubscriptions.remove(subscription);

           }, onError: (e, stackTrace) {

-            _listSubscriptions.remove(sub);

+            _listSubscriptions.remove(subscription);

             _emitError(e, stackTrace);

           }, cancelOnError: true);

-          _listSubscriptions.add(sub);

+          _listSubscriptions.add(subscription);

         } else if (event is FileSystemModifyEvent) {

           if (!event.isDirectory) {

             _emitEvent(ChangeType.MODIFY, path);

@@ -219,15 +217,17 @@
   /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it

   /// contain any events relating to [path].

   Map<String, Set<FileSystemEvent>> _sortEvents(List<FileSystemEvent> batch) {

-    var eventsForPaths = {};

+    var eventsForPaths = <String, Set>{};

 

     // Events within directories that already have events are superfluous; the

     // directory's full contents will be examined anyway, so we ignore such

     // events. Emitting them could cause useless or out-of-order events.

     var directories = unionAll(batch.map((event) {

       if (!event.isDirectory) return new Set();

-      if (event is! FileSystemMoveEvent) return new Set.from([event.path]);

-      return new Set.from([event.path, event.destination]);

+      if (event is FileSystemMoveEvent) {

+        return new Set.from([event.path, event.destination]);

+      }

+      return new Set.from([event.path]);

     }));

 

     isInModifiedDirectory(path) =>

@@ -322,7 +322,7 @@
     var fileExists = new File(path).existsSync();

     var dirExists = new Directory(path).existsSync();

 

-    var events = [];

+    var events = <FileSystemEvent>[];

     if (fileExisted) {

       if (fileExists) {

         events.add(new ConstructableFileSystemModifyEvent(path, false, false));

@@ -357,7 +357,7 @@
     _watchSubscription = null;

 

     // Emit remove events for any remaining files.

-    for (var file in _files.toSet()) {

+    for (var file in _files.paths) {

       _emitEvent(ChangeType.REMOVE, file);

     }

     _files.clear();

diff --git a/lib/src/file_watcher.dart b/lib/src/file_watcher.dart
index 9b31537..17c5f2e 100644
--- a/lib/src/file_watcher.dart
+++ b/lib/src/file_watcher.dart
@@ -2,11 +2,8 @@
 // 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 watcher.file_watcher;
-
 import 'dart:io';
 
-import 'watch_event.dart';
 import '../watcher.dart';
 import 'file_watcher/native.dart';
 import 'file_watcher/polling.dart';
diff --git a/lib/src/file_watcher/native.dart b/lib/src/file_watcher/native.dart
index 1862e7b..f413a72 100644
--- a/lib/src/file_watcher/native.dart
+++ b/lib/src/file_watcher/native.dart
@@ -2,8 +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.
 
-library watcher.file_watcher.native;
-
 import 'dart:async';
 import 'dart:io';
 
diff --git a/lib/src/file_watcher/polling.dart b/lib/src/file_watcher/polling.dart
index 3480ae2..3f2e9f1 100644
--- a/lib/src/file_watcher/polling.dart
+++ b/lib/src/file_watcher/polling.dart
@@ -2,8 +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.
 
-library watcher.file_watcher.polling;
-
 import 'dart:async';
 import 'dart:io';
 
diff --git a/lib/src/path_set.dart b/lib/src/path_set.dart
index e9f7d32..3726e1f 100644
--- a/lib/src/path_set.dart
+++ b/lib/src/path_set.dart
@@ -2,8 +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.
 
-library watcher.path_dart;
-
 import 'dart:collection';
 
 import 'package:path/path.dart' as p;
@@ -21,32 +19,24 @@
 
   /// The path set's directory hierarchy.
   ///
-  /// Each level of this hierarchy has the same structure: a map from strings to
-  /// other maps, which are further levels of the hierarchy. A map with no
-  /// elements indicates a path that was added to the set that has no paths
-  /// beneath it. Such a path should not be treated as a directory by
-  /// [containsDir].
-  final _entries = new Map<String, Map>();
-
-  /// The set of paths that were explicitly added to this set.
-  ///
-  /// This is needed to disambiguate a directory that was explicitly added to
-  /// the set from a directory that was implicitly added by adding a path
-  /// beneath it.
-  final _paths = new Set<String>();
+  /// Each entry represents a directory or file. It may be a file or directory
+  /// that was explicitly added, or a parent directory that was implicitly
+  /// added in order to add a child.
+  final _Entry _entries = new _Entry();
 
   PathSet(this.root);
 
   /// Adds [path] to the set.
   void add(String path) {
     path = _normalize(path);
-    _paths.add(path);
 
-    var parts = _split(path);
-    var dir = _entries;
+    var parts = p.split(path);
+    var entry = _entries;
     for (var part in parts) {
-      dir = dir.putIfAbsent(part, () => {});
+      entry = entry.contents.putIfAbsent(part, () => new _Entry());
     }
+
+    entry.isExplicit = true;
   }
 
   /// Removes [path] and any paths beneath it from the set and returns the
@@ -59,110 +49,140 @@
   /// empty set.
   Set<String> remove(String path) {
     path = _normalize(path);
-    var parts = new Queue.from(_split(path));
+    var parts = new Queue.from(p.split(path));
 
     // Remove the children of [dir], as well as [dir] itself if necessary.
     //
     // [partialPath] is the path to [dir], and a prefix of [path]; the remaining
     // components of [path] are in [parts].
-    recurse(dir, partialPath) {
+    Set<String> recurse(dir, partialPath) {
       if (parts.length > 1) {
         // If there's more than one component left in [path], recurse down to
         // the next level.
         var part = parts.removeFirst();
-        var entry = dir[part];
-        if (entry == null || entry.isEmpty) return new Set();
+        var entry = dir.contents[part];
+        if (entry == null || entry.contents.isEmpty) return new Set();
 
         partialPath = p.join(partialPath, part);
         var paths = recurse(entry, partialPath);
         // After removing this entry's children, if it has no more children and
         // it's not in the set in its own right, remove it as well.
-        if (entry.isEmpty && !_paths.contains(partialPath)) dir.remove(part);
+        if (entry.contents.isEmpty && !entry.isExplicit) {
+          dir.contents.remove(part);
+        }
         return paths;
       }
 
       // If there's only one component left in [path], we should remove it.
-      var entry = dir.remove(parts.first);
+      var entry = dir.contents.remove(parts.first);
       if (entry == null) return new Set();
 
-      if (entry.isEmpty) {
-        _paths.remove(path);
-        return new Set.from([path]);
+      if (entry.contents.isEmpty) {
+        return new Set.from([p.join(root, path)]);
       }
 
-      var set = _removePathsIn(entry, path);
-      if (_paths.contains(path)) {
-        _paths.remove(path);
-        set.add(path);
+      var set = _explicitPathsWithin(entry, path);
+      if (entry.isExplicit) {
+        set.add(p.join(root, path));
       }
+
       return set;
     }
 
     return recurse(_entries, root);
   }
 
-  /// Recursively removes and returns all paths in [dir].
+  /// Recursively lists all of the explicit paths within [dir].
   ///
-  /// [root] should be the path to [dir].
-  Set<String> _removePathsIn(Map dir, String root) {
-    var removedPaths = new Set();
+  /// [dirPath] should be the path to [dir].
+  Set<String> _explicitPathsWithin(_Entry dir, String dirPath) {
+    var paths = new Set<String>();
     recurse(dir, path) {
-      dir.forEach((name, entry) {
+      dir.contents.forEach((name, entry) {
         var entryPath = p.join(path, name);
-        if (_paths.remove(entryPath)) removedPaths.add(entryPath);
+        if (entry.isExplicit) paths.add(p.join(root, entryPath));
+
         recurse(entry, entryPath);
       });
     }
 
-    recurse(dir, root);
-    return removedPaths;
+    recurse(dir, dirPath);
+    return paths;
   }
 
   /// Returns whether [this] contains [path].
   ///
   /// This only returns true for paths explicitly added to [this].
   /// Implicitly-added directories can be inspected using [containsDir].
-  bool contains(String path) => _paths.contains(_normalize(path));
+  bool contains(String path) {
+    path = _normalize(path);
+    var entry = _entries;
+
+    for (var part in p.split(path)) {
+      entry = entry.contents[part];
+      if (entry == null) return false;
+    }
+
+    return entry.isExplicit;
+  }
 
   /// Returns whether [this] contains paths beneath [path].
   bool containsDir(String path) {
     path = _normalize(path);
-    var dir = _entries;
+    var entry = _entries;
 
-    for (var part in _split(path)) {
-      dir = dir[part];
-      if (dir == null) return false;
+    for (var part in p.split(path)) {
+      entry = entry.contents[part];
+      if (entry == null) return false;
     }
 
-    return !dir.isEmpty;
+    return !entry.contents.isEmpty;
   }
 
-  /// Returns a [Set] of all paths in [this].
-  Set<String> toSet() => _paths.toSet();
+  /// All of the paths explicitly added to this set.
+  List<String> get paths {
+    var result = <String>[];
+
+    recurse(dir, path) {
+      for (var name in dir.contents.keys) {
+        var entry = dir.contents[name];
+        var entryPath = p.join(path, name);
+        if (entry.isExplicit) result.add(entryPath);
+        recurse(entry, entryPath);
+      }
+    }
+
+    recurse(_entries, root);
+    return result;
+  }
 
   /// Removes all paths from [this].
   void clear() {
-    _paths.clear();
-    _entries.clear();
+    _entries.contents.clear();
   }
 
-  String toString() => _paths.toString();
-
   /// Returns a normalized version of [path].
   ///
   /// This removes any extra ".." or "."s and ensure that the returned path
   /// begins with [root]. It's an error if [path] isn't within [root].
   String _normalize(String path) {
-    var relative = p.relative(p.normalize(path), from: root);
-    var parts = p.split(relative);
-    // TODO(nweiz): replace this with [p.isWithin] when that exists (issue
-    // 14980).
-    if (!p.isRelative(relative) || parts.first == '..' || parts.first == '.') {
-      throw new ArgumentError('Path "$path" is not inside "$root".');
-    }
-    return p.join(root, relative);
-  }
+    assert(p.isWithin(root, path));
 
-  /// Returns the segments of [path] beneath [root].
-  List<String> _split(String path) => p.split(p.relative(path, from: root));
+    return p.relative(p.normalize(path), from: root);
+  }
+}
+
+/// A virtual file system entity tracked by the [PathSet].
+///
+/// It may have child entries in [contents], which implies it's a directory.
+class _Entry {
+  /// The child entries contained in this directory.
+  final Map<String, _Entry> contents = {};
+
+  /// If this entry was explicitly added as a leaf file system entity, this
+  /// will be true.
+  ///
+  /// Otherwise, it represents a parent directory that was implicitly added
+  /// when added some child of it.
+  bool isExplicit = false;
 }
diff --git a/lib/src/resubscribable.dart b/lib/src/resubscribable.dart
index 2844c1e..aeefe93 100644
--- a/lib/src/resubscribable.dart
+++ b/lib/src/resubscribable.dart
@@ -2,8 +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.
 
-library watcher.resubscribable;
-
 import 'dart:async';
 
 import '../watcher.dart';
diff --git a/lib/src/stat.dart b/lib/src/stat.dart
index d36eff3..05ee9ba 100644
--- a/lib/src/stat.dart
+++ b/lib/src/stat.dart
@@ -2,8 +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.
 
-library watcher.stat;
-
 import 'dart:async';
 import 'dart:io';
 
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 007c84c..d263f2f 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -2,8 +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.
 
-library watcher.utils;
-
 import 'dart:async';
 import 'dart:io';
 import 'dart:collection';
@@ -33,7 +31,7 @@
 /// [broadcast] defaults to false.
 Stream futureStream(Future<Stream> future, {bool broadcast: false}) {
   var subscription;
-  var controller;
+  StreamController controller;
 
   future = future.catchError((e, stackTrace) {
     // Since [controller] is synchronous, it's likely that emitting an error
@@ -94,7 +92,7 @@
 /// microtasks.
 class BatchedStreamTransformer<T> implements StreamTransformer<T, List<T>> {
   Stream<List<T>> bind(Stream<T> input) {
-    var batch = new Queue();
+    var batch = new Queue<T>();
     return new StreamTransformer<T, List<T>>.fromHandlers(
         handleData: (event, sink) {
       batch.add(event);
diff --git a/lib/src/watch_event.dart b/lib/src/watch_event.dart
index be6d70c..54093a5 100644
--- a/lib/src/watch_event.dart
+++ b/lib/src/watch_event.dart
@@ -2,8 +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.
 
-library watcher.watch_event;
-
 /// An event describing a single change to the file system.
 class WatchEvent {
   /// The manner in which the file at [path] has changed.
diff --git a/lib/watcher.dart b/lib/watcher.dart
index 79dcc0d..b3cebe6 100644
--- a/lib/watcher.dart
+++ b/lib/watcher.dart
@@ -2,8 +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.
 
-library watcher;
-
 import 'dart:async';
 import 'dart:io';
 
diff --git a/pubspec.yaml b/pubspec.yaml
index 2e033f1..c0152e0 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,14 +1,17 @@
 name: watcher
-version: 0.9.7
+version: 0.9.8-dev
 author: Dart Team <misc@dartlang.org>
-homepage: http://github.com/dart-lang/watcher
+homepage: https://github.com/dart-lang/watcher
 description: >
   A file system watcher. It monitors changes to contents of directories and
   sends notifications when files have been added, removed, or modified.
 environment:
   sdk: '>=1.9.0 <2.0.0'
 dependencies:
+  async: '^1.2.0'
+  collection: '^1.0.0'
   path: '>=0.9.0 <2.0.0'
 dev_dependencies:
+  benchmark_harness: '^1.0.4'
   scheduled_test: '^0.12.0'
   test: '^0.12.0'
diff --git a/test/directory_watcher/shared.dart b/test/directory_watcher/shared.dart
index ff48cb2..c95ee39 100644
--- a/test/directory_watcher/shared.dart
+++ b/test/directory_watcher/shared.dart
@@ -246,6 +246,37 @@
       expectModifyEvent("new/file.txt");
     });
 
+    test('notifies when a file is replaced by a subdirectory', () {
+      writeFile("new");
+      writeFile("old/file.txt");
+      startWatcher();
+
+      deleteFile("new");
+      renameDir("old", "new");
+      inAnyOrder([
+        isRemoveEvent("new"),
+        isRemoveEvent("old/file.txt"),
+        isAddEvent("new/file.txt")
+      ]);
+    });
+
+    test('notifies when a subdirectory is replaced by a file', () {
+      writeFile("old");
+      writeFile("new/file.txt");
+      startWatcher();
+
+      renameDir("new", "newer");
+      renameFile("old", "new");
+      inAnyOrder([
+        isRemoveEvent("new/file.txt"),
+        isAddEvent("newer/file.txt"),
+        isRemoveEvent("old"),
+        isAddEvent("new")
+      ]);
+    }, onPlatform: {
+      "mac-os": new Skip("https://github.com/dart-lang/watcher/issues/21")
+    });
+
     test('emits events for many nested files added at once', () {
       withPermutations((i, j, k) =>
           writeFile("sub/sub-$i/sub-$j/file-$k.txt"));
diff --git a/test/file_watcher/native_test.dart b/test/file_watcher/native_test.dart
index 30d0eca..cbf11b6 100644
--- a/test/file_watcher/native_test.dart
+++ b/test/file_watcher/native_test.dart
@@ -6,7 +6,6 @@
 
 import 'package:scheduled_test/scheduled_test.dart';
 import 'package:watcher/src/file_watcher/native.dart';
-import 'package:watcher/watcher.dart';
 
 import 'shared.dart';
 import '../utils.dart';
diff --git a/test/no_subscription/mac_os_test.dart b/test/no_subscription/mac_os_test.dart
index d5b1c8e..499aff3 100644
--- a/test/no_subscription/mac_os_test.dart
+++ b/test/no_subscription/mac_os_test.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 @TestOn('mac-os')
+@Skip("Flaky due to sdk#23877")
 
 import 'package:scheduled_test/scheduled_test.dart';
 import 'package:watcher/src/directory_watcher/mac_os.dart';
diff --git a/test/path_set_test.dart b/test/path_set_test.dart
index 13e797c..d3420d3 100644
--- a/test/path_set_test.dart
+++ b/test/path_set_test.dart
@@ -6,8 +6,6 @@
 import 'package:test/test.dart';
 import 'package:watcher/src/path_set.dart';
 
-import 'utils.dart';
-
 Matcher containsPath(String path) => predicate((set) =>
     set is PathSet && set.contains(path),
     'set contains "$path"');
@@ -42,10 +40,6 @@
       set.add(p.absolute("root/path/to/file"));
       expect(set, containsPath("root/path/to/file"));
     });
-
-    test("that's not beneath the root throws an error", () {
-      expect(() => set.add("path/to/file"), throwsArgumentError);
-    });
   });
 
   group("removing a path", () {
@@ -78,7 +72,7 @@
       expect(set, isNot(containsPath("root/path/to/two")));
       expect(set, isNot(containsPath("root/path/to/sub/three")));
     });
-  
+
     test("that's a directory in the set removes and returns it and all files "
         "beneath it", () {
       set.add("root/path");
@@ -110,10 +104,6 @@
       expect(set.remove(p.absolute("root/path/to/file")),
           unorderedEquals([p.normalize("root/path/to/file")]));
     });
-
-    test("that's not beneath the root throws an error", () {
-      expect(() => set.remove("path/to/file"), throwsArgumentError);
-    });
   });
 
   group("containsPath()", () {
@@ -143,10 +133,6 @@
       set.add("root/path/to/file");
       expect(set, containsPath(p.absolute("root/path/to/file")));
     });
-
-    test("with a path that's not beneath the root throws an error", () {
-      expect(() => set.contains("path/to/file"), throwsArgumentError);
-    });
   });
 
   group("containsDir()", () {
@@ -198,13 +184,13 @@
     });
   });
 
-  group("toSet", () {
+  group("paths", () {
     test("returns paths added to the set", () {
       set.add("root/path");
       set.add("root/path/to/one");
       set.add("root/path/to/two");
 
-      expect(set.toSet(), unorderedEquals([
+      expect(set.paths, unorderedEquals([
         "root/path",
         "root/path/to/one",
         "root/path/to/two",
@@ -216,7 +202,7 @@
       set.add("root/path/to/two");
       set.remove("root/path/to/two");
 
-      expect(set.toSet(), unorderedEquals([p.normalize("root/path/to/one")]));
+      expect(set.paths, unorderedEquals([p.normalize("root/path/to/one")]));
     });
   });
 
@@ -227,7 +213,7 @@
       set.add("root/path/to/two");
 
       set.clear();
-      expect(set.toSet(), isEmpty);
+      expect(set.paths, isEmpty);
     });
   });
 }
diff --git a/test/utils.dart b/test/utils.dart
index 7dd8332..e3c2a1b 100644
--- a/test/utils.dart
+++ b/test/utils.dart
@@ -2,8 +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.
 
-library watcher.test.utils;
-
 import 'dart:io';
 
 import 'package:path/path.dart' as p;
@@ -13,9 +11,6 @@
 import 'package:watcher/src/stat.dart';
 import 'package:watcher/src/utils.dart';
 
-// TODO(nweiz): remove this when issue 15042 is fixed.
-import 'package:watcher/src/directory_watcher/mac_os.dart';
-
 /// The path to the temporary sandbox created for each test. All file
 /// operations are implicitly relative to this directory.
 String _sandboxDir;
@@ -292,7 +287,7 @@
       // Make sure we always use the same separator on Windows.
       path = p.normalize(path);
 
-      var milliseconds = _mockFileModificationTimes.putIfAbsent(path, () => 0);
+      _mockFileModificationTimes.putIfAbsent(path, () => 0);
       _mockFileModificationTimes[path]++;
     }
   }, "write file $path");
@@ -316,7 +311,7 @@
     to = p.normalize(to);
 
     // Manually update the mock modification time for the file.
-    var milliseconds = _mockFileModificationTimes.putIfAbsent(to, () => 0);
+    _mockFileModificationTimes.putIfAbsent(to, () => 0);
     _mockFileModificationTimes[to]++;
   }, "rename file $from to $to");
 }
@@ -349,9 +344,10 @@
 /// Returns a set of all values returns by [callback].
 ///
 /// [limit] defaults to 3.
-Set withPermutations(callback(int i, int j, int k), {int limit}) {
+Set/*<S>*/ withPermutations/*<S>*/(/*=S*/ callback(int i, int j, int k),
+    {int limit}) {
   if (limit == null) limit = 3;
-  var results = new Set();
+  var results = new Set/*<S>*/();
   for (var i = 0; i < limit; i++) {
     for (var j = 0; j < limit; j++) {
       for (var k = 0; k < limit; k++) {