Merge pull request #91 from michalt/custom-watcher-factory
Implement the ability to register custom watcher implementations
diff --git a/lib/src/custom_watcher_factory.dart b/lib/src/custom_watcher_factory.dart
new file mode 100644
index 0000000..b7f81fa
--- /dev/null
+++ b/lib/src/custom_watcher_factory.dart
@@ -0,0 +1,83 @@
+import '../watcher.dart';
+
+/// Defines a way to create a custom watcher instead of the default ones.
+///
+/// This will be used when a [DirectoryWatcher] or [FileWatcher] would be
+/// created and will take precedence over the default ones.
+abstract class CustomWatcherFactory {
+ /// Uniquely identify this watcher.
+ String get id;
+
+ /// Tries to create a [DirectoryWatcher] for the provided path.
+ ///
+ /// Returns `null` if the path is not supported by this factory.
+ DirectoryWatcher createDirectoryWatcher(String path, {Duration pollingDelay});
+
+ /// Tries to create a [FileWatcher] for the provided path.
+ ///
+ /// Returns `null` if the path is not supported by this factory.
+ FileWatcher createFileWatcher(String path, {Duration pollingDelay});
+}
+
+/// Registers a custom watcher.
+///
+/// It's only allowed to register a watcher factory once per [id] and at most
+/// one factory should apply to any given file (creating a [Watcher] will fail
+/// otherwise).
+void registerCustomWatcherFactory(CustomWatcherFactory customFactory) {
+ if (_customWatcherFactories.containsKey(customFactory.id)) {
+ throw ArgumentError('A custom watcher with id `${customFactory.id}` '
+ 'has already been registered');
+ }
+ _customWatcherFactories[customFactory.id] = customFactory;
+}
+
+/// Tries to create a custom [DirectoryWatcher] and returns it.
+///
+/// Returns `null` if no custom watcher was applicable and throws a [StateError]
+/// if more than one was.
+DirectoryWatcher createCustomDirectoryWatcher(String path,
+ {Duration pollingDelay}) {
+ DirectoryWatcher customWatcher;
+ String customFactoryId;
+ for (var watcherFactory in customWatcherFactories) {
+ if (customWatcher != null) {
+ throw StateError('Two `CustomWatcherFactory`s applicable: '
+ '`$customFactoryId` and `${watcherFactory.id}` for `$path`');
+ }
+ customWatcher =
+ watcherFactory.createDirectoryWatcher(path, pollingDelay: pollingDelay);
+ customFactoryId = watcherFactory.id;
+ }
+ return customWatcher;
+}
+
+/// Tries to create a custom [FileWatcher] and returns it.
+///
+/// Returns `null` if no custom watcher was applicable and throws a [StateError]
+/// if more than one was.
+FileWatcher createCustomFileWatcher(String path, {Duration pollingDelay}) {
+ FileWatcher customWatcher;
+ String customFactoryId;
+ for (var watcherFactory in customWatcherFactories) {
+ if (customWatcher != null) {
+ throw StateError('Two `CustomWatcherFactory`s applicable: '
+ '`$customFactoryId` and `${watcherFactory.id}` for `$path`');
+ }
+ customWatcher =
+ watcherFactory.createFileWatcher(path, pollingDelay: pollingDelay);
+ customFactoryId = watcherFactory.id;
+ }
+ return customWatcher;
+}
+
+/// Unregisters a custom watcher and returns it.
+///
+/// Returns `null` if the id was never registered.
+CustomWatcherFactory unregisterCustomWatcherFactory(String id) =>
+ _customWatcherFactories.remove(id);
+
+Iterable<CustomWatcherFactory> get customWatcherFactories =>
+ _customWatcherFactories.values;
+
+final _customWatcherFactories = <String, CustomWatcherFactory>{};
diff --git a/lib/src/directory_watcher.dart b/lib/src/directory_watcher.dart
index e0ef3fc..3fe5004 100644
--- a/lib/src/directory_watcher.dart
+++ b/lib/src/directory_watcher.dart
@@ -5,10 +5,11 @@
import 'dart:io';
import '../watcher.dart';
+import 'custom_watcher_factory.dart';
import 'directory_watcher/linux.dart';
import 'directory_watcher/mac_os.dart';
-import 'directory_watcher/windows.dart';
import 'directory_watcher/polling.dart';
+import 'directory_watcher/windows.dart';
/// Watches the contents of a directory and emits [WatchEvent]s when something
/// in the directory has changed.
@@ -29,6 +30,9 @@
/// watchers.
factory DirectoryWatcher(String directory, {Duration pollingDelay}) {
if (FileSystemEntity.isWatchSupported) {
+ var customWatcher =
+ createCustomDirectoryWatcher(directory, pollingDelay: pollingDelay);
+ if (customWatcher != null) return customWatcher;
if (Platform.isLinux) return LinuxDirectoryWatcher(directory);
if (Platform.isMacOS) return MacOSDirectoryWatcher(directory);
if (Platform.isWindows) return WindowsDirectoryWatcher(directory);
diff --git a/lib/src/file_watcher.dart b/lib/src/file_watcher.dart
index c4abddd..c41e5e6 100644
--- a/lib/src/file_watcher.dart
+++ b/lib/src/file_watcher.dart
@@ -5,6 +5,7 @@
import 'dart:io';
import '../watcher.dart';
+import 'custom_watcher_factory.dart';
import 'file_watcher/native.dart';
import 'file_watcher/polling.dart';
@@ -29,6 +30,10 @@
/// and higher CPU usage. Defaults to one second. Ignored for non-polling
/// watchers.
factory FileWatcher(String file, {Duration pollingDelay}) {
+ var customWatcher =
+ createCustomFileWatcher(file, pollingDelay: pollingDelay);
+ if (customWatcher != null) return customWatcher;
+
// [File.watch] doesn't work on Windows, but
// [FileSystemEntity.isWatchSupported] is still true because directory
// watching does work.
diff --git a/lib/watcher.dart b/lib/watcher.dart
index 107ac8f..f5c7d3e 100644
--- a/lib/watcher.dart
+++ b/lib/watcher.dart
@@ -5,15 +5,20 @@
import 'dart:async';
import 'dart:io';
-import 'src/watch_event.dart';
import 'src/directory_watcher.dart';
import 'src/file_watcher.dart';
+import 'src/watch_event.dart';
-export 'src/watch_event.dart';
+export 'src/custom_watcher_factory.dart'
+ show
+ CustomWatcherFactory,
+ registerCustomWatcherFactory,
+ unregisterCustomWatcherFactory;
export 'src/directory_watcher.dart';
export 'src/directory_watcher/polling.dart';
export 'src/file_watcher.dart';
export 'src/file_watcher/polling.dart';
+export 'src/watch_event.dart';
abstract class Watcher {
/// The path to the file or directory whose contents are being monitored.
diff --git a/test/custom_watcher_factory_test.dart b/test/custom_watcher_factory_test.dart
new file mode 100644
index 0000000..6c9ffd8
--- /dev/null
+++ b/test/custom_watcher_factory_test.dart
@@ -0,0 +1,146 @@
+import 'dart:async';
+
+import 'package:test/test.dart';
+import 'package:watcher/watcher.dart';
+
+import 'utils.dart';
+
+void main() {
+ _MemFs memFs;
+ final defaultFactoryId = 'MemFs';
+
+ setUp(() {
+ memFs = _MemFs();
+ registerCustomWatcherFactory(_MemFsWatcherFactory(defaultFactoryId, memFs));
+ });
+
+ tearDown(() async {
+ unregisterCustomWatcherFactory(defaultFactoryId);
+ });
+
+ test('notifes for files', () async {
+ var watcher = FileWatcher('file.txt');
+
+ var completer = Completer<WatchEvent>();
+ watcher.events.listen((event) => completer.complete(event));
+ await watcher.ready;
+ memFs.add('file.txt');
+ var event = await completer.future;
+
+ expect(event.type, ChangeType.ADD);
+ expect(event.path, 'file.txt');
+ });
+
+ test('notifes for directories', () async {
+ var watcher = DirectoryWatcher('dir');
+
+ var completer = Completer<WatchEvent>();
+ watcher.events.listen((event) => completer.complete(event));
+ await watcher.ready;
+ memFs.add('dir');
+ var event = await completer.future;
+
+ expect(event.type, ChangeType.ADD);
+ expect(event.path, 'dir');
+ });
+
+ test('unregister works', () async {
+ unregisterCustomWatcherFactory(defaultFactoryId);
+
+ watcherFactory = (path) => FileWatcher(path);
+ try {
+ // This uses standard files, so it wouldn't trigger an event in
+ // _MemFsWatcher.
+ writeFile('file.txt');
+ await startWatcher(path: 'file.txt');
+ deleteFile('file.txt');
+ } finally {
+ watcherFactory = null;
+ }
+
+ await expectRemoveEvent('file.txt');
+ });
+
+ test('registering twice throws', () async {
+ expect(
+ () => registerCustomWatcherFactory(
+ _MemFsWatcherFactory(defaultFactoryId, memFs)),
+ throwsA(isA<ArgumentError>()));
+ });
+
+ test('finding two applicable factories throws', () async {
+ // Note that _MemFsWatcherFactory always returns a watcher, so having two
+ // will always produce a conflict.
+ registerCustomWatcherFactory(_MemFsWatcherFactory('Different id', memFs));
+ expect(() => FileWatcher('file.txt'), throwsA(isA<StateError>()));
+ expect(() => DirectoryWatcher('dir'), throwsA(isA<StateError>()));
+ });
+}
+
+class _MemFs {
+ final _streams = <String, Set<StreamController<WatchEvent>>>{};
+
+ StreamController<WatchEvent> watchStream(String path) {
+ var controller = StreamController<WatchEvent>();
+ _streams
+ .putIfAbsent(path, () => <StreamController<WatchEvent>>{})
+ .add(controller);
+ return controller;
+ }
+
+ void add(String path) {
+ var controllers = _streams[path];
+ if (controllers != null) {
+ for (var controller in controllers) {
+ controller.add(WatchEvent(ChangeType.ADD, path));
+ }
+ }
+ }
+
+ void remove(String path) {
+ var controllers = _streams[path];
+ if (controllers != null) {
+ for (var controller in controllers) {
+ controller.add(WatchEvent(ChangeType.REMOVE, path));
+ }
+ }
+ }
+}
+
+class _MemFsWatcher implements FileWatcher, DirectoryWatcher, Watcher {
+ final String _path;
+ final StreamController<WatchEvent> _controller;
+
+ _MemFsWatcher(this._path, this._controller);
+
+ @override
+ String get path => _path;
+
+ @override
+ String get directory => throw UnsupportedError('directory is not supported');
+
+ @override
+ Stream<WatchEvent> get events => _controller.stream;
+
+ @override
+ bool get isReady => true;
+
+ @override
+ Future<void> get ready async {}
+}
+
+class _MemFsWatcherFactory implements CustomWatcherFactory {
+ @override
+ final String id;
+ final _MemFs _memFs;
+ _MemFsWatcherFactory(this.id, this._memFs);
+
+ @override
+ DirectoryWatcher createDirectoryWatcher(String path,
+ {Duration pollingDelay}) =>
+ _MemFsWatcher(path, _memFs.watchStream(path));
+
+ @override
+ FileWatcher createFileWatcher(String path, {Duration pollingDelay}) =>
+ _MemFsWatcher(path, _memFs.watchStream(path));
+}