Skip to content

Commit

Permalink
Merge pull request #42 from Workiva/rap-2123_disposable_not_mixinable
Browse files Browse the repository at this point in the history
RAP-2123 Enable mixing in browser version of Disposable
  • Loading branch information
aaronstgeorge-wf authored Jun 28, 2017
2 parents 195896c + 81225ea commit a78af98
Show file tree
Hide file tree
Showing 7 changed files with 583 additions and 399 deletions.
187 changes: 182 additions & 5 deletions lib/src/browser/disposable_browser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,191 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import 'dart:async';
import 'dart:html';

import 'package:meta/meta.dart';
import 'package:w_common/func.dart';

import 'package:w_common/src/common/disposable.dart' as disposable_common;

class Disposable extends disposable_common.Disposable {
class _InnerDisposable extends disposable_common.Disposable {
Func<Future<Null>> onDisposeHandler;

@override
Future<Null> onDispose() {
return onDisposeHandler();
}
}

/// Allows the creation of managed objects, including helpers for common
/// patterns.
///
/// There are four ways to consume this class: as a mixin, a base class,
/// an interface, and a concrete class used as a proxy. All should work
/// fine but the first is the simplest and most powerful. Using the class
/// as an interface will require significant effort.
///
/// In the case below, the class is used as a mixin. This provides both
/// default implementations and flexibility since it does not occupy
/// a spot in the class hierarchy.
///
/// Helper methods, such as [manageStreamSubscription] allow certain
/// cleanup to be automated. Managed subscriptions will be automatically
/// canceled when [dispose] is called on the object.
///
/// class MyDisposable extends Object with Disposable {
/// StreamController _controller = new StreamController();
///
/// MyDisposable(Stream someStream) {
/// manageStreamSubscription(someStream.listen((_) => print('some stream')));
/// manageStreamController(_controller);
/// }
///
/// Future<Null> onDispose() {
/// // Other cleanup
/// }
/// }
///
/// The [manageDisposer] helper allows you to clean up arbitrary objects
/// on dispose so that you can avoid keeping track of them yourself. To
/// use it, simply provide a callback that returns a [Future] of any
/// kind. For example:
///
/// class MyDisposable extends Object with Disposable {
/// StreamController _controller = new StreamController();
///
/// MyDisposable() {
/// var thing = new ThingThatRequiresCleanup();
/// manageDisposer(() {
/// thing.cleanUp();
/// return new Future(() {});
/// });
/// }
/// }
///
/// Cleanup will then be automatically performed when the containing
/// object is disposed. If returning a future is inconvenient or
/// otherwise undesirable, you may also return `null` explicitly.
///
/// Implementing the [onDispose] method is entirely optional and is only
/// necessary if there is cleanup required that is not covered by one of
/// the helpers.
///
/// It is possible to schedule a callback to be called after the object
/// is disposed for purposes of further, external, cleanup or bookkeeping
/// (for example, you might want to remove any objects that are disposed
/// from a cache). To do this, use the [didDispose] future:
///
/// var myDisposable = new MyDisposable();
/// myDisposable.didDispose.then((_) {
/// // External cleanup
/// });
///
/// Below is an example of using the class as a concrete proxy.
///
/// class MyLifecycleThing implements DisposableManager {
/// Disposable _disposable = new Disposable();
///
/// MyLifecycleThing() {
/// _disposable.manageStreamSubscription(someStream.listen(() => null));
/// }
///
/// @override
/// void manageStreamSubscription(StreamSubscription sub) {
/// _disposable.manageStreamSubscription(sub);
/// }
///
/// // ...more methods
///
/// Future<Null> unload() async {
/// await _disposable.dispose();
/// }
/// }
///
/// In this case, we want `MyLifecycleThing` to have its own lifecycle
/// without explicit reference to [Disposable]. To do this, we use
/// composition to include the [Disposable] machinery without changing
/// the public interface of our class or polluting its lifecycle.
class Disposable implements disposable_common.Disposable {
/// Disables logging enabled by [enableDebugMode].
static void disableDebugMode() =>
disposable_common.Disposable.disableDebugMode();

/// Causes messages to be logged for various lifecycle and management events.
///
/// This should only be used for debugging and profiling as it can result
/// in a huge number of messages being generated.
static void enableDebugMode() =>
disposable_common.Disposable.enableDebugMode();

final _InnerDisposable _disposable = new _InnerDisposable();

@override
Future<Null> get didDispose => _disposable.didDispose;

@override
int get disposalTreeSize => _disposable.disposalTreeSize;

@override
bool get isDisposed => _disposable.isDisposed;

@override
bool get isDisposedOrDisposing => _disposable.isDisposedOrDisposing;

@override
bool get isDisposing => _disposable.isDisposing;

@override
Future<T> awaitBeforeDispose<T>(Future<T> future) => _disposable
.awaitBeforeDispose(future);

@override
Future<Null> dispose() {
_disposable.onDisposeHandler = this.onDispose;
return _disposable.dispose();
}

@override
Future<T> getManagedDelayedFuture<T>(Duration duration, T callback()) =>
_disposable.getManagedDelayedFuture(duration, callback);

@override
Timer getManagedPeriodicTimer(
Duration duration, void callback(Timer timer)) =>
_disposable.getManagedPeriodicTimer(duration, callback);

@override
Timer getManagedTimer(Duration duration, void callback()) =>
_disposable.getManagedTimer(duration, callback);

@override
Completer<T> manageCompleter<T>(Completer<T> completer) => _disposable
.manageCompleter(completer);

@override
void manageDisposable(disposable_common.Disposable disposable) =>
_disposable.manageDisposable(disposable);

@override
void manageDisposer(disposable_common.Disposer disposer) =>
_disposable.manageDisposer(disposer);

@override
void manageStreamController(StreamController controller) =>
_disposable.manageStreamController(controller);

@override
void manageStreamSubscription(StreamSubscription subscription) =>
_disposable.manageStreamSubscription(subscription);

/// Callback to allow arbitrary cleanup on dispose.
@protected
@override
Future<Null> onDispose() async {
return null;
}

/// Adds an event listener to the document object and removes the event
/// listener upon disposal.
///
Expand Down Expand Up @@ -60,11 +240,8 @@ class Disposable extends disposable_common.Disposable {
void _subscribeToEvent(EventTarget eventTarget, String event,
EventListener callback, bool useCapture) {
eventTarget.addEventListener(event, callback, useCapture);

var disposable = new disposable_common.InternalDisposable(() {
_disposable.manageDisposer(() {
eventTarget.removeEventListener(event, callback, useCapture);
});

disposable_common.addInternalDisposable(this, disposable);
}
}
17 changes: 4 additions & 13 deletions lib/src/common/disposable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,13 @@ class _ObservableTimer implements Timer {
/// A function that, when called, disposes of one or more objects.
typedef Future<dynamic> Disposer();

/// Allows the creation of managed objects, including helpers for common patterns.
/// Allows the creation of managed objects, including helpers for common
/// patterns.
///
/// There are four ways to consume this class: as a mixin, a base class,
/// an interface, and a concrete class used as a proxy. All should work
/// fine but the first is the simplest
/// and most powerful. Using the class as an interface will require
/// significant effort.
/// fine but the first is the simplest and most powerful. Using the class
/// as an interface will require significant effort.
///
/// In the case below, the class is used as a mixin. This provides both
/// default implementations and flexibility since it does not occupy
Expand Down Expand Up @@ -480,12 +480,3 @@ class Disposable implements _Disposable, DisposableManagerV3 {
}
}
}

// TODO: Refactor so that we don't have to use this hacky solution to access
// _internalDisposables from our browser Disposable class. Our original thought
// was to use a factory to return an implementation class, but because consumers
// use Disposable as a mixin, we can't give it a constructor.
void addInternalDisposable(
Disposable disposable, InternalDisposable internalDisposable) {
disposable._internalDisposables.add(internalDisposable);
}
7 changes: 6 additions & 1 deletion test/unit/browser/browser_stubs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import 'dart:html';

import 'package:mockito/mockito.dart';
import 'package:w_common/disposable_browser.dart';

import '../stubs.dart';

class DisposableThing extends Disposable with StubDisposable {}
class BrowserDisposable extends Object with Disposable, StubDisposable {}

class MockEventTarget extends Mock implements EventTarget {}
87 changes: 41 additions & 46 deletions test/unit/browser/disposable_browser_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,21 @@ import 'dart:html';
import 'package:test/test.dart';
import 'package:mockito/mockito.dart';

import '../disposable_common.dart';
import './browser_stubs.dart';

void main() {
group('Browser Disposable', () {
DisposableThing thing;

setUp(() {
thing = new DisposableThing();
});
testCommonDisposable(() => new BrowserDisposable());

group('events on global singleton', () {
BrowserDisposable disposable;
String eventName;
bool useCapture;
EventListener callback;

setUp(() {
disposable = new BrowserDisposable();
callback = (_) {};
eventName = 'event';
useCapture = true;
Expand All @@ -43,10 +42,10 @@ void main() {
() async {
var document = new MockEventTarget();

thing.subscribeToDocumentEvent(eventName, callback,
disposable.subscribeToDocumentEvent(eventName, callback,
documentObject: document, useCapture: useCapture);
verify(document.addEventListener(eventName, callback, useCapture));
await thing.dispose();
await disposable.dispose();
verify(document.removeEventListener(eventName, callback, useCapture));
});

Expand All @@ -55,53 +54,49 @@ void main() {
() async {
var window = new MockEventTarget();

thing.subscribeToWindowEvent(eventName, callback,
disposable.subscribeToWindowEvent(eventName, callback,
windowObject: window, useCapture: useCapture);
verify(window.addEventListener(eventName, callback, useCapture));
await thing.dispose();
await disposable.dispose();
verify(window.removeEventListener(eventName, callback, useCapture));
});
});

test(
'subscribeToDomElementEvent should remove listener when thing is disposed',
() async {
var element = new Element.div();
var event = new Event('event');
var eventName = 'event';
int numberOfEventCallbacks = 0;
EventListener eventCallback = (_) {
numberOfEventCallbacks++;
};
var shouldNotListenEvent = new Event('shouldNotListenEvent');

thing.subscribeToDomElementEvent(element, eventName, eventCallback);
expect(numberOfEventCallbacks, equals(0));

element.dispatchEvent(shouldNotListenEvent);
expect(numberOfEventCallbacks, equals(0));

element.dispatchEvent(event);
expect(numberOfEventCallbacks, equals(1));

await thing.dispose();

element.dispatchEvent(event);
expect(numberOfEventCallbacks, equals(1));

thing.subscribeToDomElementEvent(element, eventName, eventCallback);
expect(numberOfEventCallbacks, equals(1));
group('events on DOM element', () {
BrowserDisposable disposable;

element.dispatchEvent(event);
expect(numberOfEventCallbacks, equals(2));

element.dispatchEvent(event);
expect(numberOfEventCallbacks, equals(3));
setUp(() {
disposable = new BrowserDisposable();
});

element.dispatchEvent(shouldNotListenEvent);
expect(numberOfEventCallbacks, equals(3));
test(
'subscribeToDomElementEvent should remove listener when thing is disposed',
() async {
var element = new Element.div();
var event = new Event('event');
var eventName = 'event';
int numberOfEventCallbacks = 0;
EventListener eventCallback = (_) {
numberOfEventCallbacks++;
};
var shouldNotListenEvent = new Event('shouldNotListenEvent');

disposable.subscribeToDomElementEvent(
element, eventName, eventCallback);
expect(numberOfEventCallbacks, equals(0));

element.dispatchEvent(shouldNotListenEvent);
expect(numberOfEventCallbacks, equals(0));

element.dispatchEvent(event);
expect(numberOfEventCallbacks, equals(1));

await disposable.dispose();
numberOfEventCallbacks = 0;

element.dispatchEvent(event);
expect(numberOfEventCallbacks, equals(0));
});
});
});
}

class MockEventTarget extends Mock implements EventTarget {}
Loading

0 comments on commit a78af98

Please sign in to comment.