Events and Data
MIT License
EvDa is a collection of about 30 or so functions to construct a modular no-kitchen-sink style application. This is an event system.
It's been in development since 2008 and includes a suite with over 315 tests.
This schematic overview has been created to document the normal flow of an event call. Note that none of the optional things need to be assigned. At its most minimal, this library acts as a duck-typed object store ... essentially identical to javascript's regular object.
You can set a key through many kinds of notations. You can increment or decrement a number, add or remove a member from a set, or just assign a literal value.
When this is done:
Event systems provide a supplemental, indirect layer for application flow on top of the facilities provided by the underlying language. Because of this, normal methodologies of debugging often fail and leave code cryptic.
I've often seen projects where I think "what talks to what and how? What is the separation of concerns and where does this code live?" along with "why are things so indirect and convoluted?".
Sound familiar?
With the proper tooling to inspect code one could be on-boarded much faster to a project and find out what's going on. Such callback and routing based approaches often construct a rather inpenetrable walled garden of complexity where the inner-mechanisms of the project aren't laid bare for inspection but are complexly interwoven upon layers of undocumented ephemeral abstraction.
Because these systems seek to provide a supplemental flow upon the language, it's the responsibility of the implementer of such systems to provide the fundamental constructs that allow for the debugging and inspection of things which are built on top of them.
This means there needs to be features such as watching, logging, debugging, and tracing at the implementation level (which is on top of the callback system) and not be solely reliant on what is provided by the underlying language.
EvDa tries to alleviate this by keeping track of:
As an extreme analogy, not providing these tools of insight at the library's layer of abstraction would be like trying to debug a JavaScript web app by having the browsers' executable open with gdb.
Oftentimes when you need to solve a problem and are looking for an off-the-shelf solution, there's a rewrite cost beyond the immediate problem. There's presumptions and demands upon the existing application in order for the solution to work.
Let's introduce two broad terms:
To compute the equation for ATS we define:
So ATS is:
ATS = DIY - (FW + FW_rewrite + FW_training + FW_testing + FW_regression)
The base computation, DIY - FW > 0
is only possible if the proposed solution actually reduces complexity or simplifies a problem as opposed to merely redefining it in other terms.
Even under these constraints, if the other FW_*
variables are too high, ATS can often be negative. This means that using an off-the-shelf solution can be a net loss in productivity. This is counter to the conventional wisdom that such moves will almost always save time.
In practice this is far less often the case. In order to make ATS positive, one has to reduce the FW_*
metrics as much as possible by having a light-handed solution that can be implemented quickly, seemlessly, and in a side-effect free way without any impositions on how anything else works. Also, the solution has to provide benefits that are beyond preferential syntax and has to provide a real-world material decrease in complexity.
This is done by taking a few steps back to offer generalized primitives that seek to reduce intellectual load and collapse theories leading to a cognitive reduction of the details of implementation as opposed to further expounding on them.
Instead of giving a feature-based solution to a problem, basic building blocks are given to construct these features from primitives.
EvDa seeks to be 100% agnostic as to the MV* approach or other libraries used and intended to be just below a level of complexity where someone has to take an architectural opinion as to how to use it.
That is to say that it takes no opinion on how say, a routes architecture should be created, but instead, gives fundamental building blocks to construct a variety of them.
Similarly, no decree is made about how to do two-way data-binding or reactive design. Instead, there's generic tools which makes composing such a system convenient and easy.
It's been used in ember, angular, react, backbone, and extjs projects to supplement features that aren't in these libraries and also to get multiple libraries that don't play nice with each other to interact.
In so doing, since FW_rewrite
becomes 0, that means that everything but FW_training
also becomes 0.
Getting FW_training
near 0 can only be done through great care in writing documentation for humans with plenty of examples.
Let's start with demonstrating how EvDa is purely agnostic.
EvDa supports namespaced events while not having any opinion on how to do them.
You can do something like this:
var
content = EvDa(),
toolbar = EvDa(),
api = EvDa();
And then have those as three separate "namespaces".
You can set a context inside of a constructor (with EvDa-helper)
function Person(name) {
this.events = EvDa(this);
this.name = name;
this.events.expose('greet');
this.greet(function(when) {
return "Good " + when + "! My name is " + this.name + ".";
});
}
new Person('john').greet('morning');
>> Good morning! My name is John.
EvDa support object bubbling and dot notation. So you can do something like:
var ev = EvDa();
ev('content', function(obj) {
...
});
ev('content.key', 'value');
ev({
toolbar: {
key: 'something'
}
});
etc...
So as you can see here, there's "namespaces" as per the definition of what a namespace entails, but no direct decree as to how those ought to be done. This library empowers your personal liberty as a programmer, instead of restricting it to a preconceived implementation that you may not be able to integrate easily.
var ev = EvDa();
creates an event namespace, ev. You can use one for the entire app, that's fine.
You can also seed it with initialization values by passing in an object, for instance:
var ev = EvDa({key: 'value'});
And can specify the order that callbacks run in a simplified manner:
ev.after('key', function() { });
ev.first('key', function() { });
And have test that permit or deny something being set.
ev.test('key', function() { return false; });
What about a way to console log whenever anything gets set?
ev('', function(el){ console.log(el) })
>> fn0
What about setting 2 keys to one value?
ev(['key1', 'key2'], value);
>> [value, value]
What about having two callbacks for this?
ev({
'key1': function() { ... },
'key2': function() { ... },
})
>> {key1: fn0, key2: fn1 }
What about having just one?
ev(['key1','key2'], function(new_value) {
console.log(key + ' was set to ' + new_value);
})
>> [fn0, fn1]
What about making them run just once?
ev(['key1','key2'], function(new_value) {
console.log(key + ' was set to ' + new_value);
}, {once: 1})
>> [fn0, fn1]
What about unregistering one of them?
var list = ev(['key1','key2'], function(new_value) {
console.log(key + ' was set to ' + new_value);
});
ev.del(list[0]);
>> list[0]
And setting the other one to only running once?
list[1].once = true;
And then running something after that?
list[1].after(function() {
console.log('this will be run after');
});
>> fn0
And now running that chain?
ev.fire(['key1', 'key2']);
>> ['value1', 'value2']
That's nice you think, but some libraries will completely make chain functions inaccessible ... for instance what if we do this?
var ret = ev
.before('key', fn0)
.test(fn1)
.on(fn2)
.after(fn3);
>> [fn0, fn1, ..., fnX]
And then you want to unregister the third one on the list, the "on" function? Easy! Address it like an array:
ev.del(ret[2]);
>> ret[2]
What if you want to repurpose the third function for something else?
ev('something else', ret[3]);
>> ret[3]
What if you want to take some callbacks as a group and then disabled them?
var
some_group = ev.after(['key1','key2']),
handle = ev.disable(some_group);
Now what if you want to assign all of those to another key?
ev.on('key3', some_group);
>> fn0
And now you want to re-enable them and then increment all the keys at once?
ev.enable(some_group);
>> [ fn0, fn1, ..., fnX ]
ev.incr(['key1','key2','key3']);
>> [1, 1, 1]
There you go ...
There's also a nice way to diagnose things.
A nice way to find out if there's events associated with say, 'key1' you can do something like:
>> ev.events('key1')
Object { first: undefined, on: Array[3], after: undefined, test: undefined, or: undefined, set: undefined }
>> ev.events('key1').on[0].$
{ ref: ['onname'], ix: 1, last: Date 2016-02-23T20:48:26.346Z, line: [(stack trace)] }
This tells us that the first function that will be run in the on handler of key 1 is registered for just one function, onname; it's been called once, last at 2016-02-23T20:48:26.346Z and was registered from 1 place.
We can thus do an action and then see if these numbers change. Things like disabling a group or setting something as running once is also stored here for inspection.
There's other tools to to trace execution and do various other inspections.
Pretend I know how to get something, like say, a user profile, but I don't want to load it unnecessarily.
For example:
ev.setter('user.profile', function(trigger) {
$.get("/user/profile", function(data) {
trigger(data);
});
});
Then later on I can call this:
viewProfile: function(){
ev.whenSet('user.profile', function(profile) {
TheLatestFadInTemplating( profile );
});
}
Here, the request for the data user.profile inside of viewProfile made evda say "hrmm, I don't have it, do I know how to get it?"
And sure enough it does. It runs the callback which sets user.profile, and then runs the templating code and there you go. Let me do some kind of weird diagram because I like to waste my time:
/-> no? -> Run code whenever it's set then.
/
/-> no? -> Do I have a setter?
/ \
/ \-> yes? Run the setter. Then Run the code.
viewProfile -> Does user.profile exist?
\
\_> yes? run code immediately.
Oh but what if you declare the two things in the reverse order? That works. Sure. What about this?
viewProfile: function(){
if (var profile = ev.isset('user.profile')) {
TheLatestFadInTemplating( profile );
};
}
Let's say that we have a function:
this.once("something", function(){});
and then I do
this.trigger("something");
But what if the trigger runs BEFORE I register the handler? Then the trigger falls on the floor and the handler goes into neverland. That's not asynchronous.
I still have to know what will load before what; that's what synchronous is. Like, that is synchronous's definition. Something that's really Asynchronous would be something like:
this.trigger("something"); << the trigger could run here
this.whenSet("something", function(){}); << This will run after trigger.
this.trigger("something"); << OR here, it doesn't matter.
Syntax notations:
key
key
key
key
key
key
maintaining the orderkey
(alias to setDel
)key
key
If value, lambda, and meta are absent, this is a getter. eg., ev('key')
=> 'value'
If value is not a function, then it's a setter. If meta is set, then this object gets passed around to the trigger functions.
If value is a function, this registers a callback in the "ON" block.
If the first argument an array then each element of the array is ran on the rest of the arguments.
ev.once(handle)
at any future time to make sure that the callback only runs once more, then deregisters. This is different from a delete wherein it will deregister right away.If value is in object, its keys and values are run through the handler again, following the above rules. Note that you can do something like ev({}, undefined, meta)
to pass the same meta information to all the tuples in the hash.
Looking at the last style, one can do the following:
var handleList = ev({
a: function(value) { console.log(value) },
b: function(value) { console.log(value) }
});
ev({
a: 1,
b: 2
});
_.each(handleList, function(handle) {
ev.del(handle);
});
ev({
a: 1,
b: 2
});
key
to value
or undefined if a value is omitted. Although undefined is a falsy value, the engine checks for set membership so it won't be fooled by things like undefined and null._opts
section gives options for how the flow of the setter is run. This is a kind of "multiple dispatch" that is needed for internal unification. The options are:
function(meta, isFinal)
if set, this is a function that gets passed the meta object before each test and prior to the value actually being set. Since meta.value
is the value that will be set in the system, this can permit any permutations done by the testers or other handlers to be taken into consideration before the final meta.value
is set.isFinal
is true then it means this is the last call prior to being set.meta.value
at the end of the coroutine function is the one that will be sent to the after
and on
listeners - in this sense is more of a middleware than a knuthian coroutine - but since its passed as a lambda during the actual set as opposed to a decoupled listener, the flow of control more closely resembles that of a coroutine than a middleware stack.true
vlaue or else it acts as a test.Example:
ev.set('key', 1);
// sets the key to the array [1, 2, 3] and passes
// meta to the callback.
ev.set('key', [1,2,3], 'meta');
// sets the key to an undefined value.
ev.set('key');
set
also has something called a handy setter. For instance, say you have some kind of promise system
like so:
var session = EvDa();
remote('/Login').then(session).fail(...);
But you didn't want all of session to be filled. Pretend you wanted to scope it conveniently. This is where the handy setter comes in:
remote('/Login').then(session.set('user_data')).fail(...);
In this use-case, session('user_data')
gets set to undefined, and returns a function which will take in
an argument to set the user_data
. This sounds multi-layered and hard but it isn't. It does what you
expect it to do. For instance:
var cb = session.set('user_data');
cb({username: 'some user', id: 123});
session('user_data.id') == 123
.isSet('key')
will return false henceforthExample:
ev('key', 1);
> 1
ev(''); // see <a href="#bubble">bubbling and globals</a> at the end.
> { key: 1 }
ev.unset('key')
> 1
ev('')
> {}
$.extend
or _.extend
on the existing value, merging back in.Example:
ev.push('key', 1);
> [1]
ev.push('key', 2);
> [1,2]
Example:
ev.pop('key');
> 2
The set functions are convenience wrappers on top of the basic value-based functionalities. Their are a few differences:
meta.set
.meta.value
fails to pass the unique set test, then no more tests are run and the or
event handlers are called.meta.value
can be modified before the set-inclusion rules are actually applied.array
then it gets flattened and appended.Example:
ev.setadd('key', 1);
> [1]
ev.setadd('key', 1); << no triggers are run
> [1]
ev.setadd('key', [2, 3]);
> [1, 2, 3]
setAdd
but maintains the order of the set at a slight complexity cost.setDel
existing for symmetrical reasons.Example:
ev.setAdd('key', [1, 2, 3]);
> [1, 2, 3]
ev.setDel('key', 2);
> [1, 3]
Example:
ev.setAdd('key', 1);
> [1]
ev.setToggle('key', 1);
> []
ev.setToggle('key', 1);
> [1]
Example:
ev.incr('key')
> 1
ev.incr('key', 2)
> 3
Also if the second argument is a string, then this gets treated as an expression.
A place where this would be useful is say if you have a volume button and you want
it to go up 110% and down 90% as opposed to a +/- 10 amount. Here (as of 0.1.80)
you can use incr
as a general purpose mod
as follows:
ev.incr('volume', '*(11/10)')
> 1.100
ev.incr('volume', '*(10/11)')
> 1.000
In fact, .mod
is aliased to .incr
to make this look more semantically meaningful:
ev.mod('volume', '*(11/10)')
> 1.100
ev.mod('volume', '*(10/11)')
> 1.000
Does the same thing.
You can set ceilings and floors by denying the values through tests such as:
ev.test('volume', function(val, meta) {
meta(val > 10 || val < 0);
});
Example:
ev.decr('key')
> 0
ev.decr('key', 2)
> -2
True
.False
.True
.These triggers have different semantic values although they all function in much the same way. If you want to view or modify the current triggers assigned you can do so by not providing a secondary argument like so:
ev.on("key");
With that you can do things like re-order, remove, or add things manually.
It's worth noting that the second argument can be an array of functions so you can do things like
ev.on('key', ev.on('key1'));
To duplicate functionality.
ev.del
to deregister.ev.del
to deregister.ev.set
or ev(key, value)
and thus suppress the "ON" and "AFTER" functions.result
and supplied in an object in the second argument. Calling the function with anything.result()
means "go ahead".true
. (you can return a boolean of false
to represent test failure)..result()
is aliased to .done()
and can also be invoked by calling the object, as in function(val, meta) { meta() }
.meta.value
cascades down through the test suite as the value to belambda
from the last test. This allows for an asynchronous cascading of middleware and mutation of data.Example:
// set up a test condition that tells whether
// the test should succeed or not - only go forward
// when value is truthy.
ev.test('key', function(value, meta) {
meta(value == true);
});
// this will only run 1 time because
// the test will fail on the second
// execution.
ev.after('key', function(){
console.log('here');
});
// this should run.
ev.set('key', true);
// sets the key to an undefined value.
ev.set('key', false);
test
is registered and the test suite failsExample:
ev.test('key', function(val, meta){
meta(false);
}).or('key', function(val) {
console.log("failure");
});
Also, these need not be chained as above:
ev.test( ... );
ev.or( ... );
Executes lambda when key === value
OR test(value) == true
Note: By default, this handler runs every time that key gets set to value. To make this a one-time run, you can do the following:
ev.once(ev.when('key', 'value', callback))
There is also an object-style way of doing this which offers more fidelity then the regular isset
which just checks to see
if something is or is not set. In this mode you can do things like:
ev.when({ key1: 'value1', key2: 'value2' }, function() ... );
This model above helps handle multiple dependencies where each one takes on a specific value.
If invoked as .when(key, lambda)
then this works identical to an .isset()
so long as the key isn't an object.
Example:
var
ev = EvDa(),
handle = ev('key', function(value) {
console.log(value);
});
ev('key', 1);
ev.del(handle);
ev('key', 2); << this will not be run.
Inline example:
var ev = EvDa();
ev('key', function(value) {
console.log(value);
if(value == 1) {
// Using the callee reference works as
// a valid way to deregister the function.
ev.del(arguments.callee);
}
});
ev('key', 1);
ev('key', 2); << this will not be run.
State that the setter for a key is a callback. This will be run if there are things blocked on it.
Returns whether it was run immediately or not.
Useful for asynchronous operations, such as a login screen; wherein you only want to give it to the user when applicable
The lambda function may have a function that it runs when its ready. That's to say something like this:
ev.setter("username", function(done) { $.get("/whoami", done); });
Now in some template I'm doing something like this
ev.isSet('username', function(who){
$("#header").html(
template({
username: who
})
);
});
If lambda is not set, returns true if key exists, false if it is not
If lambda is set,
undefined
is returned.You can pass in K/V object style arguments similar to the ev() notation above.
You can also pass an array of things ... all of them need to be set for the lambda to run.
first
, on
, after
, or or
events.{ _isok: true }' to the
meta` for detection.Example 1:
ev.test('key', function(value) {
return (value == 'value');
});
if (ev.isOK('key', 'value')) {
...
}
Example 2:
ev.test('key', function(value, meta) {
if(meta._isok) {
// run a test for the checker
} else {
// do something else..
}
return (value == 'value');
});
if (ev.isOK('key', 'value')) {
ev('key', 'value');
...
}
.isSet
for syntactic sugar.ev.set(key, ev(key), {noset: true})
.ev.group()
call.a.b
, a.b
events are ran before a
.a.b = 3
and a.c = 4
, then a's callback would get { b: 3, c: 4 }
. For a.b.c = 1
you'll get { b: { c: 1 } }
.''
.With no arguments you get this:
{
data: ... The keys and current values
events: ... The events (on, after, before, test ...)
locks: ... Used to prevent recursion, it can also (if buggy) prevent firing
testLocks: ... Used to prevent test recursion
removed: ... Removed and completed setters or other events
lastReturn: ... The last function return value for each key (useful for debugging closures)
log: ... A log (see below) of the previous values
globs: ... Regex style event listening
trace: ... Functions to run each time, see <a href='#sniff'>sniff</a> for more information.
}
For instance, say I had two test conditions and I wanted to make the second one registered run first.
ev.test('key', fun2);
ev.test('key', fun1);
I can use this to re-order the events.
var list = ev.debug('key', 'test').events;
list.unshift(list.pop());
And there I go.
ev().traceList
is also exposed. It's anThe traceList
parameter can be directly manipulated.
By default the bubbled top, "", is set to be ignored. This can be un-ignored with ev.sniff('')
.
With no arguments:
Otherwise:
[]
)Both invocations:
release/
directory, a version
string is tacked on to the global EvDa
object.git describe
along with a date of the build as run from the tools/deploy.sh
directory.Callback hell is an example of an action at a distance antipattern. In general terms, this is the same problem that you get from C unions or C++ operator overloading.
You see a seemingly linear state of events such as (in EvDa):
...
ev.set('key', 'value');
...
or in C:
some_union.prop = 1;
or in C++:
c = a + b;
And you think you know what's going on. But really, anything could be happening. Not only, but if you load up a debugger you could be jumping around to weird parts of the code or worse yet, go through quite a few layers of scaffolding and redirection before getting to the actual thing that is happening.
The powerful abstraction and comprehension that these methodologies afford can also violate the separation of concerns and make code do too many things at once. At the end you have code that works like some marvelous complex watch with interacting gears and not a straight-forward easy to dissect or recompose thing.
The generalized event listener and multiple-dispatch model suffer from this problem.
EvDa seeks to try to solve this through introspection tools.
This is all the hot-sauce these days, as if it's some kind of miraculous difficult thing to achieve.
Here's something I did for my indycast project called easy_bind
.
You can see how nicely evda plays with underscore, modern js, and jquery:
The invocation here that I want to do is have a number of li > a
style selectors and
input[type=text]
. I want to have two way data-binding so that I can do something like:
easy_bind(['email', 'notes', 'duration', 'station']);
And then have those keys in the data always reflect the selected things on the UI.
Do we need to build the Entire Application with Ember or Angular for this? Of course not!
Here's a basic function that can easily do two-way databinding without trying to redefine javascript as something crazy-hard and really weird or annotating your HTML in order to satisfy some supposedly sophisticated library:
function easy_bind(list, instance) {
if (!instance) {
if (ev) {
instance = ev;
} else {
throw "Can't find any instance to attach to";
}
}
// There's some claim that you can feature-test everything.
// Some totally bogus claim, that is.
var
isiDevice = navigator.userAgent.match(/ip(hone|od|ad)/i),
listenEvent = isiDevice ? 'touchend' : 'click';
// Take each "query" to bind to from the list
_.each(list, function(what) {
// Look for a node with that name
var node = document.querySelector('#' + what);
if(!node) {
// And if it doesn't exist, see if some input has it
node = document.querySelector('input[name="' + what + '"]');
// Alright that didn't work, let's bail
if(!node) {
throw new Error("Can't find anything matching " + what);
}
}
// If we are looking at an input box, then we can just grab
// the value of it
if(node.nodeName == 'INPUT') {
$(node).on('blur focus change keyup', function() {
instance(what, this.value, {node: this});
});
// This is the 'two-way' ...
instance(what, function(val){
if(val !== undefined) {
node.value = val;
}
});
} else {
// Otherwise there's some complex UL/LI structure that is mostly an artifact
// of the limitations of CSS. Regardless, after all the onion-style wrappings,
// there should be an <a> tag underneath
$("a", node).on(listenEvent, function(){
// This tricks stupid iDevices into not screwing with the user.
// (Requiring a user to tap twice to select anything. WTF apple...)
var mthis = this;
setTimeout(function(){
instance(what, mthis.getAttribute('data') || mthis.innerHTML);
}, 0);
});
// Here's the two-way
instance(what, function(val) {
$("a", node).removeClass("selected");
if (val) {
$("a", node).filter(function(){return this.innerHTML == val}).addClass("selected");
$("a[data='" + val + "']", node).addClass("selected");
}
});
}
instance.fire(what);
});
}
The two way data-binding is ok for the local in-memory structures but what if I want to make this work independently with a syncing layer (yes, completely independently as in one doesn't know about the other.)
Here's an easy_sync
implementation from the same project:
// First we have a local-storage getter/setter that is determined
// based on argument length - to make our lives easier.
function ls(key, value) {
if (arguments.length == 1) {
return localStorage[key] || false;
} else {
localStorage[key] = value;
}
return value;
}
// Now we use it to sync things to the local storage
function easy_sync(list) {
_.each(list, function(what) {
if(ls(what)) {
ev(what, ls(what));
}
ev.after(what, function(value) {
ls(what, value);
});
});
return ev('');
}
Using the two above functions I was able to write something like:
easy_bind(['email', 'notes', 'station', 'duration']);
var map = easy_sync(['email', 'station']);
if(map.station) ...
And then have the two way data-binding with remote syncing and I don't have to be restricted to how I'm going to structure my end-points, or what templating engine I'm using or whatever else.
You can just do this one single thing, and not get a bunch of garbage with it. Refreshing.
For convenience, these have been included in evda-helper.js
.
Sure.