Skip to content

Instantly share code, notes, and snippets.

@unscriptable
Created March 21, 2012 01:21
Show Gist options
  • Save unscriptable/2143389 to your computer and use it in GitHub Desktop.
Save unscriptable/2143389 to your computer and use it in GitHub Desktop.
event hub with strategies
define(function () {
var undef;
/**
* This strategy will disable a data item during REST operations.
* This strategy only makes sense in a pessimistic network.
* We need to integrate promises into the EventHub in order for this
* strategy to work. Alternatively, we could have item states of
* 'adding', 'removing', 'updating', but that feels brutish.
*/
return function createStrategy () {
var pendingItems, longEvents;
pendingItems = [];
longEvents = {
save: {},
remove: {}
};
return {
// TODO: shoud we prevent other events while busy or is setting the disabled state good enough?
allowEvent: undef,
processEvent: function (item, type, sourceNode, destNode, allNodes) {
if (type in longEvents) {
if (destNode == sourceNode) {
// TODO: determine when/how to enable after we determine if using promises or not
destNode.disable(true);
}
}
else {
return false;
}
}
};
};
});
define(function () {
var allowed, denied, prevented, undef;
function EventHub () {
this._nodes = [];
this._strategy = undef;
// this isn't very wirey
this._eventTypes = [
'update', 'save', 'remove', 'select', 'edit', 'disable'
];
}
// // unique flags
// allowed = {};
// denied = {};
// prevented = {};
//
// EventHub.eventApproval = {
// allow: allowed,
// deny: denied,
// prevent: prevented
// };
EventHub.prototype = {
/**
* Strategies determine if an event gets onto the network and then how
* it's processed by the other nodes in the network. (nodes are
* adapters). Strategies can be composed/combined.
* Should we compose them in this object or should we assume they've
* already been composed? (This impl assumed pre-composed.)
* @param strategy {Object} a strategy object has two methods:
* strategy.allowEvent {Function} function (data, type, sourceNode, allNodes)
* strategy.processEvent {Function} function (data, type, sourceNode, allNodes)
*/
setStrategy: function (strategy) {
// type == 'update', 'add', 'remove', 'select', 'edit'
// strategies return an array of nodes that get event? or true/false?
// sync properties strategy
// cursor strategy
this._strategy = strategy;
},
/**
* Applies the strategies to determine if the event is allowed on the
* network. Strategies should return false to deny the event.
* I think we need a way to indicate that an event should be not only
* denied, *but also prevented/reverted*. For instance, if a node sends
* a 'select:true' event when the FreezeSelectionWhileEditing strategy
* is applied, the node should be set back in the select:false state.
* @param sourceNode
* @param type
* @param data
*/
allowEvent: function (sourceNode, type, data) {
var allNodes, strategy, receiverNodes;
allNodes = this._nodes;
strategy = this._strategy;
if (strategy && strategy.allowEvent) {
return strategy.allowEvent(data, type, sourceNode, allNodes);
}
else {
return allowed;
}
},
/**
* Applies strategies to determine how to process an event for each
* node in the network. The default is to simply send the event to
* all nodes except the source node. Strategies could decide to
* send different events, skip nodes, etc.
* @param sourceNode
* @param type
* @param data
*/
processEvent: function (sourceNode, type, data) {
var nodes, strategy, processFunc, i, node;
nodes = this._nodes;
strategy = this._strategy;
processFunc = strategy && strategy.processEvent;
i = nodes.length;
while ((node = nodes[i++])) {
if (!processFunc || undef === processFunc(data, type, sourceNode, node, nodes)) {
forwardEvent(data, type, sourceNode, node, nodes);
}
}
},
addNode: function (node) {
var i, type;
// sniff for event hooks
for (i = 0; i < this._eventTypes.length; i++) {
type = this._eventTypes[i];
if (typeof node[type] == 'function') {
node[type] = this._overrideNodeEvent(node, type, node[type]);
}
}
},
_overrideNodeEvent: function (node, type, origEvent) {
var self = this;
return function (data) {
var allowed, result;
allowed = self.allowEvent(node, type, data);
if (allowed == prevented) {
throw new Error('not allowed');
}
result = origEvent.call(node, data);
if (allowed == allowed) {
self.processEvent(node, type, data);
}
return result;
};
}
};
function forwardEvent (data, type, sourceNode, destNode) {
if (sourceNode != destNode) {
destNode[type](data);
}
}
return EventHub;
});
define(function () {
var allow, disallow, undef;
allow = true;
disallow = false;
return function createStrategy () {
var nodeBeingEdited;
return {
allowEvent: function (isEditing, type, sourceNode, allNodes) {
if (nodeBeingEdited) {
if (type == 'edit') {
if (isEditing) {
// don't allow another node to enter editing state
return disallow;
}
else if (nodeBeingEdited == sourceNode) {
// node is leaving editing state
nodeBeingEdited = undef;
return allow;
}
else {
// TODO: throw if we get an isEditing==false from a node that isn't being edited?
return disallow;
}
}
else {
// disallow select while editing
return type == 'select' ? disallow : allow;
}
}
else {
if (type == 'edit' && isEditing) {
nodeBeingEdited = sourceNode;
}
return allow;
}
},
processEvent: undef
};
};
});
define(function () {
var undef;
/**
* This strategy only set nodes with the same identifier to
* select:true (isSelected == true).
*/
return function createStrategy (options) {
var identifier;
identifier = options.symbolizer;
return {
allowEvent: undef,
processEvent: function (isSelected, type, sourceNode, destNode, allNodes) {
if ('select' in destNode && type == 'select' && isSelected) {
destNode['select'](identifier(sourceNode) != identifier(destNode) || isSelected);
return true;
}
}
};
};
});
define({
myStore: { $ref: 'resource!my/endpoint' },
myView: {
wire: 'my/databoudView',
get: 'viewNode',
// these should be in the view's spec, not here. In the view, they
// need to convert a dom event to a cola event (like they do here),
// but that means we also need a colaNode in the view's spec.
// colaNode: { bind: { adapts: 'viewNode', bindings: { /*...*/ } } }
on: {
'click:button.save': {
colaHub: 'save' // 'commit' ?
},
'click:button.cancel': {
colaHub: 'cancel' // 'rollback'?
}
}
},
colaHub: {
bind: {
nodes: [
{ $ref: 'myStore' },
{ $ref: 'myView' }
],
// we could probably compose these into common groups
// and alow dev to set a default set for their app?
strategies: [
{ module: 'cola/strategy/FreezeSelectionWhileEditing' },
{ module: 'cola/strategy/SelectSingleItemAtATime' }
],
// these are composed into a single comparator that is injected
// into the network
sortBy: {
name: 1,
updatedDate: -2
}
// does it make sense to include a key here, too?
},
// a place to put event handlers!
on: {
remove: {
controller: 'handleItemRemoved'
}
}
},
controller: {
handleItemRemoved: function (item) { alert('item removed: ' + item.id); }
},
plugins: [
{ module: 'wire/dojo/store' },
{ module: 'wire/dojo/on' },
{ module: 'wire/cola' }
]
});
define(function () {
var allow, disallow, undef;
allow = true;
disallow = false;
/**
* This strategy defers all update events (property change events) until
* the node currently being edited leaves the editing state (edit:false).
* The converse of this strategy is RevertPropertyChangesAtRollback.
*/
// TODO: propagate property changes when finished editing
return function createStrategy () {
var nodeBeingEdited;
return {
allowEvent: function (isOn, type, sourceNode, allNodes) {
if (nodeBeingEdited) {
if (type == 'edit') {
if (isOn) {
// don't allow another node to enter editing state
return disallow;
}
else if (nodeBeingEdited == sourceNode) {
// node is leaving editing state
nodeBeingEdited = undef;
return allow;
}
else {
// TODO: throw if we get an isOn==false from a node that isn't being edited?
return disallow;
}
}
else {
// disallow select while editing
return type == 'set' ? disallow : allow;
}
}
else {
if (type == 'edit' && isOn) {
nodeBeingEdited = sourceNode;
}
return allow;
}
},
processEvent: undef
};
};
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment