Skip to content

Instantly share code, notes, and snippets.

@jonnyreeves
Created November 26, 2011 16:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jonnyreeves/1395965 to your computer and use it in GitHub Desktop.
Save jonnyreeves/1395965 to your computer and use it in GitHub Desktop.
PureMVC RelaxedNotificationMap
package uk.co.jonnyreeves.puremvc
{
import flash.errors.IllegalOperationError;
import org.puremvc.as3.multicore.interfaces.INotification;
/**
* Maps a PureMVC Notification to a Function delegate. Add the mappings in your Mediator's Constructor and then
* delegate the calls to listNotificationInterests() and handleNotification() through to this class.
*
* @author Jonny Reeves
*/
public class NotificationMap
{
/**
* Constant used to indicate that a mapping is interested in all Notifications of the supplied notificationName
* regardless of Type.
*/
public static const ALL_TYPES : String = "*";
private const _notificationTypeByNotificationName : Object = {};
/**
* Creates a mapping between the supplied Notification Name and a callback function. The callback function
* should only accept a single argument of type INotification.
*
* @param notificationName The Name of the Notification you want to map to the supplied callback function
* @param callback Function delegate which will be called should the PureMVC framework supply an
* INotification of the corrospoinding notificationName param. This delegate should
* expect a single argument, the INotification instance.
* @param notificaitonType Optional, the supplied callback will only be invoked if the `type` property of
* the incoming INotification matches this supplied value.
*/
public function add(notificationName : String, callback : Function, notificationType : String = ALL_TYPES) : void
{
// No invalid Callbacks (must accept a single argument).
if (callback.length != 1) {
throw new ArgumentError("Refusing to map supplied callback function to Notification: " + notificationName + ". The callback function must accept a single argument (the notification body)");
}
// No Overwrites.
if (contains(notificationName, notificationType)) {
throw new IllegalOperationError("The supplied Notification: " + notificationName + " is already mapped to a callback");
}
const callbacksByNotificationType : Object = _notificationTypeByNotificationName[notificationName] ||= {};
callbacksByNotificationType[notificationType] = callback;
}
/**
* Returns true if a mapping exists for the supplied Notification Name and Type combination.
*/
public function contains(notificationName : String, notificationType : String = ALL_TYPES) : Boolean
{
const callbacksByNotificationType : Object = _notificationTypeByNotificationName[notificationName];
if (callbacksByNotificationType == null) {
return false;
}
return callbacksByNotificationType[notificationType] != null;
}
/**
* Removes any mappings that may exist for the supplied Notificaiton Name regardless of Notification Type.
*/
public function remove(notificationName : String) : void
{
delete _notificationTypeByNotificationName[notificationName];
}
/**
* Removes the mapping that may exist for the supplied Notification Name and Type.
*/
public function removeByType(notificationName : String, notificationType : String) : void
{
const callbacksByNotificationType : Object = _notificationTypeByNotificationName[notificationName];
if (callbacksByNotificationType) {
delete callbacksByNotificationType[notificationType];
}
}
/**
* Clears the map, removing all mappings.
*/
public function removeAll() : void
{
for (var key : * in _notificationTypeByNotificationName) {
delete _notificationTypeByNotificationName[key];
}
}
/**
* Returns an Array of NotificationNames which have been added to this NotificationMap instance; this can be
* used to satisfy the PureMVC IMediator#listNotificationInterests() method.
*/
public function listNotificationInterests() : Array
{
const notificationNames : Array = [];
for (var key : * in _notificationTypeByNotificationName) {
notificationNames.push(key);
}
return notificationNames;
}
/**
* Handles an incoming PureMVC notificaiton by invoking the callback regsitered to the supplied notificaiton
* name.
*/
public function handleNotification(note : INotification) : void
{
const callbacksByNotificationType : Object = _notificationTypeByNotificationName[note.getName()];
if (callbacksByNotificationType)
{
// Invoke the `all types` callback, regardless of the incoming Notification's type property.
if (callbacksByNotificationType[ALL_TYPES] != null) {
callbacksByNotificationType[ALL_TYPES](note);
}
// Invoke the callback specific to this type.
const noteType : String = note.getType();
if (noteType && callbacksByNotificationType[noteType]) {
callbacksByNotificationType[noteType](note);
}
}
}
}
}
package uk.co.jonnyreeves.puremvc
{
import org.puremvc.as3.multicore.core.View;
import org.puremvc.as3.multicore.interfaces.INotification;
import org.puremvc.as3.multicore.patterns.observer.Observer;
/**
* Provides an extention to the default NotificationMap which caches Notifications broadcast via
* the Facade whilst the client Mediator is unregistered from the View's Observer Map.
*
* Map you Notifications in your Mediator's Constructor as per ususal and then tell this Map
* which notifications you would like cached via the RelaxedNotificationMap.rememberNotification()
* method. Then, when your Mediator's `onRegister` is called, invoke
* RelaxedNotificationMap.relayCachedNotifications() at which point the most recent 'remembered'
* Notification will be redispatched to the client Mediator.
*
* @author John Reeves
*/
public class RelaxedNotificationMap extends NotificationMap
{
private const _registeredObserverMap : Object = {};
private const _cachedNotificationMap : Object = {};
private var _pendingRememberanceSet : Object;
private var _multitonKey : String;
private var _mediatorName : String;
public function RelaxedNotificationMap(mediatorName : String)
{
super();
_mediatorName = mediatorName;
}
public function initializeNotifier(multitonKey : String) : void
{
if (_multitonKey != null) {
return;
}
_multitonKey = multitonKey;
// Register all the mappings which were waiting for the Core Key to be supplied.
if (_pendingRememberanceSet != null) {
for (var notificationName : String in _pendingRememberanceSet) {
rememberNotification(notificationName);
}
_pendingRememberanceSet = null;
}
}
override public function removeAll() : void
{
super.removeAll();
// Clear all cached notifications.
clearNotificationCache(fetchCachedNotifications());
// Clear all observers.
for (var notificationName : String in _registeredObserverMap) {
forgetNotification(notificationName);
}
// And any pending chaps who were left waiting...
_pendingRememberanceSet = null;
}
public function rememberNotification(notificationName : String) : void
{
// This instance must be supplied with the multitonKey before we can access the
// relevant 'View' instance for this PureMVC Core.
if (_multitonKey == null) {
const rememberanceSet : Object = _pendingRememberanceSet ||= {};
rememberanceSet[notificationName] = true;
return;
}
// Don't allow multiple mappings for the same notificaitonName
if (_registeredObserverMap[notificationName] != null) {
return;
}
// Register an Observer against the View Singleton which will cache notifications as they
// are broadcast.
const observer : Observer = new Observer(cacheNotification, this);
_registeredObserverMap[notificationName] = observer;
View.getInstance(_multitonKey).registerObserver(notificationName, observer);
}
public function forgetNotification(notificationName : String) : void
{
if (_pendingRememberanceSet && _pendingRememberanceSet[notificationName]) {
delete _pendingRememberanceSet[notificationName];
}
else {
const observer : Observer = _registeredObserverMap[notificationName];
if (observer) {
View.getInstance(_multitonKey).removeObserver(notificationName, this);
delete _registeredObserverMap[notificationName];
}
}
}
public function relayCachedNotifications() : void
{
// Retrieve the list of Notifications that this Map recieved and then clear them.
// It's important to clear the Notification Cache before we supply the notifications
// to the Mediator in-case it decides it wants to remember other notifications :S
const cachedNotifications : Array = fetchCachedNotifications();
clearNotificationCache(cachedNotifications);
// Now Relay the notification to the Mediator instance.
for each (var notification : INotification in cachedNotifications) {
handleNotification(notification);
}
}
private function cacheNotification(note : INotification) : void
{
// No need to cache notifications whilst the Mediator is registered.
if (View.getInstance(_multitonKey).hasMediator(_mediatorName)) {
trace("Mediator is registered with the Facade, not caching: " + note.getName());
return;
}
// Only store the last notification of this name.
_cachedNotificationMap[note.getName()] = note;
}
private function fetchCachedNotifications() : Array
{
const result : Array = [];
for each (var notification : INotification in _cachedNotificationMap) {
result.push(notification);
}
return result;
}
private function clearNotificationCache(notifications : Array) : void
{
for each (var notification : INotification in notifications) {
delete _cachedNotificationMap[notification.getName()];
}
}
}
}
package uk.co.jonnyreeves.puremvc
{
import com.iwi.util.puremvc.unitest.PureMVCTestCase;
import com.iwi.util.puremvc.unitest.PureMVCTestFacade;
import org.flexunit.asserts.assertEquals;
import org.flexunit.asserts.assertFalse;
import org.flexunit.asserts.assertTrue;
import org.puremvc.as3.multicore.interfaces.INotification;
/**
* @author jonny
*/
public class TestRelaxedNotificationMap extends PureMVCTestCase
{
private var _notificationMap : RelaxedNotificationMap;
private var _mediator : RelaxedNotificationMapClient;
private var _recievedNotificatons : Array;
public static const NOTE_A : String = "NOTE_A";
[Before]
override public function setup() : void
{
super.setup();
_mediator = new RelaxedNotificationMapClient();
_notificationMap = _mediator.getNotificationMap();
_recievedNotificatons = [];
}
[After]
override public function teardown() : void
{
super.teardown();
}
[Test]
public function notificationSentWhilstMediatorIsNotRegistered_notificationIsRetrievedFromCache() : void
{
initializeNotificationMap();
// Instruct the notification map to listen for NOTE_A and then cache that notification when the
// client Mediator is removed.
_notificationMap.add(NOTE_A, recordNotification);
_notificationMap.rememberNotification(NOTE_A);
// Send the notification before the Mediator is registered against the Facade.
facade.sendNotification(NOTE_A);
assertFalse(notificationCount); // Sanity check.
// Now re-register our hero Mediator - NOTE_A should be replayed to it.
facade.registerMediator(_mediator);
assertTrue("Notification was broadcast from the Cache", notificationCount);
}
[Test]
public function notificationIsRemovedFromTheCacheAfterItIsBroadcast() : void
{
initializeNotificationMap();
_notificationMap.add(NOTE_A, recordNotification);
_notificationMap.rememberNotification(NOTE_A);
facade.sendNotification(NOTE_A);
// Assert the notification is retrieved from the cache when the Mediator is re-registered for the
// first time.
facade.registerMediator(_mediator);
assertEquals("Notification was broadcast from Cache", 1, notificationCount);
// Reset the flag and re-register the Mediator.
facade.removeMediator(_mediator.getMediatorName());
facade.registerMediator(_mediator);
assertEquals("Notification was consumed form the Cache", 1, notificationCount);
}
[Test]
public function onlyTheLatestNotificationIsCached() : void
{
initializeNotificationMap();
_notificationMap.add(NOTE_A, recordNotification);
_notificationMap.rememberNotification(NOTE_A);
facade.sendNotification(NOTE_A, "FIRST");
facade.sendNotification(NOTE_A, "SECOND");
facade.registerMediator(_mediator);
assertEquals("Notificaiton Cache only caches the most recent notification", 1, notificationCount);
assertEquals("Notificaiton Cache only caches the most recent notification", "SECOND", (_recievedNotificatons[0] as INotification).getBody());
}
[Test]
public function notificaitonIsNotCachedWhilstMediatorIsRegistered() : void
{
_notificationMap.add(NOTE_A, recordNotification);
_notificationMap.rememberNotification(NOTE_A);
facade.registerMediator(_mediator);
facade.sendNotification(NOTE_A);
assertEquals("Notification was routed whilst the Mediator was registered", 1, notificationCount);
facade.removeMediator(_mediator.getMediatorName());
facade.registerMediator(_mediator);
assertEquals("Notification was not stored in the Cache whilst the Mediator was registered", 1, notificationCount);
}
[Test]
public function rememberancesCanBeMappedBeforeTheMultitonKeyIsSupplied() : void
{
// The map is being configured before it can gain access the View singleton (as no
// Core Key has been supplied). This is how we expect the map to be used as clients
// will create their mappings in the Mediator's Constructor.
_notificationMap.add(NOTE_A, recordNotification);
_notificationMap.rememberNotification(NOTE_A);
// Curley-shuffle.
initializeNotificationMap();
facade.sendNotification(NOTE_A);
facade.registerMediator(_mediator);
assertTrue("Notification was broadcast from the Cache", notificationCount);
}
[Test]
public function notificationsCanBeForgotten() : void
{
initializeNotificationMap();
_notificationMap.add(NOTE_A, recordNotification);
_notificationMap.rememberNotification(NOTE_A);
// Lest we forget...
_notificationMap.forgetNotification(NOTE_A);
facade.sendNotification(NOTE_A);
// Now check that the notification is not pulled from the cache.
facade.registerMediator(_mediator);
assertFalse("Notificaiton was not broadcast from the cache", notificationCount);
}
private function initializeNotificationMap() : void {
// Typically your Mediator will take care of this when it is first registered against the Facade.
_notificationMap.initializeNotifier(PureMVCTestFacade.KEY);
}
private function recordNotification(note : INotification) : void {
_recievedNotificatons.push(note);
}
private function get notificationCount() : int {
return _recievedNotificatons.length;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment